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 throughGET /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-DATAcontains a base64url-encoded JSON payload with the fields that are signed.EXN-SIGNcontains the Ed25519 signature of the decodedEXN-DATApayload.EXN-DATAandEXN-SIGNmust use base64url without=padding.EXN-SIGN-VERSIONmust be1.- The signed payload must contain:
api_key: same value asEXN-API-KEYidempotency_key: same value asEXN-IDEMPOTENCY-KEY; empty string for signedGETrequeststimestamp: Unix time in milliseconds, same value asEXN-TIMESTAMPsign_version: same value asEXN-SIGN-VERSIONmethod: uppercase HTTP method, for examplePOSTpath: request path with query string exactly as sent by the clientbody_hash: SHA-256 hash of the request body, base64url-encoded without padding
EXN-SIGNsigns the decodedEXN-DATApayload bytes, not the base64url string.- For requests without a body,
body_hashis 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 asACCOUNT_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 statusPartner 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 asTRANSFER_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
codeanderror_messageas 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_typefield:own_accountfor your own account,another_clientfor 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 withREQUEST_INVALID_PARAMETERS destination_typemust match the destination account. If it does not — for example,own_accountis sent with an account that is not yours — the API responds withACCOUNT_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 transfersscope, and are always sent from the Partner Reward Wallet; any other source account is rejected withTRANSFER_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 withTRANSFER_NOT_SUPPORTEDorTRANSFER_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
GETrequests,EXN-IDEMPOTENCY-KEYmust be present with an empty value, and the signedidempotency_keyfield 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
201with the originaltransfer_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_FUNDSafter 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
100and100.00are the same. Reusing a key with a different source account, destination account, or amount is rejected withREQUEST_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
codeanderror_messageare 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_REUSEDmeans 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_idand the transfer amounts) or an error - a created transfer can still be in the
processingstatus; its final status (doneorrejected) is available throughGET /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;
codeanderror_messageare the stable machine-readable contract, anddescriptionis 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_UNAVAILABLEandSYSTEM_INTERNAL_ERRORmay be retried with exponential backoff, reusing the same idempotency key for the same requestTRANSFER_OUTCOME_UNKNOWN: resend the exact same request with the same idempotency key (see Recovery after an unknown outcome)TRANSFER_LIMIT_REACHEDmay be retried later; changing the amount does not helpREQUEST_RATE_LIMITis 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
201response 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, orREQUEST_RATE_LIMITon 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 isprocessing. 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_UNKNOWNand lost responses do not return atransfer_id, soGET /v1/transfers/{transfer_id}cannot be used to recover them; it needs thetransfer_idfrom a201responseTRANSFER_NOT_FOUNDmeans 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
- API Key: ExnApiKeyAuth
- API Key: ExnTimestampAuth
- API Key: ExnDataAuth
- API Key: ExnSignatureAuth
- API Key: ExnSignVersionAuth
- API Key: ExnIdempotencyKeyAuth
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 |
Unix timestamp in milliseconds. The value must match the timestamp
field in the decoded EXN-DATA payload.
Security Scheme Type: | apiKey |
|---|---|
Header parameter name: | EXN-TIMESTAMP |
Base64url-encoded JSON payload without '=' padding. The decoded payload
must conform to SignedRequestData and is the payload signed by
EXN-SIGN.
Security Scheme Type: | apiKey |
|---|---|
Header parameter name: | EXN-DATA |
Ed25519 signature of the decoded EXN-DATA payload bytes, encoded as base64url without '=' padding. The signature is not calculated over the base64url-encoded EXN-DATA string.
Security Scheme Type: | apiKey |
|---|---|
Header parameter name: | EXN-SIGN |
Signature protocol version. The only supported value is 1, and it
must match the sign_version field in the decoded EXN-DATA payload.
Security Scheme Type: | apiKey |
|---|---|
Header parameter name: | EXN-SIGN-VERSION |
Idempotency key. For POST requests it is required
and must be a UUID; for signed GET requests it is required with an empty
value. The value must match idempotency_key in EXN-DATA. Keys are
unique per client and do not expire; see Idempotency in the API
description.
Security Scheme Type: | apiKey |
|---|---|
Header parameter name: | EXN-IDEMPOTENCY-KEY |