OpenShopy
Dokumentacja API

Orders, Fulfillment, Returns, Invoices

Order lifecycle & enums

text
status:             open → processing → completed   (or cancelled at any point)
payment_status:      unpaid → awaiting_confirmation → paid → partially_refunded → refunded
fulfillment_status:  unfulfilled → partial → fulfilled → returned
shipment status:     pending → label_created → in_transit → out_for_delivery → delivered (or failed / returned)
return status:       requested → approved → received → refunded (or rejected)

Transitions implemented:

  • Customer self-reports payment via POST /payments/:token/mark-sent → payment_status becomes awaiting_confirmation (DB RPC customer_mark_paid).
  • Merchant confirms with POST /orders/:id/confirm-payment → payment_status: paid, paid_at set, and if order status was open it auto-advances to processing. If the store has auto_issue_invoice enabled, an invoice is issued automatically.
  • POST /orders/:id/mark-unpaid reverts payment to unpaid (clears paid_at, confirmer, customer_marked_paid_at).
  • POST /orders/:id/cancel sets status: cancelled, restocks items (unless restock:false), abandons any linked cart.
  • Shipment creation/update recomputes fulfillment_status from item fulfilled_quantity/returned_quantity/quantity. When all shipments are delivered, fulfillment is fulfilled, and payment is paid, order auto-completes (status: completed).
  • Returns: creating a return doesn't change status by itself; PATCH /returns/:id to received/refunded increments returned_quantity and (if restock) restocks; setting to refunded also records the refund.

Finding an order

:id path params for order-scoped routes accept: UUID, numeric number, or exact order name (e.g. #1023).

Orders

GET /orders

Scope: orders:read, secret required.

Query params:

ParamTypeNotes
page,limitintpagination
statuscsvfilters status IN (...) — values: open,processing,completed,cancelled
payment_statuscsvunpaid,awaiting_confirmation,paid,partially_refunded,refunded
fulfillment_statuscsvunfulfilled,partial,fulfilled,returned
currencycsvPLN,USD,EUR
customer_idcsv of uuid
sourcecsvapi,manual,checkout
emailstringILIKE exact-ish match
created_after / created_beforeISO date
updated_afterISO date

Returns full order objects (secret serialization — includes internal_note, tags, utm, source, payment.confirmed_by), ordered newest first, with meta pagination.

GET /orders/track (no scope required — but a valid key is required)

Public order status lookup by customer. Query params: number (required, digits), email (required). Returns a single order with non-secret serialization (no internal_note/tags/utm/confirmed_by). 404 if not matched.

GET /orders/:id

Scope: orders:read, secret required. Returns {full:true, secret:true} order — includes timeline (order_events) and returns.

POST /orders

Scope: orders:write, secret required. Creates an order directly (no cart). Body:

FieldTypeRequiredConstraints
currencyenumnoPLN|USD|EUR, defaults to store default
linesarrayyesmin 1, max 200; each { variant_id: uuid, quantity: int(1-10000) }
emailstringyesvalid email
phonestring|nullnomax 40
customerobjectno{ first_name?, last_name?, company?, accepts_marketing? }
shipping_addressaddress|nullnosee address schema below
billing_addressaddress|nullno
shipping_rate_iduuid|nullno
pickup_location_iduuid|nullno
discount_codestring|nullnomax 60
buyer_vat_idstring|nullnomax 30
customer_notestring|nullnomax 2000
internal_notestring|nullnomax 5000
languageenumnopl|en
tagsstring[]nomax 20, each max 60
utmrecord<string,string>|nullno
send_notificationboolnodefault true
reserve_stockboolnodefault true — decrements stock
allow_out_of_stockboolnodefault false — when true skips stock-check
mark_paidboolnoif true, immediately confirms the payment after creation

Address schema used throughout: { first_name?, last_name?, company?, line1?, line2?, city?, postal_code?, region?, country_code?(ISO-2, uppercased), phone? } — all nullable/optional strings with max lengths (100/100/200/200/200/100/20/100/—/40). .strict() — unknown keys rejected.

Side effects: computes a price quote, rejects with 422 checkout_invalid if there are blocking issues (variant_not_found, product_unavailable, insufficient_stock, shipping_unavailable, discount_invalid, discount_not_applicable — full issue list in details.issues). Looks up/creates a customer by email, generates payment_reference from the store's transfer-title template, decrements stock (unless reserve_stock:false), increments discount usage counters, inserts an order.created event, sends order_created notification (unless disabled), and fires customer.created/order.created automations.

Response 201: full order object ({full:true, secret:true}).

PATCH /orders/:id

Scope: orders:write, secret required. Body (all optional): status (open|processing|completed only — cannot set cancelled here, use /cancel), email, phone, shipping_address, billing_address, internal_note, customer_note, tags, buyer_vat_id. Status changes are rejected on cancelled orders with 422 order_cancelled). Returns full order.

POST /orders/:id/confirm-payment

Scope: orders:write, secret required. Body: { notify?: bool } (default true). Errors: 422 order_cancelled, 422 invalid_state if not unpaid/awaiting_confirmation. No-op (returns as-is) if already paid. Returns updated order.

