Orders

AdminUpdated Sep 24, 2026

Orders

The order is the core resource in Shippified. Each order is one purchase. It can be built from several messages: a Discord checkout embed, an order-confirmation email, a shipping email, and carrier tracking lookups can all contribute to the same record.

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 and set sale price.

DELETE

/api/orders/:id

Permanently delete an order.

POST

/api/orders/reparse

Rebuild all orders from their stored source messages.

GET

/api/orders/:id/tracking

Carrier tracking for an order (cached).

POST

/api/orders/:id/tracking/refresh

Carrier tracking, bypassing 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.

All order endpoints require authentication and only see orders that belong to the authenticated account. Request bodies larger than 2 MiB are rejected with 413.

The order object

{
  "id": "ord_mpvu9uim_aeh5t",
  "userId": "usr_4f1c2a9b7e0d3c11",
  "source": "email",
  "sources": ["discord", "email"],
  "botId": "bot_m1x2y3z4",
  "botName": "Checkout monitor",
  "emailSourceId": "email_m1x2y3z5",
  "templateId": "hayha",
  "templateName": "Hayha",
  "eventType": "order_shipped",
  "parser": "shape:walmart-order_shipped",
  "store": "walmart",
  "storeLabel": "Walmart",
  "orderNumber": "2000123-45678",
  "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 of the response entirely. They are never sent as null.

Identity and provenance

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 this order.

sources

array of the same values

Every channel that has contributed to this order. More than one entry means the order was confirmed by more than one channel.

botId, botName, botAvatar

string

The Discord intake bot that first reported the order, if any.

emailSourceId

string

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

templateId, templateName

string

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

parser

string

Which parser handled the latest message. Values: shape:<id> (built-in), custom:<id> (your template), generic discord / generic email (no shape matched), or manual. Useful for debugging.

intakeIds

string[]

IDs of the stored messages that make up this order. Email IDs are email:<Message-ID>, or email:sha:<hash> when the message has no Message-ID. Discord IDs are webhook:<random>. Re-delivered emails are recognized by this ID and skipped. POST /api/orders/reparse rebuilds orders from these messages. Orders created by hand have none.

userEdited

string[]

Names of fields entered by hand through PATCH (including salePriceCents). Re-parsing keeps these values as they are, and a mailbox rebuild leaves orders with hand-entered data untouched.

rawText

string

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

Lifecycle

Field

Type

Description

status

enum

Where the order is in its lifecycle. See Status.

eventType

enum

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

receivedAt

timestamp

When the order was first seen. For email this is the message's own Date header, not the time Shippified ingested it. When messages merge, the earliest value is kept.

updatedAt

timestamp

Last change of any kind. Missing on older records. Fall back to receivedAt when it is absent.

eventTime

string

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

Item and store

Field

Type

Description

store

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

Retailer key.

storeLabel

string

Display name, e.g. "Best Buy".

orderNumber

string

The retailer's order number. This is the key used to merge messages into one order.

itemSummary

string

Product name. "Unparsed order" means the parser found none.

productUrl, imageUrl

string

Product link and image.

sku, size

string

As extracted.

quantity

integer

Number of units. Absent means one unit.

customerName

string

Profile, buyer, or customer name from the message.

custom

object (string → string)

Values from custom mappings in your templates, keyed by customKey.

Money

Field

Type

Description

total, price

string

Display strings copied from the message, e.g. "$1,099.00". Do not do arithmetic on these.

costCents

integer

What you paid, in cents. Parsed from price (or total if there is no price) when the message arrives, or set by hand.

salePriceCents

integer

What you sold it for, in cents. Only ever set by you. Profitability reports use it. Spend totals never do.

Shipping and carrier

Field

Type

Description

trackingNumber

string

Tracking number.

carrier

string

Carrier name as provided or detected, e.g. "UPS", "FEDEX", "USPS", "DHL", "DHL Express", or a picker key such as "dhl-express". Free text: detection reads carrier keys and names (including names inside longer labels such as "USPS Ground Advantage") before falling back to the tracking-number format.

estimatedDelivery

YYYY-MM-DD

Estimated arrival date, from a message, a PATCH, or the carrier's estimate on a live lookup.

actualDelivery

YYYY-MM-DD

Set when a carrier lookup confirms delivery: the date of the carrier's delivered scan, or the day Shippified learned of it if the scan is undated.

shippingAddress, billingAddress

object

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

weightGrams

number

Package weight from the carrier.

dimensionsCm

object

{ length, width, height }.

signedBy

string

Name on the delivery signature.

serviceLevel

string

Carrier service, copied verbatim, e.g. "Ground".

rawCarrierResponse

object

Raw response from the most recent carrier lookup. Only stored when it is 16 KB or less.

Status

status is always derived from real signals. It cannot be set through the API.

Value

Meaning

ordered

Order placed, no tracking yet.

shipped

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

delivered

A delivery message arrived, or a carrier lookup confirmed delivery.

canceled

The retailer or bot reported a cancellation.

issue

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

Status only moves forward. When messages merge, the order keeps the highest-ranked status:

issue (0) < ordered (1) < canceled (1.5) < shipped (2) < delivered (3)

A partial-cancellation email therefore can't move a shipped order back to canceled, and a late order-confirmation email can't move a delivered order back.

How the status is derived from a message, first match 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

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 delivery 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 standard pagination envelope.

Query parameter

Description

status

One of ordered, canceled, shipped, delivered, issue. Any other value is ignored and does not filter.

store

Exact store key, e.g. walmart.

source

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

carrier

Case-insensitive exact match on carrier.

hasTracking

true returns only orders with a tracking number. false returns only orders without one.

from

YYYY-MM-DD. Returns orders with receivedAt on or after this date (UTC).

to

YYYY-MM-DD. Returns orders with receivedAt up to 23:59:59 UTC on this date.

q

Case-insensitive substring search across itemSummary, orderNumber, trackingNumber, storeLabel, and botName.

limit, offset

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

curl -s https://shippified.net/api/orders/ord_mpvu9uim_aeh5t \
  -H "Authorization: Bearer $SHIPPIFIED_API_KEY"

Returns the order object itself, with no wrapper. Returns 404 {"error": "Order not found"} if the order doesn't exist or belongs to another account.

Create an order

POST /api/orders

Creates an order by hand, for purchases that no connected bot or inbox will report. The request goes through the same ingest path as parsed messages. If orderNumber matches an existing order, the request merges into that order instead of creating a new one.

Body field

Type

Required

Notes

itemSummary

string

Yes

Must be non-empty after trimming.

store

string

No

One of target, walmart, amazon, bestbuy, unknown. Any other value becomes unknown.

storeLabel

string

No

Defaults to the label for store.

orderNumber

string

No

The merge key. Strongly recommended.

trackingNumber

string

No

When set, the order starts as shipped.

carrier

string

No

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

total

string

No

Display string.

costCents

integer

No

Rounded. Values of 0 or less are dropped.

salePriceCents

integer

No

Rounded.

quantity

integer

No

Must be positive. Otherwise it is dropped.

productUrl, imageUrl, sku, size

string

No

receivedAt

string

No

ISO 8601: YYYY-MM-DD or a full timestamp. Anything else is rejected with 400. Defaults to now.

emailSourceId

string

No

Links the order to one of your email sources. Ignored if the source doesn't exist.

eventType

string

No

Defaults to order_shipped when trackingNumber is set, otherwise order_placed. Must be a value from Event type; anything else is rejected with 400.

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",
    "…": "…"
  },
  "merged": false
}

