Request access

Integration guide · Quickstart · Reference · Catalog API

POD Mfg Partner API

Version v1 · Base URL https://app.podmf.com/api/v1

The POD Mfg Partner API lets a brand that doesn't sell on Shopify connect its own system: read your published products and variants (with your retail and wholesale prices), map your own SKUs, send orders (we charge your card on file and make them exactly like a Shopify order), and receive signed webhooks when orders and products change.

Phase 1: GET /me, GET /products, GET /products/{id}, GET /variants/{id}, PATCH /variants/{id} (external_sku only). Phase 2: POST /orders, GET /orders, GET /orders/{id}, POST /orders/estimate, POST /orders/{id}/cancel, POST /orders/{id}/retry_payment. Phase 3: /webhook_endpoints (create, list, get, update, delete, rotate the secret), GET /events, GET /events/{id}, POST /events/{id}/resend, and the webhooks themselves (see webhooks). Phase 4 (this version of the spec): test-mode simulations POST /orders/{id}/simulate/ship, …/simulate/payment_failed, …/simulate/cancel.

Start here: the quickstart "Your first order in 15 minutes" at https://podmf.com/docs/partner-api-quickstart (test key, products, order, webhook, signature check).

Webhooks. Add an endpoint (here or on the Developers page) and pick the events it wants (["*"] = all). Live endpoints must be https; every endpoint must be a public address (no private, loopback, link-local or internal hosts; redirects are not followed). Each delivery is a POST of the event envelope { id, type, created_at, livemode, api_version, data } with PODMFG-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" keyed with your endpoint's secret>, PODMFG-Event-Id and PODMFG-Event-Type. Verify the signature over the raw body and reject a t more than 5 minutes off (samples: https://podmf.com/docs/partner-api-quickstart#verify). Answer any 2xx within 10 seconds; anything else is retried after 1 m, 5 m, 30 m, 2 h, 6 h, 12 h, 24 h and 24 h (9 tries, about 3 days). After that the delivery has failed: we email you and it shows on the Developers page, where you (or POST /events/{id}/resend) can send it again. An endpoint whose deliveries fail for good 5 times in a row is turned off (we email you). Delivery is at least once: dedupe on the event id. Order isn't guaranteed: use order_status and the times in data. Test-mode events (test orders) go only to test endpoints; product events are sent in both modes. Events are kept 30 days (GET /events to backfill).

Orders. A live order is charged at once (wholesale + shipping + tax) to the card you saved on the Developers page, then goes to production. A declined card leaves the order payment_failed; we email your billing contact and retry automatically after 1, 6 and 24 hours, then hold it (on_hold, payment_failed_final): fix the card and call retry_payment. Nothing unpaid is ever made. A test-key order is never charged and never made: its payment is simulated (status: paid, charge.status: simulated); the simulate endpoints (Sandbox) then ship it, fail its payment or cancel it, with the same webhooks a live order gets. Send Idempotency-Key on every POST: a retry with the same key and body returns the first answer (Idempotent-Replayed: true) and never makes a second order or charge. external_order_id is unique per brand and mode (409 duplicate_external_order_id). US addresses only (422 country_not_supported); an address we can't ship to is 422 address_invalid; one a person should check (no house number, territories, APO/FPO) is accepted and held (on_hold, address_review); a blank out of stock at the supplier holds the order (blank_out_of_stock). Sales tax is not charged in v1 (tax: 0).

Keys. Make keys on the Developers page in POD Mfg. Send one as Authorization: Bearer podmfg_test_… (test mode: orders are never charged or produced) or podmfg_live_…. A key is shown once; we keep only a hash. Keys made for the Catalog API (podk_…) keep working here with the products:read scope.

Conventions. JSON only. Times are ISO 8601 UTC. Money is integer cents with a currency (USD). Ids are prefixed strings (prd_, var_, ord_, shp_, evt_, whk_, mer_, con_). Lists are cursor-paginated: ?limit=50&starting_after=<id> → { data, has_more, next_cursor }. Every response carries X-Request-Id; include it when you contact us. Rate limit: 10 requests a second per key, bursts of 50 (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After on 429). Within v1, changes are additive only: ignore fields you don't know.

