Skip to content

Testing and go-live checklist

Test keys and live keys use the same base URL (https://api.example.com/v1), and only slip verification returns simulated results (if the team gave you the URL of a separate test environment, use that instead). Test in this order:

  1. Check your signer against the request test vectors and the webhook test vector, without calling the API.
  2. Call read-only endpoints: GET /v1/account and GET /v1/banks confirm your key, your signing and the IP allow-list.
  3. Test slip verification with a test key. Ask the team for a test key (pk_test_...) and test mode, then use payload values starting with TESTFAIL / TESTDUP to cover every branch (details).
  4. Run real payments with small amounts. Pay-ins and payouts are always real, even with a test key. Use your own team’s bank accounts as payers and the minimum amounts from your account limits.
CaseHowExpected
Successful paymentCreate a pay-in and transfer amountpayin.succeeded webhook; the order is marked paid
ExpiryCreate a pay-in and do not paypayin.expired webhook
CancelPOST /v1/payins/{id}/cancelResponse with status canceled, no webhook
SlipUpload a slip for a pending pay-inmatched: true and a payin.succeeded webhook
PayoutCreate a payout to your team’s accountpayout.succeeded webhook
DuplicateCreate a pay-in for a payer with an open one409 pending_payin_exists
Returning payerCreate two pay-ins (one after the other) with the same inline payer accountBoth responses have the same payerId
Name changedSend an inline payer with a known account but a different namePay-in created, warnings contains payer_name_differs; you log it
Wrong amountBelow payinMinimum422 amount_below_minimum
Bad signatureChange the body after signing401 invalid_signature
Repeated webhookClick Send callback again in the consoleYour system does not credit twice
Webhook outageStop your endpoint while a payment succeedsYour reconciliation job catches up
  • Production uses the live key (pk_live_...); no test key left anywhere.
  • The secret lives in a secret manager or environment variable, never in code, repositories or logs.
  • A new secret was created before launch if the testing secret was ever shared or pasted elsewhere.
  • The API is called from servers only, never from a browser or an app.
  • Server clocks are synced with NTP.
  • If you use an IP allow-list, the team has all your production outgoing IPs.
  • Every request, including retries, gets a new timestamp, nonce and signature.
  • Amounts are sent as strings, and payers are shown the pay-in’s amount (not requestedAmount).
  • Each pay-in and payout sends exactly one of payer (inline bank account) or payerId, and warnings in responses are logged.
  • If you show your own QR code, it is rendered from payTo.qrPayload; if you show the ready-made image, the request uses ?include=paymentImage (it is not returned by default).
  • Every payment has a unique reference, and you search by reference before re-creating after a timeout.
  • Your code branches on error.code and handles pending_payin_exists, insufficient_balance, not_available_now and maintenance.
  • The requestId of every response is logged.
  • Amounts are checked against limits from GET /v1/account before creating payments.
  • The endpoint is public HTTPS and verifies the signature on the raw body.
  • Events with a timestamp older than 5 minutes are rejected.
  • You reply 2xx within 10 seconds and do heavy work in the background.
  • Duplicates are handled with data.id + data.status, and finished orders never move back.
  • Unknown event types get a 2xx.
  • A scheduled reconciliation job runs, since there are no automatic retries.
  • Your team knows how to use Send callback again and read the callback log in the console.
  • Your available balance (balance.available) is monitored so payouts do not fail.
  • You know how to reach the team: support@example.com, always with the requestId and the payment id.