📦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
AdminUpdated Sep 26, 2026

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

GET

/api/orders

List and filter orders (paginated).

GET

/api/orders/:id

Get one order.

POST

/api/orders

Create an order by hand, or merge into an existing one by order number.

PATCH

/api/orders/:id

Fill in missing fields; set or clear the sale price.

DELETE

/api/orders/:id

Delete an order permanently.

POST

/api/orders/reparse

Rebuild orders from their stored messages with the current templates.

GET

/api/orders/:id/tracking

Carrier tracking for one order (cached).

POST

/api/orders/:id/tracking/refresh

Carrier tracking, skipping the cache.

POST

/api/orders/sync-tracking

Refresh up to 50 in-flight orders at once.

GET

/api/carrier-status

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

id

string

Order ID (ord_…).

userId

string

Owning account.

source

"discord" | "email" | "manual"

Channel of the most recent message that created or updated the order.

sources

array of the same

Every channel that has contributed. More than one entry means two channels confirmed the order.

botId, botName, botAvatar

string

The Discord intake bot that first reported the order.

emailSourceId

string

The email source that first reported it, or the one linked when it was created by hand.

templateId, templateName

string

The built-in shape or custom template that matched the first message.

parser

string

What parsed the latest message: shape:<id> (built-in), custom:<id> (your template), generic discord / generic email (nothing matched; generic patterns only) or manual. Handy for debugging.

intakeIds

string[]