merged: true means an order with the same orderNumber already existed and your request was folded into it. The response status is still 201, and order is the merged record, which keeps the existing order's id. In a merge, existing non-empty values win. The new request only fills fields that were empty, and status never moves backward.

Error

Status

itemSummary missing or blank

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

receivedAt not an ISO 8601 date

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

eventType not a known value

400 {"error": "eventType must be one of: order_placed, order_shipped, …"}

Body larger than 2 MiB

413

Free-plan monthly limit reached (new orders only)

429 {"error": "…", "reason": "plan_limit_reached", "cap": 100, "used": 100}

Creating an order fires order.created, or order.merged for a merge. It also fires order.shipped / order.delivered when the eventType implies that transition. See Webhooks.

Update an order

PATCH /api/orders/:id

Updates follow a fill-missing-only model. You can fill fields that are empty, but you can't overwrite a value the parser or a carrier already set. This keeps order data traceable to real messages. Fields not listed below are ignored.

Body field

Rule

salePriceCents

Always writable. Rounded to an integer. Send null to clear it.

costCents

Written only if the order has no costCents.

trackingNumber

Written only if empty. If the order is ordered, it moves to shipped.

carrier

Written only if empty.

orderNumber

Written only if empty.

estimatedDelivery

Written only if empty. Must start with YYYY-MM-DD. Only the date part is stored.

