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.

Response200 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.

Response200 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 delivery row, emails the download URL / licence key).
  • Setting fulfillmentStatus: shipped:
    • Stamps shippedAt.
    • Sends the buyer a shipping notification (uses trackingCourier + trackingNumber if set).
  • Setting paymentStatus: canceled from awaiting_payment is a terminal cancel; the order is dropped.
  • Cancelling from payment_confirmed is 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, and manual_order.shipped are on the roadmap and may appear in future webhook docs — not emitted today. The outbox is silent on this path. Poll /v1/manual-orders or GET /v1/manual-orders/:id for 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