📦Orders
The order object, how status is worked out, and every endpoint to list, create, edit, delete, re-parse and track orders.
- Written for
- Developers reading or writing Shippified orders
- Applies to
- All plans
- Deprecated
- + Deprecated
An order is one purchase. It is usually built from several messages: a Discord checkout post, an order-confirmation email, a shipping email and carrier tracking lookups can all add to the same record. The order number ties them together: a message whose order number matches an existing order is merged into it instead of creating a new one.
All order endpoints need a token and only see orders in the token's account. For the parameter-by-parameter spec see the Orders reference and Tracking reference.
Endpoints
Method | Path | Purpose |
|---|---|---|
|
| List and filter orders (paginated). |
|
| Get one order. |
|
| Create an order by hand, or merge into an existing one by order number. |
|
| Fill in missing fields; set or clear the sale price. |
|
| Delete an order permanently. |
|
| Rebuild orders from their stored messages with the current templates. |
|
| Carrier tracking for one order (cached). |
|
| Carrier tracking, skipping the cache. |
|
| Refresh up to 50 in-flight orders at once. |
|
| Which carriers have live tracking on this server. |
The order object
{
"id": "ord_mpvu9uim_aeh5t",
"userId": "usr_4f1c2a9b7e0d3c11",
"source": "email",
"sources": ["discord", "email"],
"botId": "bot_m1x2y3z4",
"botName": "Hayha — Target",
"emailSourceId": "email_m1x2y3z5",
"templateId": "hayha",
"templateName": "Hayha",
"eventType": "order_shipped",
"parser": "shape:target-order_shipped",
"store": "target",
"storeLabel": "Target",
"orderNumber": "912004578812",
"itemSummary": "Example Console Bundle",
"imageUrl": "https://cdn.example.com/p/12345.png",
"total": "$499.99",
"price": "$499.99",
"quantity": 1,
"trackingNumber": "1Z999AA10123456784",
"carrier": "UPS",
"estimatedDelivery": "2026-09-27",
"costCents": 49999,
"salePriceCents": 62000,
"status": "shipped",
"receivedAt": "2026-09-22T18:04:51.000Z",
"updatedAt": "2026-09-24T09:12:03.114Z",
"rawText": "…",
"intakeIds": ["webhook:m1x2y3z4ab12cd", "email:[email protected]"]
}Fields with no value are left out, never sent as null.
Identity and where it came from
Field | Type | Description |
|---|---|---|
| string | Order ID ( |
| string | Owning account. |
|
| Channel of the most recent message that created or updated the order. |
| array of the same | Every channel that has contributed. More than one entry means two channels confirmed the order. |
| string | The Discord intake bot that first reported the order. |
| string | The email source that first reported it, or the one linked when it was created by hand. |
| string | The built-in shape or custom template that matched the first message. |
| string | What parsed the latest message: |
| string[] | The stored messages that make up the order: |
| string[] | Fields you set through |
| string | Text of the first message (up to 4,000 characters). For hand-created orders, the |
Lifecycle
Field | Type | Description |
|---|---|---|
| enum | Where the order is. See Status. |
| enum | The kind of message that moved the order furthest. See Event type. |
| timestamp | When the order was first seen. For email, the message's own |
| timestamp | Last change. Missing on older records; fall back to |
| string | Event time from the source: the email |
Item and store
Field | Type | Description |
|---|---|---|
|
| Retailer key. |
| string | Display name, such as |
| string | The retailer's order number. The merge key. |
| string | Product name. |
| string | Product link and image. |
| string | As extracted. |
| integer | Units. Absent means one. |
| string | Profile, buyer or customer name from the message. |
| object | Extra values from |
Money
Field | Type | Description |
|---|---|---|
| string | Display text copied from the message, such as |
| integer | What you paid, in cents. Parsed from |
| integer | What you sold it for, in cents. Only ever set by you. Used by profitability, never by spend totals. |
Shipping and carrier
Field | Type | Description |
|---|---|---|
| string | Tracking number. |
| string | Carrier as given or detected, such as |
|
| Expected arrival: from a message, a |
|
| Set when a carrier lookup confirms delivery. |
| object |
|
| number, object | From the carrier. |
| string | From the carrier. |
| object | Raw response from the latest carrier lookup, kept only when it's 16 KB or less. |
Status
status is always worked out from real signals. It can't be set through the API.
Value | Meaning |
|---|---|
| Placed; no tracking yet. |
| A tracking number is known, or a shipping message arrived. |
| A delivery message arrived, or a carrier confirmed delivery. |
| The retailer or bot reported a cancellation. |
| The parser couldn't identify the store or the item. Check it by hand. |
From a single message, the first matching rule wins:
eventTypeisorder_delivered→deliveredeventTypeisorder_shipped, or a tracking number is present →shippedeventTypeisorder_canceled→canceledStore is
unknownand item is"Unparsed order"→issueOtherwise →
ordered
Status only moves forward. When messages merge, the higher-ranked status wins:
issue (0) < ordered (1) < canceled (1.5) < shipped (2) < delivered (3)So a partial-cancellation email can't move a shipped order back to canceled, and a late confirmation email can't move a delivered order back.
Event type
Value | Meaning |
|---|---|
| Checkout or order confirmation. |
| Shipping confirmation. |
| Out for delivery, delayed, ready for pickup or another update. |
| Delivered. |
| Cancellation. |
| Couldn't be classified. |
List orders
GET /api/orders returns orders newest first by receivedAt, in the pagination envelope.
Query parameter | Description |
|---|---|
|
|
| Exact store key, such as |
|
|
| Exact match on |
|
|
|
|
|
|
| Substring search, ignoring case, across |
| Default 50, max 200. |
- curl
- TypeScript SDK
- MCP
curl -s "https://shippified.net/api/orders?status=shipped&hasTracking=true&from=2026-09-01&limit=100" \
-H "Authorization: Bearer $SHIPPIFIED_API_KEY"const page = await shippified.orders.list({
status: "shipped",
hasTracking: true,
from: "2026-09-01",
limit: 100,
});"List my shipped Shippified orders since September 1 that have tracking." (tool list_orders)
{
"items": [ { "id": "ord_mpvu9uim_aeh5t", "status": "shipped", "…": "…" } ],
"total": 37,
"limit": 100,
"offset": 0,
"hasMore": false
}Get an order
GET /api/orders/:id returns the order object with no wrapper, or 404 {"error": "Order not found"} if it doesn't exist or belongs to another account.
- curl
- TypeScript SDK
- MCP
curl -s https://shippified.net/api/orders/ord_mpvu9uim_aeh5t \
-H "Authorization: Bearer $SHIPPIFIED_API_KEY"const order = await shippified.orders.get("ord_mpvu9uim_aeh5t");"Show me order ord_mpvu9uim_aeh5t." (tool get_order)
Create an order
POST /api/orders records a purchase that no bot or inbox will report. It goes through the same merge logic as parsed messages: if orderNumber matches an existing order, your request is merged into that order instead of creating a new one. This is what the dashboard's New order dialog calls.
Body field | Type | Required | Notes |
|---|---|---|---|
| string | Yes | Must be non-empty after trimming. |
| string | No |
|
| string | No | Defaults to the label for |
| string | No | The merge key. Strongly recommended. |
| string | No | When set, the order starts as |
| string | No | Stored as sent. If omitted, detected from |
| string | No | Display text. |
| integer | No | Rounded. 0 or less is dropped. |
| integer | No | Rounded. |
| integer | No | Must be positive, otherwise dropped. |
| string | No | |
| string | No |
|
| string | No | Links the order to one of your email sources so later emails for it are checked against that source's rules. Ignored if the source doesn't exist. |
| string | No | One of the event types. Defaults to |
- curl
- TypeScript SDK
- MCP
curl -s -X POST https://shippified.net/api/orders \
-H "Authorization: Bearer $SHIPPIFIED_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"itemSummary": "Example Console Bundle",
"store": "bestbuy",
"orderNumber": "BBY01-000000000001",
"trackingNumber": "1Z999AA10123456784",
"costCents": 49999,
"quantity": 1
}'const { order, merged } = await shippified.orders.create({
itemSummary: "Example Console Bundle",
store: "bestbuy",
orderNumber: "BBY01-000000000001",
trackingNumber: "1Z999AA10123456784",
costCents: 49999,
quantity: 1,
});"Add a Shippified order: Example Console Bundle from Best Buy, order BBY01-000000000001, tracking 1Z999AA10123456784, cost $499.99." (tool create_order)
Response 201 Created:
{
"order": {
"id": "ord_mq1a2b3c_x9y8z",
"source": "manual",
"sources": ["manual"],
"parser": "manual",
"store": "bestbuy",
"storeLabel": "Best Buy",
"orderNumber": "BBY01-000000000001",
"itemSummary": "Example Console Bundle",
"trackingNumber": "1Z999AA10123456784",
"carrier": "UPS",
"eventType": "order_shipped",
"status": "shipped",
"costCents": 49999,
"quantity": 1,
"receivedAt": "2026-09-24T15:20:00.000Z",
"rawText": "Example Console Bundle"
},
"merged": false
}merged: true means an order with that orderNumber already existed. The status is still 201, and order is the existing record (same id) after the merge. In a merge, existing non-empty values win: your request only fills empty fields, and status never moves back.
Error | Response |
|---|---|
|
|
Bad |
|
Unknown |
|
Free-plan monthly limit (new orders only) |
|
A create fires order.created (or order.merged), plus order.shipped or order.delivered when the eventType implies it. See Webhooks. It doesn't post a status card to the workspace's Discord updates webhook (the Shippified pings webhook); only orders arriving through intake and carrier tracking changes do.
Update an order
PATCH /api/orders/:id follows a fill-missing-only rule: you can fill fields that are empty, but you can't overwrite a value a message or carrier already set. This keeps order data traceable to real messages.
Body field | Rule |
|---|---|
| Always writable. Rounded. Send |
| Only if the order has none. |
| Only if empty. An |
| Only if empty. |
| Only if empty. |
| Only if empty. Must start with |
| Only if empty. Must be a positive integer. |
| Only while the current value is |
| Ignored. |
A value counts as empty when it's missing, null or "". Values that break a rule are skipped silently; the request still returns 200. The response is the order (no wrapper), so compare it with what you sent. Every field actually written is added to userEdited.
- curl
- TypeScript SDK
- MCP
curl -s -X PATCH https://shippified.net/api/orders/ord_mpvu9uim_aeh5t \
-H "Authorization: Bearer $SHIPPIFIED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"salePriceCents": 62000, "trackingNumber": "1Z999AA10123456784"}'const updated = await shippified.orders.patch("ord_mpvu9uim_aeh5t", {
salePriceCents: 62000,
trackingNumber: "1Z999AA10123456784",
});
// Clear the sale price
await shippified.orders.patch("ord_mpvu9uim_aeh5t", { salePriceCents: null });"Set the sale price of order ord_mpvu9uim_aeh5t to $620." (tool update_order)
Events:
A
PATCHthat writes at least one field firesorder.updatedwith{ order, patch }. A cleared sale price appears as"salePriceCents": nullinpatch.Adding a tracking number to an
orderedorder also firesorder.shippedwithsource: "manual".A
PATCHthat changes nothing writes nothing, fires nothing, and returns the order unchanged.
Delete an order
DELETE /api/orders/:id returns 200 {"ok": true} or 404 {"error": "Order not found"}. Deletion is permanent and fires no webhook event. The order drops out of analytics; share cards already created keep their frozen snapshot. It's the same action as Delete order in the dashboard.
- curl
- TypeScript SDK
- MCP
curl -s -X DELETE https://shippified.net/api/orders/ord_mpvu9uim_aeh5t \
-H "Authorization: Bearer $SHIPPIFIED_API_KEY"await shippified.orders.delete("ord_mpvu9uim_aeh5t");"Delete order ord_mpvu9uim_aeh5t." (tool delete_order)
If a mailbox rebuild later reads the same email again, the order can come back.
Re-parse orders
POST /api/orders/reparse rebuilds every order that has stored messages, using the current built-in shapes and your current custom templates. Run it after you create or fix a custom template to apply the change to past orders. It waits until done and returns a summary.
- curl
- TypeScript SDK
- MCP
curl -s -X POST https://shippified.net/api/orders/reparse \
-H "Authorization: Bearer $SHIPPIFIED_API_KEY"const summary = await shippified.orders.reparse();"I fixed my template. Re-parse my Shippified orders and tell me what changed." (tool reparse_orders)
{ "reparsed": 412, "changed": 37, "merged": 3, "removed": 5, "skipped": 21 }Field | Meaning |
|---|---|
| Orders rebuilt from their messages. |
| Rebuilt orders whose content actually changed. |
| Orders folded into another because they now share an order number. |
| Orders deleted because none of their messages count as order mail any more. |
| Orders with no stored messages (hand-created or older), plus orders that would have been removed but have a sale price or hand-entered values. |
Kept from the old record: id, the earliest receivedAt, the union of sources, intakeIds, userEdited; the carrier and user fields (salePriceCents, estimatedDelivery, actualDelivery, addresses, weightGrams, dimensionsCm, signedBy, serviceLevel, rawCarrierResponse); every field in userEdited; and carrier-confirmed delivery (an order with actualDelivery stays delivered). Everything else is rebuilt. Emails you imported by hand are never dropped as non-order mail. Re-parsing fires no webhook events.
Large workspaces can take a while. Use a generous client timeout.
Tracking
For how carriers are detected, see the tracking guide.
Which carriers are live
Live lookups only run for carriers switched on for the server. Ask rather than hard-coding:
- curl
- TypeScript SDK
- MCP
curl -s https://shippified.net/api/carrier-status \
-H "Authorization: Bearer $SHIPPIFIED_API_KEY"const { carriers } = await shippified.orders.carrierStatus();"Which carriers have live tracking on Shippified?" (tool get_carrier_status)
{
"carriers": [
{ "key": "ups", "label": "UPS", "configured": false, "envVars": ["UPS_CLIENT_ID", "UPS_CLIENT_SECRET"] },
{ "key": "fedex", "label": "FedEx", "configured": false, "envVars": ["FEDEX_CLIENT_ID", "FEDEX_CLIENT_SECRET"] },
{ "key": "dhl", "label": "DHL", "configured": true, "envVars": ["DHL_API_KEY"] },
{ "key": "dhl-express", "label": "DHL Express", "configured": true, "envVars": ["DHL_API_KEY"] },
{ "key": "usps", "label": "USPS", "configured": false, "envVars": [], "note": "Free USPS tracking API is unavailable. Paid USPS Web Tools key required." }
]
}The values above are an example; your answer reflects the server you call. configured: true means live lookups, the background tracker and sync-tracking cover that carrier. envVars names the server settings a self-hosted instance needs; there's nothing to set on your account. USPS never has live tracking.
Get tracking
GET /api/orders/:id/tracking answers from cache while it's fresh (5 minutes for packages in transit, 24 hours once delivered) and otherwise runs a live lookup. POST /api/orders/:id/tracking/refresh has the same response but always looks up live.
- curl
- TypeScript SDK
- MCP
curl -s https://shippified.net/api/orders/ord_mpvu9uim_aeh5t/tracking \
-H "Authorization: Bearer $SHIPPIFIED_API_KEY"
curl -s -X POST https://shippified.net/api/orders/ord_mpvu9uim_aeh5t/tracking/refresh \
-H "Authorization: Bearer $SHIPPIFIED_API_KEY"const cached = await shippified.orders.getTracking("ord_mpvu9uim_aeh5t");
const live = await shippified.orders.refreshTracking("ord_mpvu9uim_aeh5t");"Refresh tracking on order ord_mpvu9uim_aeh5t and tell me the latest scan." (tools get_order_tracking, refresh_order_tracking)
A live answer (example):
{
"track": {
"carrier": "dhl-express",
"trackingNumber": "1234567890",
"delivered": false,
"events": [
{ "date": "2026-09-23T14:10:00Z", "location": "Louisville, KY", "description": "Departed facility", "status": "in_transit" }
]
},
"fetchedAt": "2026-09-24T15:21:09.002Z",
"stale": false,
"carrierKey": "dhl-express",
"liveUnavailable": false,
"credsRequired": false,
"trackingNumber": "1234567890",
"order": { "id": "ord_mpvu9uim_aeh5t", "status": "shipped", "…": "…" }
}A carrier without live tracking on the server (here UPS):
{
"error": "Live UPS tracking isn't enabled on this Shippified server — showing updates from your order emails.",
"carrierKey": "ups",
"liveUnavailable": true,
"credsRequired": true,
"trackingNumber": "1Z999AA10123456784"
}Field | Meaning |
|---|---|
|
|
| Carrier or lookup error, or the notice for carriers without live tracking. |
| When the carrier was asked. |
| Always |
|
|
|
|
| Legacy field. |
| After a live lookup: the order with the result applied. |
| After a live lookup that replaced an expired cache entry: the previous result, so you can still show events if the new lookup failed. |
Situation | Response |
|---|---|
Order has no tracking number |
|
Carrier can't be detected |
|
Carrier not switched on |
|
A live lookup that returns data also updates the order: it fills the carrier fields it received (never wiping ones it didn't), sets estimatedDelivery from the carrier's estimate, moves ordered to shipped once there are scans, and on delivery sets actualDelivery and moves ordered or shipped to delivered. canceled and issue orders keep their status. Each live answer fires tracking.refreshed; order.shipped and order.delivered fire only on a real change. A real status change also posts a status card to the workspace's Discord updates webhook, if one is set and that event type is switched on.
Bulk sync
POST /api/orders/sync-tracking looks up, live, up to 50 orders with status ordered or shipped, a tracking number, and a carrier with live tracking. Other orders are skipped, not counted as failures.
- curl
- TypeScript SDK
- MCP
curl -s -X POST https://shippified.net/api/orders/sync-tracking \
-H "Authorization: Bearer $SHIPPIFIED_API_KEY"const { total, refreshed, failed, delivered } = await shippified.orders.syncTracking();"Sync tracking on my in-flight Shippified orders." (tool sync_tracking)
{ "total": 12, "refreshed": 11, "failed": 1, "delivered": 4 }total: 0 means there was nothing to look up.
Background refresh
You don't need to poll. Every 5 minutes the server plans a batch of carrier lookups across all workspaces and applies each result exactly as the endpoints above do, events included.
Rule | Behaviour |
|---|---|
Candidates | Status |
How often | Up to 30 days old (by |
Batch | Up to 40 lookups per 5-minute run. |
Priority | Never-checked packages first, then the most overdue; workspaces take turns. |
Deduplication | One lookup per carrier and tracking number, applied to every order carrying it. |
Daily budget | Each carrier account has a rolling 24-hour cap; DHL and DHL Express share one of 240 lookups a day. Lookups from the endpoints above count against it. A carrier rate-limit response pauses that account for 30 minutes. |
Because the budget is shared, packages can be checked less often than the table says when many are in flight. Call the refresh endpoint when you need a fresh answer, and subscribe to webhooks instead of polling for changes.
Troubleshooting
My PATCH returned 200 but nothing changed
The field already had a value, so fill-missing-only skipped it. Only salePriceCents can be overwritten. Compare the returned order with your request.
I created the same order twice
The second request had no orderNumber (or a different one). Only a matching order number merges. Delete the extra with DELETE /api/orders/:id.
Tracking says "Live … tracking isn't enabled on this Shippified server"
That carrier has no live tracking on the server (check GET /api/carrier-status). The order still moves forward from shipping and delivery emails.
An order has status issue
The parser found neither a store nor an item name. Fill itemSummary with PATCH (allowed while it's "Unparsed order"), or write a custom template and re-parse.