Developer Guide

Idempotency and reconciliation: the engineering that stops money going missing

A payment charged twice, or a payment wrongly refunded while it actually succeeded, both come from the same root cause: treating an uncertain outcome as a certain one. This guide covers idempotency, polling, refunds, ledgers and reconciliation, with the exact mechanics Payonclick's BBPS, DMT and verification APIs use for each.

Every payment API eventually faces the same two failure modes: the same payment charged twice because a client retried blindly, and a payment marked failed and refunded while it actually went through on the provider's side. Idempotency and reconciliation are the two disciplines that stop both. Here is how they work in general, and how Payonclick's BBPS, DMT and verification APIs implement each piece.

Why payment APIs fail in two directions

A network call to any payment rail can time out without telling you whether the money moved. Two wrong reactions to that uncertainty cause almost all payment-API incidents:

  • Retrying under a new reference turns one uncertain payment into two real ones.
  • Assuming failure and refunding immediately can credit a wallet for a payment that actually succeeded downstream, days later, when the rail's own status catches up.

The fix for both is the same discipline: one reference identifies one payment intent for its entire life, and only a confirmed, dispatched status change is allowed to move money.

Idempotency keys: one reference, one outcome

An idempotency key, Payonclick's APIs call it client_reference, is a value you generate once per payment intent and send with every retry of that same intent. The API's job is to recognise a repeat and never process it twice.

  • On the BBPS API, client_reference is unique per user account. Retry a bill payment with the same reference and you get the original transaction back with duplicate: true, not a second payment.
  • On the verification API, the same rule applies to a lookup: resend the same reference and you get the stored result, with no second charge.
  • On the DMT partner API, your client_reference is stored as an idempotency key scoped to your account. A repeat returns the original transfer with duplicate: true, and if two identical requests race, only one transfer survives.

Generate your reference once, when you first form the intent to pay, not once per HTTP attempt. If your code path can retry a request, it must reuse the same reference on every retry.

Timeouts, pending status and polling backoff

A payment that returns PENDING is not an error and not a success, it is unresolved. Resolve it by asking again on a schedule, never by trying again with a new reference.

For BBPS, the API enforces a minimum wait of 120 seconds before the first status check even runs a live query; earlier checks return the stored status only, because some billers report no record for a payment they will accept moments later. After that, poll every 30 to 60 seconds. Behind the scenes, Payonclick's status job re-checks pending bill payments every 5 minutes, backing off per transaction at 2, 5 and 15 minutes, for up to 48 hours.

Build your own polling loop around the same shape: an initial wait long enough that an early check cannot be misleading, then a modest interval, backing off if the answer is still pending after several tries.

A transaction state machine

