Recovery and Idempotency
What this page solves
Recovering safely when the outcome of a transfer creation is uncertain, without sending the money twice.
When to use it
Timeout or lost connection
The creation request timed out, or the connection was lost before you received a response.
TRANSFER_OUTCOME_UNKNOWN
The creation response is
TRANSFER_OUTCOME_UNKNOWN(HTTP 500, code 7008).Temporary or internal error
The creation response is
SYSTEM_TEMPORARY_UNAVAILABLEorSYSTEM_INTERNAL_ERROR.Non-API response
An HTML page, a JSON body from a proxy or load balancer, an unexpected success status, or a
201without a readabletransfer_id.System stopped
Your system stopped while a creation request was in flight.
Failed retry
A retry after any of these cases received an error.
Before you start
Key and request
The idempotency key and the exact request of the uncertain attempt. Generate the key and store it together with the request before the first request.
Saved record
A record in your database with the key, request, time of every attempt, and one of the states below. Keep it across restarts, and process each record with one sender at a time, so that two requests with the same key are never in flight at once.
Never retry a creation request with a new idempotency key before confirming the outcome of the previous attempt. A new key can send the money twice.
Record states
These states belong to the record in your integration. They are separate from the transfer statuses returned by the API: processing, done, and rejected.
| State | Meaning |
|---|---|
new | No request with the key may have created a transfer yet. |
reserved | A request with the key may have created a transfer. Only the same request with the same key may be sent. |
created | A 201 returned a readable transfer_id. Store it to check the transfer status. |
closed | The key is retired: it is never sent again — not after a restart, and not by a pending resend. |
Recommended flow
- 1Before every request, change the record to
reservedand save the attempt time in the same database transaction. Wait for the transaction to commit before sending. Note whether the record was alreadyreserved. If saving fails, do not send the request. - 2Send the exact same request with the same idempotency key. The timestamp and the signature are generated fresh for every attempt.
- 3Handle the response as listed in What to expect in return.
- 4If you stop sending for any reason other than a
201, retire its key first: change the record toclosed. Only then check Transaction History and, if needed, contact Customer Support. - 5After a
201, continue with the normal flow: track the transfer until it isdoneorrejected.
A request in flight has an unknown outcome until you record its answer, so after a restart a reserved record still means that a transfer may exist. The transfer may also still be created or completed after you received the response. Idempotency keys do not expire, so a retry with the same key never executes the transfer twice, at any time.
TRANSFER_OUTCOME_UNKNOWN and lost responses do not return a transfer_id, so there is nothing to look up with GET /v1/transfers/{transfer_id}. Resend the same request, or check Transaction History.
Main endpoints
| Operation | Purpose |
|---|---|
POST /v1/transfers | Resend the same request with the same key |
GET /v1/transfers/{transfer_id} | Track the transfer after the retry returns 201 |
Example request
The retry sends the same EXN-IDEMPOTENCY-KEY and the same request fields as the original attempt. The timestamp and the signature are generated fresh for every attempt.
POST /v1/transfers HTTP/1.1
EXN-API-KEY: <your API key>
EXN-IDEMPOTENCY-KEY: 0f8fad5b-d9cb-469f-a165-70867728950e
EXN-TIMESTAMP: <current Unix time in milliseconds>
EXN-SIGN-VERSION: 1
EXN-DATA: <base64url-encoded signed payload>
EXN-SIGN: <base64url-encoded signature>
Content-Type: application/json
{"destination_type":"own_account","source_account":"8066808","destination_account":"9484938209348","amount":"100.00"}
What to expect in return
| Response | The record was new before the request | The record was already reserved |
|---|---|---|
201 with a transfer_id | The transfer was created. Change the record to created. | The transfer exists — created by an earlier attempt or by this retry, and executed only once. Change the record to created. |
TRANSFER_OUTCOME_UNKNOWN, SYSTEM_INTERNAL_ERROR, SYSTEM_TEMPORARY_UNAVAILABLE, a non-API response, an unexpected success status, no response, or a 201 without a readable transfer_id | The outcome is unknown; the record stays reserved. Resend the same request with the same key, with exponential backoff, within your retry budget. | The same. |
AUTH_INVALID_API_KEY, AUTH_INVALID_SIGNATURE, AUTH_PERMISSION_DENIED, or REQUEST_RATE_LIMIT | No transfer was created; change the record back to new. Fix the cause — the API key, the signature, the scopes or allowed IPs of the key, or the request rate — and resend the same request with the same key. | The outcome is still unknown; the record stays reserved. Fix the cause and resend the same request with the same key. |
REQUEST_IDEMPOTENCY_KEY_REUSED | The key is already bound to another transfer; this request created none. Close the record and send this transfer with a new record and a new key. | The retry differs from the original request. Keep the record reserved, recover the original request from your saved record, and resend it unchanged with the same key. If you cannot recover it, close the record and check Transaction History. |
| Any other error response of this API | This request created no transfer. Close the record, fix the request or the account state, and send it as a new transfer with a new record and a new key. | The outcome is still unknown. The source account, funds, limits, and eligibility are checked before the idempotency key, so the error does not show whether a transfer exists: for example, TRANSFER_INSUFFICIENT_FUNDS can mean that the first attempt already used the funds. Do not change the request or the key. Close the record, then check Transaction History. |
If no 201 arrives within your retry budget, you can keep resending the same request with the same key later. If you stop instead, close the record first, then check Transaction History.
Checking Transaction History, in the Payments & wallet section of your Personal Area, after you closed the record:
- A created transfer appears there shortly after it is created, including while it is
processing. If it is not there, wait a few minutes after your last attempt and check again. - Transaction History does not show the idempotency key. Recognize the transfer by its source and destination accounts, amount, and time. The time shown can be later than the time of your attempt.
- If the transfer is there, it was created. Do not send it again.
- If it is not there, or several transfers match, contact Customer Support with the source and destination accounts, the amount, and the time of every attempt. A transfer missing from Transaction History does not prove that none will be created: an earlier attempt may still be completing. Customer Support's confirmation that no transfer was created is final: after it, the earlier attempts can no longer create a transfer. Only then send the transfer with a new record and a new key.
How idempotency works:
- Retrying the identical request with the same key never executes the transfer twice. The retry goes through the checks of the source account, funds, limits, and eligibility before the key is looked up. If it passes them, it returns
201with the sametransfer_id, whatever the current status. If it fails them, it returns that error, even though the transfer exists. The amounts in that response are calculated for the retry and can differ from the amounts of the transfer, for example after an exchange rate change; the transfer itself does not change. Its amounts are shown in Transaction History. - An identical request has the same source account, destination account, and amount; amounts are compared as numbers, so
100and100.00are the same. Reusing a key with a different source account, destination account, or amount is rejected withREQUEST_IDEMPOTENCY_KEY_REUSED. - Keys are unique per Exness client: a key is bound to your transfer regardless of which of your API keys sent the request, so a retry after an API key rotation is safe. Keys do not expire.
- An error response of this API is a JSON body whose
codeanderror_messageare a pair from Payments API error codes. Any other response — an HTML page, or a JSON body from a proxy or load balancer — is a non-API response and counts as no response. - A key bound to a
rejectedtransfer returns that transfer. To try again, send a new request with a new key. - Key format: a UUID in its canonical text form, for example
0f8fad5b-d9cb-469f-a165-70867728950e. Generate a new random UUID for every new transfer. A missing key or a key that is not a valid UUID is rejected withREQUEST_INVALID_PARAMETERS.
Common errors
| HTTP | code | error_message | Retry |
|---|---|---|---|
| 500 | 7008 | TRANSFER_OUTCOME_UNKNOWN | Only with the same idempotency key and the exact same request |
| 503 | 2 | SYSTEM_TEMPORARY_UNAVAILABLE | Yes — exponential backoff, same key, same request |
| 500 | 3 | SYSTEM_INTERNAL_ERROR | Yes — exponential backoff, same key, same request; contact support if it repeats |
| 429 | 3015 | REQUEST_RATE_LIMIT | After a wait, with the same key and the same request |
| 409 | 3016 | REQUEST_IDEMPOTENCY_KEY_REUSED | Depends on the record state; see What to expect in return |
| 422 | 7007 | TRANSFER_LIMIT_REACHED | Later, as a new transfer with a new key; changing the amount does not help. On a retry after an unknown outcome, see What to expect in return |
Recovery and reconciliation
- Manual verification. You can always review your transfers in Transaction History in the Payments & wallet section of your Personal Area — for example, during an incident or reconciliation.
- Wrong destination. A created transfer cannot be cancelled or reversed through the API. If a transfer was sent to a wrong account, contact Customer Support.
- Checklist for your retry code:
- one idempotency key and one durable record per transfer, stored before the first request;
- before every request, save the
reservedstate and attempt time in the same database transaction; nothing is sent if saving fails; - one sender at a time per record;
- retries reuse the stored key and the identical request;
- if you stop sending for any reason other than a
201, the key is retired first, and no restart or pending resend sends it again; - a new key is used only for a new transfer, or after Customer Support confirms that no transfer was created.
Related pages
Check a transfer status · Core Concepts · Code Examples · Troubleshooting Your API Requests