Manual orders
A manual order is an order placed outside the regular checkout flow — bank-transfer payments, WhatsApp orders, phone orders, marketplace re-entries. They land in the ManualOrder table (not in CheckoutSession) because they have no upstream payment session: the merchant manually confirms payment after seeing it land in their bank account.
Manual orders are how the free tier does commerce: the workspace has no Plugipay module enabled, so every checkout from the public storefront flows to /v1/storefront/public/:merchantSlug/manual-checkout, which writes a manual-order row and sends the buyer the merchant's bank details.
The endpoints here are the merchant view — list, retrieve, transition status. The buyer-facing create endpoint lives under Public storefront.
All endpoints require an sk_* key.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/manual-orders |
List manual orders |
GET |
/v1/manual-orders/:id |
Retrieve one (includes merchant-only fields) |
PATCH |
/v1/manual-orders/:id |
Transition status / set tracking / add notes |
List
GET /v1/manual-orders
Returns manual orders newest-first by placedAt. Cursor-paginated.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
limit |
integer | 25 |
Page size, clamped to [1, 100]. |
cursor |
string | — | Opaque cursor from meta.page.nextCursor. |
paymentStatus |
awaiting_payment | payment_confirmed | canceled |
— | Filter by payment status. |
fulfillmentStatus |
preparing | ready_to_ship | shipped | delivered |
— | Filter by fulfillment status. |
Response — 200 OK. Paginated envelope. Each data[i] is a normalised DTO:
{
"id": "mord_01HX...",
"orderNumber": "ACME-1042",
"productId": "prod_01HX...",
"productName": "Field Notes Notebook",
"variantId": null,
"variantName": null,
"quantity": 2,
"amount": 150000,
"currency": "IDR",
"paymentStatus": "awaiting_payment",
"fulfillmentStatus": "preparing",
"customerName": "Alice Tan",
"customerEmail": "alice@example.com",
"paymentMethod": "bank_transfer",
"trackingCourier": null,
"trackingNumber": null,
"placedAt": "2026-05-13T10:42:00Z",
"paymentConfirmedAt": null,
"shippedAt": null
}
Retrieve
GET /v1/manual-orders/:id
Returns the full DTO above plus the merchant-only fields that are not surfaced in list for compactness:
| Field | Description |
|---|---|
customerPhone |
Buyer's phone (for chasing). |
shippingAddress |
Full address payload. |
merchantPaymentNote |
Free-text note recorded by the merchant when confirming payment. |
merchantShippingNote |
Free-text note when packing. |
Errors
| Status | error.code |
When |
|---|---|---|
404 |
RESOURCE_NOT_FOUND |
Order doesn't exist or is in another workspace. |
Update
PATCH /v1/manual-orders/:id
The single mutation endpoint. Use it to confirm payment, update fulfillment status, attach tracking, or annotate.
Request body
| Field | Type | Description |
|---|---|---|
paymentStatus |
awaiting_payment | payment_confirmed | canceled |
Transition the payment state. payment_confirmed triggers fulfillment. |
fulfillmentStatus |
preparing | ready_to_ship | shipped | delivered |
Move along the packing/shipping flow. |
trackingCourier |
string (≤80) | null |
Courier name; free-form (e.g. JNE, J&T). Set to null to clear. |
trackingNumber |
string (≤80) | null |
Waybill or tracking number. |
merchantPaymentNote |
string (≤1000) | null |
Note tied to the payment confirmation. |
merchantShippingNote |
string (≤1000) | null |
Note tied to the shipment. |
Response — 200 OK. The updated normalised DTO.
Side effects
- Setting
paymentStatus: payment_confirmed:- Stamps
paymentConfirmedAt. - Triggers email to the buyer confirming receipt.
- For digital/license products, runs the same fulfilment path as a paid checkout (creates a
deliveryrow, emails the download URL / licence key).
- Stamps
- Setting
fulfillmentStatus: shipped:- Stamps
shippedAt. - Sends the buyer a shipping notification (uses
trackingCourier+trackingNumberif set).
- Stamps
- Setting
paymentStatus: canceledfromawaiting_paymentis a terminal cancel; the order is dropped. - Cancelling from
payment_confirmedis not supported — refund the buyer out-of-band and leave the row for history.
curl -X PATCH https://storlaunch.forjio.com/api/v1/manual-orders/mord_01HX... \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"paymentStatus":"payment_confirmed","merchantPaymentNote":"Bank transfer received via BCA"}'
// Node — confirm payment then mark shipped a day later
await client.manualOrders.update('mord_01HX...', { paymentStatus: 'payment_confirmed' });
// ... merchant packs the order, takes it to the courier ...
await client.manualOrders.update('mord_01HX...', {
fulfillmentStatus: 'shipped',
trackingCourier: 'JNE',
trackingNumber: 'JNE001234567',
});
The manual-order object
| Field | Type | Nullable | Description |
|---|---|---|---|
id |
string | no | mord_ + 26-char ULID. |
orderNumber |
string | no | Human-readable. Format <MERCHANT-SLUG-PREFIX>-<seq>. |
productId / productName |
string | no | What was ordered (snapshot at place time, so renaming a product doesn't rewrite history). |
variantId / variantName |
string | yes | Picked variant. |
quantity |
integer | no | Units ordered. |
amount |
integer | no | Smallest-unit total. |
currency |
string | no | ISO 4217. |
paymentStatus |
enum | no | awaiting_payment / payment_confirmed / canceled. |
fulfillmentStatus |
enum | no | preparing / ready_to_ship / shipped / delivered. |
customerName / customerEmail / customerPhone |
string | yes | Buyer contact (last two only on retrieve, not list). |
paymentMethod |
string | yes | bank_transfer, qris, whatsapp, cod. |
trackingCourier / trackingNumber |
string | yes | Courier + waybill. |
placedAt / paymentConfirmedAt / shippedAt |
string (ISO 8601 UTC) | yes | Lifecycle stamps. |
shippingAddress |
object | yes | Full address (retrieve only). |
merchantPaymentNote / merchantShippingNote |
string | yes | Merchant annotations (retrieve only). |
Events
Manual orders don't currently emit dedicated webhook events. The lifecycle is observable via the order's polling API; if you want push notifications, the planned future event types are:
Reserved but not emitted.
manual_order.placed,manual_order.payment_confirmed, andmanual_order.shippedare on the roadmap and may appear in future webhook docs — not emitted today. The outbox is silent on this path. Poll/v1/manual-ordersorGET /v1/manual-orders/:idfor now.
When paymentStatus flips to payment_confirmed for a digital/license product, the same downstream fulfilment fires as for a paid checkout — you will receive product.purchased. That's the signal you can rely on today.
Next
- Public storefront → Manual checkout — how buyers create manual orders.
- Products — the catalogue the orders reference.
- Shipping — for tier upgrades when the merchant wants automated couriers.