Engineering

How to test a reseller API integration in a sandbox

A sandbox proves your code handles a completed order, a failed one, a retried request and a signed webhook. The test plan, and where live still differs.

The short answer

A sandbox is an isolated copy of a reseller API that answers requests, enforces idempotency keys and signs webhooks the same way the live side does, but never spends real money or reaches a real upstream provider. Test it with five cases: a normal order, an order that cannot be filled, a retry that reuses an idempotency key, a webhook whose signature fails verification, and an order placed against a wallet that cannot cover it. Passing all five proves your code reacts correctly to each outcome; it does not prove a live upstream provider will behave identically, so recheck pricing, provider availability and funding the moment you switch to a live key.

A sandbox is a copy of the API that answers requests the same way the live side does, but spends no real money and reaches no real upstream provider. Testing in one proves that your own code handles every outcome the API can return: a completed order, a failed one, a retried request, a signature that does not verify, and a wallet that cannot cover the bill. It does not prove that a real registry, carrier or licence distributor will behave the same way a built-in test provider does, so a sandbox pass is the start of going live, not the end of it.

This matters most for a reseller API specifically, because every order turns into a debit against your balance and a request at a third party. Idempotency keys and signed webhooks exist because that side effect cannot be undone by rolling back a database row, and a sandbox is where you confirm your code actually uses them.

What can a sandbox prove?

Stripe's own definition is a good one to borrow: "A sandbox is an isolated test environment. You can use your sandbox to test Stripe functionality in your account, and experiment with new features without affecting your live integration" (Stripe: Sandboxes). The same idea applies to a reseller API. A sandbox can prove:

  • Your client sends the right headers, in the right shape, including an Idempotency-Key on every route that spends money.
  • Your code treats each documented status code the way the documentation says to: retry a 429 or a 5xx, do not retry a 4xx unchanged, and read error rather than parsing message.
  • Your webhook handler verifies a signature before trusting a payload, and does not process the same delivery twice.
  • Your order logic reads the final status of an order rather than assuming the first response is the last word, for orders that settle slightly later.
  • Your UI or billing system correctly reflects a reservation, a capture and a release, not just a single debit.

All of that is about your code's reaction to an outcome. None of it depends on whether the thing behind the API is a real registry or a stand-in for one, which is why a sandbox can prove it without spending anything.

What can't a sandbox prove?

A sandbox is only as good as its test provider, and a test provider is simpler than anything real. Twilio's test credentials are explicit about one gap: "When you authenticate with test credentials, Twilio doesn't charge your account, update the state of your account, or connect to real phone numbers," and critically, "SMS messages and calls sent by using test credentials don't trigger status callbacks" (Twilio: Test credentials). An integration that only works because a callback always arrives in the sandbox will not survive a provider that is occasionally slow or unreachable.

Plaid's Sandbox documentation names two more honestly: it "does not always reflect institution-specific behaviors and quirks" and "Sandbox data is not always based on a consistent data source across different API calls" (Plaid: Sandbox). Stripe's own sandboxes have documented limits too - "You can't test IC+ pricing in a sandbox" and "You can't create connections between a Connect platform's sandbox and connected account sandboxes" (Stripe: Sandboxes).

The pattern across all three: a sandbox can prove your code's logic is correct for every outcome it is given, but it cannot prove the live provider will actually produce those outcomes in the same mix, at the same speed, or at all. Pricing is the clearest example in any reseller API: test prices exist so you are not guessing at real ones, but they are not the prices you will be charged once you place a live order.

What test plan should you run before going live?

