Skip to content

Conventions

Every success wraps the result in data, with a small meta block:

{
"data": { /* the resource(s) */ },
"meta": { "requestId": "0f8c…", "apiVersion": "1.1" }
}
  • meta.requestId — also returned as the X-Request-Id response header. Quote it in support requests.
  • meta.apiVersion — the REST API version (1.1).
MethodUsed forSuccess
GETReading anything200
POSTCreating a purchase order or line items201 (+ Location when a new PO is created)
POSTLifecycle actions (/status, /receive, /complete, …)200
PATCHPartial updates to a header or a set of line items200
DELETEDeleting a purchase order or a set of line items200 (with a body — never an empty 204)

PUT is not used anywhere. Sending a method an endpoint doesn’t support returns 405 with an Allow header listing what it does support; an unknown path returns 404.

  • Bodies are JSON objects. Send Content-Type: application/json — but we parse the body either way, so a wrong header still gets you a precise error rather than a confusing one.
  • No body is fine where an endpoint doesn’t need one (DELETE /purchase-orders/{id}, POST /purchase-orders/{id}/complete, POST /purchase-orders/{id}/shopify-sync/reset). An omitted body is treated as {}.
  • Bodies are capped at 1 MB. Anything larger, anything that isn’t valid JSON, and anything that isn’t a JSON object (a bare array, string, or number) returns 400.
  • Unknown fields are rejected, not ignored — the error names the field. That’s deliberate: a typo in a field name would otherwise silently do nothing.
  • The URL wins. The purchase-order id comes from the path. You may repeat it in the body, but if it differs from the path you get a 400 — that combination always means a bug in the caller.

Every line-item endpoint takes (or returns) a list, even for one item — there are no single-item variants:

Terminal window
# add ONE line item: still an array
curl -s -X POST ".../api/v1/purchase-orders/po_7c3e/line-items" \
-H "Authorization: Bearer $LSTK_TOKEN" -H "Content-Type: application/json" \
-d '{ "lineItems": [{ "variantId": "var_9a01", "quantityOrdered": 30, "unitCost": 12.5 }] }'

Deleting takes its ids in the query string:

Terminal window
curl -s -X DELETE ".../api/v1/purchase-orders/po_7c3e/line-items?lineItemIds=pol_31aa,pol_31ab" \
-H "Authorization: Bearer $LSTK_TOKEN"

Batch your changes into one call wherever you can — it’s faster and, because every call costs one token from your rate limit, a lot cheaper than a loop.

This is the single most common mistake, so it’s worth stating plainly:

EndpointNumbers mean
/receiveThe new total received for that line. Sending 30 twice leaves it at 30, not 60.
/confirmThe new total confirmed for that line. 0 un-confirms.
/unreceiveHow much to take back — a delta. Sending 5 twice removes 10.

/receive and /confirm are therefore safe to repeat. /unreceive is not; see Retrying safely below.

Read the current totals from GET /api/v1/purchase-orders/{id}/line-items before you compute a new one.

Most write responses carry a sideEffects object. It reports work that happened after your purchase order was saved — pushing stock to Shopify, updating incoming quantities, syncing tags.

"sideEffects": {
"shopifySync": { "status": "queued", "syncedLineItemCount": 3 },
"incoming": { "outcome": "applied" }
}

How to read it:

  • Only the parts that actually ran appear. An endpoint that never touches Shopify has no shopifySync key at all — that’s not a failure.
  • shopifySync.status:
    • queued — accepted for delivery to Shopify. Not yet confirmed applied; poll GET /purchase-orders/{id} if you need certainty.
    • disabled — your shop has auto-sync-on-receiving switched off. Nothing reaches Shopify until you call POST /purchase-orders/{id}/shopify-sync.
    • skipped — nothing needed syncing.
    • failed — it did not go through. A recovery field on the response tells you what to do (usually: POST …/shopify-sync/reset, then POST …/shopify-sync).
  • outcome: "failed" on any other slot works the same way — the order changed, the follow-on didn’t, and recovery says how to fix it.
  • supplierUpdate is the one slot that isn’t about Shopify: it appears only on PATCH …/line-items with updateSupplier: true and reports, per line, whether the supplier’s catalogue entry was updated (applied / skipped / failed). See Editing a line can also update the supplier’s catalogue.

