Request access

Integration guide

Connect your store to POD Mfg.

You sell. We make and ship every order, and send the tracking back. This guide takes you from your first sign-in to your first live order, whether your store is on Shopify or any other website or system.

Choose your connection

There are two ways to send orders to POD Mfg. Pick the one that matches where your store lives. Everything after the order (production, shipping, tracking) is the same.

Your store is on Shopify

Connect Shopify

  • We publish your products to your Shopify store.
  • Paid Shopify orders reach us on their own.
  • We mark each order fulfilled in Shopify with tracking.
  • No developer needed: a few minutes in Shopify's admin.
Shopify setup steps

Any other website or system

Connect via API

  • WooCommerce, a custom site, a school or team platform, an ERP: anything that can call a web API.
  • Your system reads your products and sends us orders.
  • We charge your saved card per order, then make and ship it.
  • Tracking comes back to your system automatically.
  • Needs a developer to connect your system. Part 2 is written for them.
API setup steps

On Shopify? Follow Connect your Shopify store and skip the rest of this page. The API steps below are for every other kind of store.

Part 1

For the business owner

Setting up the API connection, step by step. Names in bold are exactly what you'll see in POD Mfg at app.podmf.com.

  1. Create your account, then sign in

    Where: app.podmf.com/start-store

    Fill in the start form with your business details and choose Our own site or system. Confirm your email with the link we send, then sign in at app.podmf.com (with Google, or the password you set from our sign-in email). No waiting list.

    The person who signs up is the brand's owner. Only an owner can connect a store, add the card and make API keys.

  2. Accept the Terms of Service

    Where: Shown the first time you sign in

    Read the terms to the end, tick the box, and press Accept and continue. Nothing works through the API until an owner has accepted the current terms.

  3. Open Developers & API and connect

    Where: Dashboard → Developers & API → Connect via API

    This is your control panel for the API connection. Press Connect via API. A Setup checklist appears and ticks itself off as you go.

  4. Add a card for production charges

    Where: Developers & API → Card for production charges → Add card

    Each live order is charged to this card when it comes in. The card is saved on a secure Stripe page: Stripe keeps it and we never see the number. Use Replace card to change it later.

  5. Design your products and set your retail price

    Where: Dashboard → Design Studio, then Products → New product

    Put your art on a garment in the Design Studio and approve it. Our team checks every file before anything is printed.

    On Products, press New product, pick the approved design, choose colors and sizes, and enter your Price (what your customer pays you). You can set a different price per size.

    Your price is yours to set. What you pay us per item is shown as You pay in the Product mapping table on the Developers & API page, next to your retail price.

  6. Publish

    Where: Products → your product → Publish

    Press Publish. Only published products can be ordered; Pause takes a product off sale.

    Products that carry names or numbers (team personalization) can't be ordered through the API yet. Ask your account manager about those.

  7. Make a test key

    Where: Developers & API → Make a key

    Give the key a name (for example "Website"), set Mode to Test (never charged or made), and press Make key.

    Copy the key straight away: it is shown once. Test keys start with podmfg_test_. Test orders are never charged and never made, so your developer can try anything.

  8. Hand off to your developer

    Where: Developers & API → Make a key → Copy setup instructions for your developer

    This button copies a short note with the web address of the API, the docs, and the steps. If you just made the test key it includes it too; otherwise send the key separately.

    Send your developer that note and a link to this page (Part 2 is written for them). Send keys through a password manager or a secure channel, not in a public chat.

  9. Place a test order

    Where: Setup checklist → Place one now, or API orders → Create a test order

    Your developer will send test orders from your website. You can also make one yourself. On a test order the Test tools let you Mark shipped (fake tracking), Fail payment or Cancel, to see what your system does when that happens to a real order.

  10. Add a webhook endpoint

    Where: Developers & API → Webhook endpoints → Add endpoint

    A webhook endpoint is a web address on your system where we post news about your orders: paid, on hold, shipped with tracking, cancelled. Your developer gives you the address, or adds it for you.

    Pick Test orders while you build, then add one for Live orders (https only). The endpoint's secret is shown once: give it to your developer. Send test event checks it's listening.

  11. Go live

    Where: Setup checklist → Make your live key

    When the checklist shows a card on file, at least one published product and a webhook endpoint, Make a key offers Live. Make a live key (it starts with podmfg_live_) and give it to your developer securely.

    From the first live order on, every order is charged to your card and made for real.

Live-key checklist

Before you make the live key, check:

  • A card is saved under Card for production charges.
  • Your products are published, with the retail price you want.
  • A webhook endpoint for Live orders is added and your developer has its secret.
  • Your developer has tested ordering, shipping, a failed payment and a cancel with the test key.
  • No product you sell through the API carries names or numbers.
  • You ship to US addresses only (international orders are refused for now).