Downloads: OpenAPI 3.1 (YAML) · OpenAPI (JSON) · Postman collection (Bruno and Insomnia import it too; set apiKey to your test key).

Account

GET /me

Who this key is (a connection test)

Your brand, the key's mode and scopes, and whether a card is on file.

Answers Merchant

  • 200 The merchant behind the key.
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

Products

GET /products

List published products

Your published products, newest first, each with its variants. Needs products:read. Unknown query parameters are refused (400 parameter_unknown).

ParameterInTypeNotes
limit query integer

Page size, 1 to 100.

starting_after query string

The next_cursor of the previous page (the last id you got).

updated_since query string

Only products changed at or after this time (ISO 8601).

status query string

Only published is available.

Answers ProductList

  • 200 A page of products.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

GET /products/{id}

One published product

The product with its variants, mockup images and prices. Needs products:read.

ParameterInTypeNotes
id (required) path string

Answers Product

  • 200 The product.
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

GET /variants/{id}

One variant

A variant of a published product. Needs products:read.

ParameterInTypeNotes
id (required) path string

Answers Variant

  • 200 The variant.
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

PATCH /variants/{id}

Set your own SKU for a variant

The only change allowed on products in v1. external_sku is unique among your variants (409 external_sku_taken); null clears it. Orders may name a variant by its id, our sku or your external_sku. Needs orders:write.

ParameterInTypeNotes
id (required) path string

Body object

FieldTypeNotes
external_sku (required) string | null

Printable ASCII; surrounding spaces are trimmed. At most 100 characters.

{
  "external_sku": "TEAM-TEE-NAVY-M"
}

Answers Variant

  • 200 The updated variant.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 409 The change clashes with existing data or the order's state (external_sku_taken, duplicate_external_order_id, idempotency_key_reused, idempotency_key_in_use, order_not_cancellable, charge_in_progress, order_not_payable, test_mode, order_state, too_many_webhook_endpoints, no_webhook_endpoint).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

Orders

GET /orders

List orders

Your orders in the key's mode (a test key sees test orders only), newest first. Needs orders:read.

ParameterInTypeNotes
limit query integer

Page size, 1 to 100.

starting_after query string

The next_cursor of the previous page (the last id you got).

status query OrderStatus
external_order_id query string
created_since query string

Only orders created at or after this time (ISO 8601).

Answers OrderList

  • 200 A page of orders.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

POST /orders

Create an order

Needs orders:write. Each line names exactly one of variant_id, sku (ours) or external_sku (yours); unknown fields are refused. Answers 201 with the order, its status and the charge result: a live order is charged now (charge.status succeeded → status: paid; failed → status: payment_failed, retried automatically; pending → Stripe is still working on it). A held order (on_hold) isn't charged until we release it.

ParameterInTypeNotes
Idempotency-Key header string

Any unique string (a UUID works), 1 to 255 printable characters. Kept 24 hours per brand and mode. Same key + same body → the first answer again; same key + another body → 409 idempotency_key_reused; while the first is still running → 409 idempotency_key_in_use. A request that failed frees its key.

Body OrderCreate

{
  "external_order_id": "1042",
  "external_order_url": "https://merchant-admin.example.com/orders/1042",
  "line_items": [
    {
      "external_line_id": "li_1",
      "quantity": 2,
      "retail_price": 2800,
      "variant_id": "var_31xaQ8dZr0mJxWb7fKp2sT"
    }
  ],
  "metadata": {
    "anything": "merchant wants echoed back"
  },
  "packing_slip": {
    "message": "Thanks for supporting the team!",
    "show_prices": false
  },
  "ship_to": {
    "address1": "123 Main St",
    "address2": "Apt 4",
    "city": "Irvine",
    "company": null,
    "country": "US",
    "email": "jane@example.com",
    "name": "Jane Smith",
    "phone": "+19495551234",
    "postal_code": "92618",
    "state": "CA"
  },
  "shipping_method": "standard"
}