quantity

Written only if empty. Must be a positive integer.

itemSummary

Written only if the current value is "Unparsed order".

status, actualDelivery, everything else

Ignored. Status is derived, and delivery is carrier-driven.

A value counts as empty if it is missing, null, or an empty string. Values that don't meet a rule are skipped silently. The request still returns 200.

Every field the request actually writes (except the derived status) is added to the order's userEdited list, so re-parsing and mailbox rebuilds keep your values.

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

The response is the updated order object with no wrapper. To see whether a field was written, compare the returned values with what you sent.

To clear a sale price:

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

Sending "" for salePriceCents has no effect.

Events:

  • A PATCH that writes at least one field fires order.updated with { order, patch }, where patch lists the fields that were applied (a cleared sale price appears as "salePriceCents": null).

  • A PATCH that adds a tracking number to an ordered order also fires order.shipped.

  • A PATCH that changes nothing writes nothing, fires no event, and returns the order unchanged with 200.

Delete an order

DELETE /api/orders/:id

curl -s -X DELETE https://shippified.net/api/orders/ord_mpvu9uim_aeh5t \
  -H "Authorization: Bearer $SHIPPIFIED_API_KEY"

Returns 200 {"ok": true} or 404. Deletion is permanent and fires no webhook event. The order disappears from analytics. Share cards already created keep their frozen snapshot. The dashboard offers the same action as Delete order in every order's drawer.

Important: If a stored source message is ingested again later, for example during a mailbox rebuild, the order can reappear.

Re-parse orders

POST /api/orders/reparse

Rebuilds every order that has stored source 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.

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 stored messages.

changed

Rebuilt orders whose content actually changed.

merged

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

removed

Orders deleted because none of their messages are now recognized as order mail.

skipped

Orders with no stored messages, such as hand-created or older records, plus orders that would have been removed but have hand-entered data. They are left unchanged.

What re-parsing keeps from the old record:

  • id, userId, the earliest receivedAt, the union of sources, intakeIds, and userEdited.

  • Carrier and user fields: salePriceCents, estimatedDelivery, actualDelivery, shippingAddress, billingAddress, weightGrams, dimensionsCm, signedBy, serviceLevel, rawCarrierResponse.

  • Every field listed in userEdited.

  • A carrier-confirmed delivery: an order with actualDelivery stays delivered.

Everything else is rebuilt from the messages. An order is only removed if it has no salePriceCents and no userEdited entries. Emails you imported by hand with POST /api/email/import are never dropped as non-order mail. Re-parsing fires no webhook events.

Note: The request runs synchronously and can take a while on large workspaces. Use a generous client timeout.

Tracking

For how carriers are detected and which ones are supported, see the tracking guide.

Get tracking

GET /api/orders/:id/tracking

Returns cached results when they are fresh: 5 minutes for packages in transit, 24 hours once delivered. Otherwise it runs a live carrier lookup.

Live lookups are only made for carriers that have live tracking enabled on the server. On shippified.net that is currently DHL and DHL Express. UPS and FedEx numbers are detected and labelled, but return the "live unavailable" response described below, as do USPS and unrecognised numbers. Call GET /api/carrier-status to see what's live instead of hard-coding it.

curl -s https://shippified.net/api/orders/ord_mpvu9uim_aeh5t/tracking \
  -H "Authorization: Bearer $SHIPPIFIED_API_KEY"
{
  "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"
}

Field

Meaning

track

