Skip to Content
Seller guideAPI integration

18. API integration

◀ Contents

Inkica’s API lets your own system send orders to Inkica automatically — no need to open the dashboard and create each order by hand. It’s a good fit when you:

  • Sell on your own website or a channel Inkica doesn’t connect to out of the box.
  • Have your own tool/software that manages orders and want to push them straight to the factory.
  • Want to automate the whole flow: order arrives → create an Inkica order → track shipping.

If you sell on eBay, Etsy, TikTok Shop, Shopify, or Amazon, the simpler way is to connect your shop — orders arrive on their own, no programming needed. The API is for everything else.

This page is written for both you and the developer who will build the integration — feel free to send them the link to this page.

How it works

  1. Create an API token on the Inkica dashboard (one time).
  2. Look up the catalog to get the variant_id of the product you want to sell (shirt model, color, size).
  3. Create an order — send the recipient’s address + the print files. The order is created as a draft, with nothing charged yet.
  4. Send to production — Inkica deducts the money from your wallet and passes the order to the factory.
  5. Track the order — read the status and the tracking number once the order is handed to the carrier, or register a webhook so Inkica notifies you automatically.

1. Create an API token

  1. Open the account menu (bottom of the sidebar) → API.
  2. Click Create token and give it a memorable name (for example “Website sync”).
  3. Copy the token right away — it looks like ink_pat_... and is shown only once. Once you close the dialog you can’t view it again; the only option is to create a new token.

🔒 Treat the token like a password. Anyone with the token can create orders and spend the money in your wallet. Don’t paste the token into code that runs in a browser, send it over chat, or commit it to GitHub. If you suspect it has leaked, go to Settings → API and click Revoke to revoke it right away — that token stops working immediately.

Each account can have up to 10 tokens at a time. Create a separate token for each system/tool, so that revoking one doesn’t stop the others.

2. Call the API

Base URLhttps://api.inkica.com/api/v1
AuthenticationHeader Authorization: Bearer ink_pat_...
FormatJSON (Content-Type: application/json), currency USD
Full reference (try the API live)api.inkica.com/api/v1/docs 

Quickly test your token by fetching the product list:

curl https://api.inkica.com/api/v1/catalog/blueprints?limit=5 \ -H "Authorization: Bearer ink_pat_xxxxxxxxxxxx"

If you get a list of products back, the token works.

3. Choose a product (catalog)

The catalog has 2 levels:

  • Blueprint — the base product template, for example “US Unisex Heavy Cotton Tee”.
  • Variant — one specific color × size combination of that blueprint, for example Black / L. When creating an order, you choose by variant_id.

List blueprints

GET /v1/catalog/blueprints?q=tee&page=1&limit=20

You can also filter by category (for example clothing). Each blueprint has an id, name, product_type, images, and print_areas — the positions that can be printed.

Details of one blueprint

GET /v1/catalog/blueprints/1
{ "id": 1, "name": "US Unisex Heavy Cotton Tee 2D", "colors": [{ "name": "Black", "hex": "#000000" }], "sizes": ["S", "M", "L", "XL", "2XL"], "included_print_area": "front", "print_areas": [ { "position": "front", "fee": 4.2, "width": 38, "height": 58, "unit": "cm" }, { "position": "back", "fee": 4.2, "width": 38, "height": 58, "unit": "cm" }, { "position": "sleeve_left", "fee": 3.35, "width": 4, "height": 3.5, "unit": "inch" } ] }
  • print_areas[].width / height / unit — the size of the print area, used to prepare print files with the right proportions.
  • fee — the print fee for that position.

List variants (with prices)

GET /v1/catalog/blueprints/1/variants?page=1&limit=100
{ "data": [ { "id": 20139, "sku": "IUSG5000-S-ASH", "color": "ASH", "size": "S", "is_available": true, "price": 7.58 } ], "page": 1, "limit": 100, "total": 821 }