Answers Order

  • 201 The order.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 409 The change clashes with existing data or the order's state (external_sku_taken, duplicate_external_order_id, idempotency_key_reused, idempotency_key_in_use, order_not_cancellable, charge_in_progress, order_not_payable, test_mode, order_state, too_many_webhook_endpoints, no_webhook_endpoint).
  • 422 Well-formed but can't be done (address_invalid, country_not_supported, variant_not_found, sku_not_found, external_sku_not_found, variant_not_available, price_unavailable, shipping_method_unavailable, personalization_unsupported, amount_too_small, url_not_allowed).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.
  • 503 Payments, shipping quotes or webhook set-up are offline for a moment (payments_unavailable, shipping_unavailable, webhooks_unavailable). Retry with the same Idempotency-Key.

POST /orders/estimate

Price an order without making it

The same body as POST /orders (external_order_id optional). Answers the wholesale total, shipping and tax, and whether the order would be held. Nothing is stored or charged. Needs orders:write or orders:read.

Body OrderEstimateRequest

{
  "line_items": [
    {
      "quantity": 12,
      "sku": "PMG-ST350-NAV-M-8F2KA19C0D"
    }
  ],
  "ship_to": {
    "address1": "123 Main St",
    "city": "Irvine",
    "country": "US",
    "name": "Jane Smith",
    "postal_code": "92618",
    "state": "CA"
  },
  "shipping_method": "standard"
}

Answers OrderEstimate

  • 200 The estimate.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 422 Well-formed but can't be done (address_invalid, country_not_supported, variant_not_found, sku_not_found, external_sku_not_found, variant_not_available, price_unavailable, shipping_method_unavailable, personalization_unsupported, amount_too_small, url_not_allowed).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.
  • 503 Payments, shipping quotes or webhook set-up are offline for a moment (payments_unavailable, shipping_unavailable, webhooks_unavailable). Retry with the same Idempotency-Key.

GET /orders/{id}

One order

Status, the price snapshot, charges, refunds and shipments. Needs orders:read.

ParameterInTypeNotes
id (required) path string

Answers Order

  • 200 The order.
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

POST /orders/{id}/cancel

Cancel an order before production

Allowed until the order is in_production; a paid order is refunded in full. Cancelling a cancelled order answers it again. After production starts: 409 order_not_cancellable (contact us). Needs orders:write.

ParameterInTypeNotes
id (required) path string
Idempotency-Key header string

Any unique string (a UUID works), 1 to 255 printable characters. Kept 24 hours per brand and mode. Same key + same body → the first answer again; same key + another body → 409 idempotency_key_reused; while the first is still running → 409 idempotency_key_in_use. A request that failed frees its key.

Body object

FieldTypeNotes
reason string

At most 200 characters.

Answers Order

  • 200 The cancelled order (with its refund).
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 409 The change clashes with existing data or the order's state (external_sku_taken, duplicate_external_order_id, idempotency_key_reused, idempotency_key_in_use, order_not_cancellable, charge_in_progress, order_not_payable, test_mode, order_state, too_many_webhook_endpoints, no_webhook_endpoint).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

POST /orders/{id}/retry_payment

Charge a failed order again

For a live order that is payment_failed, or on_hold with payment_failed_final (e.g. after you saved a new card). Answers the paid order, or 402 payment_failed when the card is declined again. Test orders: a simulated failure (POST /orders/{id}/simulate/payment_failed) retries into simulated success (order.paid); any other test order is 409 test_mode. Needs orders:write.

ParameterInTypeNotes
id (required) path string
Idempotency-Key header string

Any unique string (a UUID works), 1 to 255 printable characters. Kept 24 hours per brand and mode. Same key + same body → the first answer again; same key + another body → 409 idempotency_key_reused; while the first is still running → 409 idempotency_key_in_use. A request that failed frees its key.

Answers Order

  • 200 The order after the charge.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 402 The card was declined again (payment_error, payment_failed). The order's charge says why.
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 409 The change clashes with existing data or the order's state (external_sku_taken, duplicate_external_order_id, idempotency_key_reused, idempotency_key_in_use, order_not_cancellable, charge_in_progress, order_not_payable, test_mode, order_state, too_many_webhook_endpoints, no_webhook_endpoint).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

Webhooks

GET /events

List events