A sandbox test plan should give your code every outcome the API documents, not just the one that happens if you do nothing unusual. Five cases cover most of what breaks in production.

  1. The happy path. Place an order, wait for its status to settle, and confirm the matching webhook arrives and your handler applies it. In ResellerHQ, order a number with POST /v1/numbers/orders, confirm the response's status is completed, then send yourself a test message and confirm number.message.received reaches your endpoint (Quickstart).
  2. A failed order. Order something your account's capabilities do not cover - a country GET /v1/numbers/options does not list, for example - and confirm the order answers 422 no_offer and that your wallet balance is unchanged. Wallet documents why: a reservation that never finds a provider is never made, so a failed order should always cost nothing.
  3. A retry with an idempotency key. Send the same order twice with the same Idempotency-Key and confirm the second response is identical to the first, including the order id, rather than a second order. Then send the same key with a different request body and confirm you get a 422, which tells you the server is comparing the stored request and not only the key (Idempotency).
  4. A webhook with a bad signature. Register an endpoint, trigger a real event, and feed your handler a copy of the delivery with one byte of webhook-signature changed, or with the body parsed to JSON and re-serialised before verifying. Standard Webhooks' own warning is why the second case matters: "many webhook consumers often accidentally parse the body as json, and then serialize it again, which can cause for failed verification due to minor changes in serialization of JSON" (Standard Webhooks: specification). A correct handler rejects both.
  5. Running out of funds. Add a small amount of test funds, then place an order that costs more than you added, and confirm you get 402 insufficient_funds with nothing charged - a direct consequence of the hold-then-capture design covered in prepaid wallet billing for a reseller platform: an order that cannot be held is never captured, so it cannot cost anything in the first place. This is the one failure mode every reseller account will eventually hit for real, usually on an automatic renewal, so it is worth seeing on purpose first.
import { randomUUID } from 'node:crypto';

const base = 'https://resellerhq.com/api/v1';
const headers = (key: string) => ({
  authorization: `Bearer ${process.env.RHQ_API_KEY}`,
  'content-type': 'application/json',
  'idempotency-key': key,
});

async function order(body: unknown, key: string) {
  const res = await fetch(`${base}/numbers/orders`, {
    method: 'POST',
    headers: headers(key),
    body: JSON.stringify(body),
  });
  return { status: res.status, body: await res.json() };
}

const key = randomUUID();
const body = { country: 'US', duration: '30d', customerId: process.env.CUSTOMER_ID };

const first = await order(body, key); // completed: the happy path
const retry = await order(body, key); // identical response: the idempotency test
const mismatch = await order({ ...body, duration: '1d' }, key); // 422: same key, different body

What must behave the same in sandbox and live for the test to mean anything?

A sandbox test is only worth running if the parts it exercises - request validation, status codes, idempotency handling and webhook signing - work exactly the same way on both sides. If they do not, a passing sandbox test tells you nothing about live behaviour. The table below compares how three well-documented sandboxes split "same as live" from "allowed to differ," alongside ResellerHQ's own split.

Behaves the same as liveDocumented to differ from live
Stripe sandboxesRate limits; API request/response shapeNo real card network or payment provider involved; some pricing (IC+) cannot be tested; sandbox test cards only work in a sandbox (Stripe: Sandboxes, Stripe: Testing use cases)
Twilio test credentialsRequest validation for buying numbers, sending SMS, calls and LookupNo status callbacks ever fire; every other resource answers 403 Forbidden (Twilio: Test credentials)
Plaid SandboxSame endpoints, plus sandbox-only ones for firing test webhooks on demandDoes not reflect institution-specific quirks; sends no emails or texts; data is not always consistent across calls (Plaid: Sandbox)
ResellerHQ sandboxHTTP status codes; idempotency key storage and comparison; webhook headers, signing and the retry schedule; wallet hold, capture and release mechanics (Sandbox and live)Orders are filled by a built-in test provider, not a real registry or carrier; prices are test prices; domain availability is a fixed function of the name rather than a live registry check; numbers come from a +1 555 test range and only receive a message you send yourself (Sandbox and live)

The practical rule this table supports: trust a sandbox for anything your own code decides - what to retry, how to store a key, whether a signature verifies - and recheck anything the sandbox stands in for, such as whether a provider actually has the number, domain or seats you are about to pay for.

