Sandbox
The sandbox is a full order simulator. It is not a copy of production data — you create your own test sellers, buyers and orders and drive them through the lifecycle.
Sandbox routes require sandbox credentials on the sandbox host. Live credentials get
403 SANDBOX_ONLY. Sandbox traffic is additionally capped at 60 requests/minute per app, and each app has its
own isolated namespace — cross-app lookups return 404.
Every sandbox endpoint on this page links to its API reference entry, with the full request body, response schema, examples and error responses. The same entries are listed under Sandbox in the API reference sidebar.
Catalogue shape — one variant per sandbox product
A sandbox product carries one SKU, one price and one stock number.
POST /open/v1/products and POST /open/v1/products/bulk therefore accept a single entry in
variants; a payload with two or more is rejected with 400 VALIDATION_ERROR naming the
variants field (message: the sandbox simulator supports a single variant per product). Model each colour
or size as its own sandbox product while testing on the sandbox host. Production still allows up to 100 variants —
confirm multi-variant behaviour against production before relying on it.
Getting fixtures fast
Seeds one seller, one buyer and seven products. Idempotent — it does not delete existing data, so it is safe to call again to recover defaults.
Of the seven products, five published ones can be ordered; one is a draft and one is archived, and
POST /sandbox/orders refuses both. A seller that already has products gets no new ones, and
productsCount reports the products that seller has.
The response also lists those products in products: each one's id,
title, sku, price (centavos), stock and status. Pass an
id as items[].productId on POST /sandbox/orders. Your app credentials are enough;
GET /open/v1/products needs a seller access token. Pick a product whose status is
live and whose stock is above 0. The unlisted (draft) and archived products are
refused. A re-seed returns the same ids.
Idempotency keys
Every sandbox POST accepts an Idempotency-Key header and replays the original
response when you retry with the same key. No sandbox route rejects a request that omits it: the call is processed and
the response carries X-MallPlus-Idempotency: absent. The API reference marks the header as required on
POST /sandbox/seed, POST /sandbox/sellers and POST /sandbox/buyers, but the
sandbox does not enforce that. Send a fresh key on every sandbox POST anyway: without one, a retried
create makes a second seller, buyer or order.
Provisioning your own test data
| Endpoint | Description |
|---|---|
| GET /sandbox/sellers | List your test sellers |
| POST /sandbox/sellers | Create a test seller |
| GET /sandbox/buyers | List your test buyers |
| POST /sandbox/buyers | Create a test buyer with a shipping address |
Test buyers are data records on the order payload — a name and a shipping address. There is no buyer login or buyer-facing app; the sandbox models the seller's side of the transaction.
Driving the order lifecycle
A sandbox order response carries the public status, one of the five published values
(READY_TO_SHIP, SHIPPED, DELIVERED, CANCELLED, RETURN_REFUND),
or PENDING while the test buyer has not paid. PENDING is returned only by the
/sandbox/orders routes. This is what each step returns:
POST /sandbox/orders ──> PENDING ──pay-mock──> READY_TO_SHIP ──process-mock──> READY_TO_SHIP ──ship-mock──> SHIPPED ──deliver-mock──> DELIVERED
└───────────────────────ship-mock───────────────────────┘
pay-mock moves status from PENDING to READY_TO_SHIP;
process-mock does not change status, it changes what the simulator allows next. Behind the public status the simulator keeps internal states, which never appear in
status: pay-mock records the payment
(paidAt is set), and process-mock makes it internally PROCESSED (a tracking number is assigned
and bookingStatus becomes READY_FOR_HANDOVER). Run pay-mock first: process-mock
and ship-mock on an unpaid order return 400 INVALID_TRANSITION. process-mock is optional;
ship-mock works whether or not the order was processed, and process-mock works only once, on a
paid, unprocessed order. The INVALID_TRANSITION message names the internal state (for example
Must be READY_TO_SHIP or PROCESSED); branch on the code, not the message.
To see that an order has been processed, read bookingStatus on the order or its tracking:
handover_status is READY_FOR_HANDOVER.
An unpaid order is not a seller order yet — the same as production. Until pay-mock,
GET /orders omits it, GET /orders/{id} and every order action return 404 NOT_FOUND, and no
order.created webhook is sent. Seller Center lists it as Unpaid. pay-mock is when it first
appears there, as READY_TO_SHIP, and when order.created is sent.
Cancellation works the same as production: any time before the order ships.
Any order whose status reads READY_TO_SHIP can be cancelled: POST /orders/{id}/cancel
and POST /orders/bulk-cancel are accepted from READY_TO_SHIP and PROCESSED, the two internal simulator
states behind that status. From
SHIPPED on, the cancel returns 422 ORDER_NOT_CANCELLABLE.
| Endpoint | Effect |
|---|---|
| POST /sandbox/orders | Place a buyer-driven test order. Returns status PENDING (unpaid), with no bookingStatus |
| GET /sandbox/orders | List test orders |
| GET /sandbox/orders/{id} | Get one test order |
| POST /sandbox/orders/{id}/pay-mock | Mock payment; required before process-mock or ship-mock. status becomes READY_TO_SHIP and the order appears on GET /orders |
| POST /sandbox/orders/{id}/process-mock | Mock shipment arrangement. Assigns a sandbox tracking number; bookingStatus becomes READY_FOR_HANDOVER. status stays READY_TO_SHIP |
| POST /sandbox/orders/{id}/ship-mock | READY_TO_SHIP → SHIPPED, processed or not |
| POST /sandbox/orders/{id}/deliver-mock | SHIPPED → DELIVERED |
| GET /sandbox/orders/{id}/tracking | Tracking number and handover status once process-mock or ship-mock has run. Before that: 422 SHIPMENT_NOT_ARRANGED |
Each transition fires the same webhooks as production, so the sandbox is the right place to build and test your webhook consumer end to end.
Returns in sandbox
A sandbox return is opened on a delivered order. Get an order there first, in this order:
POST /sandbox/orders— the order readsPENDING(unpaid).POST /sandbox/orders/{id}/pay-mock— pays it; it now readsREADY_TO_SHIP.POST /sandbox/orders/{id}/ship-mock— moves it toSHIPPED.POST /sandbox/orders/{id}/deliver-mock— moves it toDELIVERED.
Then open the return from the console: Sandbox → Test Order (the Order Simulator). Find the order,
click Initiate Return, enter a reason and submit. The simulator returns every item on the order at its full
quantity. You can also drive the order to DELIVERED from the same page with Mark as Paid,
Process Order, Pickup and Deliver.
The return is created in requested status and the order moves to RETURN_IN_REVIEW.
If your app is authorized for that test account and subscribed to them, the return.created and
order.status.updated webhooks are delivered. The order then offers Approve Return and Reject Return.
| When | What you see |
|---|---|
The order is not DELIVERED — including an order already marked Complete | 400 INVALID_TRANSITION — Return can only be initiated on a Delivered order (current: status) |
| The order already has a return in progress | 400 INVALID_TRANSITION — A return is already in progress for this order |
| The order does not exist or belongs to another app | 404 NOT_FOUND — Order not found |
The simulator shows the message as an error on the Initiate Return dialog. There is no return window in the sandbox: a delivered order can be returned however long ago it was delivered, and only one return can be open on an order at a time.
Opening a return over the API
Instead of the Order Simulator you can open the return with
POST /open/v1/returns. It is a
seller-scoped call: sign it with your sandbox credentials and send the X-MallPlus-Access-Token and
X-MallPlus-Seller-Id of a seller that granted orders:write.
The API reference marks POST /returns as not yet available and shows no request
examples for it. That is permanent, not a pending release: returns are opened by the buyer, a seller cannot create one
in Seller Center, and production refuses the call with 403 OPERATION_NOT_SUPPORTED. The sandbox accepts the
call as described here so you can stand in for the buyer; do not build a production return flow on it.
- Place the order for the seller your access token belongs to: the
sellerIdonPOST /sandbox/ordersmust be that seller'ssel_…id. An order of any other seller returns404 NOT_FOUND— Order not found. - Drive it to
DELIVEREDwithpay-mock,ship-mockanddeliver-mock, as above. - Take the line-item ids from
items[].idon the order (POST /sandbox/orders,GET /sandbox/orders/{id}andGET /orders/{id}return the same value). - Call
POST /open/v1/returns:
POST /open/v1/returns
Idempotency-Key: 5b0e7c1e-0d1f-4c55-9a51-3f1f6f0c2b7a
{
"orderId": "<order id>",
"items": [ { "itemId": "<items[].id>", "quantity": 1 } ],
"reason": "Item arrived damaged"
}
orderId, items (1–100 lines, each with itemId and a positive
quantity) and reason are required; each line may also carry its own reason. No other
field is accepted. Idempotency-Key is optional here, but send one so a retry does not open a second return.
An itemId that is not a line of the order returns 404 NOT_FOUND and no return is created.
The response is 201 with the return: its id (ret_…), status
requested, and the lines you sent. The order moves to RETURN_REFUND. A subscribed app receives
return.created and order.status.updated (DELIVERED → RETURN_REFUND),
as from the Order Simulator. From there the return actions work on the sandbox:
| Call | Return | Order status |
|---|---|---|
| GET /returns/{id} | Read it | Unchanged |
| POST /returns/{id}/approve | requested → approved | Stays RETURN_REFUND |
| POST /returns/{id}/reject | requested → rejected | CANCELLED |
| POST /returns/{id}/approve-refund | requested or approved → refunded, with no approve needed first. Idempotency-Key is required | Unchanged |
| POST /returns/{id}/dispute | requested or approved → disputed | Unchanged |
As in the Order Simulator, a return opens only on a delivered order with no return in progress.
Anything else returns 400 INVALID_TRANSITION and no return is created: Return can only be initiated on a
Delivered order (current: …), or A return is already in progress for this order.
Default test shipping
You do not configure a shipping channel to test in the sandbox. Every sandbox product — whether you
created it with POST /open/v1/products or got it from POST /open/v1/sandbox/seed — is auto-assigned
the default test channel, so an order prices end to end with no setup.
| Field | Value |
|---|---|
| Channel code | MALLPLUS_TEST |
| Channel name | Mall+ Test Delivery |
| Option id | ship_mallplus_test |
| Fee | ₱50.00 (5000 centavos, PHP) |
| Estimate | 2-3 business days |
Omit shippingOptionId on POST /sandbox/orders and the order falls back to this channel:
shipping comes back as 5000 and shippingChannel as MALLPLUS_TEST. To exercise
carrier-specific rates instead, pass an explicit shippingOptionId from GET /open/v1/shipping/options
(for example ship_jt at ₱85.00) and shipping reflects that quote rather than the ₱50.00 default.
The ₱50.00 flat fee is a sandbox figure — a fixed number chosen so a test order always prices. It is not a Mall+ price list. Live shipping is quoted per order from the real enabled channels, so validate real rates against production before you go live.
Payouts in sandbox
The sandbox has no payout ledger and the Sandbox Seller Center has no payout, income or balance page. A sandbox payout is worked out from your test order each time you read GET /open/v1/payouts, so the API is the only place to see one.
To produce a payout, take an order to DELIVERED:
- POST /sandbox/orders — use the
sellerIdyour access token belongs to, or the payout will not appear in that seller's list. - POST /sandbox/orders/{id}/pay-mock
- POST /sandbox/orders/{id}/ship-mock — no payout yet
- POST /sandbox/orders/{id}/deliver-mock — the payout appears
| Field | Sandbox value |
|---|---|
| When it appears | Once the order is DELIVERED. Nothing is listed while the order reads READY_TO_SHIP or SHIPPED. Marking the order Complete in the Order Simulator keeps the payout |
| releasedAt | The time the order was delivered. There is no hold, and a release time later than now is never listed |
| status | Always released |
| grossAmount | The order total: line items plus shipping, in PHP pesos. The sandbox order reports its total in centavos, so grossAmount is that figure divided by 100 |
| commissionAmount | A flat 10% of grossAmount, rounded down to the centavo |
| netAmount | grossAmount minus commissionAmount |
| id | payout_ followed by the order id; filter one order with order_id |
Example: an order of 129900 with the default 5000 shipping (a sandbox order
total of 134900) has grossAmount 1349, commissionAmount 134.9 and
netAmount 1214.1.
Returns. An order that goes into a return is not listed, whether the return is open, approved,
refunded or rejected. A rejected return leaves the sandbox order CANCELLED, so it never gets a payout.
Sandbox settlement is simplified. In production a payout is released only after the return window closes (or the order completes), not at delivery; an open return holds the release; a rejected return lets the order settle; and the deduction is the real commission, fees, shipping deduction and tax, not a flat 10%. Test your parsing and reconciliation against the sandbox, but do not rely on its timing or amounts. Payout reports exist only in the production Seller Center.
Persistence
Sandbox orders and returns are durable and survive platform restarts. Other sandbox entities —
test sellers, buyers and products — are ephemeral and may be reset during a deployment; re-run
POST /sandbox/seed to restore the defaults. Those ephemeral entities are also swept on a best-effort
basis roughly a day after creation (orders and returns are never swept), so design your test scenarios to be
re-runnable rather than long-lived.
Seeing API-created listings in Seller Center
Products you create with POST /open/v1/products under a sandbox OAuth token belong to the seeded sel_… for the shop that consented. To inspect them in the UI, open Tools → Test Account → Login Seller Center for that same shop (same Shop ID / sandboxId). There is no separate Seller Center login named Sandbox Default Store — that name is only the seeded seller's display label on the token.
Match the seller_id your token carries to a shop on Tools → Test Account: each shop row
lists its OAuth sel_… seller id(s) (one per app that has consented). Open that shop's
Login Seller Center to see the listings. Direct Seller Center URLs are rejected — always enter from the Test
Account row.
Going live
- Complete your integration against the sandbox host with
mp_…credentials. - Request go-live in the console. Your app is reviewed and, once approved, issued
mp_live_…credentials. - Point your client at
https://open.mallplus.phand swap in the live credential and secret together — a sandbox client ID left in place returns401 INVALID_CREDENTIALS, and a live client ID signed with the sandbox secret returns401 INVALID_SIGNATURE. - Re-run the seller authorization flow. Sandbox grants do not carry over.
- Re-create your webhook subscriptions against the production callback URL.