POST /orders/:id/mark-unpaid

Scope: orders:write, secret required. No body. Returns updated order.

POST /payments/:token/mark-sent

Scope: checkout (usable by publishable keys — this is the customer-facing "I sent the transfer" action). :token is the order's payment_token (exposed as payment.page_url = https://your-openshopy-domain/order/<payment_token> in the order serialization). No auth beyond the key; no ownership check beyond the token itself. Response: { "data": { "payment_status": "awaiting_confirmation" } } (or unchanged status if already past that state).

POST /orders/:id/cancel

Scope: orders:write, secret required. Body: { reason?: string|null (max 500), restock?: bool (default true), notify?: bool (default true) }. No-op if already cancelled. Returns updated order.

POST /orders/:id/notes

Scope: orders:write, secret required. Body: { message: string(1-5000) }. Appends an internal timeline event (not customer-visible). Returns 201 { "ok": true }.

POST /orders/:id/notifications

Scope: orders:write, secret required. Body: { event: "order_created"|"payment_received"|"order_shipped"|"order_delivered"|"order_cancelled" }. Force-resends that notification template. Returns 201 with the notify result.

Fulfillments (shipments)

POST /orders/:id/fulfillments

Scope: orders:write, secret required.

Body:

FieldTypeRequiredNotes
carrier_codestring|nullnomax 40; if given, resolves carrier_name and tracking_url from store's carriers table/template
carrier_namestring|nullnomax 100, overrides lookup
tracking_numberstring|nullnomax 100
tracking_urlstring|nullnomust be a URL
statusenumnopending|label_created|in_transit|out_for_delivery|delivered; default in_transit
itemsarrayno[{ order_item_id: uuid, quantity: int≥1 }]; omitted = auto-fulfill all remaining quantities on every line
notifyboolnodefault true — sends order_shipped

Validates requested quantities against each item's remaining (quantity - fulfilled_quantity); 422 unprocessable on mismatch. Recomputes fulfillment_status; fires shipment.created and (if now fully fulfilled) order.fulfilled automations. Response 201 with the shipment row (items is the JSON array of what was shipped in this shipment).

PATCH /fulfillments/:id

Scope: orders:write, secret required. Body: { status?: pending|label_created|in_transit|out_for_delivery|delivered|failed|returned, tracking_number?, tracking_url?, carrier_code?, notify?: bool }. Setting status:"delivered" sets delivered_at, sends order_delivered (unless notify:false), and may auto-complete the order (see lifecycle above). Returns updated shipment.

DELETE /fulfillments/:id

Scope: orders:write, secret required. Reverts fulfilled_quantity on affected order items, recomputes fulfillment status, deletes the shipment row. Returns { "id", "deleted": true }.

Refunds

POST /orders/:id/refunds

Scope: orders:write, secret required. Body: { amount: int≥1 (minor units), reason?: string|null (max 500), notify?: bool }. Only allowed when payment_status is paid or partially_refunded (else 422 not_paid). amount must be between 1 and order.total - order.refunded_total (else 422 unprocessable). Sets payment_status to refunded if fully refunded else partially_refunded. Fires refund.issued automation, sends refund_issued notification. Response 201 with full order.

Returns

POST /orders/:id/returns

Scope: orders:write, secret required. Body: { items: [{ order_item_id: uuid, quantity: int≥1 }] (min 1), reason?: string|null (max 500), restock?: bool (default true), note?: string|null (max 2000) }. Validates quantities against fulfilled_quantity - returned_quantity. Computes a proportional refund_amount (capped at remaining unrefunded order total) but does not refund automatically — just creates a requested return. Response 201 with the return row.

PATCH /returns/:id

Scope: orders:write, secret required. Body: { status: requested|approved|received|refunded|rejected, refund_amount?: int|null, notify?: bool }. Moving to received or refunded (from a prior non-received/refunded state) applies returned_quantity increments and restocks (if the return's restock flag was true). Moving to refunded additionally records a refund with refund_amount (or the return's stored amount) if > 0. Returns the updated return row.

Invoices

Invoices are generated by OpenShopy. Read them here or export them for accounting from Import & export → Reports in the dashboard.

GET /invoices

Scope: invoices:read, secret required. Paginated. Filters: order_id, kind (invoice, proforma, correction, credit_note), issued_after (compares to issue_date). Returns raw invoice rows.

GET /invoices/:id

Scope: invoices:read, secret required. Returns raw invoice row or 404 not_found.

POST /orders/:id/invoices

Scope: invoices:write, secret required. Body: { kind?: "invoice"|"proforma" } (default invoice). Issues a new invoice document for the order. Response 201.

POST /invoices/:id/corrections

Scope: invoices:write, secret required. Body: { reason: string(1-500), items: [{ quantity: number≥0, unit_net: int≥0 }] }. Issues a correction document. Response 201.

POST /invoices/:id/credit-notes

Scope: invoices:write, secret required. Body: { reason: string(1-500), amount_gross: int≥1 }. Issues a credit note. Response 201.

POST /invoices/:id/cancel

Scope: invoices:write, secret required. No body. Cancels the invoice.