CustomniaAPI
On this page

Customnia Partner API

Customnia is a print house in Long Island City, NY and Compton, CA. Stores, apps and marketplaces send us orders their customers already paid for; we print or embroider them and ship them, in the store's name if asked. This API is how orders get to us and how you learn what happens to them.

Ordering for yourself, not for a store's customers? See Order by API: sign up by email, retail prices, pay by card.

Base URL
https://api.customnia.com/v1
Format
Requests and answers are JSON; designs are uploaded as multipart/form-data.
Version
1, November 2026

Authentication

Every request carries your API key. Keys are issued by Customnia; write to hello@customnia.com. Keep the key on your server; never in a browser or a mobile app.

curl https://api.customnia.com/v1/catalog \
  -H "Authorization: Bearer cn_live_…"

Test mode

You also get a test key, cn_test_…. Build and test your integration with it: every call works and is checked and priced like a real one, but orders placed with it are kept apart, never printed, packed or charged. Their numbers start with TEST- and they carry "test": true. A test key sees only test orders, the live key only real ones.

POST/orders/{id}/simulate — test key only: {"status": "shipped"} moves a test order the way production would (printing, ready, shipped with a test UPS tracking number, delivered, cancelled), and the order.updated webhook follows within a minute.

Designs and mockups are the same in both modes: a design made with the test key can be ordered with the live key.

Errors and limits

StatusMeaning
401No key or a wrong one.
404Not found, or not yours.
409Not possible now: printing has started, the order is being placed, the design has no print file yet. The body says which (error).
422Invalid input: {"message": "…", "errors": {"field": ["…"]}}.
429More than 240 requests a minute. Wait and retry.

Money and times

Catalog

GET/catalog — blanks you can order, their colors and sizes, your price for every color × size, photos and where the print sits on them. Changes rarely: cache it for an hour.

{
  "currency": "USD", "production_days": 4,
  "shipping": {"first_item": 650, "additional_item": 200, "countries": ["US"]},
  "locations": [{"id": 1, "name": "NYC"}, {"id": 2, "name": "LA"}],
  "blanks": [{
    "id": 4, "name": "Unisex Next Level Cotton T-Shirt", "category": "T-Shirts", "decoration": "print",
    "print_area": {"width_in": 14, "height_in": 16, "width_px": 2100, "height_px": 2400, "dpi": 150},
    "sizes": [{"id": 3, "name": "M"}, …],
    "colors": [{"id": 1, "name": "White", "hex": "#FFFFFF", "photo": "https://…jpg",
                "photo_print_area": {"preview_x": 160, "preview_y": 115, "preview_width": 280, "preview_height": 320},
                "prices": {"3": 1360, …},
                "back_photo": "https://…jpg", "back_print_price": 320}, …],
    "back_print_area": {"width_in": 14, "height_in": 16, "width_px": 2100, "height_px": 2400, "dpi": 150}
  }, …]
}

Filters: /catalog?category=HOODIE&color=Black (also brand, size, label; each takes an id or a name, case aside). The answer always carries filters with every value they can take. GET/catalog/{blank_id} is one blank.

Each blank also has brand, sku, labels, printing_speeds, has_front, has_back, platen (print area in pixels on every blank); each color sku, dark, text_hex, print_location_id, same_day.

photo_print_area is in a 600 × 600 frame laid over the photo: scale it to the photo's size to place a preview.

Blanks with a back_print_area are printed on the back too. A back print adds the color's back_print_price to each piece; back_photo and back_photo_print_area work like the front ones.

Designs

A design is your artwork on one blank. Order items refer to it; every color of the blank can be ordered with it.

POST/designs — blank_id and file: a PNG, at least 600 × 600 px; for a full print use the blank's print_area.width_px × height_px. Transparent background prints best. Add back_file for the back (blanks with a back_print_area only), or send only back_file for a back-only print. Without either you get an empty design to open in the designer.

curl https://api.customnia.com/v1/designs -H "Authorization: Bearer cn_live_…" \
  -F blank_id=4 -F file=@sunset.png -F back_file=@logo.png

201 {"id": 57, "blank_id": 4, "ready": true, "sides": ["front", "back"],
     "print_file_url": "https://…png", "back_print_file_url": "https://…png",
     "mockup_url_template": "https://api.customnia.com/v1/designs/57/mockups/{color_id}.jpg",
     "back_mockup_url_template": "https://api.customnia.com/v1/designs/57/mockups/{color_id}.jpg?side=back"}