How do you move from sandbox to live?

  1. Switch sides and create a new key. In ResellerHQ, a key only ever reaches the side of the account it was created on: a rhq_test_ key cannot reach live data and a rhq_live_ key cannot reach sandbox data, with no parameter to switch between them (Sandbox and live). Switch the dashboard to Live and open Developers to create the live key.
  2. Fund the live wallet for real. Test funds cannot be withdrawn or spent on the live side, so a live account starts at zero until it is funded; live funds are currently added by invoice, as card payments are not connected yet (Wallet).
  3. Register a live webhook endpoint. Endpoints are per side, so a sandbox endpoint does not receive live events; create one on the live side and store its new signing secret (Webhooks).
  4. Check what the live key can actually order. GET /v1/account reports which products the calling key can order, and it is worth calling before assuming sandbox parity (Sandbox and live).
  5. Recheck provider-specific prerequisites. Live phone numbers depend on the account having a carrier enabled, and live domains and mailbox licences depend on this deployment holding a registrar or distributor key; while either is missing, that product's live routes answer 422 not_available regardless of anything your sandbox test proved (Sandbox and live, Domains).

Where ResellerHQ fits

Every account has two sides, sandbox and live, created in the same step as the account and kept completely separate: separate wallet, separate orders, separate webhook endpoints and separate API keys. A sandbox key starts with rhq_test_ and a live key starts with rhq_live_, and each can only reach its own side (Sandbox and live).

In the sandbox, orders are filled by a built-in test provider rather than a real registry or carrier, and prices shown there are test prices, not real ones. You add test funds yourself from the Wallet page, and they cannot be withdrawn or moved to the live side (Quickstart). Phone numbers are issued instantly from a +1 555 test range and never receive a real SMS; from a number's page in the dashboard you can choose Send a test SMS, which delivers a message to that number and fires the matching number.message.received webhook exactly as a real message would, which is the most direct way to run the happy-path and webhook tests above without writing any extra test infrastructure (Sandbox and live, Quickstart). Domain availability in the sandbox is a fixed function of the name, so the same search always gives the same answer, and nothing is registered anywhere; mailbox licences are not provisioned with Google or Microsoft either (Sandbox and live).

The limits, honestly stated: ResellerHQ is in early access. Phone numbers are open in the dashboard; domains and mailbox licences can be ordered in the sandbox today, but their live routes answer 422 not_available until this deployment is given a registrar key and a licence distributor connection. Live phone numbers depend on the account having a carrier enabled. There is no public live price list yet, so the "moving to live" checklist above is mostly about wallet funding, keys and webhook endpoints rather than a change in pricing you can compare in advance.

Questions

  • It is a separate, isolated copy of your account that runs against a built-in test provider instead of a real registry, carrier or licence distributor. Requests, errors, idempotency keys and webhook signatures work the same way as on the live side, but nothing is registered, rented or billed for real.
  • It proves your own code handles every documented outcome correctly: success, failure, a retry, a bad signature and an underfunded wallet. It cannot prove how a real upstream provider will behave, because the sandbox's test provider is simpler than any real registry or carrier and never has an outage, a rejected registrant or a network timeout unless you write code to simulate one.
  • Send the same purchase request twice with the same Idempotency-Key header and confirm the second response matches the first exactly, including its order id, instead of creating a second order. Then send the same key with a different request body and confirm you get a 422, which tells you the server is comparing the stored request and not just the key.
  • Trigger a real event from your sandbox, capture the delivery, then replay it to your own endpoint with one character of the webhook-signature header changed, or with the raw body re-serialised through a JSON parser first. A handler that is verifying correctly rejects both; a handler that is not will process a payload it should have refused.
  • A sandbox key can only ever reach the sandbox side of the account and a live key can only ever reach the live side; there is no request parameter that switches between them. In ResellerHQ a sandbox key starts with rhq_test_ and a live key starts with rhq_live_, and the two sides have separate wallets, orders and webhook endpoints.
  • No, not in ResellerHQ. The sandbox wallet is funded with test funds you add yourself, which cannot be withdrawn or moved to the live side, and sandbox orders are filled by a built-in test provider rather than a real registry or carrier.

Sources

  1. Stripe: Sandboxes docs.stripe.com
  2. Stripe: Testing use cases docs.stripe.com
  3. Stripe: API keys docs.stripe.com
  4. Twilio: Test credentials twilio.com
  5. Plaid: Sandbox plaid.com
  6. Standard Webhooks: specification github.com

Keep reading

More on selling under your own name

Start selling under your own brand

  1. Sign up with an email, no call needed1 minute
  2. Get test keys and $5,000 of pretend moneyInstantly
  3. Take order #0001 under your own nameToday