Get started
Quickstart
Take your first sandbox payment in about 15 minutes.
This walkthrough uses Node.js 18+, but every step works the same in any language — see Authentication for Python, Go, PHP and Java.
Get sandbox credentials
In the Developer Console:
- Create an app and apply for payments (sandbox works before your app is published).
- Once payments are Enabled, open Payments → API keys and create a Sandbox key. Copy the
key_id(ok_test_…) and the secret — the secret is shown only once. - Set your callback URL and copy the callback signing secret (also shown once).
Store them as environment variables:
export HTP_API_BASE=
export HTP_KEY_ID=ok_test_xxxxxxxx
export HTP_API_SECRET=xxxxxxxxxxxxxxxx
export HTP_CALLBACK_SECRET=xxxxxxxxxxxxxxxxAdd a signing helper
Every request is signed with HMAC-SHA256. Save this as htp.js:
import crypto from 'node:crypto';
const { HTP_API_BASE, HTP_KEY_ID, HTP_API_SECRET } = process.env;
export async function htp(method, path, body, idempotencyKey = '') {
const raw = body === undefined ? '' : JSON.stringify(body);
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(18).toString('base64url'); // 24 URL-safe chars
const bodyHash = crypto.createHash('sha256').update(raw).digest('hex');
const stringToSign = [method, path, timestamp, nonce, idempotencyKey, bodyHash].join('\n');
const signature = crypto.createHmac('sha256', HTP_API_SECRET).update(stringToSign).digest('base64');
const headers = {
'Content-Type': 'application/json',
'X-Key-Id': HTP_KEY_ID,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
};
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
const res = await fetch(HTP_API_BASE + path, { method, headers, body: raw || undefined });
const json = await res.json();
if (!res.ok) {
throw Object.assign(new Error(json.message), { code: json.error_code, status: res.status, traceId: json.trace_id });
}
return json.data;
}Create a payment
import crypto from 'node:crypto';
import { htp } from './htp.js';
const orderNo = 'A' + Date.now();
const payment = await htp(
'POST',
'/open/v1/payments',
{ out_order_no: orderNo, amount: '1.00', currency: 'USDT', description: 'Test order' },
crypto.randomUUID().replaceAll('-', ''), // Idempotency-Key — reuse it if you retry
);
console.log(payment.id, payment.status); // … CREATED
console.log(payment.checkout_url);Pay with a test user
Open checkout_url in the HTP App, signed in as one of the app's test users (add them in the console under Test users), and confirm with the payment PIN.
Use a test user, not your own account
A developer can never pay their own app (SELF_PAYMENT_FORBIDDEN), in sandbox or production. Sandbox payments can only be paid by the app's test users (SANDBOX_USER_ONLY for anyone else).
Receive the callback
HTP POSTs payment.paid to your callback URL. Verify the signature before trusting it — copy the handler from Callbacks.
{
"event_id": "9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b",
"event": "payment.paid",
"created_at": "2026-11-07T09:05:12Z",
"data": { "id": "0b7a9c62-…", "out_order_no": "A1730970000000", "status": "PAID", "amount": "1.00", "net_amount": "0.99", "user_sub": "2f6c0d1e8a9b4c7d", "…": "…" }
}No public URL yet? Query the order instead:
const order = await htp('GET', `/open/v1/orders/${payment.id}`);
console.log(order.status); // PAIDYou're done
Next: handle refunds, test the failure cases in Sandbox & testing, then work through the Go-live checklist.