The events of the key's mode, newest first, exactly as they were (or would have been) sent: to poll, or to backfill after your endpoint was down. Kept 30 days. Needs orders:read.

ParameterInTypeNotes
limit query integer

Page size, 1 to 100.

starting_after query string

The next_cursor of the previous page (the last id you got).

type query EventType
since query string

Only events created at or after this time (ISO 8601).

Answers EventList

  • 200 A page of events.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

GET /events/{id}

One event

The envelope exactly as sent. Needs orders:read.

ParameterInTypeNotes
id (required) path string

Answers Event

  • 200 The event.
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

POST /events/{id}/resend

Send an event again

To one endpoint (webhook_endpoint_id) or to every active endpoint of the key's mode that wants the event's type. Each delivery starts the retry schedule again; it goes out within seconds. 409 no_webhook_endpoint when no active endpoint wants it. Needs orders:write.

ParameterInTypeNotes
id (required) path string
Idempotency-Key header string

Any unique string (a UUID works), 1 to 255 printable characters. Kept 24 hours per brand and mode. Same key + same body → the first answer again; same key + another body → 409 idempotency_key_reused; while the first is still running → 409 idempotency_key_in_use. A request that failed frees its key.

Body object

FieldTypeNotes
webhook_endpoint_id string

Pattern ^whk_[0-9A-Za-z]{22}$.

Answers object

  • 200 Queued.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 409 The change clashes with existing data or the order's state (external_sku_taken, duplicate_external_order_id, idempotency_key_reused, idempotency_key_in_use, order_not_cancellable, charge_in_progress, order_not_payable, test_mode, order_state, too_many_webhook_endpoints, no_webhook_endpoint).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

GET /webhook_endpoints

List webhook endpoints

Your endpoints in the key's mode, newest first. Never the secrets. Needs orders:write.

ParameterInTypeNotes
limit query integer

Page size, 1 to 100.

starting_after query string

The next_cursor of the previous page (the last id you got).

Answers WebhookEndpointList

  • 200 A page of endpoints.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

POST /webhook_endpoints

Add a webhook endpoint

In the key's mode (a test key makes a test endpoint). Live endpoints must be https; the host must be a public address. Answers the endpoint with its signing secret, the only time it is shown (a replay with the same Idempotency-Key answers secret: null). At most 16 endpoints per mode. Needs orders:write.

ParameterInTypeNotes
Idempotency-Key header string

Any unique string (a UUID works), 1 to 255 printable characters. Kept 24 hours per brand and mode. Same key + same body → the first answer again; same key + another body → 409 idempotency_key_reused; while the first is still running → 409 idempotency_key_in_use. A request that failed frees its key.

Body WebhookEndpointCreate

{
  "description": "Order system",
  "enabled_events": [
    "order.shipped",
    "order.payment_failed",
    "order.cancelled"
  ],
  "url": "https://merchant.example.com/podmfg/webhooks"
}

Answers WebhookEndpointWithSecret

  • 201 The endpoint, with its secret.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 409 The change clashes with existing data or the order's state (external_sku_taken, duplicate_external_order_id, idempotency_key_reused, idempotency_key_in_use, order_not_cancellable, charge_in_progress, order_not_payable, test_mode, order_state, too_many_webhook_endpoints, no_webhook_endpoint).
  • 422 Well-formed but can't be done (address_invalid, country_not_supported, variant_not_found, sku_not_found, external_sku_not_found, variant_not_available, price_unavailable, shipping_method_unavailable, personalization_unsupported, amount_too_small, url_not_allowed).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.
  • 503 Payments, shipping quotes or webhook set-up are offline for a moment (payments_unavailable, shipping_unavailable, webhooks_unavailable). Retry with the same Idempotency-Key.

GET /webhook_endpoints/{id}

One webhook endpoint

Needs orders:write.

ParameterInTypeNotes
id (required) path string

Answers WebhookEndpoint

  • 200 The endpoint.
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

PATCH /webhook_endpoints/{id}

Change a webhook endpoint

Any of url, enabled_events, description (null clears it), status (active / disabled). Turning an endpoint back on forgets its failures and resumes deliveries that were waiting. Needs orders:write.