Normalized result: carrier, trackingNumber, delivered, events[] (each { date, location, description, status }, where 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 message, if any.

fetchedAt

When the data was fetched from the carrier.

stale

Always false in responses. Stale cache entries trigger a live lookup instead.

carrierKey

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

liveUnavailable

true when no live lookup is possible for this package: USPS, an unrecognised carrier, or a carrier without live tracking on this server. track is absent and error carries a human-readable notice. Build a timeline from the order's own fields instead, as the dashboard does.

credsRequired

Legacy field kept for older clients. true when the detected carrier has no live tracking on this server (including USPS); false for an unrecognised carrier. Prefer liveUnavailable.

order

After a live lookup: the order with the carrier result applied, so you can pick up status and delivery-date changes without another request.

lastKnown

On GET, after a live lookup that replaced an expired cache entry: the previous cached result, so you can show the last known 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 error, carrierKey: null, liveUnavailable: true. Set a carrier on the order if you know it.

Carrier has no live tracking on this server

200 with error, carrierKey, liveUnavailable: true, credsRequired: true. No carrier request is made.

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

A live lookup that returns data also updates the order:

  • It fills shippingAddress, weightGrams, dimensionsCm, signedBy, serviceLevel and rawCarrierResponse when the carrier returns them. Fields the carrier didn't return are never wiped.

  • It sets estimatedDelivery from the carrier's estimate while the package is in transit.

  • It moves ordered to shipped once the carrier has scan events.

  • When the carrier reports delivery, it sets actualDelivery and moves ordered or shipped to delivered.

  • canceled and issue orders keep their status.

Every carrier response fires tracking.refreshed. order.shipped and order.delivered fire only on a real status transition.

Force refresh

POST /api/orders/:id/tracking/refresh has the same response, but always does a live lookup.

Bulk sync

POST /api/orders/sync-tracking refreshes up to 50 orders with status ordered or shipped that have a tracking number on a carrier with live tracking, bypassing the cache. Orders on USPS, unrecognised carriers, or carriers without live tracking are skipped rather than counted as failures.

{ "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 to keep orders current. Every 5 minutes the server plans a bounded batch of carrier lookups across all workspaces and applies each result to the orders exactly as the endpoints above do, so status, estimatedDelivery, actualDelivery and the tracking.refreshed / order.shipped / order.delivered events all move without any client involvement.

Rule

Behaviour

Candidates

Orders with status ordered or shipped, a tracking number, and a carrier with live tracking.

Cadence

By order age (receivedAt): up to 30 days old, every 30 minutes; 30–90 days, every 12 hours; older than 90 days, not polled in the background (manual refresh still works).

Batch size

Up to 40 lookups per 5-minute tick, never overlapping the previous tick.

Priority

Never-checked packages first, then the most overdue.

Fairness

Workspaces take turns (round-robin) within each batch.

Deduplication

One lookup per carrier and tracking number, applied to every order carrying it.

Carrier budgets

A rolling 24-hour cap per carrier API account. DHL and DHL Express share one account with a cap of 240 lookups a day. Lookups from the endpoints above count against the same cap. Calls to one account are spaced out, and a rate-limit response pauses that account for 30 minutes.

Because the DHL budget is shared across all workspaces, packages may be checked less often than the cadence above when many are in flight. If you need a fresh answer, call POST /api/orders/:id/tracking/refresh. Subscribe to webhooks rather than polling for changes.

Carrier status

GET /api/carrier-status lists every carrier Shippified recognises and whether live tracking is enabled on this server.

{
  "carriers": [
    { "key": "ups", "label": "UPS", "configured": false, "envVars": ["…"] },
    { "key": "fedex", "label": "FedEx", "configured": false, "envVars": ["…"] },
    { "key": "dhl", "label": "DHL", "configured": true, "envVars": ["…"] },
    { "key": "dhl-express", "label": "DHL Express", "configured": true, "envVars": ["…"] },
    { "key": "usps", "label": "USPS", "configured": false, "envVars": [], "note": "Free USPS tracking API is unavailable. Paid USPS Web Tools key required." }
  ]
}

configured: true means live lookups, background refresh and sync-tracking apply to that carrier. envVars names the server settings a self-hosted instance needs to enable it; there is nothing to configure on your account.

Related

Was this page helpful?
Orders