GET/designs/{id} — the same; ready turns true once the design has a print file (after the designer's Save).

GET/designs?page=1&per_page=25 — your designs, newest first, with meta.total and meta.last_page. customized_blank_ids=172945,172944 finds designs by their id in the old design API; blank_id narrows to one blank.

Designs from layers

If you have your own editor, send its layers instead of a PNG: a fabric.js canvas per side, the way Customnia's designers keep them. We render the print files and previews from them; ready turns true when the print file exists.

POST /v1/designs
{"blank_id": 4, "color_id": 1, "notes": "Left chest",
 "front_layers": {"version": "5.3.0", "objects": [{"type": "image", "uuid": "u-1", "src": "https://…/logo-light.png",
                  "left": 140, "top": 20, "width": 1000, "height": 1000, "scaleX": 0.2, "scaleY": 0.2}]},
 "back_layers": null,
 "alternative": {"front": {"u-1": {"src": "https://…/logo-dark.png"}}}}

201 {"id": 58, "customized_blank_id": 173010, "blank_id": 4, "color_id": 1,
     "color": {"id": 1, "name": "White", "hex": "#FFFFFF", "dark": false}, "print_location_id": 11,
     "origin": "api", "test": false, "ready": false, "editable": true, "sides": ["front"],
     "layers": {"front": {"canvas": {…}, "alternative": {"u-1": {"src": "…dark.png"}}, "fabric_version": 5, "attached_files": {}}, "back": null},
     "previews": {"front": null, "back": null},
     "print_files": {"front": {"main": null, "alternative": null, "clear_background": null, "ready_print": null, "ready_print_alternative": null}, "back": null},
     "print_file_url": null, "back_print_file_url": null, …}

PATCH/designs/{id} — only what you send changes: front_layers / back_layers (null removes the side), alternative, color_id, notes; or a new picture for a side as file / back_file (multipart POST /designs/{id} with _method=PATCH). A side that changes gets a new print file. Once the design is in a placed order: 409 design_locked — copy it instead.

Copy and claim

POST/designs/{id}/copy — {"blank_id": 6, "color_id": 2} (blank_id defaults to the design's own): a new design of yours on that blank and color. Layers are scaled into the new print area; between light and dark colors objects with an alternative swap to it. POST/designs/{id}/layers/copy takes the same and answers {"front": {"canvas": …, "alternative": …}, "back": null} without saving anything.

POST/designs/claim — {"customized_blank_ids": [172945, 172944]}: designs you made through the old design API become yours here, keeping their id (customized_blank_id). A design that another partner has, or that carries another partner's mark, a Customnia customer's design, or a design in another store's order cannot be claimed. Claiming again returns the same design. Live key only.

{"claimed": [{"id": 61, "customized_blank_id": 172945, "origin": "claimed", …}],
 "rejected": [{"customized_blank_id": 172944, "error": "taken", "message": "The design belongs to another partner."}]}

Replace a picture

POST/designs/media/replace — a picture changed in your media library, changed in your designs. On a light garment the light variant is the object's picture and dark its alternative; the other way round on a dark one. new given means that variant changes (null removes the alternative). Without design_ids every design of yours with the picture changes.

POST/designs/text/replace — a text changed at once in your designs: from: {text, font} finds the words (the font only when given), to: {text, color_light, color_dark} is what they become. On a light garment color_light is the fill and color_dark its alternative; the other way round on a dark one. Without design_ids every design of yours with the words changes.

{"replacements": [{"design_ids": [57, 58],
  "light": {"current": "https://…/logo-light.png", "new": "https://…/logo2-light.png"},
  "dark":  {"current": "https://…/logo-dark.png",  "new": "https://…/logo2-dark.png"}}]}
→ {"updated": [57, 58]}

A test key reads every design of yours but changes only designs made with a test key.

Mockups

GET/designs/{id}/mockups/{color_id}.jpg — the design on the photo of the blank in that color, 1000 × 1000 JPEG, made once and kept; ?side=back for the back. Download it and host it yourself (e.g. upload it to Shopify); the URL needs your key.

Designer link

POST/designs/{id}/editor-link — {"return_url": "https://your.app/back", "email": "seller@…", "brand_name": "Brooklyn Tees"} → {"url": "…", "expires_in": 43200}. Send your user to url: our designer opens with this design; after Save the browser goes to return_url. Then fetch the design again for its new print file.

Quotes

POST/quotes — what an order would cost, before you charge anyone.

{"items": [{"design_id": 57, "color_id": 1, "size_id": 3, "quantity": 2}]}
→ {"items": [{… "price": 1360}], "totals": {"items": 2720, "shipping": 850, "total": 3570, "currency": "USD"}}

Orders

POST/orders — an order your customer already paid for. reference is your own order id and makes the call safe to retry: the same reference returns the order you already placed (200) instead of a new one (201).

{
  "reference": "shopify:brooklyn-tees:1001",
  "label": "#1001",
  "seller": {"name": "Brooklyn Tees", "domain": "brooklyn-tees.myshopify.com", "packing_name": "Brooklyn Tees",
             "logo_url": "https://brooklyn-tees.com/logo.png", "packing_note": "Thanks for shopping small!"},
  "customer": {"email": "jamie@example.com"},
  "shipping_address": {"first_name": "Jamie", "last_name": "Rivera", "address1": "350 5th Ave", "city": "New York",
                       "state": "NY", "zip": "10118", "country": "US", "phone": "+12125550123"},
  "items": [{"design_id": 57, "color_id": 1, "size_id": 3, "quantity": 2}],
  "warehouse_id": 1,
  "notes": "Gift"
}

The answer is the order:

{
  "id": "CFK3625MM", "reference": "shopify:brooklyn-tees:1001", "test": false, "status": "received",
  "ready_by": "2026-11-06T18:00:00-05:00", "location": "NYC",
  "can_cancel": true, "can_change_address": true,
  "items": [{"design_id": 57, "color_id": 1, "color": "White", "size_id": 3, "size": "M", "quantity": 2}],
  "totals": {"items": 2720, "shipping": 850, "total": 3570, "currency": "USD"},
  "shipments": []
}

GET/orders/{id} — by our number (CFK3625MM) or by your reference.

Statuses

statusMeaning
receivedIn the production queue.
printingPrinting has started; it can no longer be cancelled.
readyMade and packed, waiting for the carrier.
shippedHanded to the carrier; shipments has the tracking number.
deliveredThe carrier delivered it.
cancelledCancelled before printing (by you or by us).

A shipment: {"carrier": "UPS", "service": "Ground", "tracking_number": "1Z…", "tracking_url": "https://…", "shipped_at": "…", "delivered_at": null}.

Cancel and change the address

POST/orders/{id}/cancel — {"reason": "customer"}. Works while can_cancel is true; otherwise 409 printing_started and the order is made.

PATCH/orders/{id}/address — the same fields as shipping_address. Works while can_change_address is true; otherwise 409 too_late.

Webhooks

Give us a URL and we POST order.updated whenever an order's status, ready-by date, tracking or delivery changes. Answer 2xx; otherwise we retry after 1, 5, 15 and 60 minutes, then after 3, 6 and 12 hours.

POST https://your.app/webhooks/customnia
X-Customnia-Event: order.updated
X-Customnia-Delivery: 1842
X-Customnia-Signature: t=1793580000,v1=5f2c…

{"event": "order.updated", "sent_at": "…", "order": { …the order as above… }}

Check the signature: HMAC-SHA256 of t + "." + raw body with your webhook secret must equal v1; reject old t values.

// PHP
[$t, $v1] = sscanf($header, 't=%d,v1=%s');
$ok = hash_equals(hash_hmac('sha256', $t . '.' . $rawBody, $secret), $v1) && abs(time() - $t) < 300;

// Node
const [t, v1] = header.match(/t=(\d+),v1=(\w+)/).slice(1);
const ok = crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(
  crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')));

Checking for changes

GET/orders?updated_since=2026-11-01T00:00:00Z&limit=100 — orders that changed since then, newest first. Use it as a safety net next to webhooks (e.g. every 10 minutes), or instead of them.

Connect an AI agent (MCP)

The API is also an MCP server at https://api.customnia.com/mcp (Streamable HTTP), for AI agents and coding tools: Claude Code, Cursor, VS Code and others that take a server URL with a header. It uses your API key (without one it only offers buyers' sign-up), sees exactly what your key sees and calls the same endpoints, with the same limits.

claude mcp add --transport http customnia https://api.customnia.com/mcp \
  --header "Authorization: Bearer cn_test_…"
// Cursor: ~/.cursor/mcp.json
{"mcpServers": {"customnia": {"url": "https://api.customnia.com/mcp",
  "headers": {"Authorization": "Bearer cn_test_…"}}}}

Tools: search_catalog, get_blank, create_design (from a PNG URL or layers), get_design, list_designs, copy_design, get_mockup (the image itself), designer_link, quote_order, get_order, list_orders, cancel_order, change_order_address; with a test key also place_order and simulate_order; with the live key claim_designs. Stores' real orders are not placed through MCP: place them from your server with POST /orders. Buyers' keys (see Order by API) add get_account, setup_payments and a live place_order. Resources: the API summary, openapi.yaml, the migration steps and the ordering steps.

Start with the test key. Give the agent the live key only for reading orders and designs, cancelling, or claiming old designs.

Moving from the old design API

The old API at design.customnia.com/api (and its copy on designer.customnia.com) had no keys and is being retired. Everything it did is here, with your key. Claim your existing designs first (POST /designs/claim with their ids); after that they are reachable by their new id, and customized_blank_id stays the id the checkout and your database already use.

Moving a whole project? Step-by-step instructions (Markdown, written so a coding agent can follow them): find the old calls, claim designs, replace calls and fields, test, cut over.

OldNew
GET|POST /api/blanks, GET /api/blanks/filtersGET /catalog (+ filters); categories → category_id / category / category_code on each blank, printingSpeeds / labels / brands → filters
GET /api/blanks/{id}GET /catalog/{blank_id}
POST /api/blanks/{id}/customize-blanks {cb: {blank_printing_location_id, front_layers, back_layers}, alternative, notes}POST /designs {blank_id, print_location_id (or color_id), front_layers, back_layers, alternative, notes}
GET /api/customize-blanks/{id}GET /designs/{id}
POST /api/customize-blanks/list {items}GET /designs?customized_blank_ids=…
PUT /api/customize-blanks/{id}PATCH /designs/{id} (only the sides sent change; 409 design_locked instead of 400)
POST /api/customize-blanks/{id}/copy {blank_id, color_id}POST /designs/{id}/copy — the copy is a design of yours (id, customized_blank_id)
POST /api/customize-blanks/{id}/layers/copyPOST /designs/{id}/layers/copy; layers.front.main → front.canvas, layers.front.alternative → front.alternative
POST /api/customize-blanks/media/replacePOST /designs/media/replace; customize_blanks → design_ids (or none: all of yours), is_changed_light + light_current + light_new → light: {current, new}, same for dark
POST /api/customize-blanks/text/replacePOST /designs/text/replace; customize_blanks → design_ids (or none: all of yours), from and to unchanged; the answer is updated (your design ids) instead of changed

Fields

OldNew
id (customized_blank)customized_blank_id; the design's own id is id
blank_printing_location_id, blank_printing_location.color_model.{id, hex, dart}print_location_id, color.{id, hex, dark}, color_id
front_artwork.fabric_layers (a JSON string)layers.front.canvas (an object)
front_artwork.alternative_layer, .attached_files, .fabric_versionlayers.front.alternative, .attached_files, .fabric_version (5 when empty)
front_artwork.preview_imagepreviews.front
front_artwork.platen_image, .alternative_platen_image, .readyprint_mainprint_file_url (= print_files.front.main), print_files.front.alternative, print_files.front.ready_print
back_artwork.*layers.back, previews.back, back_print_file_url, print_files.back
notesnotes (sent at the top level, not in cb)
blank: mode_preview, platen_front_width, brand_name, images, sizes[].size, colors[].dart, printingLocations[] (id, color_id, preview_front_url, layout_front)preview_mode, platen.width_px, brand, images, sizes[].name, colors[].dark, per color print_location_id, photo, photo_print_area, back_photo, back_photo_print_area
blank cost, colors[].cost, sizes[].cost, front_printing_cost, store marginyour price per color × size in colors[].prices and back_print_price; Customnia's retail "from" price in retail_price_from

Questions, a key, a webhook secret: hello@customnia.com.