TestingSandbox

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

EndpointDescription
GET /sandbox/sellersList your test sellers
POST /sandbox/sellersCreate a test seller
GET /sandbox/buyersList your test buyers
POST /sandbox/buyersCreate 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.

EndpointEffect
POST /sandbox/ordersPlace a buyer-driven test order. Returns status PENDING (unpaid), with no bookingStatus
GET /sandbox/ordersList test orders
GET /sandbox/orders/{id}Get one test order
POST /sandbox/orders/{id}/pay-mockMock 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-mockMock shipment arrangement. Assigns a sandbox tracking number; bookingStatus becomes READY_FOR_HANDOVER. status stays READY_TO_SHIP
POST /sandbox/orders/{id}/ship-mockREADY_TO_SHIP → SHIPPED, processed or not
POST /sandbox/orders/{id}/deliver-mockSHIPPED → DELIVERED
GET /sandbox/orders/{id}/trackingTracking 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:

  1. POST /sandbox/orders — the order reads PENDING (unpaid).
  2. POST /sandbox/orders/{id}/pay-mock — pays it; it now reads READY_TO_SHIP.
  3. POST /sandbox/orders/{id}/ship-mock — moves it to SHIPPED.
  4. POST /sandbox/orders/{id}/deliver-mock — moves it to DELIVERED.

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.

WhenWhat you see
The order is not DELIVERED — including an order already marked Complete400 INVALID_TRANSITION — Return can only be initiated on a Delivered order (current: status)
The order already has a return in progress400 INVALID_TRANSITION — A return is already in progress for this order
The order does not exist or belongs to another app404 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.

  1. Place the order for the seller your access token belongs to: the sellerId on POST /sandbox/orders must be that seller's sel_… id. An order of any other seller returns 404 NOT_FOUND — Order not found.
  2. Drive it to DELIVERED with pay-mock, ship-mock and deliver-mock, as above.
  3. Take the line-item ids from items[].id on the order (POST /sandbox/orders, GET /sandbox/orders/{id} and GET /orders/{id} return the same value).
  4. 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:

CallReturnOrder status
GET /returns/{id}Read itUnchanged
POST /returns/{id}/approverequested → approvedStays RETURN_REFUND
POST /returns/{id}/rejectrequested → rejectedCANCELLED
POST /returns/{id}/approve-refundrequested or approved → refunded, with no approve needed first. Idempotency-Key is requiredUnchanged
POST /returns/{id}/disputerequested or approved → disputedUnchanged

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.

FieldValue
Channel codeMALLPLUS_TEST
Channel nameMall+ Test Delivery
Option idship_mallplus_test
Fee₱50.00 (5000 centavos, PHP)
Estimate2-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:

  1. POST /sandbox/orders — use the sellerId your access token belongs to, or the payout will not appear in that seller's list.
  2. POST /sandbox/orders/{id}/pay-mock
  3. POST /sandbox/orders/{id}/ship-mock — no payout yet
  4. POST /sandbox/orders/{id}/deliver-mock — the payout appears
FieldSandbox value
When it appearsOnce 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
releasedAtThe time the order was delivered. There is no hold, and a release time later than now is never listed
statusAlways released
grossAmountThe 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
commissionAmountA flat 10% of grossAmount, rounded down to the centavo
netAmountgrossAmount minus commissionAmount
idpayout_ 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

  1. Complete your integration against the sandbox host with mp_… credentials.
  2. Request go-live in the console. Your app is reviewed and, once approved, issued mp_live_… credentials.
  3. Point your client at https://open.mallplus.ph and swap in the live credential and secret together — a sandbox client ID left in place returns 401 INVALID_CREDENTIALS, and a live client ID signed with the sandbox secret returns 401 INVALID_SIGNATURE.
  4. Re-run the seller authorization flow. Sandbox grants do not carry over.
  5. Re-create your webhook subscriptions against the production callback URL.