What happens on each order

  1. Order arrivesYour website sends the order to POD Mfg the moment your customer pays you.
  2. We charge your cardWhat you pay per item, plus shipping, charged to your saved card right away.
  3. We make itA paid order goes to production exactly like a Shopify order. Nothing unpaid is ever made.
  4. We ship itPacked and shipped to your customer under your brand.
  5. Tracking comes backYour system gets the carrier and tracking number, one notice per parcel, and marks the order shipped.

You can follow every order on the Developers & API page under API orders, and see each charge under Charges for API orders. Your customer pays you on your website; POD Mfg charges you, not your customer.

Billing, holds and cancellations

What you're charged

Per order: what you pay per item (the You pay column on Product mapping, with quantity discounts applied on the real order) plus shipping. The amount is fixed when the order arrives. Test orders are never charged.

Sales tax

How sales tax applies to API orders is still being finalised with our finance team. Ask your account manager before you rely on the current charges for your own tax records.

A declined card

The order waits as payment failed and we email your billing contact. We try the card again after 1, 6 and 24 hours, then hold the order. Replace the card and press Retry payment. Nothing is made until it's paid. If your bank asks to confirm the payment, save the card again with Replace card, then press Retry payment.

Holds

We hold an order, without charging it, when a person should check the address (no house number, US territories, military mail) or the garment is out of stock at our supplier. Our team releases it, or you can cancel it. Addresses we can't ship to are refused straight away so you can fix them.

Cancellations and refunds

An order can be cancelled until it goes into production, and a paid one is refunded in full. Once production has started it can't be cancelled through the API: contact us about that order.

What we ship

US addresses only for now. Shipping is the cheapest service unless your website asks for the fastest one. The shipping amount is set when the order arrives.

Part 2

For your developer

Everything you need to connect a store to POD Mfg. The full detail is in the API reference, the 15-minute quickstart (full cURL, Node and Python samples), the OpenAPI 3.1 spec and the Postman collection (Bruno and Insomnia import it too).

The basics

Base URL
https://app.podmf.com/api/v1 (paths below are relative to it)
Auth
Authorization: Bearer <key> on every request. Keep keys on your server, never in a web page.
Keys
podmfg_test_... test mode: orders are stored and answered but never charged and never made. podmfg_live_... live mode. Same base URL; the key decides the mode. A test key never sees live data and the other way round.
Format
JSON only. Times are ISO 8601 UTC. Money is integer cents plus currency (2800 is $28.00 USD). Ids are prefixed (prd_, var_, ord_, shp_, evt_, whk_).
Lists
Cursor pages: ?limit=50&starting_after=<id> answers { data, has_more, next_cursor }.
Versioning
Within v1 changes are additive only: ignore fields you don't know.
Coverage
US ship-to only. Products with names or numbers can't be ordered yet (422 personalization_unsupported).

Check your key:

curl https://app.podmf.com/api/v1/me \
  -H "Authorization: Bearer $PODMFG_API_KEY"

The minimal flow

  1. GET /products: your published products, each with variants (a color and size) carrying our id (var_...), our sku, your external_sku, retail_price, wholesale_price and in_stock. Use ?updated_since= to sync changes. Images are signed links that expire after 24 hours: re-fetch rather than store them.
  2. Map variants to your store. Either store our variant_id / sku against your products, or set your own SKU on ours with PATCH /variants/{id} { "external_sku": "..." } (or on the Developers page, Product mapping, which also downloads the whole catalog as CSV with every id).
  3. POST /orders when your customer pays. Each line names exactly one of variant_id, sku or external_sku. Always send an Idempotency-Key and your external_order_id (unique per brand and mode). Answers 201 with the order's status and charge. A live order is charged now: paid, or payment_failed (retried automatically), or on_hold (not charged until released). A test order answers paid with charge.status: simulated.
  4. Handle webhooks (below): mark the order shipped with tracking on order.shipped, and react to order.paid, order.payment_failed, order.on_hold and order.cancelled.
curl -X POST https://app.podmf.com/api/v1/orders \
  -H "Authorization: Bearer $PODMFG_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "external_order_id": "1042",
    "shipping_method": "standard",
    "ship_to": { "name": "Jane Smith", "address1": "123 Main St", "city": "Irvine",
                 "state": "CA", "postal_code": "92618", "country": "US" },
    "line_items": [
      { "variant_id": "var_...", "quantity": 2, "external_line_id": "li_1" }
    ]
  }'

