Skip to content
Pay
Esc
↑↓navigate↵open⌘Jpreview
On this page

Best practices

How to build a RazPay integration that never charges twice, never misses a result and keeps money movements under control.

Each section covers one part of an integration, with links to the detail. Use the go-live checklist before you launch.

Keys and secrets

  • Keep API keys and the webhook signing secret on your server, in environment variables or a secrets manager. Never put them in a browser, a mobile app or a repository.
  • Give each key only the permissions it needs. Most services only need collect.
  • Use a separate key for the service that sends payouts or refunds, so a leaked collect key can’t move money out.
  • Revoke a key as soon as you suspect it has leaked, then create a new one. Revoking takes effect immediately.
  • Saving a webhook URL replaces the signing secret, and the new one is shown once. Update your server straight away, or it will reject deliveries.

References

  • Make external_reference unique per intended payment, not per attempt: order-1001, not a random value generated on each try.
  • Save the reference in your database before calling RazPay, so you can always retry or look up the payment after a crash.
  • If a request times out or returns a 500 or 429, retry with the same reference. You get the original transaction back, and the customer is never charged twice.
  • After a failed result, use a new reference for the next attempt. A transaction never changes once it has failed.
  • References are unique across collections, payouts and refunds in a project, so prefix them by purpose if your ids could overlap.

See References and retries.

Collecting payments

  • Fulfil orders only when you see successful. A 200 from POST /payments/collect means the request was accepted, not that the customer paid.
  • Read data.status on every response. A retry returns the transaction as it is now, which may no longer be pending.
  • Don’t treat a slow payment as failed. Customers have about 5 minutes to approve, and a transaction can stay pending for up to 24 hours before RazPay closes it.
  • Before fulfilling, check that the amount and currency match the order.
  • Check for a failed to initiate payment: message. It means the provider refused the request, the reference is used up, and no webhook will follow.
  • Tell the customer to check their phone, and show failure_reason if the payment fails. It’s written to be shown to a user.

Handling webhooks

  • Verify the signature on the raw request body before doing anything else, and reject a t more than 5 minutes from your clock. Keep your server’s clock synced.
  • Answer with a 2xx within 10 seconds. Record the event and do slow work afterwards, or RazPay will treat the delivery as failed and retry.
  • Deduplicate on the event name plus the external_reference of the object the event is about. Use a database unique constraint with no expiry, not an in-memory cache.
  • Record the event in the same database transaction as the business change, so two copies arriving together can’t both be applied.
  • Compare updated_at with the value you stored and skip older events, because a retried event can arrive after a newer one.
  • Ignore event types you don’t handle, but still answer them with a 2xx.
  • Keep payouts and refunds separate from payments in your code. They have their own payout.* and refund.* events, so never treat them as money coming in.

Never missing a result

Treat webhooks as the main signal and polling as the backup:

  • Run a periodic job that looks up anything you still have as pending with GET /payments/{reference}.
  • If a page is waiting on the customer, poll every few seconds for a couple of minutes at most.
  • After an outage longer than the retry window of about 10 hours, catch up with GET /transactions. Rows can repeat across pages during a sync, so de-duplicate by reference.

Sending money out

Payouts can’t be reversed once MTN accepts them, so check before you send:

  • Set a Maximum single payout and a Daily payout limit on every project that sends payouts. They limit the damage from a bug or a leaked key.
  • Confirm the recipient with a wallet check using ?account=disbursement, and show the owner’s name before paying.
  • Check the disbursement balance before a large batch, and keep it funded. Refunds are paid from this account too, not the collection account.
  • Check for a failed to send payout: or failed to send refund: message. As with collections, the provider refused the request, the reference is used up and no webhook will follow.
  • Give each refund its own new reference, and treat a pending refund as money that’s already committed. Leave out amount only when you mean to refund everything that’s left.

Recurring payments and invoices

  • Update your records on preapproval.cancelled and preapproval.expired, and stop charging under them. Customers can cancel from their phone, so this can happen at any time. See Recurring payments.
  • Charge under a pre-approval with a new external_reference for each billing period, such as gym-2026-11-sipho.
  • Treat a paid invoice as income when you get invoice.paid. It carries both the invoice and the collection written to the ledger.

Requests and errors

  • Stay under one request per second sustained. On a 429, wait for Retry-After before retrying, and watch X-RateLimit-Remaining to slow down first. See Rate limits.
  • Check success before reading data. On failure, message says what went wrong in words you can show a user. See Errors.
  • Send amounts in major units (30.00, not 3000), in the currency of your MTN environment.
  • Store provider_ref and financial_tx_id. You’ll need them when you contact MTN about a transaction.
  • Use metadata for your own ids rather than encoding them in description, which customers can see.
  • Keep API keys, signing secrets and signature headers out of your logs.

Go-live checklist

  • Keys are stored server-side, with payout and refund only on the services that need them.
  • References are saved before each call and reused on retries.
  • Orders are fulfilled only on successful, after checking the amount and currency.
  • A provider refusal at collect is handled without waiting for a webhook.
  • The webhook handler verifies the raw body and the 5-minute limit, and answers within 10 seconds.
  • Duplicate and out-of-order events have been tested.
  • A periodic job resolves anything still pending.
  • Payout limits are set, and recipients are confirmed before paying.
  • Every flow you use has been tested against production with small amounts. See Sandbox.

Was this page helpful?