GuidesOrder StatusOrder Status API Guide

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_SEARCH across your order book. Run one LAST_UPDATE_SEARCH call 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 endpointHTTPPromoStandards methodVersion
/v1.0.0/suppliers/{supplier_code}/order-status-detailsGETgetOrderStatusDetails1.0.0
/v1.0.0/suppliers/{supplier_code}/order-status-typesGETgetOrderStatusTypes1.0.0
/v2.0.0/suppliers/{supplier_code}/order-statusGETgetOrderStatus2.0.0
/v2.0.0/suppliers/{supplier_code}/issues/{issue_id}GETgetIssue2.0.0
/v2.0.0/suppliers/{supplier_code}/service-methodsGETgetServiceMethods2.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 ODRSTAT credentials configured under your account before you can call the endpoints. Set them via the Credentials API.
  • Environment: defaults to PROD. Pass ?environment=STAGING to 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_typeMeaningreference_numberstatus_timestampUse when
PO_SEARCHSearch by Purchase Order NumberrequiredYou have a single, specific PO.
SO_SEARCHSearch by Sales (factory) Order NumberrequiredYou have the supplier’s SO number.
ALL_OPEN_SEARCHAll orders not in Complete / CanceledFirst-time backfill.
LAST_UPDATE_SEARCHOrders updated since status_timestamp (UTC)requiredIncremental sync (recommended).

⚠️ Anti-pattern: Looping PO_SEARCH over 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. Use LAST_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:

StrategyCalls per hourCalls per dayLikely outcome
PO_SEARCH per open order, sequentially5,000120,000Throttled within minutes; never finishes.
PO_SEARCH per order, parallelized5,000120,000Same total cost; supplier complaints.
ALL_OPEN_SEARCH per supplier496Works, but transfers the whole open book every tick.
LAST_UPDATE_SEARCH per supplier496Only 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

NameTypeDescription
supplier_codestringSupplier code (e.g., GEM, HIT, PCNA, SanMar).

Query params

NameTypeRequiredDescription
query_typeenumyesPO_SEARCH · SO_SEARCH · LAST_UPDATE_SEARCH · ALL_OPEN_SEARCH
reference_numberstringcond.Required for PO_SEARCH / SO_SEARCH.
status_timestampdatetimecond.Required for LAST_UPDATE_SEARCH. Send as ISO-8601 UTC.
environmentenumnoPROD (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
}

Errors401/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:

IDNameNotes
10Order ReceivedAcknowledged but not yet entered.
11Order Entry HoldAction needed.
20Order Confirmed
30Pre-Production
40General HoldAction needed.
41Credit HoldAction needed.
42Proof HoldAction needed.
43Art HoldAction needed.
44Back Order HoldAction needed.
60In Production
70In Storage
75Partial ShipmentTracking lives in OSN — see §8.
80CompleteTracking lives in OSN — see §8.
99CanceledTerminal.

In v2.0.0 the equivalent string enum is: received, confirmed, preproduction, inProduction, inStorage, partiallyShipped, shipped, complete, canceled.


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:

  1. Advance the cursor only on success. A network blip or 502 from the supplier should leave the cursor where it was so the next tick re-tries.
  2. Idempotent upsert. Key on (supplier, purchaseOrderNumber, factoryOrderNumber) and only overwrite when the incoming validTimestamp is newer. Retries and overlap windows then become safe.
  3. Always send UTC. status_timestamp is timezone-sensitive and suppliers interpret naive datetimes inconsistently. Always send ISO-8601 with Z (or +00:00).
  4. Overlap window. Subtract 5–10 minutes from run_started before 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 volumeSuggested cadence
High (1k+ open POs)every 15 minutes
Typicalevery hour
Low / batch back-officeevery 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 == true and the ResponseToArray to find out who at the supplier you should email/call.
  • On v2.0.0, follow IssueArray[].issue_id into /v2.0.0/suppliers/{code}/issues/{issue_id} for the full issue payload (history, notes, etc.).
  • The v2.0.0 auditURL is 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) or 80 (Complete), tracking numbers live in the Order Shipment Notification (OSN) service, not in ODRSTAT. 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 referenceOrder Status 1.0.0 · Order Status 2.0.0

11. v1.0.0 vs v2.0.0 — which should I use?

ChooseWhen
v1.0.0You’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.0The 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

StatusMeaning
200OK.
401Missing or invalid PSRESTful credentials.
403Authenticated but no access to this supplier’s ODRSTAT.
429Rate limit. Honor Retry-After and back off.
502/504Supplier upstream timeout — retry with backoff.
500Internal 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