Skip to main content
Version: 1.0.0

Exness Payments API

Core concepts:

  • Transfer = movement of funds between Exness accounts: from a client's account either to another account of the same client (destination_type: own_account) or to an account of another client (destination_type: another_client)
  • Every created transfer is identified by transfer_id; its status is available through GET /v1/transfers/{transfer_id} to its sender and its recipient — to any of their API keys whose scopes cover the transfer type, not only to the key that created it
  • Account balance and transfer status are available through read operations
  • Transfers cannot be cancelled or reversed through the API. Check the destination account before sending. If a transfer was sent to a wrong account, contact Customer Support

General API rules:

Authorization:

  • The Exness Payments API authenticates requests by request signing with a public API key.
  • Signed requests use the EXN-* headers documented in the security schemes.
  • EXN-DATA contains a base64url-encoded JSON payload with the fields that are signed.
  • EXN-SIGN contains the Ed25519 signature of the decoded EXN-DATA payload.
  • EXN-DATA and EXN-SIGN must use base64url without = padding.
  • EXN-SIGN-VERSION must be 1.
  • The signed payload must contain:
    • api_key: same value as EXN-API-KEY
    • idempotency_key: same value as EXN-IDEMPOTENCY-KEY; empty string for signed GET requests
    • timestamp: Unix time in milliseconds, same value as EXN-TIMESTAMP
    • sign_version: same value as EXN-SIGN-VERSION
    • method: uppercase HTTP method, for example POST
    • path: request path with query string exactly as sent by the client
    • body_hash: SHA-256 hash of the request body, base64url-encoded without padding
  • EXN-SIGN signs the decoded EXN-DATA payload bytes, not the base64url string.
  • For requests without a body, body_hash is the SHA-256 hash of the empty string, base64url-encoded without padding.
  • Query strings must be signed exactly as transmitted. Clients must not reorder or re-encode query parameters after signing.
  • Duplicate query parameters are rejected.
  • The request timestamp must be within the accepted time window around the server time; the window size is provided with your API credentials. Clients should generate a fresh signature for every request.
  • The API key must be allowed to access the requested account, source IP, and operation scope. A request outside the scopes of your API key, or from a source IP that is not allowed for it, is rejected with AUTH_PERMISSION_DENIED. An account that is not available to your API key is always reported as ACCOUNT_NOT_FOUND, the same as an account that does not exist.
  • API key scopes for this API:
    • Internal transfers: transfers between your own accounts (destination_type: own_account), account balance, and transfer status
    • Partner transfers: transfers from your Partner Reward Wallet to another Exness client (destination_type: another_client), account balance, and transfer status
  • A transfer creation request that is not covered by the scopes of your API key is rejected with AUTH_PERMISSION_DENIED. A transfer whose type is not covered by the scopes of your API key is reported as TRANSFER_NOT_FOUND.
  • Authentication headers are modeled as OpenAPI security schemes, not as ordinary operation parameters.
  • Signed-request errors (AUTH_INVALID_API_KEY, AUTH_INVALID_SIGNATURE) are common to all Exness APIs; the cases that cause them are described in the Request signing section of the Exness API documentation.
  • Authentication errors use the public error model. Clients should use code and error_message as the stable contract and must not rely on human-readable text.

Signed POST example (the request body, sent exactly as these bytes):

POST /v1/transfers HTTP/1.1
EXN-API-KEY: your-api-key
EXN-IDEMPOTENCY-KEY: 0f8fad5b-d9cb-469f-a165-70867728950e
EXN-TIMESTAMP: 1787576070123
EXN-SIGN-VERSION: 1
EXN-DATA: <base64url_encoded_payload>
EXN-SIGN: <base64url_encoded_signature>
Content-Type: application/json

{"destination_type":"own_account","source_account":"8066808","destination_account":"9484938209348","amount":"100.00"}

Decoded EXN-DATA payload for the signed POST:

{
"api_key": "your-api-key",
"idempotency_key": "0f8fad5b-d9cb-469f-a165-70867728950e",
"timestamp": 1787576070123,
"sign_version": 1,
"method": "POST",
"path": "/v1/transfers",
"body_hash": "1LVQQ9_kd766kYfgSbEEoHrcOkx9Kcl08gT0AYMMolg"
}

