Idempotency and trade outcomes
How to retry without placing a second order, and what each order result means.
When a request times out, you need to retry without placing a second order, and to know whether the first one went through.
Idempotency keys
Every mutating call accepts an Idempotency-Key header:
POST /users/current/accounts/acc_7Qx/trade
Idempotency-Key: sig-4411-acc_7Qx
X-Actor: copier
X-Reason: signal 4411
- Same key and same body: you get the stored result. Nothing is sent to the broker again.
- Same key and a different body:
409 idempotency_conflict. You probably reused a key by mistake. - On the
/users/current/…/tradeendpoint,clientIdin the body works as the key if you don’t send the header. - The
/users/current/…provisioning routes take an optionalIdempotency-Key(existing clients don’t send one, and nothing changes for them). This covers account create, update and delete,deploy,undeployandredeploy,credentials,increase-reliabilityand theenable-*routes, plus provisioning profile create, update, delete and file upload. The key belongs to the credential that sent it, as for the secret-minting calls below. A replay of aDELETEreturns the stored204, not a404. A4xxanswer (unknown account, validation error) stores nothing, so you can fix the request and reuse the key. Broker and manager passwords are never stored: the key is bound to a hash of them, so a retry with a different password is a409. - Account creation also accepts the
transaction-idheader, with its usual behaviour: the same id and body return the first answer (now also withIdempotent-Replayed: true), a different body is409 ConflictError, and a retry while the first call is still running gets202 AcceptedErrorwithRetry-After.transaction-idandIdempotency-Keyshare one store, so the same value sent under either header is the same request. You can send both headers if they hold the same value. Two different values are a400 ValidationError. - Calls that create or rotate a secret (
POST /v1/agent-keys,POST /v1/pretrade-hooksand itsrotate-secret,POST /users/current/tokens,POST /users/current/narrow-down-auth-token,POST /v1/readonly-keysand itsrotate,POST /v1/sandbox/keys, and in the dashboardPOST /dashboard/api-keysand itsrotate) never show the secret again. A replay has the same id, with the secret field (key,secret,tokenorapiKey) set tonulland a notice that it was already shown. A replayed rotate rotates nothing: the value from the first answer stays the working one. If you lost it, revoke (or rotate) and create a new one.narrow-down-auth-tokenanswers{token, id}, whereidis the token record you revoke withDELETE /users/current/tokens/{id}.- For these calls the protection is short-lived: a retry that arrives after one of our restarts
or deploys may create a second credential. After retrying one of these calls, list the
credentials (for example
GET /v1/agent-keys) and revoke any duplicate. - The key belongs to the credential that sent it. Another API key, token or user of the same
organisation that sends the same
Idempotency-Keygets its own result, not yours. - If the credential was created but the call then failed, the answer is
500withoutcome: unknownandcreated_id, the id of the credential that now exists (never its secret). Revoke it (or rotate it) by that id, then send a new request with a new key.
- For these calls the protection is short-lived: a retry that arrives after one of our restarts
or deploys may create a second credential. After retrying one of these calls, list the
credentials (for example
- A duplicate that arrives while the first call is still running gets
409 idempotency_in_flightwithRetry-After, and does nothing.
Make keys deterministic from your own ids (signal id plus account id, say), so a crashed worker that restarts produces the same key.
A key is 1 to 255 printable characters. A longer key, or one with control characters, tabs or
bidi marks, is 400 invalid_idempotency_key (keys are never truncated, so two long keys can’t
collide). On the /v1 API and the account-management routes a stored result is kept for 24 hours; after that the key
can be used for a new request. Each credential holds at most 10,000 stored results: past that, its
least recently used results are dropped first (a call still running is never dropped).
A stored result comes back with the response header Idempotent-Replayed: true. The first call
never carries it, so you can tell a replay from a fresh execution without comparing bodies. The
header is on every surface that honours Idempotency-Key: the /users/current/… and
terminal routes, the native /v1 API, the agent API and guarded execution.
Outcomes
Order responses carry the broker’s code (numericCode, stringCode) and our outcome:
outcome | Meaning |
|---|---|
applied | It happened. The broker confirmed it, or we found the result in the terminal. |
no_change | Nothing needed doing, e.g. closing a position that’s already closed. Counts as success. |
partial | Part of the volume was filled. |
rejected | The broker refused it. Nothing happened. |
unknown | We can’t confirm either way. See below. |
is_effective on the /v1 and terminal routes tells you whether the call changed anything, so a
repeated close is distinguishable from the first one.
When the answer is “unknown”
Sometimes the broker link drops after the request left the terminal. MT5 reports this as 10031
(TRADE_RETCODE_CONNECTION) and, depending on the moment, 10011, 10012, 10028 or 10039. MT4’s
equivalents are 128 (trade timeout) and 146 (trade context busy). The order may have executed.
Tickwire then checks the terminal’s positions, orders and deals for the request. If it finds the
result, the outcome is applied. If it can confirm nothing happened, rejected. If it
can’t tell yet, unknown.
When you get unknown:
- Don’t blindly retry with a new key. That’s how double positions happen.
- Retry with the same key after a few seconds. We re-check before doing anything.
- Or reconcile yourself from
positionsandhistory-deals, matching on yourclientId.
The audit log
Every mutating call is recorded: time, X-Actor, X-Reason, the route, the idempotency key and
the outcome. On the trading and terminal routes (/users/current/accounts/{id}/trade,
/mt4/{id}/…, /mt5/{id}/…), a call without X-Actor is recorded with actor unknown. The
request log still shows which key sent it (keyId).
curl -s "https://api.tickwire.io/users/current/accounts/acc_7Qx/audit?limit=50" -H "auth-token: $TW_KEY"
Account and profile changes (account.created, account.deployed, provisioning_profile.updated
and so on) and narrowed tokens (token.narrowed) go to your organisation’s audit log, which the
dashboard shows under Audit. Each entry has the actor (X-Actor, or api-key:<id> of the
credential that called when you don’t send one), the calling credential’s key_id (always, even
with an X-Actor), the reason (X-Reason, percent-decoded, at most 500 characters) and the
idempotency key. Control characters (including line breaks) and bidi marks are removed from
these values. Passwords and token values are never written there. A replay is not logged again.
X-Actor is a free label, but it can’t claim an identity the server assigns: a value starting
with api-key:, user: or firebase: that isn’t the calling credential’s own is
400 invalid_actor, and nothing is changed.