ParameterInTypeNotes
id (required) path string

Body WebhookEndpointUpdate

{
  "enabled_events": [
    "*"
  ]
}

Answers WebhookEndpoint

  • 200 The endpoint.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 422 Well-formed but can't be done (address_invalid, country_not_supported, variant_not_found, sku_not_found, external_sku_not_found, variant_not_available, price_unavailable, shipping_method_unavailable, personalization_unsupported, amount_too_small, url_not_allowed).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

DELETE /webhook_endpoints/{id}

Delete a webhook endpoint

Nothing more is sent to it; deliveries still waiting end as failed. Needs orders:write.

ParameterInTypeNotes
id (required) path string

Answers object

  • 200 Deleted.
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

POST /webhook_endpoints/{id}/rotate_secret

Rotate an endpoint's signing secret

A new secret, shown once in this answer; deliveries are signed with it from now on (the old one stops at once; update your handler first, or accept both while you switch). Needs orders:write.

ParameterInTypeNotes
id (required) path string
Idempotency-Key header string

Any unique string (a UUID works), 1 to 255 printable characters. Kept 24 hours per brand and mode. Same key + same body → the first answer again; same key + another body → 409 idempotency_key_reused; while the first is still running → 409 idempotency_key_in_use. A request that failed frees its key.

Answers WebhookEndpointWithSecret

  • 200 The endpoint, with its new secret.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 409 The change clashes with existing data or the order's state (external_sku_taken, duplicate_external_order_id, idempotency_key_reused, idempotency_key_in_use, order_not_cancellable, charge_in_progress, order_not_payable, test_mode, order_state, too_many_webhook_endpoints, no_webhook_endpoint).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.
  • 503 Payments, shipping quotes or webhook set-up are offline for a moment (payments_unavailable, shipping_unavailable, webhooks_unavailable). Retry with the same Idempotency-Key.

Sandbox

Test-mode simulations, so you can build your webhook handler end to end before going live.

POST /orders/{id}/simulate/cancel

Test mode: POD Mfg cancels a test order

