Test & go live
Sandbox & testing
Test the full payment flow safely before going live.
How the sandbox works
- Use an
ok_test_…key. Orders areenvironment: "SANDBOX"and are invisible to production keys. - Real HTP wallets and real USDT are used, but only between you and your test users — nobody else can pay or be paid (
SANDBOX_USER_ONLY). - Fees are calculated and then rebated to your wallet.
- Limits are the lower of your approved limits and 100 USDT per payment, 500 per day, 2000 per month, counted separately from production.
- Works before your app is published.
Set up test users
In the console, Test users → Add (up to 20) by HTP account. Each test user:
- Signs in to your app with Sign in with HTP — this creates the
subyou charge. - Needs a wallet with some USDT and a payment PIN in the HTP App.
You (the developer) can sign in to your sandbox app, but cannot pay your own app (SELF_PAYMENT_FORBIDDEN) — use a test user for payments.
Test plan
| # | Scenario | Expected |
|---|---|---|
| 1 | Create payment → test user pays in the HTP App | payment.paid received and verified; order PAID |
| 2 | Same create request retried with the same Idempotency-Key | Same id returned, one order |
| 3 | Same out_order_no, new Idempotency-Key |
409 ORDER_ALREADY_EXISTS |
| 4 | expire_minutes: 1, wait |
Order reported CLOSED, no checkout_url, cashier shows expired |
| 5 | Close a CREATED payment, then open its link |
Cashier shows closed; order CLOSED |
| 6 | Partial refund, then refund the rest | Two REFUND orders, payment.refunded twice, refunded_amount = amount |
| 7 | Refund more than remaining | 422 REFUND_AMOUNT_EXCEEDED |
| 8 | Amount above 100 USDT | 422 LIMIT_EXCEEDED |
| 9 | payer_sub of a user who never signed in |
422 USER_NOT_AUTHORIZED |
| 10 | Wrong secret / old timestamp / reused nonce | 401 SIGNATURE_INVALID |
| 11 | Callback with a tampered body (use the Callback verifier) | Your endpoint returns 401 |
| 12 | Same callback delivered twice (console Resend) | Processed once |
| 13 | Stop your server, pay, start it again after a few minutes | Delivery retried and succeeds; or Resend from the console |
| 14 | Download the statement CSV | Matches your database |
| 15 | Payout to a test user (if approved) | SUCCEEDED, payout.completed received |
Debugging
- Every response has a
trace_id— log it. - The console Callbacks page shows
last_status_codeandlast_errorfor each delivery. SIGNATURE_INVALID: compare yourstring_to_signwith the Signature generator using the same inputs.- The callback URL must be reachable from the internet over HTTPS with a valid certificate and resolve to a public IP. For local testing use a tunnel (ngrok, Cloudflare Tunnel) and set the tunnel URL as the callback.