Developers
Developer guides
Connect your store, website or AI agent to artwork, designs, orders, tracking and signed webhooks. Orders cost the same as on the website, with no API fee.
Getting started
The account Owner makes API keys under Developers in the dashboard. Each key has scopes and is bound to stores, and a key is shown once: only its hash is stored. Keep it in your secret manager, never in an agent’s instructions. To rotate one, press Rotate next to it under Developers: a new key with the same name, scopes and stores replaces it, and the old one stops working at once.
Start with a sandbox key (sandbox keys start with pod_test_). It runs the same API, but nothing is charged, printed or emailed, and it never changes a live Shopify store. A production key (pod_live_…) does real work. Check what a key carries before you send orders:
curl https://toolboxpod.com/api/v1/me \
-H "Authorization: Bearer $TOOLBOX_POD_KEY"Sandbox orders
Purchase a sandbox order with testOutcome (succeed, decline or uncertain) to choose the result. Nothing ships it on its own: POST /api/v1/orders/{orderId}/simulate with ship, deliver or close gives a paid sandbox order a TEST package and the same status changes and webhooks a production order gets. Activating a product mapping or publishing a product needs a production key (403 practice_mode_live_store).
Buying with a production key
A production purchase also needs the Owner to turn on purchasing for the integration and store, under a spending limit. Keys never manage cards, staff or spending limits.
Stores for an agent
An Owner can issue a stores:setup bootstrap key. POST /api/v1/stores with an Idempotency-Key creates an API store and returns its own key once; an exact replay returns the IDs without the secret, so if that answer is lost the Owner rotates the new key. It cannot grant scopes it does not have.
Errors and limits
| Status | Means |
|---|---|
401 | The key is missing, unknown or revoked. |
403 | A scope is missing, or a sandbox key tried to change a live store. |
404 | Not found, or not visible to this key. |
409 | The request conflicts with the current state. |
422 | The input is invalid, or names a field or list filter the call does not take (unknown_field, invalid_query). |
429 | Too many requests: wait as long as Retry-After says. |
Every error carries a code and a message, and every response an x-request-id to quote to support. Per key and minute: 600 reads, 120 writes and 20 purchases.
Artwork to order
GET /api/v1/catalog/garmentsThe garments, colors and sizes: use the codes it returns.GET /api/v1/catalog/rateshas the prices and where we ship.POST /api/v1/artworkUpload a PNG, JPEG or WebP: the raw bytes, a multipartfile, or JSON with an HTTPSurlorbase64. Same quality checks as the website.POST /api/v1/designsPlace it on the shirt. Withapprove: true(scopedesigns:activate) it is approved in the same call. Wait forproduction.stateto beregistered(usually seconds) before you buy.POST /api/v1/ordersOne recipient, a store the key may use and your ownexternalReference. It comes back quoted, with theofferto accept. The same reference with a different body is refused (409).POST /api/v1/orders/{orderId}/purchasePay it: send theofferback asaccepted, with anIdempotency-Key. After an unclear answer (timeout, 5xx), retry with the same key and body. A refusal before any payment (a 409 such asstale_quote, which carries the fresh offer to pay, or a 422) frees the key, so you may send the corrected request with it. Once a payment was tried, the key replays its result: retry a held order with a new key.GET /api/v1/orders/{orderId}Status, packages and tracking, or let the webhooks below tell you.
AUTH="Authorization: Bearer $TOOLBOX_POD_KEY"
# 1. Upload the artwork: the answer carries its id (art_…)
curl https://toolboxpod.com/api/v1/artwork -H "$AUTH" \
-H "Content-Type: image/png" --data-binary @artwork.png
# 2. Place it on the shirt and approve it in one call
curl https://toolboxpod.com/api/v1/designs -H "$AUTH" \
-H "Content-Type: application/json" -H "Idempotency-Key: design-1" -d '{
"sides": { "front": { "artworkId": "art_…", "placementModel": "collar-v1" } },
"approve": true
}'
# 3. Order it: the answer carries the order id (ord_…) and its offer
curl https://toolboxpod.com/api/v1/orders -H "$AUTH" \
-H "Content-Type: application/json" -d '{
"storeId": "sto_…", "externalReference": "web-1001",
"recipient": { "name": "Ada Lovelace", "street1": "1 Analytical Way",
"city": "Springfield", "state": "OR", "postal": "97477", "country": "US" },
"lines": [{ "designId": "rev_…", "size": "L", "color": "BLK",
"quantity": 1, "title": "Sunset tee" }]
}'
# 4. Pay it: "accepted" is the offer from step 3, exactly as it came back
curl https://toolboxpod.com/api/v1/orders/ord_…/purchase -H "$AUTH" \
-H "Content-Type: application/json" -H "Idempotency-Key: pay-web-1001" \
-d '{ "accepted": { …the offer… }, "testOutcome": "succeed" }'Lists that page (GET /orders, /events, /artwork, /designs, /products, /billing/charges, /billing/refunds and the AI job lists) take cursor and limit: send the returned nextCursor back as cursor until it is null. Pages run newest first and never repeat or skip a row. A field or query parameter a call does not take is refused with a 422 that names it, never ignored.
Webhooks
Register an HTTPS endpoint with POST /api/v1/webhook-endpoints (scope webhooks:manage) or under Developers in the dashboard. You can have several endpoints: an integration keeps up to 10 active in each mode, each at a different URL. Every endpoint lists the events it wants, has its own signing secret (shown once) and gets only its own mode’s events, so sandbox events go to sandbox endpoints. Each delivery is a JSON POST with three headers:
Pod-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>
Pod-Event-Id: evt_…
Pod-Event-Type: order.paidVerify Pod-Signature against the raw body before you parse it: HMAC-SHA256 of t + "." + rawBody with that endpoint’s secret, compared in constant time, refusing a t more than five minutes old. With several endpoints, each one checks its own secret. Store the event, then answer 2xx within 10 seconds, and skip repeats by Pod-Event-Id: a redelivery keeps its ID.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received, before JSON.parse.
export function verifyWebhook(rawBody, header, secret) {
const parts = String(header || "").split(",").map((s) => s.trim());
const t = parts.find((s) => /^t=\d+$/.test(s))?.slice(2);
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret)
.update(t + ".").update(rawBody).digest();
return parts.filter((s) => /^v1=[0-9a-f]{64}$/.test(s)).some((s) =>
timingSafeEqual(expected, Buffer.from(s.slice(3), "hex")));
}A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours and 24 hours; 20 failures in a row disable the endpoint. Test a receiver with POST /api/v1/webhook-endpoints/{endpointId}/ping and send an event again with POST /api/v1/events/{eventId}/redeliver. Events can arrive out of order: read the order for its current state.
design.registeredorder.quotedorder.paidorder.heldorder.shippedorder.closedorder.cancelledpackage.shippedrefund.sentreplacement.createdsupport_request.receivedrate_card.noticeping
Import a Shopify store
Connect the products a Shopify store already sells, so Toolbox POD prints and ships their orders. Their titles, photos, prices and descriptions stay as they are in Shopify. Selling on Shopify is invite-only for now (request access at toolboxpod.com/shopify).
POST /api/v1/stores/{storeId}/shopify-products/syncRead the store’s products from Shopify again (202). A big store is read in the background:GETthe same path untilstateiscomplete. Nothing is written to Shopify.GET /api/v1/stores/{storeId}/shopify-productsThe products with their photo and connection.tab=not_connected(the default) is what is left to import;qsearches titles, vendors and SKUs. Page withcursor.POST /api/v1/artworkUpload each product’s print file and keep itsart_…id.POST /api/v1/stores/{storeId}/shopify-imports/previewFor up to 25 products: the shirt, each variant’s color and size match, and the print warnings. Show them to a person before you connect.POST /api/v1/stores/{storeId}/shopify-importsConnect up to 25 products per request, each with{ "assetId": "art_…" }or an existing{ "designId": "rev_…" }. Every item answersdoneorfailedon its own, and sending the same request again changes nothing.
AUTH="Authorization: Bearer $TOOLBOX_POD_KEY"
# 0. The Shopify store's id: GET /me lists them in shopifyStores
STORE=$(curl -s https://toolboxpod.com/api/v1/me -H "$AUTH" | jq -r '.shopifyStores[0].id')
# 1. What is not connected yet (the answer's nextCursor pages on)
curl "https://toolboxpod.com/api/v1/stores/$STORE/shopify-products?tab=not_connected&limit=100" -H "$AUTH"
# 2. Connect a batch of up to 25: each with its uploaded print file or a design
curl https://toolboxpod.com/api/v1/stores/$STORE/shopify-imports -H "$AUTH" \
-H "Content-Type: application/json" -d '{
"items": [
{ "shopifyProductId": "gid://shopify/Product/8123456789", "design": { "assetId": "art_…" } },
{ "shopifyProductId": "gid://shopify/Product/8123456790", "design": { "designId": "rev_…" } }
]
}'The key needs products:write. An item that makes or approves a design (an uploaded print file, or a design that is not approved yet) also needs designs:activate; without it the whole request is refused (403 scope_missing) and nothing is written. A sandbox key is refused (403 practice_mode_live_store). A failed item names its reason: warnings_unacknowledged lists the print warnings to accept in acknowledgedWarnings, and no_variants_matched asks for matches with each variant’s color and size. Connected products show in GET /api/v1/products with origin: "shopify_import"; follow each one’s live and pending variants on the connected tab.
AI agents and MCP
Add https://mcp.toolboxpod.com/mcp as a connector in Claude, ChatGPT or any MCP client (Streamable HTTP); connectors added with https://toolboxpod.com/mcp keep working. The client finds the OAuth details and opens the Toolbox POD consent page: sign in, choose the account and approve only the scopes it needs.
A connection is live but pays for nothing until the Owner sets a spending limit, and buying or approving designs each need the Owner to turn them on. Creating stores and issuing keys is REST only. Client by client setup: MCP setup (Markdown).
Connect to https://mcp.toolboxpod.com/mcp with the client's OAuth flow, or use an Owner-issued sandbox API key from a secret manager. Read https://toolboxpod.com/api/v1/openapi.json first. Check the account, mode, scopes and store IDs with GET /api/v1/me. Use only the stores you are given. Set up a webhook receiver, verify its signatures and ping it. Report the store ID, the key's mode and the delivery result without revealing any secret. Do not make real purchases or widen spending permissions.API reference
Every endpoint with its request and response fields, examples and a request console, generated from the schemas the API runs on.