body_hash is the SHA-256 hash of the exact request body bytes as sent (the minified JSON body shown above), base64url-encoded without padding. For requests without a body it is the hash of the empty string.

Signed GET example:

GET /v1/accounts/8066808/balance HTTP/1.1
EXN-API-KEY: your-api-key
EXN-IDEMPOTENCY-KEY:
EXN-TIMESTAMP: 1787576070123
EXN-SIGN-VERSION: 1
EXN-DATA: <base64url_encoded_payload>
EXN-SIGN: <base64url_encoded_signature>

Decoded EXN-DATA payload for the signed GET:

{
"api_key": "your-api-key",
"idempotency_key": "",
"timestamp": 1787576070123,
"sign_version": 1,
"method": "GET",
"path": "/v1/accounts/8066808/balance",
"body_hash": "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"
}

Transfer destination:

  • every transfer states whose account the money goes to, using the required destination_type field: own_account for your own account, another_client for an account of another Exness client
  • for own_account, the source and destination must be different accounts; the same account in both fields is rejected with REQUEST_INVALID_PARAMETERS
  • destination_type must match the destination account. If it does not — for example, own_account is sent with an account that is not yours — the API responds with ACCOUNT_NOT_FOUND, the same answer as for an account that does not exist
  • transfers to another client are available to partners whose API key has the Partner transfers scope, and are always sent from the Partner Reward Wallet; any other source account is rejected with TRANSFER_SOURCE_NOT_ALLOWED
  • the destination can be a trading account on the Exness platform or a wallet; MT4/MT5 accounts are not supported as a source or a destination and are rejected with TRANSFER_MT_ACCOUNT_NOT_SUPPORTED; some account combinations are not supported and are rejected with TRANSFER_NOT_SUPPORTED or TRANSFER_DESTINATION_NOT_SUPPORTED
  • a transfer to another client can be declined by eligibility checks with OPERATION_NOT_ALLOWED

Monetary values and numeric precision:

  • amounts are represented as positive fixed-point decimal strings, for example "100.00"
  • amounts are expressed in the currency of the source account
  • excessive decimal precision is rejected, not rounded
  • when the source and destination accounts use different currencies, the amount is converted at execution; the creation response returns the amount of the transfer in the source account currency (source) and in the destination account currency (destination)
  • account balances are returned as non-negative decimal strings in the account currency; the balance is the amount available for transfers from the account

Idempotency:

  • every request that creates a transfer requires an idempotency key
  • signed requests use EXN-IDEMPOTENCY-KEY
  • for signed GET requests, EXN-IDEMPOTENCY-KEY must be present with an empty value, and the signed idempotency_key field must be an empty string
  • clients should use the same idempotency key when safely retrying the same request
  • retrying with the same key and an identical request never executes the transfer twice. The request goes through the checks of the source account, funds, limits, and eligibility before the key is looked up. If it passes them, the API returns 201 with the original transfer_id, whatever the current status of the transfer. 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. If it fails them, the API returns that error even though the transfer exists: for example, TRANSFER_INSUFFICIENT_FUNDS after the first transfer has used the funds
  • 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
  • idempotency keys are unique per client: a key is bound to your transfer regardless of which of your API keys sent the request
  • idempotency keys do not expire: a key used to create a transfer stays bound to that transfer, so retrying the identical request with the same key never executes it twice, at any time
  • an error response of this API is a JSON body whose code and error_message are a pair from the public error codes of this API. Any other response — for example an HTML page, or a JSON body from a proxy or load balancer — is a non-API response and counts as no response
  • before you send a request with the key, record durably, together with the key, the request, and the time of the attempt, that the request is being sent. If the record cannot be written, do not send the request. Until you record the answer, the outcome of the request is unknown, even if your system stops while the request is in flight. Send the requests of one transfer from one sender at a time
  • after TRANSFER_OUTCOME_UNKNOWN, SYSTEM_INTERNAL_ERROR, SYSTEM_TEMPORARY_UNAVAILABLE, a non-API response, or no response, the outcome is unknown, and the key stays reserved for this transfer in every later attempt, including after a restart of your system (see Recovery after an unknown outcome)
  • while no request sent with the key has had an unknown outcome, any other error response of this API means that this request created no transfer. Except after REQUEST_IDEMPOTENCY_KEY_REUSED, the key is not bound and can be reused, for example after you fix the request
  • if you stop sending a transfer for any reason other than a 201, retire its key first: record durably that the key is never sent again, so that no restart or pending resend sends it
  • REQUEST_IDEMPOTENCY_KEY_REUSED means that the key is already bound to a transfer created by a different request
  • to try again after a transfer is rejected, send a new request with a new idempotency key; the old key stays bound to the rejected transfer
  • the idempotency key must be a UUID in its canonical text form, for example 0f8fad5b-d9cb-469f-a165-70867728950e; generate a new random UUID for every new transfer
  • a creation request without an idempotency key, or with a key that is not a valid UUID, is rejected with REQUEST_INVALID_PARAMETERS