State Meaning Allowed next states What you should do
INITIATED Request accepted, not yet sent to the rail or provider PENDING, SUCCESS, FAILED Nothing; wait for the first status
PENDING Sent, outcome not yet confirmed SUCCESS, FAILED, stays PENDING Poll on a backoff; never resend with a new reference
SUCCESS Provider confirmed completion (terminal) Reconcile against the ledger and the provider's own record
FAILED Provider confirmed failure (terminal, refund follows) Confirm the refund posted; do not refund twice
DUPLICATE A repeat of an existing reference (mirrors the original transaction's state) Treat exactly as the original transaction

Every transition into SUCCESS or FAILED should come from the provider or rail's own status response, not from a timeout on your side. A timeout means unknown, never failed.

Refunds only follow a confirmed failure

Refunding on a guess is how money quietly leaks from a platform: refund an order that actually succeeded downstream, and you have paid the amount out twice, once through the failed rail, once back to the wallet.

Payonclick's own services follow one rule: a refund happens only after the provider or rail confirms failure, not on a timeout and not on an assumption. On BBPS, a failed bill payment is refunded to the wallet automatically as part of the same status-confirmation step that marks it FAILED, there is nothing left for an integrator to reverse by hand. On the DMT API, a transfer's wallet hold is released only when the transfer is marked failed, never because a status check went unanswered.

The ledger: unique references and row locks

Every wallet movement in the platform is written against a ledger with a unique reference per entry and a row-level lock taken before the balance changes. Two consequences follow directly:

  • If the same operation is attempted twice, a retried debit, a duplicate refund, the unique-reference constraint rejects the second write outright, rather than relying on application code to notice.
  • Two concurrent operations against the same wallet cannot both read a stale balance and both write a new one; the lock forces them to happen one at a time.

This is the same principle as idempotency, applied one layer down: the API's idempotency key stops a duplicate request from being processed twice; the ledger's unique reference stops a duplicate write from landing twice even if something upstream slips through.

Reconciliation: three records, one truth

No single system's opinion of a transaction's status should be trusted alone. A sound reconciliation process compares three independent records:

1

Your own platform's record

What your database says happened, from the request and the last status update you received.

2

The provider's or rail's record

What the upstream provider or bank rail reports for that reference, queried directly rather than inferred from a callback alone.

3

The bank statement

The actual money movement on the settlement account, which is the final source of truth when the first two disagree.

On top of routine reconciliation, an exposure watch can re-query transactions that were already refunded as failed, specifically to catch the case where the provider's status catches up late and shows the payment actually succeeded. Anything that still cannot be resolved automatically after a defined window drops into a manual review queue for a human to close out with the provider's own evidence, rather than being auto-resolved either way.

Reconciliation only works if every rupee has a unique, traceable reference from the moment it moves. Retrofitting reconciliation onto a system that reused references or allowed blind retries is far harder than building it in from day one.

Request signing and the audit trail

None of the above matters if a request cannot be trusted in the first place. Every Payonclick /ext/v1 call is authenticated the same way:

  • A Bearer API key, of which only a hash is stored on our side, the raw key exists nowhere in our database.
  • An X-Timestamp header, accepted only within plus or minus 300 seconds of server time, which limits how long a captured request stays replayable.
  • An HMAC-SHA256 signature computed over the HTTP method, the full path, the timestamp and a hash of the exact request body, so a tampered payload or a replayed old request both fail signature verification.
  • Per-key permissions, an IP whitelist, and a default rate limit of 60 requests a minute, so a leaked key is limited in what it can do and from where.
  • Every call is captured in an API audit trail with secrets redacted, so a dispute can be investigated after the fact without exposing the credentials themselves.

Build your own integration to treat this the same way you would want your own downstream partners to treat you: sign every write, bound every timestamp, and log everything except the secrets themselves.

FAQ

Frequently asked questions

What is an idempotency key and why does it matter?

It is a reference you generate once per payment intent and reuse on every retry of that same intent. It lets the API recognise that a repeated call is the same request again, instead of processing a timed-out call as a brand-new payment. Without one, a single network timeout can turn into a duplicate charge.

If a payment is stuck on PENDING, should I retry it?

Retry the status check, not the payment. Use the same reference to poll for the outcome on a backoff schedule; never submit the payment again under a new reference while the original is still unresolved. On Payonclick's BBPS API, wait at least 120 seconds before the first check, then poll every 30 to 60 seconds.

When should a failed payment be refunded?

Only after the provider or rail confirms the failure, never on a timeout or a guess. Refunding early risks paying out twice if the payment actually succeeds downstream later. On Payonclick's BBPS API a failed payment is refunded automatically, and on the DMT API a failed transfer's hold is released, in both cases only once the failure is confirmed.

What does a 3-way reconciliation actually compare?

Your own platform's transaction record, the provider's or rail's own status for that reference, and the bank statement showing the actual money movement. Routine reconciliation compares all three; an exposure watch specifically re-checks transactions already refunded as failed, in case the provider's status changes later.

Why does the ledger need unique references and row locks?

The unique reference stops the same debit or credit from being written twice, even if a duplicate request somehow reaches the database layer. The row lock stops two simultaneous operations on the same wallet from both reading a stale balance and corrupting the final total. Together they make the wallet balance trustworthy under concurrency.

How is a request to the API secured against tampering?

Every call carries a Bearer API key, a timestamp valid for five minutes, and an HMAC-SHA256 signature over the method, path, timestamp and body hash. Keys carry per-key permissions, an IP whitelist and a rate limit, and every call is logged in an audit trail with secrets redacted.

Should I rely on a webhook or poll for status?

Poll the status endpoints. That is the reliable path today across BBPS, DMT and verification.

Ready to get started?

Talk to the Payonclick India team about API access, a white-label platform or custom fintech software.

Keep exploring

Related services and guides

Get started

Tell us what you want to launch

Share a few details. Our team will call you back within one business day with the next steps, the documents needed and pricing for your use case.