Cancels the order as POD Mfg staff would (reason: cancelled_by_pod_mfg) and sends order.cancelled, so you can test an order you did not cancel yourself. Allowed before production, like POST /orders/{id}/cancel. Test keys and test orders only (a live key: 403 test_mode_only; a live order's id is not found with a test key). No body. Needs orders:write. Wrong state: 409 order_state.

ParameterInTypeNotes
id (required) path string
Idempotency-Key header string

Any unique string (a UUID works), 1 to 255 printable characters. Kept 24 hours per brand and mode. Same key + same body → the first answer again; same key + another body → 409 idempotency_key_reused; while the first is still running → 409 idempotency_key_in_use. A request that failed frees its key.

Answers Order

  • 200 The test order after the simulation.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 409 The change clashes with existing data or the order's state (external_sku_taken, duplicate_external_order_id, idempotency_key_reused, idempotency_key_in_use, order_not_cancellable, charge_in_progress, order_not_payable, test_mode, order_state, too_many_webhook_endpoints, no_webhook_endpoint).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

POST /orders/{id}/simulate/payment_failed

Test mode: fail a test order's payment

The simulated payment fails: the order becomes payment_failed (status_reason: card_declined, charge.status: failed) and order.payment_failed is sent. Then POST /orders/{id}/retry_payment simulates success (order.paid), or cancel it. No card is involved. Needs the order paid. Test keys and test orders only (a live key: 403 test_mode_only; a live order's id is not found with a test key). No body. Needs orders:write. Wrong state: 409 order_state.

ParameterInTypeNotes
id (required) path string
Idempotency-Key header string

Any unique string (a UUID works), 1 to 255 printable characters. Kept 24 hours per brand and mode. Same key + same body → the first answer again; same key + another body → 409 idempotency_key_reused; while the first is still running → 409 idempotency_key_in_use. A request that failed frees its key.

Answers Order

  • 200 The test order after the simulation.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 409 The change clashes with existing data or the order's state (external_sku_taken, duplicate_external_order_id, idempotency_key_reused, idempotency_key_in_use, order_not_cancellable, charge_in_progress, order_not_payable, test_mode, order_state, too_many_webhook_endpoints, no_webhook_endpoint).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

POST /orders/{id}/simulate/ship

Test mode: mark a test order shipped

Makes one simulated parcel for the whole order (carrier USPS, service Ground Advantage, a fake tracking number TEST + 16 digits) and moves the order to shipped. An order.shipped webhook goes to your test endpoints through the same pipeline (signing, retries, delivery log) as a real shipment. Nothing is made or shipped. Needs the order paid. Test keys and test orders only (a live key: 403 test_mode_only; a live order's id is not found with a test key). No body. Needs orders:write. Wrong state: 409 order_state.

ParameterInTypeNotes
id (required) path string
Idempotency-Key header string

Any unique string (a UUID works), 1 to 255 printable characters. Kept 24 hours per brand and mode. Same key + same body → the first answer again; same key + another body → 409 idempotency_key_reused; while the first is still running → 409 idempotency_key_in_use. A request that failed frees its key.

Answers Order

  • 200 The test order after the simulation.
  • 400 The request is malformed (parameter_invalid, parameter_unknown, parameter_missing, body_invalid, line_item_reference, idempotency_key_invalid, resource_missing cursor).
  • 401 No key, or not a valid one (api_key_missing, api_key_invalid).
  • 403 The key lacks a scope (missing_scope), the brand hasn't accepted the Terms of Service (terms_required), or a simulation was asked with a live key (test_mode_only).
  • 404 No such resource for your brand (resource_missing). Another brand's ids are always 404.
  • 409 The change clashes with existing data or the order's state (external_sku_taken, duplicate_external_order_id, idempotency_key_reused, idempotency_key_in_use, order_not_cancellable, charge_in_progress, order_not_payable, test_mode, order_state, too_many_webhook_endpoints, no_webhook_endpoint).
  • 429 Over 10 requests a second (bursts of 50) for this key, or too many bad keys from your address (rate_limited).
  • 500 Our fault (internal_error). Safe to retry reads.

Webhook events

Every delivery is a signed POST of the event envelope. How to check the signature, with code: verify a webhook.

order.events

order.created, order.paid, order.payment_failed, order.on_hold, order.in_production, order.delivered, order.cancelled

Payload: Event

order.shipped

A shipment was recorded (one per shipment)

Payload: Event + object

product.events

product.published, product.updated, product.unpublished (sent in both modes)

Payload: Event

Objects

Address

FieldTypeNotes
address1 (always) string

At most 200 characters.

address2 string | null

At most 200 characters.

city (always) string

At most 100 characters.

company string | null

At most 120 characters.

country (always) "US"
email string | null

At most 254 characters.

name (always) string

At most 120 characters.

phone string | null

At most 40 characters.

postal_code (always) string

Example: 92618.

state (always) string

2-letter code. Example: CA.

Charge

FieldTypeNotes
amount (always) integer
attempt integer
currency (always) "USD"
failure_code string | null

Example: card_declined.

failure_message string | null
livemode (always) boolean
next_retry_at string | null
requires_action boolean

Your bank wants you to confirm the card: save it again on the Developers page, then retry.

status (always) string

simulated / not_charged are test mode (never a real charge); a test order's failed is a simulated decline (simulate/payment_failed). One of: succeeded, failed, pending, not_attempted, simulated, not_charged.

Error

FieldTypeNotes
error (always) object
code (always) string

Example: variant_not_found.

message (always) string
param (always) string | null

Example: line_items[0].variant_id.

request_id (always) string

Example: req_abc123.

type (always) string

One of: authentication_error, permission_error, invalid_request_error, payment_error, rate_limit_error, api_error.

Event

The envelope of every webhook. data by type. order.shipped: { order_id, external_order_id, shipment: { id, carrier, service, tracking_number, tracking_url, shipped_at, line_items: [{ external_line_id, variant_id, quantity }] }, order_status } (one event per shipment: a partial shipment is its own event). Other order.*: { order_id, external_order_id, order_status, previous_status, reason, occurred_at, order } (order = the Order object when the event was made); order.payment_failed adds failure_code, failure_message, next_retry_at; order.cancelled adds refunded_amount, currency. product.*: { product_id, product_status, product } (product = the Product object; null once unpublished). ping: { webhook_endpoint_id, message }.

FieldTypeNotes
api_version (always) "v1"
created_at (always) string
data (always) object
id (always) string

Pattern ^evt_[0-9A-Za-z]{22}$.

livemode (always) boolean
type (always) EventType

EventList

FieldTypeNotes
data (always) array of Event
has_more (always) boolean
next_cursor (always) string | null
object (always) "list"

EventType

ping is the Developers page's test event; order.delivered needs a carrier tracking feed (none yet, so it is never sent today).

One of: product.published, product.updated, product.unpublished, order.created, order.paid, order.payment_failed, order.on_hold, order.in_production, order.shipped, order.delivered, order.cancelled, ping

LineItemCreate

Exactly one of variant_id, sku, external_sku.

FieldTypeNotes
external_line_id string | null

At most 100 characters.

external_sku string

At most 100 characters.

quantity (always) integer

1 to 10000.

retail_price integer | null

What your customer paid per unit, cents (packing slip / customs). Default: the product's retail price. 0 to 1000000.

sku string

At most 100 characters.

variant_id string

Pattern ^var_[0-9A-Za-z]{22}$.

Merchant

FieldTypeNotes
api_version (always) "v1"
billing (always) object
billing_mode (always) string

One of: per_order, prepaid, internal.

card_on_file (always) boolean
connection (always) null | object

The brand's API connection ("Connect via API"), or null.

id (always) string

Pattern ^mer_[0-9A-Za-z]{22}$.

key (always) object
kind (always) string

One of: partner, catalog.

livemode (always) boolean
scopes (always) array of string
livemode (always) boolean

false for a test key: orders are never charged or produced.

name (always) string
object (always) "merchant"

Order

FieldTypeNotes
cancelled_at string | null
charge (always) Charge
charges (always) array of Charge
created_at (always) string
currency (always) "USD"
external_order_id (always) string
external_order_url string | null
id (always) string

Pattern ^ord_[0-9A-Za-z]{22}$.

line_items (always) array of object
external_line_id string | null
external_sku string | null
name string
quantity integer
retail_price integer
sku string
tax integer
total integer
variant_id string
wholesale_price integer

What you pay per unit (snapshot; never recomputed).

livemode (always) boolean
metadata object
number (always) integer

Our order number (what our team calls it).

object (always) "order"
packing_slip object
paid_at string | null
refunds (always) array of object
amount integer
created_at string
currency string
status string
ship_to object
shipments (always) array of object
carrier string
id string

Pattern ^shp_[0-9A-Za-z]{22}$.

line_items array of object
shipped_at string
tracking_number string
tracking_url string | null
shipping null | Shipping
shipping_method string | null

What you asked for.

shipping_total (always) integer
status (always) OrderStatus
status_reason (always) string | null
subtotal (always) integer
tax (always) integer
total (always) integer
updated_at string

OrderCreate

FieldTypeNotes
external_order_id (always) string

At most 100 characters.

external_order_url string | null

At most 500 characters.

line_items (always) array of LineItemCreate
metadata object | null

Echoed back; at most 4 KB.

packing_slip object | null
message string | null

At most 300 characters.

show_prices boolean
ship_to (always) Address
shipping_method string | null

standard (default: the cheapest), expedited (the fastest), or one of our service keys (e.g. ups_ground).

OrderEstimate

FieldTypeNotes
currency (always) "USD"
line_items (always) array of object
external_line_id string | null
external_sku string | null
in_stock (always) boolean
quantity (always) integer
retail_price (always) integer
sku (always) string
total (always) integer
variant_id (always) string
wholesale_price (always) integer

Per unit at this order's quantity.

livemode (always) boolean
object (always) "order_estimate"
shipping (always) Shipping
shipping_total (always) integer
subtotal (always) integer
tax (always) integer
total (always) integer
would_hold (always) object | null
detail string
reason string

OrderEstimateRequest

The OrderCreate body with external_order_id optional.

OrderList

FieldTypeNotes
data (always) array of Order
has_more (always) boolean
next_cursor (always) string | null
object (always) "list"

OrderShippedData

FieldTypeNotes
external_order_id (always) string
order_id (always) string

Pattern ^ord_[0-9A-Za-z]{22}$.

order_status (always) OrderStatus
shipment (always) object
carrier (always) string

Example: USPS.

id (always) string

Pattern ^shp_[0-9A-Za-z]{22}$.

line_items (always) array of object
service (always) string | null

Example: UPS Ground.

shipped_at (always) string
tracking_number (always) string
tracking_url (always) string | null

OrderStatus

received → payment_pending → paid → in_production → partially_shipped → shipped → delivered; payment_failed (retrying), on_hold (status_reason: address_review, blank_out_of_stock, payment_failed_final, artwork_issue, manual_review), cancelled, refunded (money returned after shipping).

One of: received, payment_pending, paid, in_production, partially_shipped, shipped, delivered, payment_failed, cancelled, on_hold, refunded

Product

FieldTypeNotes
blank (always) object
brand (always) string | null

Example: Sport-Tek.

name (always) string

Example: PosiCharge Competitor Tee.

style (always) string | null

Example: ST350.

created_at (always) string
decoration_method (always) string | null

One of: dtg, dtf, sublimation, embroidery, finished, null.

description (always) string
id (always) string

Pattern ^prd_[0-9A-Za-z]{22}$.

images (always) array of object

Mockups. Links are signed and work for 24 hours; fetch the product again for fresh ones.

color (always) string | null
is_primary (always) boolean
position (always) string

One of: front, back.

url (always) string
object (always) "product"
published_at (always) string | null
status (always) string

One of: published.

title (always) string
updated_at (always) string
variants (always) array of Variant

ProductList

FieldTypeNotes
data (always) array of Product
has_more (always) boolean
next_cursor (always) string | null

Pass as starting_after for the next page.

object (always) "list"

Shipping

FieldTypeNotes
amount integer
carrier string
days_max integer | null
days_min integer | null
label string
service string

Example: ups_ground.

Variant

FieldTypeNotes
currency (always) "USD"
external_sku (always) string | null

Yours (PATCH /variants/{id}).

id (always) string

Pattern ^var_[0-9A-Za-z]{22}$.

in_stock (always) boolean

The blank is available at the supplier (true when the supplier reports no stock number).

object (always) "variant"
options (always) object
color (always) string | null

Example: Navy.

size (always) string

Example: M.

product_id (always) string

Pattern ^prd_[0-9A-Za-z]{22}$.

retail_price (always) integer

Your retail price, cents.

sku (always) string

Ours, stable. Example: PMG-ST350-NAV-M-8F2KA19C0D.

weight_grams (always) integer | null
wholesale_price (always) integer | null

What you pay us for one unit, cents (before shipping and tax). null while its decoration price isn't set up.

WebhookEndpoint

FieldTypeNotes
created_at (always) string
description (always) string | null
disabled_at string | null
disabled_reason (always) string | null

failing = turned off after 5 deliveries in a row failed every retry. One of: merchant, failing, staff, null.

enabled_events (always) array of string
failing_since string | null
id (always) string

Pattern ^whk_[0-9A-Za-z]{22}$.

last_success_at string | null
livemode (always) boolean
object (always) "webhook_endpoint"
secret_hint (always) string

Example: whsec_…a1B2.

secret_rotated_at string | null
status (always) string

One of: active, disabled.

updated_at (always) string
url (always) string

WebhookEndpointCreate

FieldTypeNotes
description string | null

At most 200 characters.

enabled_events (always) array of string
url (always) string

At most 2048 characters. Example: https://merchant.example.com/podmfg/webhooks.

WebhookEndpointList

FieldTypeNotes
data (always) array of WebhookEndpoint
has_more (always) boolean
next_cursor (always) string | null
object (always) "list"

WebhookEndpointUpdate

FieldTypeNotes
description string | null

At most 200 characters.

enabled_events array of string
status string

One of: active, disabled.

url string

At most 2048 characters.

WebhookEndpointWithSecret

Questions about the API: privacy@podmf.com.