Skip to content

Limits

Read this before you build on it. Everything here is a real residual case, most of them have a one-click fix in your dashboard, and one of them can cost you goods.

A false confirmation means you shipped for free. No system that infers which order a payment was for can make that impossible, and this one does not claim to. It is why every confirmation carries an evidence label — so you can decide what is strong enough for what value of goods, rather than treating all four the same.

There are also no chargebacks and no dispute process. There is no mechanism for one, because no intermediary is holding the money. What reached your account is in your account, and undoing it is between you and your customer.

A late payer whose amount has since been reserved and tapped by somebody else produces a credit that could belong to either order. We park it rather than guess. The rest rule — a lapsed amount is passed over for an hour by default while any other free amount exists — makes this need a payment more than an hour late at a busy price point, but it does not make it impossible. Rest is tenant-configurable up to two hours and never shorter than an hour; see Reservation timing.

You get: a payment.ambiguous webhook and a row in Unplaced payments with both candidate orders. One click.

iOS has no system-wide UPI chooser, so some customers fall back to copying your UPI ID and typing the amount by hand. Some of them round it — ₹498.99 becomes ₹499. That amount matches no reservation.

This is the single largest source of parked credits.

You get: a payment.unplaced webhook with the right order as a candidate, and delta_paise recorded when you place it. Show the “pay this exact amount” warning next to the copy fallback; it is the cheapest prevention there is.

If the same UPI handle pays the same amount within 30 minutes, the second credit is parked as a duplicate. That is right far more often than it is wrong — accidental double payments are common — but a genuine repeat purchase at the same price inside half an hour gets caught by it too.

You get: payment.duplicate with the candidate order listed. One click.

Android apps can read the UTR back from the UPI app and confirm on the bank’s own reference number, independent of the amount. iOS grants an app nothing back. iOS payments confirm on the amount like everyone else’s. This is a platform limit and no future version fixes it.

If every amount from your list price down to the bottom of your discount band is reserved at once, reserve returns 503 pool_exhausted rather than handing out an amount that cannot be told apart from another order.

Widening the band is not the fix — it buys capacity by giving away more money per order. If your volume is genuinely past what a 99-paise band holds, the fix is a merchant VPA from your own bank, whose channel returns your order reference directly and removes amount variation completely. Money still never touches us. That feed is not built yet.

Every public endpoint is rate limited, always keyed on an identity you already hold — a tenant, a page token, or a device — never on IP address, so one office behind a NAT is never throttled as if it were an attacker. A 429 carries {"error":"rate_limited","retry_after_seconds":60} and a retry-after header telling you how long to wait.

what it limits ceiling keyed by
the pay page (/p/{token}/*) 60/60s the page token
the rest of the API (POST /v1/intents, .../reserve, and the rest of /v1/*) 120/60s your tenant
requesting a sign-in link (POST /app/login) 60/60s per address, plus 30/60s in aggregate the address, then everyone
pairing a device (POST /v1/devices/pair) 10/60s per source, plus 60/60s in aggregate the source, then everyone
the reporter app (POST /v1/reports) 60/60s your tenant and device together
replaying a webhook delivery (.../replay) 10/60s your tenant

A reusable product link (GET/POST /pr/{slug}) reuses the pay page’s and the API’s own counters, keyed on the link’s slug instead.

Signup and device pairing are the two actions an unauthenticated caller can take, so each carries a second, constant-keyed ceiling on top of its own per-address or per-source one — otherwise N addresses or N source IPs would each get their own budget, and our own domain would end up mailing, or a guesser probing, without limit. The aggregate ceiling caps that. Because Cloudflare’s counters are per data-centre, it is a strong bound, not a strict global guarantee.

Order matching itself scales the way the reservation capacity table describes. Webhook delivery does not, and we would rather you knew the real number than a reassuring one.

On the Workers Free plan, one delivery attempt costs three subrequests — a claim, the POST to your endpoint, and the write that records the result — out of a 50-subrequest budget the five-minute sweep also spends on other bookkeeping before it delivers anything at all. That works out to roughly 144 deliveries an hour, globally, across every tenant, every event type, and every retry — not 144 per tenant.

At real volume this bites: 1,000 orders an hour with ordinary abandonment puts intent.expired alone past 300 events an hour, and expiry events get no inline first attempt, so all of it comes out of this same budget. Past roughly 480 orders an hour the queue grows faster than it drains.

Nothing is lost when this happens — an undelivered event waits its turn and stays visible and replayable in Settings — but it can wait. A Workers Paid plan raises the subrequest budget from 50 to 10,000 and this ceiling rises with it. This is a stated ceiling, not a solved problem.

We have no figure for how long your bank takes to send a credit alert email, or how long Gmail takes to forward it. We will not quote one we have not measured.

What we do instead: a credit that reaches us more than 30 minutes after the bank’s own timestamp is parked as stale rather than matched, and one with no usable timestamp is parked too. We never fall back to the time we happened to receive something. The reporter app removes the question entirely for anyone who installs it — on a live credit, 160 ms from SMS to server.

No order binding, which is the root of all of it

Section titled “No order binding, which is the root of all of it”

Everything above is engineering around one fact: the order reference the payer sets never reaches the payee. A payment gateway’s webhook carries your order_id directly and this entire correlation layer disappears. It costs about 2% in platform fees, plus 0.4% MDR above ₹2,000 since 15 October 2026.

That is the trade. We think it is a good one for a lot of sellers. It is not a good one for everybody and we would rather you knew which you were.

HDFC only. The parser is built for more — keyed on the sending domain, one module and one test corpus per bank — but only HDFC has one today.

Nowhere, until you look at them. Parked credits are never dropped and never turned into an error. Your account receives money that has nothing to do with orders, and the right behaviour is to ignore all of it while leaving a trail. They are in GET /v1/credits and in Unplaced payments, with the reason and the candidates, until you place one on an order or dismiss it as not an order. Dismissing keeps the row for audit.

what happens what we do
the same credit arrives twice, or on both feeds nothing. The bank’s reference number is the primary key.
a customer pays twice duplicate, with the candidate order.
a payment lands after its reservation lapsed confirms that order, evidence late — or parks, if your policy says so.
a late payment where the amount was reused and both orders tapped ambiguous, both listed.
a credit that is not for an order at all unplaced. Ignored, kept, never actioned by us.
a forwarded mail delayed past 30 minutes, or with no date stale.
a message we cannot parse our operators are alerted. The body is never stored.
every amount in the band is held 503 with a retry-after.
you send requests faster than a limit allows 429 with retry-after; nothing you sent takes effect.
a forged email rejected at the DKIM check. Nothing stored.
a revoked or unknown device token 401. Nothing stored.
your webhook endpoint is down six attempts, then it waits in Settings for you to replay it.
your feed goes silent our operators are alerted when an account makes reservations and sees no credits at all.

Nothing guesses.