Order Status API Guide
Overview
The Order Status service gives you near-real-time visibility into a purchase order’s lifecycle from receipt through ship — without scraping inboxes or asking customer service for updates. It is the right tool to replace daily email/CSV reconciliation with a polling job that only pulls what changed since last time.
PSRESTful exposes the PromoStandards Order Status (ODRSTAT) service over HTTP/JSON. Two versions are available:
- v1.0.0 — proven, broadly implemented, numeric
statusID. Start here. - v2.0.0 — newer, richer payload (issues, products, contacts,
auditURL), string status enum. Use when the supplier supports it and you want first-class issue tracking.
TL;DR for engineers: Do not loop
PO_SEARCHacross your order book. Run oneLAST_UPDATE_SEARCHcall per supplier per polling tick and upsert the deltas. See Recommended sync pattern.
Spec: Order Status 1.0.0 · Order Status 2.0.0
1. PromoStandards mapping
Every endpoint in this guide is a thin wrapper around an official PromoStandards method.
| REST endpoint | HTTP | PromoStandards method | Version |
|---|---|---|---|
/v1.0.0/suppliers/{supplier_code}/order-status-details | GET | getOrderStatusDetails | 1.0.0 |
/v1.0.0/suppliers/{supplier_code}/order-status-types | GET | getOrderStatusTypes | 1.0.0 |
/v2.0.0/suppliers/{supplier_code}/order-status | GET | getOrderStatus | 2.0.0 |
/v2.0.0/suppliers/{supplier_code}/issues/{issue_id} | GET | getIssue | 2.0.0 |
/v2.0.0/suppliers/{supplier_code}/service-methods | GET | getServiceMethods | 2.0.0 |
Service code in our credentials/usage system: ODRSTAT.
2. Authentication & prerequisites
- Auth: API key (
X-API-Key), OAuth2 Bearer, or HTTP Basic — all three work on these endpoints. - Supplier credentials: Each supplier requires PromoStandards
ODRSTATcredentials configured under your account before you can call the endpoints. Set them via the Credentials API. - Environment: defaults to
PROD. Pass?environment=STAGINGto hit the supplier’s sandbox. - Base URL:
https://api.psrestful.com
If credentials are missing or the supplier blocks your account you’ll see 401/403 from the credential check before any SOAP call leaves our system.
3. Query types — pick the right one
order-status-details and order-status accept a required query_type:
query_type | Meaning | reference_number | status_timestamp | Use when |
|---|---|---|---|---|
PO_SEARCH | Search by Purchase Order Number | required | — | You have a single, specific PO. |
SO_SEARCH | Search by Sales (factory) Order Number | required | — | You have the supplier’s SO number. |
ALL_OPEN_SEARCH | All orders not in Complete / Canceled | — | — | First-time backfill. |
LAST_UPDATE_SEARCH | Orders updated since status_timestamp (UTC) | — | required | Incremental sync (recommended). |
⚠️ Anti-pattern: Looping
PO_SEARCHover your open-order list to “refresh statuses” will get you rate-limited, starve the supplier’s SOAP backend, and the data is stale by the time you finish. UseLAST_UPDATE_SEARCH.
Why LAST_UPDATE_SEARCH wins
A 4-row worked example for a typical merchant with 5,000 open orders across 4 suppliers, polling hourly:
| Strategy | Calls per hour | Calls per day | Likely outcome |
|---|---|---|---|
PO_SEARCH per open order, sequentially | 5,000 | 120,000 | Throttled within minutes; never finishes. |
PO_SEARCH per order, parallelized | 5,000 | 120,000 | Same total cost; supplier complaints. |
ALL_OPEN_SEARCH per supplier | 4 | 96 | Works, but transfers the whole open book every tick. |
LAST_UPDATE_SEARCH per supplier | 4 | 96 | Only changed orders move; ~10–100 KB per call. |
4. Endpoints
4.1 GET /v1.0.0/suppliers/{supplier_code}/order-status-details
Wraps PromoStandards getOrderStatusDetails.
Path params
| Name | Type | Description |
|---|---|---|
supplier_code | string | Supplier code (e.g., GEM, HIT, PCNA, SanMar). |
Query params
| Name | Type | Required | Description |
|---|---|---|---|
query_type | enum | yes | PO_SEARCH · SO_SEARCH · LAST_UPDATE_SEARCH · ALL_OPEN_SEARCH |
reference_number | string | cond. | Required for PO_SEARCH / SO_SEARCH. |
status_timestamp | datetime | cond. | Required for LAST_UPDATE_SEARCH. Send as ISO-8601 UTC. |
environment | enum | no | PROD (default) · STAGING. |
Example — incremental sync
curl -X GET "https://api.psrestful.com/v1.0.0/suppliers/GEM/order-status-details?query_type=LAST_UPDATE_SEARCH&status_timestamp=2026-04-16T13:45:00Z" \
-H "X-API-Key: $PSRESTFUL_API_KEY"Response (200 OK, abbreviated)
{
"OrderStatusArray": {
"OrderStatus": [
{
"purchaseOrderNumber": "PO-100432",
"OrderStatusDetailArray": {
"OrderStatusDetail": [
{
"factoryOrderNumber": "GEM-77821",
"statusID": 60,
"statusName": "In Production",
"responseRequired": false,
"validTimestamp": "2026-04-16T17:02:11Z",
"expectedShipDate": "2026-04-22T00:00:00Z",
"expectedDeliveryDate": "2026-04-26T00:00:00Z",
"additionalExplanation": null
}
]
}
}
]
},
"errorMessage": null
}Errors — 401/403 (credentials), 502/504 (supplier upstream), 429 (rate limit).
4.2 GET /v1.0.0/suppliers/{supplier_code}/order-status-types
Wraps PromoStandards getOrderStatusTypes. Returns the supplier’s {id, name} status dictionary used to interpret statusID from getOrderStatusDetails.
curl -X GET "https://api.psrestful.com/v1.0.0/suppliers/GEM/order-status-types" \
-H "X-API-Key: $PSRESTFUL_API_KEY"Response (200 OK)
{
"StatusArray": {
"Status": [
{"id": 10, "name": "Order Received"},
{"id": 20, "name": "Order Confirmed"},
{"id": 60, "name": "In Production"},
{"id": 75, "name": "Partial Shipment"},
{"id": 80, "name": "Complete"}
]
}
}Resolve names dynamically via this endpoint instead of hard-coding the standard table — suppliers occasionally add custom IDs.
4.3 GET /v2.0.0/suppliers/{supplier_code}/order-status
Wraps PromoStandards getOrderStatus (v2.0.0). Same query_type semantics as v1.0.0, but each OrderStatusDetail may include IssueArray, ProductArray, OrderContactArray, and an auditURL for support escalation. status is now a string enum.
curl -X GET "https://api.psrestful.com/v2.0.0/suppliers/PCNA/order-status?query_type=LAST_UPDATE_SEARCH&status_timestamp=2026-04-16T13:45:00Z" \
-H "X-API-Key: $PSRESTFUL_API_KEY"4.4 GET /v2.0.0/suppliers/{supplier_code}/issues/{issue_id}
Wraps PromoStandards getIssue. Fetches full detail for an order on hold.
curl -X GET "https://api.psrestful.com/v2.0.0/suppliers/PCNA/issues/ISS-2099" \
-H "X-API-Key: $PSRESTFUL_API_KEY"issueCategory will be one of: Order Entry Hold, General Hold, Credit Hold, Proof Hold, Art Hold, Back Order Hold, Shipping Hold, Customer Supplied Item Hold. issueStatus is Pending · Open · Closed.
4.5 GET /v2.0.0/suppliers/{supplier_code}/service-methods
Wraps PromoStandards getServiceMethods. Discovery endpoint — returns which v2.0.0 methods this supplier actually implements. Call once per supplier on startup and cache the result.
5. Status reference (v1.0.0)
Standard statusID values defined by PromoStandards:
| ID | Name | Notes |
|---|---|---|
| 10 | Order Received | Acknowledged but not yet entered. |
| 11 | Order Entry Hold | Action needed. |
| 20 | Order Confirmed | |
| 30 | Pre-Production | |
| 40 | General Hold | Action needed. |
| 41 | Credit Hold | Action needed. |
| 42 | Proof Hold | Action needed. |
| 43 | Art Hold | Action needed. |
| 44 | Back Order Hold | Action needed. |
| 60 | In Production | |
| 70 | In Storage | |
| 75 | Partial Shipment | Tracking lives in OSN — see §8. |
| 80 | Complete | Tracking lives in OSN — see §8. |
| 99 | Canceled | Terminal. |
In v2.0.0 the equivalent string enum is: received, confirmed, preproduction, inProduction, inStorage, partiallyShipped, shipped, complete, canceled.
6. Recommended sync pattern (pseudo-code)
The pattern below is the one we recommend for any merchant with more than a handful of open orders.
# Conceptual sketch — adapt to your ORM / order store.
import httpx
from datetime import datetime, timezone, timedelta
API = "https://api.psrestful.com"
API_KEY = "<your api key>"
SUPPLIERS = ["GEM", "HIT", "PCNA", "SanMar"]
OVERLAP = timedelta(minutes=10) # absorb supplier clock skew
def last_synced_at(supplier_code: str) -> datetime | None:
"""Return cursor for this supplier, or None on first run."""
row = db.fetch_one(
"SELECT last_synced_at FROM order_sync_cursor WHERE supplier = :s",
{"s": supplier_code},
)
return row["last_synced_at"] if row else None
def save_cursor(supplier_code: str, ts: datetime) -> None:
db.execute(
"INSERT INTO order_sync_cursor(supplier, last_synced_at) VALUES (:s, :t) "
"ON CONFLICT (supplier) DO UPDATE SET last_synced_at = :t",
{"s": supplier_code, "t": ts},
)
def upsert_order_status(supplier_code: str, po: str, detail: dict) -> None:
"""
detail fields (v1.0.0): factoryOrderNumber, statusID, statusName, validTimestamp,
expectedShipDate, expectedDeliveryDate, responseRequired, additionalExplanation,
ResponseToArray.
"""
db.execute(
"""
INSERT INTO order_status (supplier, po, factory_order, status_id,
status_name, valid_ts, expected_ship,
expected_delivery, needs_response, note)
VALUES (:sup, :po, :fo, :sid, :sn, :vt, :es, :ed, :nr, :n)
ON CONFLICT (supplier, po, factory_order) DO UPDATE SET
status_id = EXCLUDED.status_id,
status_name = EXCLUDED.status_name,
valid_ts = EXCLUDED.valid_ts,
expected_ship = EXCLUDED.expected_ship,
expected_delivery = EXCLUDED.expected_delivery,
needs_response = EXCLUDED.needs_response,
note = EXCLUDED.note
WHERE EXCLUDED.valid_ts >= order_status.valid_ts
""",
{
"sup": supplier_code, "po": po,
"fo": detail["factoryOrderNumber"],
"sid": detail["statusID"],
"sn": detail.get("statusName"),
"vt": detail["validTimestamp"],
"es": detail.get("expectedShipDate"),
"ed": detail.get("expectedDeliveryDate"),
"nr": detail.get("responseRequired", False),
"n": detail.get("additionalExplanation"),
},
)
def sync_supplier(client: httpx.Client, supplier_code: str) -> None:
cursor = last_synced_at(supplier_code)
run_started = datetime.now(tz=timezone.utc)
if cursor is None:
# First run: backfill every currently-open order, then flip to incremental.
params = {"query_type": "ALL_OPEN_SEARCH"}
else:
params = {
"query_type": "LAST_UPDATE_SEARCH",
"status_timestamp": cursor.isoformat(),
}
r = client.get(
f"{API}/v1.0.0/suppliers/{supplier_code}/order-status-details",
params=params,
headers={"X-API-Key": API_KEY},
timeout=120,
)
r.raise_for_status()
payload = r.json()
orders = (payload.get("OrderStatusArray") or {}).get("OrderStatus") or []
for order in orders:
po = order["purchaseOrderNumber"]
for detail in order["OrderStatusDetailArray"]["OrderStatusDetail"]:
upsert_order_status(supplier_code, po, detail)
# Only advance cursor after a successful batch.
# Subtract OVERLAP so the next tick re-pulls a small overlapping window.
save_cursor(supplier_code, run_started - OVERLAP)
with httpx.Client() as client:
for code in SUPPLIERS:
try:
sync_supplier(client, code)
except Exception as e:
log.exception("order-status sync failed for %s: %s", code, e)
# Do NOT advance the cursor — next run re-tries the same window.Cursor-safety checklist
These four small things separate a sync that works on day one from one that works on day 90:
- Advance the cursor only on success. A network blip or
502from the supplier should leave the cursor where it was so the next tick re-tries. - Idempotent upsert. Key on
(supplier, purchaseOrderNumber, factoryOrderNumber)and only overwrite when the incomingvalidTimestampis newer. Retries and overlap windows then become safe. - Always send UTC.
status_timestampis timezone-sensitive and suppliers interpret naive datetimes inconsistently. Always send ISO-8601 withZ(or+00:00). - Overlap window. Subtract 5–10 minutes from
run_startedbefore saving the cursor to absorb clock skew between you, us, and the supplier. The idempotent upsert (#2) makes the duplicates harmless.
7. Polling cadence
| Order volume | Suggested cadence |
|---|---|
| High (1k+ open POs) | every 15 minutes |
| Typical | every hour |
| Low / batch back-office | every 4–6 hours |
Avoid sub-5-minute polls — most suppliers throttle ODRSTAT aggressively, and the underlying order state rarely changes that fast.
8. Handling held orders
Any v1.0.0 row whose statusID is one of 11, 40, 41, 42, 43, 44 (or any v2.0.0 row whose issueCategory is non-null) needs merchant action. Surface them in a dashboard rather than letting them rot.
- Check
responseRequired == trueand theResponseToArrayto find out who at the supplier you should email/call. - On v2.0.0, follow
IssueArray[].issue_idinto/v2.0.0/suppliers/{code}/issues/{issue_id}for the full issue payload (history, notes, etc.). - The v2.0.0
auditURLis a human-readable supplier link — surface it to your CS team for one-click escalation.
9. Capability probing (v2.0.0 only)
Before writing v2.0.0-specific code paths for a supplier, call getServiceMethods once on startup and cache the result. Some suppliers publish v2.0.0 but only implement a subset (e.g., getOrderStatus but not getIssue).
curl -X GET "https://api.psrestful.com/v2.0.0/suppliers/HIT/service-methods" \
-H "X-API-Key: $PSRESTFUL_API_KEY"10. Cross-references
- Tracking numbers — once
statusID == 75(Partial Shipment) or80(Complete), tracking numbers live in the Order Shipment Notification (OSN) service, not inODRSTAT. Sync the two together. - Webhooks — PromoStandards ODRSTAT is pull-only. There are no push notifications. If a supplier offers webhooks via a non-PromoStandards channel, prefer those.
- Spec reference — Order Status 1.0.0 · Order Status 2.0.0
11. v1.0.0 vs v2.0.0 — which should I use?
| Choose | When |
|---|---|
| v1.0.0 | You’re integrating a supplier that only publishes v1.0.0 (most still do), or you already key your downstream system off the numeric statusID. |
| v2.0.0 | The supplier supports it (verify with getServiceMethods) and you want richer issue tracking, product detail, contact info, or the auditURL. |
A common pattern is to call v1.0.0 for the bulk sync and v2.0.0 only for the held-order subset where the richer issue payload pays for itself.
12. Error handling & rate limits
| Status | Meaning |
|---|---|
200 | OK. |
401 | Missing or invalid PSRESTful credentials. |
403 | Authenticated but no access to this supplier’s ODRSTAT. |
429 | Rate limit. Honor Retry-After and back off. |
502/504 | Supplier upstream timeout — retry with backoff. |
500 | Internal error. Safe to retry idempotently. |
Wrap each per-supplier sync in its own try/except so one supplier’s outage never blocks the others.
Service Code: ODRSTAT