The stored messages that make up the order: email:<Message-ID> (or email:sha:<hash> when there's no Message-ID) and webhook:<random>. Hand-created orders have none.

userEdited

string[]

Fields you set through PATCH (including salePriceCents). Re-parsing and mailbox rebuilds keep these values.

rawText

string

Text of the first message (up to 4,000 characters). For hand-created orders, the itemSummary.

Lifecycle

Field

Type

Description

status

enum

Where the order is. See Status.

eventType

enum

The kind of message that moved the order furthest. See Event type.

receivedAt

timestamp

When the order was first seen. For email, the message's own Date header. On a merge the earliest value is kept.

updatedAt

timestamp

Last change. Missing on older records; fall back to receivedAt.

eventTime

string

Event time from the source: the email Date, or a value a template extracted.

Item and store

Field

Type

Description

store

"target" | "walmart" | "amazon" | "bestbuy" | "unknown"

Retailer key.

storeLabel

string

Display name, such as "Best Buy".

orderNumber

string

The retailer's order number. The merge key.

itemSummary

string

Product name. "Unparsed order" means none was found.

productUrl, imageUrl

string

Product link and image.

sku, size

string

As extracted.

quantity

integer

Units. Absent means one.

customerName

string

Profile, buyer or customer name from the message.

custom

object

Extra values from custom mappings in your templates, keyed by customKey.

Money

Field

Type

Description

total, price

string

Display text copied from the message, such as "$1,099.00". Don't do arithmetic on these.

costCents

integer

What you paid, in cents. Parsed from price (or total) on arrival, or set by hand.

salePriceCents

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

trackingNumber

string

Tracking number.

carrier

string

Carrier as given or detected, such as "UPS", "FEDEX", "USPS", "DHL", "DHL Express". Free text.

estimatedDelivery

YYYY-MM-DD

Expected arrival: from a message, a PATCH, or the carrier's estimate.

actualDelivery

YYYY-MM-DD

Set when a carrier lookup confirms delivery.

shippingAddress, billingAddress

object

{ name?, line1, line2?, city, region, postalCode, country }.

weightGrams, dimensionsCm

number, object

From the carrier. dimensionsCm is { length, width, height }.

signedBy, serviceLevel

string

From the carrier.

rawCarrierResponse

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

ordered

Placed; no tracking yet.

shipped

A tracking number is known, or a shipping message arrived.

delivered

A delivery message arrived, or a carrier confirmed delivery.

canceled

The retailer or bot reported a cancellation.

issue

The parser couldn't identify the store or the item. Check it by hand.

From a single message, the first matching rule wins:

  1. eventType is order_delivered → delivered

  2. eventType is order_shipped, or a tracking number is present → shipped

  3. eventType is order_canceled → canceled

  4. Store is unknown and item is "Unparsed order" → issue

  5. Otherwise → 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

order_placed

Checkout or order confirmation.

order_shipped

Shipping confirmation.

order_update

Out for delivery, delayed, ready for pickup or another update.

order_delivered

Delivered.

order_canceled

Cancellation.

unknown

Couldn't be classified.

List orders

GET /api/orders returns orders newest first by receivedAt, in the pagination envelope.

Query parameter

Description

status

ordered, shipped, delivered, canceled or issue. Any other value is ignored and doesn't filter.

store

Exact store key, such as walmart.

source

discord, email or manual. Matches the latest source, not the sources history.

carrier

Exact match on carrier, ignoring case.

hasTracking

true: only orders with a tracking number. false: only orders without.

from

YYYY-MM-DD. Orders received on or after this date (UTC).

to

YYYY-MM-DD. Orders received up to 23:59:59 UTC on this date.

q

Substring search, ignoring case, across itemSummary, orderNumber, trackingNumber, storeLabel and botName.

limit, offset

Default 50, max 200.

curl -s "https://shippified.net/api/orders?status=shipped&hasTracking=true&from=2026-09-01&limit=100" \
  -H "Authorization: Bearer $SHIPPIFIED_API_KEY"
{
  "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 -s https://shippified.net/api/orders/ord_mpvu9uim_aeh5t \
  -H "Authorization: Bearer $SHIPPIFIED_API_KEY"

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

itemSummary

string

Yes

Must be non-empty after trimming.

store

string

No

target, walmart, amazon, bestbuy or unknown. Anything else becomes unknown.

storeLabel

string

No

Defaults to the label for store (Unknown Store for unknown).

orderNumber

string

No

The merge key. Strongly recommended.

trackingNumber

string

No

When set, the order starts as shipped.

carrier

string

No

Stored as sent. If omitted, detected from trackingNumber (UPS, FedEx, USPS, DHL, DHL Express).

total

string

No

Display text.

costCents

integer

No

Rounded. 0 or less is dropped.

salePriceCents

integer

No

Rounded.

quantity

integer

No

Must be positive, otherwise dropped.

productUrl, imageUrl, sku, size

string

No

receivedAt

string

No

YYYY-MM-DD or a full ISO 8601 timestamp. A bare date is stored at 12:00 UTC, so it shows as that day in any timezone from UTC−11 to UTC+11. Full timestamps are kept as given. Defaults to now.

emailSourceId

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.

eventType

string

No

One of the event types. Defaults to order_shipped with a tracking number, otherwise order_placed.

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
  }'

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

itemSummary missing or blank

400 {"error": "itemSummary is required"}

Bad receivedAt

400 {"error": "receivedAt must be an ISO 8601 date (YYYY-MM-DD or full timestamp)"}

Unknown eventType

400 {"error": "eventType must be one of: order_placed, order_shipped, order_update, order_delivered, order_canceled, unknown"}

Free-plan monthly limit (new orders only)

429 {"error": "Free plan limit reached — you've already added 100 of 100 orders this month. Upgrade to keep importing.", "reason": "plan_limit_reached", "cap": 100, "used": 100}

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

salePriceCents

Always writable. Rounded. Send null to clear it. "" has no effect.

costCents

Only if the order has none.

trackingNumber

Only if empty. An ordered order moves to shipped.

carrier

Only if empty.

orderNumber

Only if empty.

estimatedDelivery

Only if empty. Must start with YYYY-MM-DD; only the date is kept.

quantity

Only if empty. Must be a positive integer.

itemSummary

Only while the current value is "Unparsed order".

status, actualDelivery, anything else

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 -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"}'

Events:

  • A PATCH that writes at least one field fires order.updated with { order, patch }. A cleared sale price appears as "salePriceCents": null in patch.

  • Adding a tracking number to an ordered order also fires order.shipped with source: "manual".

  • A PATCH that 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 -s -X DELETE https://shippified.net/api/orders/ord_mpvu9uim_aeh5t \
  -H "Authorization: Bearer $SHIPPIFIED_API_KEY"

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 -s -X POST https://shippified.net/api/orders/reparse \
  -H "Authorization: Bearer $SHIPPIFIED_API_KEY"
{ "reparsed": 412, "changed": 37, "merged": 3, "removed": 5, "skipped": 21 }

Field

Meaning

reparsed

Orders rebuilt from their messages.

changed

Rebuilt orders whose content actually changed.

merged

Orders folded into another because they now share an order number.

removed

Orders deleted because none of their messages count as order mail any more.

skipped

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 -s https://shippified.net/api/carrier-status \
  -H "Authorization: Bearer $SHIPPIFIED_API_KEY"
{
  "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 -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"

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

track

carrier, trackingNumber, delivered, events[] (each { date, location, description, status }; status is in_transit, delivered, pickup, exception, warning or unknown) and, when the carrier supplies them, recipient, shippingAddress, weightGrams, dimensionsCm, signedBy, serviceLevel, raw. Absent if the lookup failed.

error

Carrier or lookup error, or the notice for carriers without live tracking.

fetchedAt

When the carrier was asked.

stale

Always false: a stale cache entry triggers a live lookup instead.

carrierKey

ups, fedex, usps, dhl or dhl-express; null when the carrier couldn't be detected.

liveUnavailable

true when no live lookup is possible (USPS, an unknown carrier, or a carrier not switched on). Build a timeline from the order's own fields, as the dashboard does.

credsRequired

Legacy field. true when the carrier is known but has no live tracking here. Prefer liveUnavailable.

order

After a live lookup: the order with the result applied.

lastKnown

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

400 {"error": "Order has no tracking number"}

Carrier can't be detected

200 with carrierKey: null, liveUnavailable: true and an error notice. Set carrier on the order if you know it.

Carrier not switched on

200 as in the UPS example. No carrier request is made.

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 -s -X POST https://shippified.net/api/orders/sync-tracking \
  -H "Authorization: Bearer $SHIPPIFIED_API_KEY"
{ "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 ordered or shipped, a tracking number, a carrier with live tracking.

How often

Up to 30 days old (by receivedAt): every 30 minutes. 30 to 90 days: every 12 hours. Older: not in the background (manual refresh still works).

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.

Was this page helpful?