Skip to main content

1. Authentication and security

VND Merchant API v2 creates deposits (PayIn) and withdrawals (PayOut), and retrieves an individual transaction status. The API base path is /api/v2. This guide uses VND and an active VND shop in its examples; use your assigned shop code as method. Use HTTPS and JSON. Ask your integration contact to enable v2 for your API key, provide its signing secret, configure the allowed source IPs, and confirm your active shop codes and their fields schemas.

Before you integrate​

Confirm with your integration contact that the revised v2 contract is available in your environment and enabled for each intended shop, currency, and payment direction. Agree on the required customer fields, available payer-account data, callback address, and PayIn return-page configuration before live payments.

Required headers​

HeaderWhenValue
X-Api-KeyEvery requestYour enabled merchant API key. Header names are case-insensitive.
X-SignatureEvery requestLowercase hexadecimal HMAC-SHA256 of the raw request body, using the API key's signing secret.
Content-TypePOST requestsapplication/json.

For GET requests, sign the empty string. The URL, path, query, timestamp, and HTTP method are not part of the v2 signature. v2 does not require X-Timestamp or X-Request-Id. Keep the exact bytes used to calculate the signature: reformatting the JSON after signing changes the signature.

X-Signature = hex(HMAC-SHA256(api_secret, raw_body))
GET raw_body = ""

Example signature helper in Node.js (pass the complete request body from a create guide as rawBody):

import { createHmac } from 'node:crypto';

function v2Signature(rawBody, apiSecret) {
return createHmac('sha256', apiSecret).update(rawBody, 'utf8').digest('hex');
}

// Sign and send the same raw JSON bytes for POST; sign '' for GET.

For a GET request, use v2Signature('', apiSecret) and send no body.

For POST, send a JSON object in the body and no query string. Query parameters are rejected with HTTP 400, including parameters that repeat signed body fields. A JSON array, scalar, or malformed JSON is also rejected. Send your signing secret only to your own signing implementation; never include it in the request or a browser URL.

Version boundaries​

v2 uses order_id, method, fields, and a JSON number for amount. The v1 names external_id, shop_code, and paymentData, its decimal-string amount and timestamp-based signature are different contracts. Unknown top-level create fields are rejected.

method is the exact code of one of your active shops. If omitted, your default active shop is used. fields is a JSON object validated against the selected shop's configured PayIn or PayOut schema. See the shop fields contract and confirm the schema for your shop code and direction before sending a request.

Payment flow​

  1. Create a deposit or withdrawal with a stable order_id and store the returned transaction_id.
  2. For a deposit, send the customer to the complete redirect_url. The page may initially show payment preparation. A withdrawal has no customer redirect.
  3. Use the status endpoint to read the current result. HTTP 200 from create means that the operation exists, not that money has been received or sent.
  4. When a callback arrives, verify its raw-body signature, durably accept the notification, respond with HTTP 200, and retrieve the current transaction status.

v1 and v2 use the same payment processing rules, balances, and configured payment routes. Their authentication and wire formats remain separate. Keep existing v1 operations on v1; a v2 status request cannot retrieve them.

Available operations​

ActionEndpointGuide
Create withdrawalPOST /api/v2/withdrawalsWithdrawals
Create depositPOST /api/v2/depositsDeposits
Get either transaction's statusGET /api/v2/transactions/{transaction_id}Transaction status

See errors, statuses, and callbacks. Only transactions created through v2 can be retrieved by the v2 status endpoint.

Retrying a create request​

If a create response is lost, retry the same endpoint with the same order_id and original request data, signing each request's exact bytes. An accepted repeat resolves to the same operation and transaction_id; it does not create another operation. Do not change method, amount, recipient, URLs, or fields on a retry. A conflict returns HTTP 409.

Idempotency is scoped to your merchant and operation type across shops. Switching shops or API versions is not a way to retry the same payment. Do not use a new ID merely because a response, callback, or payment page is delayed. Check status, or contact your integration contact if the outcome remains uncertain.

Because callbacks contain only order_id, prefer IDs unique across both deposits and withdrawals when using a shared callback endpoint. If you reuse an ID across directions, use separate callback URLs or another unambiguous server-side order mapping.