Skip to main content

Response format

The envelope

Every response under /api/v2 is wrapped in the same five fields, whether it succeeded or failed:

JSON
{
"success": true,
"message": "Your request has been successful",
"data": { "balance": 1500.25, "currency": "USDT", "signature": "K3tQ...==" },
"errorCode": 0,
"semanticCode": "SUCCESS"
}
FieldMeaning
successtrue on success, false on any error.
messageHuman-readable English. Always English — the lang header is ignored.
dataThe payload on success, and null on every error.
semanticCodeA string, and the field to branch on. SUCCESS, or one of the error codes.
errorCodeA number. Legacy, kept for backward compatibility.

On failure:

JSON
{
"success": false,
"message": "The transaction id already exists.",
"data": null,
"errorCode": 11,
"semanticCode": "DUPLICATE_TRANSACTION_ID"
}

The HTTP status is not the error

A business error comes back as HTTP 201 Created. Errors are returned as values rather than thrown, so they leave through the same path as a success, and for a POST that path ends in 201. An order rejected for insufficient balance, a duplicate order id, a locked account, a bad signature: all of them arrive as HTTP 201 with success: false.

SituationHTTP status
Success201 (200 on the one GET)
Any business error201 — read semanticCode, not the status
Request validation failed200, with errorCode 5 / INVALID_REQUEST_DATA
errorCode 38 (LOGIN_SESSION_EXPIRED)401 — the only status that means anything

So do not branch on the HTTP status in v2. Check success, then switch on semanticCode.

JavaScript
const {success, semanticCode, message, data} = await res.json(); // res.ok is meaningless here

if (success) return data;

switch (semanticCode) {
case 'DUPLICATE_TRANSACTION_ID':
return getOrder(externalOrderId); // your retry already worked
case 'PARTNER_INSUFFICIENT_BALANCE':
return topUpFundAndRetry();
default:
throw new Error(`${semanticCode}: ${message}`);
}

(v3 fixed this: its HTTP statuses are real. See v3 response format.)

semanticCode is the contract, errorCode is not

Branch on semanticCode. The numeric errorCode is a legacy field, kept so that older integrations keep working, and two things about it are worth knowing.

Fourteen numeric codes have no semanticCode of their own. They all arrive as INTERNAL_SERVER_ERROR, so to tell them apart you have to read errorCode. They are listed under unmapped codes.

message is prose, and it is not stable. On a rejected request it is generated by the server ("partnerCode should not be empty") rather than written for you. Log it, but never match on it.

Amounts and timestamps

Fiat amounts are decimals in the currency's major unit: đồng, not hundredths of a đồng. Crypto amounts are decimals too, and your fund balance is truncated to 2 decimals rather than rounded.

Timestamps look like 2026-07-14 10:32:07. They are UTC+7 and carry no zone marker, so they are neither UTC nor ISO-8601. Parse them as Asia/Ho_Chi_Minh local time.

Unknown fields

Requests are not checked strictly. Extra fields you send are accepted and ignored, and extra fields AliX adds to a response arrive without warning, so nothing you build should depend on a response having exactly the documented keys.

A typo in a field name is not rejected either. The field is simply dropped, and your signature stops matching.