Base URL: https://www.promogator.com/api/on-demand/v1 · JSON over HTTPS · Authorization: Bearer <access token> on every call except /oauth/token.
| Method | Path | Scope | What it does |
|---|---|---|---|
| POST | /oauth/token |
key (HTTP Basic) | Get an access token |
| GET | /account |
any | Your account, limits and this key |
| GET | /catalog/products |
catalog:read |
List your products |
| GET | /catalog/products/{id} |
catalog:read |
One product with its variants |
| GET | /catalog/variants/{od_sku} |
catalog:read |
One variant by our SKU |
| POST | /quotes |
catalog:read |
Price an order without placing it |
| GET | /quotes/{id} |
catalog:read |
A quote |
| POST | /orders |
orders:write |
Place an order |
| GET | /orders |
orders:read |
List your orders |
| GET | /orders/{id} |
orders:read |
One order |
| GET | /orders/by-external-id/{external_id} |
orders:read |
One order by your order number |
| GET | /orders/{id}/shipments |
orders:read |
An order's packages and tracking |
| GET | /orders/{id}/invoice |
orders:read |
An order's invoice (live orders) |
| POST | /orders/{id}/cancel |
orders:write |
Cancel an order |
| GET, POST | /webhook-endpoints |
webhooks:manage |
List or add webhook endpoints |
| PATCH, DELETE | /webhook-endpoints/{id} |
webhooks:manage |
Change or remove an endpoint |
| POST | /webhook-endpoints/{id}/test |
webhooks:manage |
Send a test event |
| POST | /webhook-endpoints/{id}/rotate-secret |
webhooks:manage |
New signing secret |
| GET | /events, /events/{id} |
webhooks:manage |
Your recent events |
Conventions
- Ids are opaque strings with a prefix:
oda_account,odp_product,odq_quote,odo_order,shp_shipment,inv_invoice,we_webhook endpoint,evt_event. Test-mode quotes and orders start withtest_. - Money is a string with two decimals in US dollars, for example
"16.95". - Times are ISO 8601 in UTC, for example
"2026-10-18T09:30:00Z". Dates areYYYY-MM-DD. - Lists return
{"object": "list", "data": [...], "has_more": true|false}. Passlimit(1–100, default 20) and, for the next page,starting_afterwith the last id you received. - Request ids: every response has an
X-Request-IDheader (req_…). Send your ownX-Request-IDand we echo it back asX-Client-Request-ID. Quote the request id when you contact us. - Errors are
application/problem+jsonwith a stablecode; see Errors, limits and idempotency. - Unknown fields in a request body are refused (
422 validation_failed), so typos never pass silently.
Authentication
POST /oauth/token
Exchange an API key for an access token. Send the Access Key ID and Secret Key with HTTP Basic; the body is grant_type=client_credentials (form-encoded or JSON) and optionally scope (space-separated, a subset of the key's scopes).
curl -X POST https://www.promogator.com/api/on-demand/v1/oauth/token \
-u "$ACCESS_KEY_ID:$SECRET_KEY" -d grant_type=client_credentials
{ "access_token": "pgod_at_…", "token_type": "Bearer", "expires_in": 3600, "scope": "catalog:read orders:read orders:write" }
Errors use the OAuth format {"error": "...", "error_description": "..."}: invalid_request, unsupported_grant_type, invalid_client (401), invalid_scope, slow_down (429, more than 20 tokens a minute). See Authentication and keys.
GET /account
{
"object": "account",
"id": "oda_01JB…",
"name": "Summit Team Stores",
"status": "active",
"livemode": false,
"billing": { "method": "card" },
"limits": { "max_order_value": "2500.00", "orders_per_day": 50 },
"api_key": { "id": "pgod_test_ak_…", "name": "Shopify sync", "env": "test", "scopes": ["catalog:read", "orders:read", "orders:write"], "expires_at": null, "probation": null, "last_used_at": "2026-10-18T09:12:44Z" }
}
billing.method is card or terms. probation is set while a new live key is on probation (see Going live).
Catalog
GET /catalog/products
Your products, sorted by product code. Query: limit, starting_after.
Each item is a product with net_price_range (min, max, currency) and variant_count instead of the full variant list.
GET /catalog/products/{id}
One product with variants: one variant per colour and size, each with od_sku, partner_sku, net_price, suggested_retail_price and orderable.
GET /catalog/variants/{od_sku}
One variant by our SKU (for example OD-000125-D1001-BLK-L). Returns 404 not_found if the SKU is not on your product list.
Quotes
POST /quotes
Prices an order without placing it. Body:
| Field | Type | |
|---|---|---|
recipient |
object | Required. See Recipient |
items |
array | Required, 1–30 lines. See Items |
notes |
string | Optional |
Returns 201 with a quote: items at your NET price, shipping, tax_details and totals. A quote is valid for 72 hours (expires_at). It does not reserve anything: the order is priced again when you place it.
If the order cannot be priced (a SKU that is not yours, an address outside the US, a quantity under the minimum…), you get 422 validation_failed with each problem in errors[].detail.
GET /quotes/{id}
A quote you created, with status open or expired.
Orders
POST /orders
Places an order. Headers:
| Header | |
|---|---|
Idempotency-Key |
Required. Unique per order, up to 255 characters (for example your store's order id). See Idempotency |
Content-Type |
application/json |
Body:
| Field | Type | |
|---|---|---|
external_id |
string | Required. Your order number, 1–100 characters, unique in your account |
recipient |
object | Required. See Recipient |
items |
array | Required. 1–30 lines. See Items |
metadata |
object | Optional. Up to 20 keys of your own; returned on the order |
requested_in_hands_date |
date | Optional YYYY-MM-DD. A note for our team, not a promise |
notes |
string | Optional note for our team |
Recipient
| Field | |
|---|---|
name |
Required |
company |
Optional |
address1 |
Required |
address2 |
Optional (apartment, suite) |
city |
Required |
state |
Required, 2-letter code |
postal_code |
Required |
country |
Optional, US only for now |
phone |
Optional, for the carrier only. We never contact your customer |
Items
| Field | |
|---|---|
od_sku or partner_sku |
Required: our SKU or your own SKU for your designs |
quantity |
Required, 1 or more |
retail_price |
Optional: your selling price for one piece. Used for California sales tax; if left out we use NET + 10% |
external_line_id |
Optional: your line id, returned on the line |
Put each SKU on one line only (add up the quantity).
Responses
201 Created: the order. Live orders are charged to your card (card billing) or invoiced (terms). Test orders areacceptedand never made.200 OKwith headerIdempotent-Replayed: true: you sent the same request with the sameIdempotency-Keyagain; this is the order you already placed.409 idempotency_conflict: theIdempotency-Keywas used with a different body.409 duplicate_external_id: an order with thisexternal_idexists; fetch it with/orders/by-external-id/{external_id}.402: payment failed (card billing):card_declined(withdecline_code),authentication_required,payment_method_requiredorpayment_failed. No order was created.422 validation_failed: the order cannot be priced as sent; seeerrors[].detail.422 limit_exceeded: a new live key's probation limit (3 orders or $500.00 in its first 24 hours).
GET /orders
Your orders, newest first. Query: limit, starting_after. A test key lists test orders; a live key lists live orders.
GET /orders/{id}
One order.
GET /orders/by-external-id/{external_id}
One order by your order number. URL-encode it (%23 for #).
GET /orders/{id}/shipments
The order's packages: {"object": "list", "data": [shipment, …], "has_more": false}. See Shipment. Test orders have no shipments.
GET /orders/{id}/invoice
The order's invoice: number, status, total, amount paid, credits and balance. Live orders only (404 for test orders).
POST /orders/{id}/cancel
Cancels an order. Body (optional): {"reason": "Customer changed their mind"}.
An order can be cancelled within 24 hours of being placed, as long as production has not started. Returns the cancelled order (and sends order.cancelled), or 409 not_cancellable with the reason in detail.
Webhook endpoints
See Webhooks for events and signatures. Endpoints belong to the mode of the key that creates them (test or live).
POST /webhook-endpoints
Body: url (required, https:// on a public host, up to 500 characters) and events (optional, default all: order.created, order.shipped, order.cancelled). Returns the endpoint with its secret (whsec_…), shown only in this response. Up to 5 endpoints per mode.
GET /webhook-endpoints
Your endpoints in this mode, each with status (enabled or disabled), disabled_reason and last_delivery (status, response_code, at).
PATCH /webhook-endpoints/{id}
Change url, events or status (enabled / disabled).
DELETE /webhook-endpoints/{id}
Removes the endpoint.
POST /webhook-endpoints/{id}/test
Sends a ping event right away and returns {"object": "webhook_test", "event_id", "delivered", "response_code", "error", "duration_ms"}.
POST /webhook-endpoints/{id}/rotate-secret
Returns the endpoint with a new secret.
Events
GET /events
Your recent events in this mode, newest first, in the same shape as webhook bodies. Query: limit.
GET /events/{id}
One event by its evt_ id (the webhook-id header of a delivery).