?test=true, no separate host and no header: a test
key can only ever see and change test things, and a live key can only ever see
and change live ones.
Every object says which world it belongs to:
A simulated courier that actually drives
A test delivery is not a stub. It is dispatched for real and a simulated courier takes it through the whole lifecycle on its own, at realistic timings: assigned, on the way to the store, picked up, on the way to the customer, delivered. Which means the things you actually need to build against are real:- Every status transition happens, in order, over several minutes.
- Every webhook fires, including the courier position pings roughly every 20 seconds, signed exactly the way a live one is.
- A courier turns up on the object with a name, a vehicle, a plate and a masked phone number.
- The tracking link works and shows the courier moving.
- Proof of delivery lands at the end: a photo, and a signature when one was captured.
Rehearse what goes wrong
The happy path is where integrations are easy. They break when an order comes back, when nobody is free to take it, or when the courier is late. On a test key you can choose which of those a delivery plays, by addingsimulate to
POST /v1/deliveries:
Everything else about the request is the same as any other create, and
everything that follows is the real thing: the same statuses, the same webhooks,
the same
charges, and the same rows on GET /v1/ledger that a live delivery
would produce in that situation, over play money.
returned: the order comes back
POST /v1/deliveries with your usual Idempotency-Key. The create
answers 201 like any other. Then the delivery goes through the normal
statuses up to en_route and does not reach delivered:
- A
delivery.status_changedwebhook fires forpicking_up,en_route,returningandreturned, withdelivery.updatedpings between them. The one forreturningcarries the store’s return code inreturn_verification.pincode, exactly as a live return does; see the return code. failure_reasoniscustomer_unavailable(see webhooks for the four values).- The money follows the live return rules. On a prepaid order the part of the
delivery fee you covered at the create comes back in full (a
subsidy_refundonGET /v1/ledger), and the wasted trip is billed as oneprovider_chargeof twice the trip fee, covering both legs.chargeson the delivery lists it as a singlereturn_feeentry (not one per leg), andrefundsstays empty. - A
returneddelivery is final.POST /v1/deliveries/{id}/retriesrefuses it with409 not_retriable, because the order ended; create a new delivery under a newexternal_idto try again.
no_courier: nobody is free
409, the same one a live create gets:
Idempotency-Key. So there is no delivery to retry with
POST /v1/deliveries/{id}/retries; the way to retry is to send the create again.
The address is checked first, so a dropoff outside the service area gets its
422 out_of_service_area instead, as it would live.
Because simulate is part of the request, sending this Idempotency-Key again
without simulate is a fresh request and creates the delivery normally.
delayed_pickup: the courier is late
picking_up about a minute after the
create (that is when the courier is assigned), en_route about 25 minutes
after it and delivered about 30 minutes after it. When you send a
pickup_ready_at, the delivery waits in scheduled and the same clock runs
from that pickup time instead of from the create. Use it to see what your
system shows during a long wait and how pickup_eta_at and dropoff_eta_at
move.
The courier_assigned status does not show in test mode
In test mode a delivery may go from dispatching straight to picking_up
without ever showing a courier_assigned status. That held in every scenario,
the normal run included. Do not wait for it: read courier_assigned_at or the
courier object, which are filled in.
Rules that apply to all three
simulateworks on a test key only. A live key that sends it gets422 validation_failedwithparam: "simulate", and so does any value that is not one of the three. Ship the same code to production and the field is refused loudly rather than ignored.- The same
Idempotency-Keywith a differentsimulateis a409 idempotency_conflict, like any other change to the body, once a request has used the key (it created a delivery, or it is still being processed). A request that was refused before it created anything, such as theno_courier409, leaves the key free: send it again with a differentsimulate, or none, and it is a new request. - There is no scenario for a courier who cancels before pickup. That cannot be
simulated, so it is not offered; test your handling of
canceledwith a cancellation of your own.
Play money
The test balance is real bookkeeping over fake money. Top it up from the partner dashboard with a fake top-up: no card and no transfer, the balance simply goes up. Everything downstream of that behaves like the live ledger:- Your share of each delivery fee is debited at create, and a create the balance
cannot cover is refused with
402 insufficient_balanceexactly as it would be live. - A cancelled or failed delivery refunds it, and bills the same wasted-trip charge for a late cancellation.
- Cash orders credit their order value on delivery and hold it, then release it
to
available_centsovernight once it is a day old, rather than on the weekly settlement cycle, so you can watch both states without waiting a week. - Withdrawals can be requested and resolve themselves to
paidabout three minutes later. No money is sent anywhere, and thewithdrawal.updatedwebhook fires for real.
GET /v1/ledger reads back on a test key too, with every row carrying
"test": true, so you can build and check your reconciliation against play
money before a peso is at stake.
Card orders in test mode take test cards only.
Test stores carry no plan. plan is null on every one of them, nothing is
ever charged for one, and no quote or delivery is refused with
plan_required. Plan Listo starts with your first live store; see
plan and billing.
The two worlds cannot see each other
A test key asking for a live object gets404 not_found, and the other way
round. It is not a permission error, because from a key’s point of view the
other world’s objects never existed.
That extends to the things you name yourself: external_id and
Idempotency-Key are unique per mode. Your staging system and your production
system can both dispatch order-841 on the same day without colliding, so you
never need mode-specific prefixes.
What test mode is not for
- It is not a load environment. The same rate limits apply, and the simulated courier takes the same few minutes a real one does.
- It is not a sandbox for addresses. The service area is enforced the same way, so a test dropoff still has to be within reach of the test store you dispatch from.
- It does not expire. Leave your staging environment pointed at a test key permanently.