Order API
Use the REST API to interact with EverShop orders.
An order can carry many shipments. Each shipment covers specific order items at specific quantities, and order.shipment_status is a rollup computed from all of them — it is not a status you set directly.
Endpoints
Create An Order
Use this endpoint to create an order from a shopping cart.
| Field Name | Field Type | Required |
|---|---|---|
| cart_id | string | Yes |
- cURL
- JavaScript
curl
-H "Accept: application/json"
--data-raw "<JSON DATA>"
https://<your domain>/api/orders
fetch('https://<your domain>/api/orders', {
headers: {
'Accept': 'application/json',
},
body: <JSON DATA>
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"order_id": 274,
"uuid": "fd0b4f0fd6704ed0b53fa0c64ae7df3c",
"integration_order_id": null,
"order_number": "10274",
"cart_id": 990,
"currency": "USD",
"customer_id": 20,
"customer_email": "customer@example.com",
"customer_full_name": "The Nguyen",
"user_ip": null,
"sid": "09d34c21-4af3-4db8-a38b-335ebf6d45fa",
"user_agent": null,
"coupon": null,
"shipping_fee_excl_tax": 0,
"shipping_fee_incl_tax": 0,
"discount_amount": 0,
"sub_total": 12345,
"total_qty": 15,
"total_weight": 81,
"tax_amount": 0,
"shipping_note": null,
"grand_total": 12345,
"shipping_method_data": {
"provider_code": "core",
"method_code": "0f1c8b7a-9d3e-4f2a-a1b6-77d4c2e9b510",
"snapshot": {
"code": "0f1c8b7a-9d3e-4f2a-a1b6-77d4c2e9b510",
"name": "Free Shipping",
"cost": 0
}
},
"shipping_address_id": 551,
"payment_method": "paypal",
"payment_method_name": "Paypal",
"billing_address_id": 552,
"shipment_status": "pending",
"payment_status": "pending",
"created_at": "2023-02-07 14:18:04",
"updated_at": "2023-02-07 14:18:04",
"items": [
{
"order_item_id": 306,
"uuid": "dc651b93008d475e9de6d85983586a2e",
"order_item_order_id": 274,
"product_id": 3,
"referer": null,
"product_sku": "NJC90842-Black-X",
"product_name": "Lite racer adapt 3.0 shoes",
"thumbnail": "/assets/catalog/8953/8037/plv3663-Black-thumb.png",
"product_weight": 5.4,
"product_price": 823,
"product_price_incl_tax": 823,
"qty": 15,
"final_price": 823,
"final_price_incl_tax": 823,
"tax_percent": 0,
"tax_amount": 0,
"discount_amount": 0,
"total": 12345,
"variant_group_id": 62,
"variant_options": "[{"attribute_code":"size","attribute_name":"Size","attribute_id":2,"option_id":4,"option_text":"X"},{"attribute_code":"color","attribute_name":"Color","attribute_id":3,"option_id":14,"option_text":"Black"}]",
"product_custom_options": null,
"requested_data": null
}
],
"shipping_address": {
"order_address_id": 551,
"uuid": "e0fbebaca66c11edb46b60d819134f39",
"recipient": "The Nguyen",
"given_name": null,
"family_name": null,
"organization": null,
"address_line_1": "12 Nguyen Hue",
"address_line_2": null,
"address_line_3": null,
"dependent_locality": "26737",
"locality": null,
"administrative_area": "VN-SG",
"postal_code": null,
"sorting_code": null,
"country": "VN",
"telephone": "+84912345678",
"extra": {}
},
"billing_address": {
"order_address_id": 552,
"uuid": "e0fd1671a66c11edb46b60d819134f39",
"recipient": "The Nguyen",
"given_name": null,
"family_name": null,
"organization": null,
"address_line_1": "12 Nguyen Hue",
"address_line_2": null,
"address_line_3": null,
"dependent_locality": "26737",
"locality": null,
"administrative_area": "VN-SG",
"postal_code": null,
"sorting_code": null,
"country": "VN",
"telephone": "+84912345678",
"extra": {}
},
"links": [
{
"rel": "edit",
"href": "/admin/order/edit/fd0b4f0fd6704ed0b53fa0c64ae7df3c",
"action": "GET",
"types": [
"text/xml"
]
}
]
}
}
order.shipping_method and order.shipping_method_name were dropped. The selection now lives in the shipping_method_data JSONB column, which holds provider_code, method_code and a snapshot of the method as quoted at checkout time.
billing_address can be nullThe billing address is optional on zero-total orders (nothing is charged, taxed or invoiced). billing_address_id is null and billing_address resolves to null. Every other order still requires one.
Shipments
An order can have many shipments in EverShop 2.2 and later. Each carries its own
items, status and tracking, and order.shipment_status is a derived rollup over
them — see Shipment Status below.
The shipment endpoints have their own reference:
| What you want to do | Where |
|---|---|
| Create a shipment, list an order's shipments, update, cancel, mark delivered, void a label | Shipment API |
| Understand the rollup, phases and status transitions | Multi-shipment And Fulfillment |
| Register a carrier so labels and tracking work | Carrier Development |
Order-level Actions
Cancel An Order
Use this endpoint to cancel an order. :id is the order uuid.
The cancellation runs in one database transaction, in this order: it validates the order, voids an uncaptured authorization at the payment provider, sets the payment status to canceled, cancels the shipments that are not final, writes an activity log entry, returns the stock and emits order_canceled. If any step fails, including the provider refusing the void, the whole cancellation is rolled back and the order keeps its status.
An order can be canceled only when its payment status is cancelable and its shipments allow it. A captured or refunded order is not cancelable; refund it first. Canceling does not refund money that was already captured.
| Field Name | Field Type | Required |
|---|---|---|
| reason | string | Yes |
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/orders/:id/cancel
fetch('https://<your domain>/api/orders/:id/cancel', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
},
body: <JSON DATA>
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {}
}
A failed cancellation answers HTTP 500 with { "error": { "status": 500, "message": "..." } }. The messages include Order not found, Order is not cancelable at this status, and any error raised by the payment method while voiding the authorization. A missing reason is rejected with 400.
Capture An Order
Captures the money for an order whose payment is authorized. This is what the admin Capture button calls. It works for any payment method that registers a capture handler: Stripe, PayPal and Cash On Delivery in core, and any gateway added by an extension.
:id is the order uuid. The request has no body, and the capture is always for the full authorized amount.
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/orders/:id/capture
fetch('https://<your domain>/api/orders/:id/capture', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
},
body: <JSON DATA>
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"paymentStatus": "stripe_captured"
}
}
paymentStatus is the new payment status, always <payment_method>_captured.
When it is allowed. The order's payment method must have a capture handler, and its current payment status must be flagged isCapturable. In core, those statuses are stripe_authorized (the store is in authorize-only mode), paypal_authorized (the PayPal intent is AUTHORIZE) and cod_pending (the cash has not been collected). The admin GraphQL field Order.canCapture reports whether both conditions hold.
What it records. A payment_transaction row with payment_action: "capture", the new payment status, and the activity Captured <amount> <currency>. Transaction ID: <id>. Stripe keeps the PaymentIntent id, so its authorization row is updated in place. PayPal issues a new capture id, which is inserted as a new row whose parent_transaction_id is the authorization. Cash On Delivery records an offline row. No event is emitted for a capture, and order_placed is not emitted again.
Errors. Every failure answers HTTP 500 with { "error": { "status": 500, "message": "..." } }, including mistakes the client made. A missing or invalid admin session answers 401.
| Message | Cause |
|---|---|
Order <uuid> not found | No order has this uuid. |
Order <uuid> has no payment method | The order has no payment method recorded. |
Payment method "<code>" does not support capture | The method has no capture handler, or it is no longer registered (for example zero_checkout). |
Order <uuid> is not capturable in its current status (<status>) | The payment status is not flagged isCapturable. This is also what a second capture of the same order returns. |
Order <uuid> has no authorization to capture | The order has no payment_transaction row to capture against. |
| The gateway's message | Passed through, for example Payment intent is not in a capturable state (requires_capture) from Stripe, or PayPal capture failed (status ...) from PayPal. |
Invalid status | The method did not register its <method>_captured payment status. |
The gateway is called outside the database transaction, and the order row is not locked. A second capture is rejected because the status is no longer capturable, and Stripe and PayPal reject a concurrent duplicate themselves.
Refund An Order
Refunds money that was captured, in full or in part. This is what the admin Refund button calls. It works for any payment method that registers a refund handler: Stripe, PayPal and Cash On Delivery in core.
:id is the order uuid. amount is in the order currency's major units (49.99), as a number or a string with at most two decimal places.
| Field Name | Field Type | Required |
|---|---|---|
| amount | string or number | Yes |
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/orders/:id/refunds
fetch('https://<your domain>/api/orders/:id/refunds', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
},
body: <JSON DATA>
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"paymentStatus": "stripe_partial_refunded",
"isFullRefund": false
}
}
isFullRefund is true when the refunds recorded so far, including this one, reach the captured amount. The payment status is then <payment_method>_refunded, and the order status moves to closed. Otherwise it is <payment_method>_partial_refunded, which can be refunded again.
When it is allowed. The order's payment method must have a refund handler, and its payment status must be flagged isRefundable: stripe_captured, stripe_partial_refunded, paypal_captured, paypal_partial_refunded, cod_captured and cod_partial_refunded in core. A fully refunded order, and paypal_pending, are not refundable. The amount must be greater than zero and no more than what is still refundable: the captured amount minus the refunds already recorded. The admin GraphQL field Order.canRefund reports whether the method and the status allow a refund.
What it records. A payment_transaction row with payment_action: "refund" whose parent_transaction_id is the capture, the new payment status, the activity Refunded <amount> <currency>. Refund ID: <id>, and the order_refunded event. Cash On Delivery refunds are recorded offline: nothing moves, but the books balance.
Errors. Request validation failures answer 400 with the first schema message (a missing amount reads must have required property 'amount'). Every other failure answers HTTP 500 with { "error": { "status": 500, "message": "..." } }. A missing or invalid admin session answers 401.
| Message | Cause |
|---|---|
Order <uuid> not found | No order has this uuid. |
Order <uuid> has no payment method | The order has no payment method recorded. |
Payment method "<code>" does not support refunds | The method has no refund handler. |
Order <uuid> is not refundable in its current status (<status>) | The payment status is not flagged isRefundable. |
Order <uuid> has no captured payment to refund | The order has no payment_transaction row that was captured. |
Refund amount must be greater than 0 and at most the remaining captured amount | The amount is zero, negative, not a number, or more than what is left. |
| The gateway's message | Passed through, for example PayPal refund failed (status ...), or an error from the Stripe API. |
Invalid status | The method did not register its <method>_refunded or <method>_partial_refunded payment status. |
Each valid request creates a new refund at the gateway, so a client that retries after a timeout can refund twice. The amount check reads the recorded refunds first and does not lock the order. What is deduplicated is the gateway's own webhook echo (charge.refunded for Stripe, PAYMENT.CAPTURE.REFUNDED for PayPal): it carries the same refund id, so it is recorded once.
Mark Every Shipment Delivered
Legacy back-compat wrapper. It sweeps every shipment on the order that is not already delivered or canceled and advances it, then returns how many it actually moved. Prefer the per-shipment endpoint above for new integrations.
| Field Name | Field Type | Required |
|---|---|---|
| order_id | string | No |
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/deliveries
fetch('https://<your domain>/api/deliveries', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
},
body: <JSON DATA>
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"order_id": 274,
"updated_count": 2
}
}
Errors: 400 Invalid order id, 400 No shipments to mark delivered.
Shipment Status
order.shipment_status is an order-level rollup derived from the per-item shipment math. You never write it directly; it is recomputed after every shipment status change and after order cancellation.
| Value | Meaning |
|---|---|
| pending | No items shipped yet. This is the value a brand-new order gets |
| partially_shipped | Some, but not all, shippable items have shipped |
| shipped | Every shippable item has shipped |
| partially_delivered | Some items delivered, others still in transit |
| delivered | Every shippable item delivered. An all-digital order short-circuits to this at creation |
| partially_canceled | Some items are in canceled shipments and nothing else has shipped |
| canceled | The whole order is canceled, or every shippable item sits in a canceled shipment |
unfullfilled is deadThe legacy "unfullfilled" value no longer exists anywhere in the system. Integrations that switch on it will never match. The initial value is pending.
The three partially_* values are rollup-only — a single shipment can never carry them. Per-shipment status values are the registered shipment statuses (shipped, delivered, canceled, plus anything an extension registers).