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:

  1. Create an app and apply for payments (sandbox works before your app is published).
  2. 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.
  3. Set your callback URL and copy the callback signing secret (also shown once).

Store them as environment variables:

bash
export HTP_API_BASE=
export HTP_KEY_ID=ok_test_xxxxxxxx
export HTP_API_SECRET=xxxxxxxxxxxxxxxx
export HTP_CALLBACK_SECRET=xxxxxxxxxxxxxxxx

Add a signing helper

Every request is signed with HMAC-SHA256. Save this as htp.js:

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

create-payment.js
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.

json
{
  "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:

js
const order = await htp('GET', `/open/v1/orders/${payment.id}`);
console.log(order.status); // PAID

You're done

Next: handle refunds, test the failure cases in Sandbox & testing, then work through the Go-live checklist.