How pricing works: a variant’s price already includes printing in 1 position (the included_print_area position, usually the front). Printing in another position adds that position’s print fee. In the example above: printing the front = $7.58; printing both front + back = $7.58 + $4.20 = $11.78.

The exact price of each line is always in unit_price when you create an order — treat that number as the source of truth.

Only use variants with is_available: true. Large blueprints have hundreds of variants — raise limit (up to 1000) or page through with page to get them all.

4. Estimate shipping (optional)

To quote shipping to your customer before they order:

POST /v1/orders/shipping
{ "line_items": [{ "variant_id": 20139, "quantity": 2 }], "address_to": { "country": "US", "region": "CA", "zip": "90001" }, "shipping_method": "standard" }

It returns { "shipping": 7.39, "currency": "USD", "is_estimate": true }. Orders shipping to the US must include zip.

If you supply your own label, add "shipping_mode": "self" — the returned shipping is then the flat handling fee (instead of a shipping fee), with is_estimate: false.

5. Create an order

POST /v1/orders
{ "external_id": "WEB-10023", "shipping_method": "standard", "address_to": { "first_name": "John", "last_name": "Smith", "email": "john@example.com", "phone": "+1 555 123 4567", "country": "US", "region": "CA", "address1": "123 Main St", "address2": "Apt 4", "city": "Los Angeles", "zip": "90001" }, "line_items": [ { "variant_id": 20139, "quantity": 2, "print_areas": { "front": "https://your-cdn.com/designs/front.png", "back": "https://your-cdn.com/designs/back.png" }, "mockups": { "front": "https://your-cdn.com/mockups/front.jpg" } } ] }
FieldRequiredNotes
external_idNo (recommended)The order ID in your own system, up to 100 characters. It must be unique among your orders. Sending the external_id of an order that’s still a draft updates that draft instead of creating a new order — safe if your system resends after a network error. Once an order has been sent to production, that external_id is locked (error 409), even if the order has been split into -F1, -F2 — so the same order is never created and paid for twice.
shipping_methodNostandard (default), priority, or express — see Fast shipping.
shipping_modeNoinkica (default — Inkica buys the label and charges shipping) or self (you supply the label, only a flat handling fee is charged) — see Supply your own shipping label.
shipping_label.label_urlWith selfPublic link to the label file (PDF) — the factory downloads it, prints it and sticks it on the parcel.
shipping_label.tracking_numberWith selfThat label’s tracking number, up to 128 characters.
shipping_label.carrierNoThe label’s carrier, for example USPS.
address_to.first_nameYes
address_to.countryYes2-letter country code, for example US.
address_to.regionYesState/province code, for example CA. For the US, the full state name (California) also works.
address_to.address1YesUp to 50 characters — if it’s longer, move the rest to address2.
address_to.city, zipYes
address_to.email, phoneNo
line_itemsYesUp to 50 lines per order.
line_items[].variant_idYesTaken from step 3.
line_items[].quantityYes1 – 999.
line_items[].print_areasYesThe print file for each position: { "position": "URL" }. The position must be in the blueprint’s print_areas (step 3). Products with a front/back (shirts, hoodies…) need at least 1 of those 2 sides. The URL must download directly to the original image file (not a web page, not a preview image) — Inkica tries to download it when the order is created and returns a 400 error if the link is dead or the image is under 600px on its long side.
line_items[].mockupsNoMockup image for each position. If omitted, the print file is used.
line_items[].personalizationNoPersonalization text (for example a name engraved on jewelry), up to 255 characters.