Send an Idempotency-Key header — any unique string up to 255 characters, a UUID is ideal — on any POST, PATCH, or DELETE. Retry the exact same request with the same key as often as you like: it will execute at most once.

Terminal window
KEY=$(uuidgen)
curl -s -X POST "https://prod.logistified.app/api/v1/purchase-orders" \
-H "Authorization: Bearer $LSTK_TOKEN" \
-H "Idempotency-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{ "poName": "Q3 restock" }'
# → 201, Location: /api/v1/purchase-orders/po_7c3e
# same key, same body — no second purchase order is created
curl -s -X POST ".../api/v1/purchase-orders" -H "Idempotency-Key: $KEY" -i
# → 201, byte-identical body, plus: Idempotent-Replayed: true

What each situation gives you:

You send…You get
The same key with the same requestThe original response, byte for byte — same status, same body, same meta.requestId — plus the header Idempotent-Replayed: true
The same key with a different request400 with "use a new key". Keys are one per request, not one per session
The same key while the first attempt is still running400 with retryable: true and Retry-After: 1. Wait a second, retry, and you’ll get the stored answer
A key you’ve never used (or one older than 24 hours)The request runs normally and its answer is stored

Details worth knowing:

  • Keys are scoped to the API key that used them, so two integrations in the same shop can’t collide.
  • “The same request” means the same method, path, query, and body. Field order and 1.0 vs 1 don’t matter; the order of items in an array does.
  • Only final answers are stored — successes and 4xx client errors. A 429 or any 5xx is not stored, so retrying really re-runs the request. That’s what you want: those are the failures where the outcome is genuinely unknown.
  • Keys last 24 hours. After that the same key is treated as new.
  • A replay is free — it doesn’t consume a rate-limit token.
  • Using the header on a GET is harmless; it’s simply ignored.
  • Browser clients: Idempotency-Key is an allowed request header and Idempotent-Replayed is an exposed response header, so cross-origin calls work.

Some endpoints are naturally safe to repeat; some aren’t. If you don’t send a key:

EndpointSafe to repeat?
POST /purchase-ordersNo — the PO number is generated for you; retry with a key
PATCH /purchase-orders/{id}Yes — last write wins
DELETE /purchase-orders/{id}Yes — the second call just returns 404
POST …/line-items, …/line-items/customNo — you’ll add duplicate lines
PATCH …/line-itemsYes — the values are absolute
DELETE …/line-itemsYes
POST …/statusNo — the second call 400s (that status is no longer a valid move)
POST …/confirm, …/receiveYes — the totals are absolute, so they converge
POST …/unreceiveDepends — see the warning below
POST …/completeYes — the second call 400s, the order stays Completed
POST …/quick-completePartly — on a failure the receipts are kept; retry is fine
POST …/shopify-syncYes — already-synced lines are skipped
POST …/shopify-sync/resetYes

List endpoints accept:

  • limit — page size (max 50).
  • cursor — an opaque page token from the previous response.

and return them inside data:

{
"data": { "purchaseOrders": [ /* … */ ], "totalCount": 128, "cursor": "3" },
"meta": { "requestId": "", "apiVersion": "1.1" }
}

Pass cursor back to fetch the next page. When cursor is null, you’ve reached the last page.

Terminal window
# page 1
curl -s -H "Authorization: Bearer $LSTK_TOKEN" \
"https://prod.logistified.app/api/v1/purchase-orders?limit=50"
# page 2 (cursor from the previous response)
curl -s -H "Authorization: Bearer $LSTK_TOKEN" \
"https://prod.logistified.app/api/v1/purchase-orders?limit=50&cursor=2"

List endpoints accept query filters (e.g. status, supplierId, search, date ranges, sku). See each endpoint in the reference for its supported filters.