Optional: POST /orders/estimate prices a cart (wholesale plus shipping) without creating anything. GET /orders/{id} and GET /orders?external_order_id= read an order back. POST /orders/{id}/cancel cancels before production (full refund if paid). POST /orders/{id}/retry_payment charges a failed order again after the owner fixes the card. shipping_method is standard (cheapest, the default) or expedited (fastest).

Webhooks

Add an endpoint on the Developers page (Webhook endpoints) or with POST /webhook_endpoints { "url": "...", "enabled_events": ["*"] }. The answer's secret (whsec_...) is shown once; POST /webhook_endpoints/{id}/rotate_secret replaces it (the old one stops at once). A test key makes a test endpoint, which gets only test-order events; live endpoints get live events and must be https://. Product events go to both. Every endpoint must be a public address; we don't follow redirects.

EventWhenWhat to do
order.paidThe charge succeeded (or simulated, in test).Note it; production follows.
order.shippedA parcel left, with carrier, tracking_number, tracking_url and its line_items. One per parcel: two boxes, two events.Mark those lines fulfilled with tracking.
order.payment_failedThe card was declined: failure_code, failure_message, next_retry_at.Flag it for the owner; we retry on our own.
order.on_holdHeld, with a reason such as address_review, blank_out_of_stock or payment_failed_final.Show it to the owner; check the address with your customer.
order.cancelledCancelled (by you or by us), with refunded_amount.Cancel it in your system.
order.created, order.in_productionAccepted; sent to production.Optional status updates.
product.published, product.updated, product.unpublishedThe owner changed a product.Re-sync that product.

Each delivery is a POST of { id, type, created_at, livemode, api_version, data } with headers PODMFG-Signature, PODMFG-Event-Id and PODMFG-Event-Type. Order events carry order_status and the whole order object.

  • Verify every delivery. PODMFG-Signature: t=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256 of "<t>.<raw body>" keyed with the endpoint's whole whsec_... secret. Use the raw bytes before JSON parsing, compare in constant time, and refuse a t more than 5 minutes from your clock. Full samples: quickstart, step 5.
  • Answer fast. Any 2xx within 10 seconds counts. Queue the work, then answer.
  • Retries. Anything else is retried after 1 m, 5 m, 30 m, 2 h, 6 h, 12 h, 24 h and 24 h: 9 tries over about 3 days. Then the delivery has failed, the owners are emailed, and the Developers page (or POST /events/{id}/resend) can send it again.
  • Dedupe on the event id. Delivery is at least once, so the same event can arrive twice.
  • Order isn't guaranteed. Use order_status and the times in data, or fetch the order.
  • Polling fallback. GET /events?since=<ISO time>&type=<type> lists the last 30 days of events exactly as sent, to backfill after downtime or instead of a webhook.
  • Auto-disable. An endpoint whose deliveries fail for good 5 times in a row is turned off and the owners are emailed. Fix it, then Turn on (or PATCH /webhook_endpoints/{id} {"status":"active"}).
// Node: check PODMFG-Signature on the RAW body before you trust a webhook.
const [t, ...sigs] = header.split(",").map((p) => p.trim().split("="));
// secret = the endpoint's whole whsec_... string
const expected = crypto.createHmac("sha256", secret)
  .update(`${t[1]}.`).update(rawBody).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t[1])) <= 300;
const ok = fresh && sigs.some(([k, v]) => k === "v1" && v.length === expected.length &&
  crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected)));

Test vector: secret whsec_test, t=1700000000, body {"a":1} gives v1=38877139021993b830af32feea6e18a8da83eb2f6e49ee50bd9e4cf4ca4d3789.

Test mode and the sandbox

With a test key, build the whole flow end to end. After you create a test order, make things happen to it: POST /orders/{id}/simulate/ship (a parcel with TEST... tracking, sends order.shipped), .../simulate/payment_failed (sends order.payment_failed; then retry_payment sends order.paid) and .../simulate/cancel (as if we cancelled it, sends order.cancelled). Simulations refuse a live key (403 test_mode_only). Send test event on the Developers page sends a signed ping to one endpoint: ignore event types you don't know.

Retries, rate limits and errors

  • Idempotency. Send Idempotency-Key (a UUID) on every POST. A timeout? Send the same request with the same key: you get the first answer (Idempotent-Replayed: true), never a second order or charge. Keys last 24 hours; a request that failed frees its key.
  • Rate limit. 10 requests a second per key, bursts of 50. Every answer has X-RateLimit-Limit and X-RateLimit-Remaining; a 429 has Retry-After. Repeated bad keys from one address are paused for a while.
  • Request ids. Every answer has X-Request-Id. Log it, and quote it when you contact us. The Developers page's API request log shows the last 30 days of calls.

Errors always have this shape, with the right HTTP status:

