Skip to main content

Environments and sandbox

Payments is the only Ha Tang product with environments. Every business has a development and a production environment, and their payments data is completely separate: bank accounts, payments, transactions, e-invoice providers, invoices and integration keys. API keys and webhook endpoints are shared by both. This page explains how to pick an environment and how to test the whole flow without real money.

What differs​

developmentproduction
BankThe development bank (simulator): confirmation code 000000, transfers simulated with an API callYour real bank account
E-invoicesThe provider's sandbox (Mắt Bão demo); nothing reaches the tax officeReal invoices under your tax code
Simulated transfersPOST /payments/simulate/transfer, plus a button on the checkout page and on the payment page in the dashboardNot available (403 environment_mismatch)
Integration keysak_test_… / sk_test_…ak_… / sk_…
Chosen withHatang-Environment: developmentNo header (the default), or Hatang-Environment: production

Choose the environment per request​

Send the Hatang-Environment header on each request:

HeaderResult
Hatang-Environment: developmentThe request reads and writes development data
Hatang-Environment: production or no headerProduction
Any other value422 validation_error: "Hatang-Environment must be development or production"

Every response repeats the environment it used in its own Hatang-Environment header, and every payments object (payment, transaction, bank connection, invoice) has an environment field.

curl https://api.hatang.dev/payments \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Hatang-Environment: development"
tip

Read the environment from configuration (HATANG_ENVIRONMENT=development on your staging server, nothing in production) rather than hard-coding it, so the same code runs in both.

Older development keys

A key that starts with ht_test_ is tied to development: without the header it uses development, and asking it for production answers 403 environment_mismatch.

An id from one environment is unknown in the other: fetching a development payment with production selected answers 404 not_found.

In the dashboard​

The bar at the top of every Payments page shows the current environment and switches it. Production is shown in red, and switching to it asks for confirmation. Other products have no switch.

The development bank​

Link it with { "provider": "simulator" } and confirm with the code 000000 (see Quickstart). By default its QR codes name MB Bank and its account number is made up (it starts with SIM), so no real bank resolves it.

To see a real VietQR for your own bank, pass bank_code and your account number:

{
"provider": "simulator",
"bank_code": "BIDV",
"account_number": "96247001234",
"account_name": "NGUYEN VAN A"
}

bank_code takes the usual short codes: MB (the default), BIDV, ACB, VCB, ICB, TCB, VPB, TPB, STB and the other VietQR banks. With your real account number, a payer's banking app really does transfer money to you. No bank is integrated in development, though, so Ha Tang does not see that money arrive: the payment only moves when you simulate the transfer or mark it paid.

Simulate a transfer​

# Pay a payment in full
curl https://api.hatang.dev/payments/simulate/transfer \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Hatang-Environment: development" \
-H "Content-Type: application/json" \
-d '{ "payment_id": "pay_123" }'

# A partial transfer
curl https://api.hatang.dev/payments/simulate/transfer \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Hatang-Environment: development" \
-H "Content-Type: application/json" \
-d '{ "payment_id": "pay_123", "amount": 50000 }'
FieldMeaning
payment_idThe payment to pay. Fills in the bank account, the amount still owed and the code
connection_idThe bank account, for a transfer with no payment. Give payment_id or connection_id
amountAnother amount, to test partial payments and overpayments. Defaults to what is still owed
contentThe transfer description as the bank would report it. Defaults to CHUYEN TIEN <code>
directionin (default) or out
referenceThe bank reference number. Reuse one to simulate the bank pushing the same transfer twice
counterparty_nameThe payer's name. Defaults to NGUYEN VAN KHACH

The call answers 202:

{
"object": "simulated_transfer",
"reference": "SIM1758274452930A1B2C3",
"amount": 50000,
"direction": "in",
"description": "CHUYEN TIEN HTBF7RDGVQ",
"connection_id": "bcn_01a0b90428fd7c499145a12c133abe41",
"duplicate": false
}
The checkout page in development, with a button that simulates the transfer
In development, the checkout page has a button that simulates the transfer

A simulated transfer goes through exactly the same processing as a real bank's: matching, events, IPN and webhooks. Your handlers receive what they will receive in production.

Errors​

ErrorWhen
403 environment_mismatchThe request is in production
422 validation_errorNeither payment_id nor connection_id, or the account is not a development bank account
409 invalid_stateThe bank account is not active
422 missing_required_fieldNo amount and nothing is owed on the payment

Test scenarios​

ScenarioHow
Paid in fullSimulate with payment_id only
Partial, then completedSimulate amount below the total, then simulate again without amount
OverpaidSimulate amount above the total
ExpiredCreate with expires_in: 60, wait a couple of minutes, check for payment.expired
Money after expirySimulate on the expired payment: it becomes paid
Payer forgot the codeSimulate with connection_id and a content without the code
Duplicate bank pushSimulate twice with the same reference and amount: the second answers duplicate: true and changes nothing
Your endpoint is downPoint ipn_url at a URL that answers 500, then read GET /payments/{id}/ipn-deliveries
danger

Your handlers must check environment. A development payment must never unlock a real order.

Next steps​

View this page as Markdown