Requirements for print files: the URL must be a public link (https://...) that downloads directly without signing in, and you must keep that link unchanged until the order is delivered — the factory downloads the file from this link at production time. Use PNG files with a transparent background and enough resolution for the print area size from step 3.

The response is the order in draft status:

{ "id": "4821", "external_id": "WEB-10023", "status": "draft", "line_items": [ { "id": "9150", "variant_id": 20139, "sku": "IUSG5000-S-ASH", "quantity": 2, "unit_price": 11.78, "print_areas": { "front": "...", "back": "..." } } ], "subtotal": 23.56, "shipping": 0, "total": 23.56, "currency": "USD", "shipment": null }

A draft order has not been charged and is not in production. Shipping is always 0 at this point — it’s calculated when you send to production. Orders created through the API also appear in the Orders section of the dashboard, alongside all your other orders.

Supply your own shipping label (self-ship)

Already have a label (bought yourself, or provided by a marketplace)? Send it with the order — the factory prints your label and sticks it on the parcel, and Inkica does not buy a label or charge shipping; you pay only a small flat handling fee (see the exact amount with POST /v1/orders/shipping or dry_run).

{ "external_id": "WEB-10024", "shipping_mode": "self", "shipping_label": { "label_url": "https://your-cdn.com/labels/WEB-10024.pdf", "tracking_number": "9400111899223197428490", "carrier": "USPS" }, "address_to": { "...": "..." }, "line_items": [ { "...": "..." } ] }
  • shipping_mode: "self" and shipping_label go together — sending only one of them returns 400.
  • The label must ship to the order’s exact address_to. Inkica doesn’t read the label: a label with the wrong address, or an expired or already-used label, sends the parcel the wrong way, and that’s on you.
  • label_url must download directly (no login) and stay unchanged until the order is delivered, same as print files.
  • Standard shipping only — combining it with priority/express returns 400 (your label decides the delivery speed).
  • One label fits one parcel, so every product in the order must be made by the same factory, and not every factory accepts outside labels. If the order can’t meet that, sending to production returns 400 — check beforehand with dry_run. Then split it into several orders, or drop shipping_mode/shipping_label to let Inkica buy the label.
  • Changed your mind while the order is still a draft: send the same external_id again without shipping_label → the order goes back to Inkica buying the label, and the old label is dropped.
  • The response has "shipping_mode": "self", and shipment shows your label + tracking right away. If you use webhooks, this order’s order:shipment:created event arrives as soon as the order is created/sent, without waiting for the factory to ship.

6. Send to production

POST /v1/orders/4821/send_to_production

Inkica calculates shipping, deducts the total from your wallet, and passes the order to the factory. If your wallet doesn’t have enough, the API returns an error and the order stays a draft — add funds and send it again.

Dry run before sending for real (dry_run)

Building your integration and don’t want to spend real money? Send "dry_run": true:

POST /v1/orders/4821/send_to_production
{ "dry_run": true }

Inkica runs every check exactly as for a real send — address, print files, stock, whether a factory can print that position, shipping — then returns the amount that would be charged, but charges nothing, produces nothing, and the order stays a draft:

{ "dry_run": true, "orders": [ { "external_id": "WEB-10023", "subtotal": 23.56, "shipping": 7.39, "total": 30.95 } ], "total": 30.95, "currency": "USD", "wallet_balance": 26.66, "sufficient_balance": false }
  • total is exactly the amount that would be charged if you sent it for real right then.
  • sufficient_balance: false → your wallet isn’t enough; a real send would be rejected — add funds first.
  • If the order has a problem (out of stock, a print position that can’t be made…) → you get the exact 400 error the real send would hit.

You can dry-run as many times as you like. When everything looks good, send again without dry_run (or with "dry_run": false) to send to production for real.

A dry run doesn’t go through the steps that happen after sending (the order moving to in_production → shipped, with tracking). To test reading status/tracking, use one small real order — cancel it right away if needed, and the money is refunded to your wallet.

⚠️ After this step the order gets a new id. The draft is replaced by the official order, so the old id (4821) can no longer be used. Be sure to save orders[].id from the response so you can track the order later:

{ "orders": [ { "id": "4822", "external_id": "WEB-10023", "status": "in_production", "shipping": 7.39, "total": 30.95 } ] }

Usually orders contains just 1 order. If the products in the order have to be made at several different factories, the order is split into several orders — the external_id then gets a suffix such as -F1, -F2…

Lost the response (for example, the network dropped midway)? Don’t send again — look the order up by external_id:

GET /v1/orders?external_id=WEB-10023

The result includes the -F1, -F2 parts if the order has been split.

7. Track orders

GET /v1/orders/4822 GET /v1/orders?status=shipped&page=1&limit=50
statusMeaning
draftDraft order — not sent to production, not charged.
pendingSent; Inkica is choosing a factory.
in_productionThe factory is producing it.
on_holdPaused — Inkica needs to check something (for example, an address problem). Contact support if it lasts a long time.
shippedHanded to the carrier — has a tracking number.
deliveredDelivered to the recipient.
cancelledCanceled.
rejectedRejected (for example, the print file doesn’t meet requirements) — review the order on the dashboard or contact support.
splitThe order was split by factory — see the child orders in split_orders.

When the order is handed to the carrier, the shipment field contains the tracking information for you to send to your customer:

"shipment": { "carrier": "usps", "tracking_number": "9400111899223456789012", "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223456789012", "label_url": "https://cdn.inkica.com/labels/4822.pdf", "status": "in_transit", "delivered_at": null }

label_url is the label file (PDF) to stick on the parcel — it’s null when the factory buys the label itself and only sends back the tracking number. tracking_url can also be null; in that case, look up tracking_number on the carrier’s website.

For orders where you supply your own label, shipment is the label and tracking number you sent, available from the moment the order is created.

Want Inkica to notify you automatically whenever an order changes status or gets tracking, instead of your system having to keep asking? Use a webhook. If you don’t use webhooks, we suggest checking orders that are pending/in_production about once an hour.

8. Cancel an order

POST /v1/orders/4822/cancel

You can only cancel while the factory hasn’t started production. A paid order is refunded to your wallet. Canceling a draft order has nothing to refund, since nothing was charged. Once the cancellation window has passed, the API returns an error — contact support if you still need help.

9. Webhook — get notified automatically

Register an address (URL) on your server — Inkica will automatically send (POST) to it whenever something changes, so you don’t have to keep asking.

Register

The quickest way: open the account menu → API → the Webhooks section → Add webhook, enter the URL and choose the events. Here you can also see each webhook’s status (running / disabled and why), whether the latest delivery succeeded or failed, the Send test button, and the delete button.

Or register through the API:

POST /v1/webhooks
{ "url": "https://your-server.com/inkica/webhooks", "events": ["order:updated", "order:shipment:created"] }
EventWhen
order:updatedThe order status changed (for example in_production → shipped). Includes previous_status.
order:shipment:createdThe order has a tracking number — you can send it to your customer right away.

Leaving events empty = receive everything. The response includes a secret like whsec_... — shown only once, so save it to verify signatures (below).

  • url must be a public https:// address — internal addresses (localhost, LAN IPs…) are not accepted.
  • Up to 5 webhooks per account, and each URL can be registered only once. List them: GET /v1/webhooks · Delete: DELETE /v1/webhooks/{id}.
  • Test right away: POST /v1/webhooks/{id}/test — Inkica sends a ping event and returns the result (delivered, status_code, error).

What Inkica sends

{ "id": "dlv_1042", "event": "order:updated", "created_at": "2026-10-17T08:06:10.000Z", "data": { "previous_status": "in_production", "order": { "id": "4822", "external_id": "WEB-10023", "status": "shipped", "shipment": { "tracking_number": "9400111899223456789012", "...": "..." }, "...": "..." } } }

data.order has exactly the same structure as GET /v1/orders/{id} — so you can reuse your existing order-handling code.

Your server needs to return a 2xx within 10 seconds. It’s best to store the event and return 200 right away, then process it later.

To make sure a request really comes from Inkica (and not someone impersonating us), every request has these headers:

HeaderContent
X-Inkica-EventThe event name
X-Inkica-DeliveryThe delivery ID (same as id in the body)
X-Inkica-TimestampWhen it was sent (Unix, seconds)
X-Inkica-Signaturesha256= + the HMAC-SHA256 of the string {timestamp}.{raw body} with the secret as the key

Recompute the signature over the raw body (before parsing JSON) and compare:

// Node.js (Express) — use express.raw() to keep the raw body const crypto = require('crypto'); app.post('/inkica/webhooks', express.raw({ type: 'application/json' }), (req, res) => { const ts = req.get('X-Inkica-Timestamp'); const expected = 'sha256=' + crypto.createHmac('sha256', process.env.INKICA_WEBHOOK_SECRET) .update(`${ts}.${req.body}`).digest('hex'); const got = req.get('X-Inkica-Signature') || ''; const valid = got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected)); if (!valid || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401); const event = JSON.parse(req.body); // ... store the event, process later res.sendStatus(200); });
# Python (Flask) import hmac, hashlib, time from flask import request, abort @app.post("/inkica/webhooks") def inkica_webhook(): ts = request.headers.get("X-Inkica-Timestamp", "") body = request.get_data() # raw body (bytes) expected = "sha256=" + hmac.new(SECRET.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, request.headers.get("X-Inkica-Signature", "")) or abs(time.time() - int(ts or 0)) > 300: abort(401) event = request.get_json() return "", 200
// PHP $body = file_get_contents('php://input'); $ts = $_SERVER['HTTP_X_INKICA_TIMESTAMP'] ?? ''; $expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $secret); if (!hash_equals($expected, $_SERVER['HTTP_X_INKICA_SIGNATURE'] ?? '') || abs(time() - (int)$ts) > 300) { http_response_code(401); exit; } $event = json_decode($body, true); http_response_code(200);

Also check that X-Inkica-Timestamp is no more than 5 minutes old, to block anyone replaying an old request.

Retries on failure

  • If your server doesn’t return a 2xx (an error, over 10 seconds, unreachable), Inkica automatically resends after 1, 5, 15, 60 minutes, then 3, 6, 12 hours — 8 attempts in total over about a day.
  • The same event can arrive more than once, and events can arrive out of order — use id to skip duplicates, and always trust the latest data.order.status (or call GET /v1/orders/{id}) instead of assuming an order.
  • If deliveries keep failing for too long (20 consecutive deliveries that fail completely), the webhook turns itself off (active: false). After you fix your server, click Send test on the API page (account menu) (or call POST /v1/webhooks/{id}/test) — a successful ping turns it back on. Changes that happen while the webhook is off are not re-sent — after turning it back on, call GET /v1/orders to refresh the status of the orders you’re tracking.
  • Orders you create through the dashboard or that come from marketplaces (eBay, Etsy…) are reported too — filter by external_id if you only care about orders created through the API.

Common errors

When something goes wrong, the API returns an HTTP code with an explanation in message:

{ "statusCode": 400, "error": "Bad Request", "message": "line_items[0].print_areas: this blueprint has no \"hood\" print position" }
CodeMeaningWhat to do
400The data sent is wrong/missing, or the action can’t be performed (insufficient wallet balance, the order can no longer be canceled…)Read message — for validation errors, message can be a list of several errors. Fix and resend.
401Missing token, wrong token, or the token has been revokedCheck the Authorization header, and create a new token if needed.
404Not found (wrong id, an order belonging to another account, or the draft order’s id that was replaced after sending to production)Double-check the id, or look it up by external_id.
409The external_id was already used for an order sent to productionThat order already exists — look it up with GET /v1/orders?external_id=... instead of creating a new one. If it really is a different order, use a different external_id.
429Too many requestsWait 1 minute and continue.

Limits

  • 600 calls / minute.
  • Up to 50 product lines per order, 10 tokens per account.
  • You can only create orders from the Inkica catalog (by variant_id); products you’ve created in the Products section can’t be used directly through the API yet.
  • Up to 5 webhooks per account.
  • There’s no separate test environment yet — use dry_run to check an order without spending money.

◀ Notifications · Contents · Next: Referral program ▶

Last updated on