Transfer processing:

  • the creation request is processed synchronously: the response contains either the created transfer (transfer_id and the transfer amounts) or an error
  • a created transfer can still be in the processing status; its final status (done or rejected) is available through GET /v1/transfers/{transfer_id}
  • transfers usually complete within a few seconds; the processing time is not guaranteed and can vary with load
  • the API returns the transfer status only; for the details of a transfer that is not done, see Transaction History in the Payments & wallet section of your Personal Area

Error handling:

  • each error response contains exactly one error; code and error_message are the stable machine-readable contract, and description is a short human-readable hint that may change without notice
  • public errors are non-retryable by default; do not retry automatically unless the documentation for the specific code says otherwise
  • SYSTEM_TEMPORARY_UNAVAILABLE and SYSTEM_INTERNAL_ERROR may be retried with exponential backoff, reusing the same idempotency key for the same request
  • TRANSFER_OUTCOME_UNKNOWN: resend the exact same request with the same idempotency key (see Recovery after an unknown outcome)
  • TRANSFER_LIMIT_REACHED may be retried later; changing the amount does not help
  • REQUEST_RATE_LIMIT is returned for Public API endpoint quota limits; wait before sending another request

Recovery after an unknown outcome:

  • the outcome is unknown after TRANSFER_OUTCOME_UNKNOWN, SYSTEM_INTERNAL_ERROR, SYSTEM_TEMPORARY_UNAVAILABLE, a non-API response, or no response at all (timeout, connection loss). The transfer may exist, and it may still be created or completed after you received the response
  • resend the exact same request with the same idempotency key, with exponential backoff, within your retry budget. A 201 response resolves the outcome: it returns the transfer created by an earlier attempt without executing it again, or the transfer created by this retry
  • after AUTH_INVALID_API_KEY, AUTH_INVALID_SIGNATURE, AUTH_PERMISSION_DENIED, or REQUEST_RATE_LIMIT on a retry, fix the cause — the API key, the signature, the scopes or allowed IPs of your API key, or the request rate — and resend the same request with the same key
  • any other error on a retry does not show whether a transfer exists, because the checks of the source account, funds, limits, and eligibility run before the key is looked up. Do not change the request or the key to get past the error
  • when you stop resending without a 201 — after such an error, or when your retry budget is spent — retire the key first: record durably that it is never sent again, so that no restart or pending resend sends it. Only then check Transaction History in the Payments & wallet section of your Personal Area. 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 as a new request with a new key
  • TRANSFER_OUTCOME_UNKNOWN and lost responses do not return a transfer_id, so GET /v1/transfers/{transfer_id} cannot be used to recover them; it needs the transfer_id from a 201 response
  • TRANSFER_NOT_FOUND means the transfer is not visible to your API key: it does not exist, you are neither its sender nor its recipient, or its type is not covered by the scopes of your API key. It does not prove that no money was moved
  • never send the transfer again with a new idempotency key while the outcome is unknown; doing so can send the money twice

Authentication​

Public API key identifier used by signed requests. The value must match the api_key field in the decoded EXN-DATA payload.

Security Scheme Type:

apiKey

Header parameter name:

EXN-API-KEY