{
  "error": {
    "type": "invalid_request_error",
    "code": "variant_not_found",
    "message": "Variant var_123 does not exist or is not published.",
    "param": "line_items[0].variant_id",
    "request_id": "req_..."
  }
}

Developer go-live checklist

  • Swap the test key for the live key the owner gives you, stored as a server secret.
  • Add a live webhook endpoint (https) and store its own whsec_ secret. Test and live endpoints have different secrets.
  • Signatures verified on the raw body; event ids deduped; work queued and answered within 10 seconds.
  • Every POST sends an Idempotency-Key; retries reuse it.
  • external_order_id is your real, unique order number.
  • Every product you sell maps to a published variant (variant_id, sku or external_sku).
  • You handle payment_failed and on_hold (tell the owner) and order.shipped (one per parcel).
  • Non-US addresses and personalized products are stopped before they reach us.
  • A backfill from GET /events runs after any downtime.

FAQ and troubleshooting

Common errors

StatusCodeWhat it meansWhat to do
401api_key_missing, api_key_invalidNo key, a mistyped key, or a revoked one.Send Authorization: Bearer <key>. Check you didn't paste a test key where a live one belongs, or the other way round.
403terms_required, missing_scope, test_mode_onlyAn owner hasn't accepted the updated terms, the key isn't allowed to do this, or a simulate call used a live key.Owner signs in and accepts; or make a key with the right permissions; simulate only with a test key.
409duplicate_external_order_idYou already sent an order with this order number (in this mode). The message names the existing ord_ id.Don't resend: fetch that order. Your order numbers must be unique.
409idempotency_key_reused, idempotency_key_in_useThe same Idempotency-Key with a different body, or while the first request is still running.One key per order attempt; reuse it only to retry the exact same request.
409order_not_cancellableThe order is already in production.Contact us about that order.
422address_invalidWe can't ship to that address (bad state code, ZIP, or missing street or city).Ask your customer to correct it, then send the order again.
422country_not_supportedThe ship-to country isn't US.US addresses only for now.
422variant_not_found, sku_not_found, external_sku_not_foundThe item isn't one of your published variants.Refresh your product list; check the SKU mapping.
422personalization_unsupportedThat product carries names or numbers.Not orderable through the API yet.
402payment_failedOnly from retry_payment: the card was declined again.Owner replaces the card, then retry.
429rate_limitedMore than 10 requests a second (bursts of 50) for one key.Wait for Retry-After seconds, then continue.
503payments_unavailable, shipping_unavailableCard payments or shipping quotes are offline for a moment. A live order isn't accepted when we can't charge it.Retry later with the same Idempotency-Key.

A declined card on POST /orders is not an error: the order is created (201) with status: payment_failed and we retry it. Every code is listed in the API reference.

Our webhook endpoint isn't getting anything.

  • Is it a public internet address? Local, private and internal addresses are never called. A test endpoint can be a development server, but it must be reachable from the internet.
  • Live endpoints must be https://, with a valid certificate. We don't follow redirects: use the final address.
  • Is it the right mode? Test endpoints only get test-order events, live endpoints only live ones.
  • Is it turned off? After 5 deliveries in a row fail for good we turn it off and email the owners. Fix it, then Turn on.
  • Answering 400? Your signature check may be reading parsed JSON instead of the raw body, or using an old secret after a rotation.
  • Press Send test event, then open Webhook events on the Developers page: each attempt shows our request and your answer. Missed events can be resent there or fetched from GET /events.

Do test orders cost anything or get made?

No. Test-key orders are never charged and never sent to production. Only a live key makes real orders.

We sent the same order twice.

With the same Idempotency-Key you got the first answer back. With the same external_order_id the second one was refused (409 duplicate_external_order_id). Either way there is one order and one charge.

Is sales tax charged?

Sales tax on API orders is being finalised. Check with your account manager before you rely on today's charges.

Can we ship outside the US?

Not yet. A non-US address is refused with 422 country_not_supported.

Can our customer cancel?

Until the order goes into production, yes: POST /orders/{id}/cancel refunds it in full. After that, contact us (409 order_not_cancellable).

Do you send a delivered notice?

order.delivered can be subscribed to but isn't sent today. Use the tracking_url from order.shipped.

Can we use Shopify and the API together?

Yes. A brand can have a Shopify store connected and an API connection at the same time. Shopify orders come in through Shopify; everything else through the API.

Getting help

Your POD Mfg account manager is your first contact for setup, billing and orders. You can also reach our team through the contact box on our request access page. Developers with API questions can email privacy@podmf.com. Include the X-Request-Id (req_...) of the call, or the ord_ id of the order, so we can find it.