OroPocket Blog
Tutorials

Testing a Money API Without Moving Money

Mohit M
August 30, 2026
A dim workspace with a laptop showing an out-of-focus terminal, a notebook and a pen beside it

Sandboxes in this category usually disappoint in one of two ways. Either it is a stub that returns the same happy payload whatever you send it, or it is a staging host quietly sharing production behaviour, so you discover the differences at go-live. Neither lets you test the thing you will actually ship bugs in, which is the failure path.

What follows is what a stateful one looks like, with the exact inputs, so you can compare it against whatever you are evaluating.

Mode selector The token prefix. oro_test_ is sandbox, oro_live_ is production
State Real. Balances move, quotes expire, users persist
Starting float ₹1,00,000, so running out of money is reachable
Prices The live feed, not a frozen fixture
Webhooks Really signed, and carry mode: "sandbox"
Reset POST /partner/sandbox/reset, optionally keeping your users
Access Immediate

The mode is the token, not a base URL

There is no separate hostname to remember and no ?test=true to forget. You authenticate with oro_test_… and you are in the sandbox; you authenticate with oro_live_… and you are not.

That sounds like a small thing. It is the difference between “which environment is this config pointing at” being a question you can get wrong at 2am and a question that answers itself from the credential.

It also means the safest possible deployment mistake. If a test key reaches production, calls fail closed against a sandbox rather than succeeding against real money.

Make it fail on purpose

A table of the failure paths that can be triggered deliberately in the OroPocket sandbox, with the exact input for each. A PAN of ABCDE1234F is approved and ZZZZZ9999Z is rejected. The Aadhaar OTP 1234 and the email code 123456 verify. A withdrawal whose amount ends in .01 fails and is refunded. Spending beyond the hundred thousand rupee float raises INSUFFICIENT_PARTNER_FUNDS, and selling more than a user holds raises INSUFFICIENT_USER_BALANCE. Letting a quote sit past ten minutes raises QUOTE_EXPIRED. The whole environment is reset with a POST to the sandbox reset endpoint.

This is the part that separates a useful sandbox from a stub. Every value here is a constant in the service, and they are fixtures rather than secrets, the same way test card numbers are published everywhere.

KYC. PAN ABCDE1234F is approved. PAN ZZZZZ9999Z is rejected. You need both, because the rejected path is the one with the awkward UI: what does your screen say, can the user retry, does your state machine get stuck.

The OTP steps. Aadhaar OTP 1234 and email code 123456 verify. Fixed values, so your integration tests do not need a mailbox.

A payout that fails. Any withdrawal whose amount ends in .01 fails and is refunded. This is the nicest one in the set, because a failed-then-refunded payout is genuinely hard to reproduce on demand and is exactly the flow where balances get double counted.

Running out of money. The float is seeded at ₹1,00,000 deliberately. Spend past it and you get INSUFFICIENT_PARTNER_FUNDS. A sandbox with infinite money cannot teach you what your app does when the wallet is empty, and the answer is usually “something embarrassing”.

A user overselling. Try to sell more than the user holds and you get INSUFFICIENT_USER_BALANCE. Note it is a different code from the partner one: your error handling should not conflate “we are out of float” with “this user does not own that much”.

A stale quote. Let a quote sit for more than ten minutes, then confirm it, and you get QUOTE_EXPIRED. Worth testing because the correct client behaviour is to re-quote and show the new price, not to retry blindly.

Resetting

curl -X POST https://api.oropocket.com/partner/sandbox/reset \
  -H "Authorization: Bearer oro_test_..." \
  -H "Content-Type: application/json" \
  -d '{"keepUsers": true}'

keepUsers defaults to false, which wipes everything. Passing true clears balances and orders while leaving your test users in place, which is what you usually want between test runs: KYC-ing the same fixtures repeatedly is the slow part.

The insufficient-funds error points at this endpoint too, so topping the float back up is the same call.

The webhooks are real

The failure mode people expect from a sandbox is that webhooks are faked, or not sent, or sent unsigned. These are signed exactly the way production ones are, with the same HMAC-SHA256 over ${timestamp}.${rawBody}, and they go through the same retry schedule.

The one difference is deliberate: the delivery is stamped as sandbox and a top-level mode: "sandbox" is injected into the payload, so a single receiver can safely handle both and filter.

That matters more than it sounds. If your webhook receiver is only ever exercised against a fake, the first real signature verification happens in production. Three things to get right in that handler: sign the raw body rather than a re-stringified one, compare in constant time rather than with ===, and reject stale timestamps so a captured delivery cannot be replayed at you later.

A test worth writing on day one

The single most valuable thing you can do with a stateful sandbox is the concurrency test, because it is the one you cannot do against a stub and the one whose bug costs real money.

Fire two confirms at the same quote at the same time. Assert that exactly one settles and the other returns QUOTE_ALREADY_CONSUMED.

QUOTE=qt_...
for i in 1 2; do
  curl -s -X POST https://api.oropocket.com/partner/buy/confirm \
    -H "Authorization: Bearer oro_test_..." \
    -H "Content-Type: application/json" \
    -d "{\"quote_id\":\"$QUOTE\"}" &
done
wait

Then do it again with an Idempotency-Key on both and assert you get the same response twice, with Idempotent-Replayed: true on the second.

If a provider’s sandbox cannot run that test meaningfully, you are not testing an integration, you are testing a mock you wrote yourself.

What a sandbox still will not tell you

Worth being straight about the limits, because a sandbox that oversells itself is its own trap.

  • Latency and load. Sandbox timings are not production timings, and neither is a guide to behaviour under your peak.
  • Real settlement. Money does not actually move, so anything downstream of settlement in your own stack is still untested.
  • Commercial terms. Rates, limits and commission on your live account are whatever your agreement says, not what the sandbox seeds.
  • Third-party edges. The fixed KYC fixtures are exactly that. Real documents fail in more interesting ways.

Sandbox access is immediate and needs no commercial conversation, so the sensible order is to build the whole flow, including the failure paths, before anyone talks about terms. If it does not work against the sandbox it will not work against production, and finding that out early costs nothing.

For how the pieces fit together end to end, we wrote up adding metals to an existing app and why apps bother at all, and if all you need is a rate rather than transactions, GET https://api.oropocket.com/public/prices is unauthenticated.

The short version

Ask three questions of any money-API sandbox. Does it keep state between calls. Can I make it run out of money. Are the webhooks really signed and really retried.

If the answer to any of those is no, the sandbox is a mock with better marketing, and the failure paths you most need to test are the ones you cannot.

The sandbox is open now

Stateful, priced off the live feed, seeded with a float you can genuinely exhaust, and it fires real signed webhooks stamped as sandbox. Access is immediate, so the whole failure-path suite can be written before any commercial conversation.

Read the docs →

Put this into practice on OroPocket

Buy 24K digital gold from ₹1. Earn Bitcoin cashback on every purchase.

GET THE APP

Join the Conversation

Be the first to share your thoughts.

READ MORE