Skip to main content

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_UNAVAILABLE or SYSTEM_INTERNAL_ERROR.

  • Non-API response

    An HTML page, a JSON body from a proxy or load balancer, an unexpected success status, or a 201 without a readable transfer_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.

The rule that protects your money

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.

StateMeaning
newNo request with the key may have created a transfer yet.
reservedA request with the key may have created a transfer. Only the same request with the same key may be sent.
createdA 201 returned a readable transfer_id. Store it to check the transfer status.
closedThe key is retired: it is never sent again — not after a restart, and not by a pending resend.
  1. 1
    Before every request, change the record to reserved and save the attempt time in the same database transaction. Wait for the transaction to commit before sending. Note whether the record was already reserved. If saving fails, do not send the request.
  2. 2
    Send the exact same request with the same idempotency key. The timestamp and the signature are generated fresh for every attempt.
  3. 3
    Handle the response as listed in What to expect in return.
  4. 4
    If you stop sending for any reason other than a 201, retire its key first: change the record to closed. Only then check Transaction History and, if needed, contact Customer Support.
  5. 5
    After a 201, continue with the normal flow: track the transfer until it is done or rejected.

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​

OperationPurpose
POST /v1/transfersResend 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​

ResponseThe record was new before the requestThe record was already reserved
201 with a transfer_idThe 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_idThe 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_LIMITNo 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_REUSEDThe 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 APIThis 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 201 with the same transfer_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 100 and 100.00 are the same. Reusing a key with a different source account, destination account, or amount is rejected with REQUEST_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 code and error_message are 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 rejected transfer 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 with REQUEST_INVALID_PARAMETERS.

Common errors​

HTTPcodeerror_messageRetry
5007008TRANSFER_OUTCOME_UNKNOWNOnly with the same idempotency key and the exact same request
5032SYSTEM_TEMPORARY_UNAVAILABLEYes — exponential backoff, same key, same request
5003SYSTEM_INTERNAL_ERRORYes — exponential backoff, same key, same request; contact support if it repeats
4293015REQUEST_RATE_LIMITAfter a wait, with the same key and the same request
4093016REQUEST_IDEMPOTENCY_KEY_REUSEDDepends on the record state; see What to expect in return
4227007TRANSFER_LIMIT_REACHEDLater, 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 reserved state 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.

Check a transfer status · Core Concepts · Code Examples · Troubleshooting Your API Requests