Wire Contract
The exact HTTP bodies the Treatink API speaks — field names, nesting, and id prefixes. In
mode: 'fixtures' the SDK's bundled transport reproduces these shapes exactly, so switching to
live is a config change, not a rewrite. Response models are strict: do not depend on extra fields.
Decimal values serialize as JSON floats.
The SDK normalizes these wire shapes into the public catalog models — partners usually consume the normalized model and only need this page when integrating server-side or debugging traffic.
ID prefixes
chn_ channel · key_ credential · prd_ product · var_ variant · bnd_ bundle ·
cut_ cutout-label · ast_ artwork asset.
Patterns: ^prd_[0-9a-f]{32}$, ^var_…, ^cut_…, ^ast_….
Pagination
Every catalog list is a page: { "data": [ … ], "has_more": bool, "next_cursor": string|null }.
Query params (strict): limit (1–100, default 20), cursor (string|null).
1. GET /v1/channel
Publishable or secret key.
{ "id": "chn_0000000000000000000000000000000a", "name": "Example Pet Shop",
"mode": "test", "key_class": "publishable",
"permissions": ["artwork_upload", "catalog_read", "channel_read"] }
2. GET /v1/catalog/products
Publishable key. ?include=variants supported.
{ "data": [
{ "id": "prd_0000000000000000000000000000abcd",
"title": "Training Treats for Dogs (6oz)",
"description": "…", "animal_type": "dog",
"category": "treats", "product_type": "treats", "status": "active" }
],
"has_more": false, "next_cursor": null }
animal_type ∈ cat|dog|horse. category ∈ health|treats. product_type ∈
biscuits|dental_treats|soft_treats|treats. status = active.
3. GET /v1/catalog/variants
Publishable key. SKU lives here.
{ "data": [
{ "id": "var_0000000000000000000000000000ef01",
"product_id": "prd_0000000000000000000000000000abcd",
"sku": "SSGTTBC", "description": "…", "short_description": "…",
"option_values": { "flavor": "peanut_butter" },
"currency": "USD", "suggested_retail_cents": 999,
"availability": "available",
"fulfillment_eligibility": { "policy": "deny_unlisted", "country_codes": ["US"] },
"catalog_image": { "url": "https://cdn.treatink.com/…", "expires_at": "2026-08-19T00:00:00Z",
"content_type": "image/png", "size_bytes": 123456, "width": 1000, "height": 1000,
"sha256": "…" },
"regulatory_label_image": { "url": "…", "expires_at": "…", "content_type": "image/png",
"size_bytes": 1234, "width": 500, "height": 700, "sha256": "…" },
"label_zone": { "x": 0.321, "y": 0.316, "width": 0.358, "height": 0.478 } }
],
"has_more": false, "next_cursor": null }
Media objects are { url, expires_at, content_type, size_bytes, width, height, sha256 }.
label_zone is normalized and may be null for no-zone products.
4. GET /v1/catalog/bundles
Publishable key.
{ "data": [
{ "id": "bnd_…", "name": "…", "description": "…", "animal_type": "dog",
"currency": "USD", "raw_total_cents": 1998, "discount_cents": 200,
"suggested_retail_cents": 1798, "variant_ids": ["var_…", "var_…"] } ],
"has_more": false, "next_cursor": null }
5. GET /v1/catalog/cutout-labels
Publishable key. These are the designer's templates.
{ "data": [
{ "id": "cut_0000000000000000000000000000aa01",
"title": "Classic Frame", "category": "standard", "theme": "light",
"pet_name_position": "bottom", "description": "…", "tags": ["yellow", "hearts"],
"mask": { "url": "https://cdn.treatink.com/…png", "expires_at": "…",
"content_type": "image/png", "size_bytes": 34567, "width": 900, "height": 1200, "sha256": "…" },
"canvas": { "width": 900, "height": 1200 },
"placement": { "…": "opaque geometry pass-through" },
"alpha_threshold": 8,
"center_pixel_alpha": 0,
"alpha_stats": { "total_pixels": 1080000, "fully_transparent_pixels": 432000,
"semitransparent_pixels": 1200, "opaque_pixels": 646800,
"fully_transparent_fraction": 0.4, "semitransparent_fraction": 0.001111,
"opaque_fraction": 0.598889 },
"fully_transparent_bounds": {
"pixels": { "x": 127, "y": 306, "width": 649, "height": 667,
"right_exclusive": 776, "bottom_exclusive": 973 },
"normalized": { "x": 0.141111, "y": 0.255, "width": 0.721111, "height": 0.555833 },
"pixel_count": 432000 },
"non_opaque_bounds": { "pixels": { "…": 0 }, "normalized": { "…": 0 }, "pixel_count": 433200 },
"center_transparent_component": {
"pixels": { "…": 0 }, "normalized": { "…": 0 }, "pixel_count": 430000,
"start_pixel": { "x": 450, "y": 600, "alpha": 0 }, "touches_canvas_edge": false },
"largest_safe_transparent_rectangle": {
"pixels": { "…": 0 }, "normalized": { "…": 0 }, "pixel_area": 300000 } }
],
"has_more": false, "next_cursor": null }
category ∈ standard|holidays|birthdays|occasions (drives the designer tab bar). theme ∈
light|dark. pet_name_position ∈ default|top|upper|bottom (the text-placement hint).
Rectangle sub-shape: { pixels:{x,y,width,height,right_exclusive,bottom_exclusive}, normalized:{x,y,width,height} }; measured bounds add pixel_count; the component adds
start_pixel + touches_canvas_edge; the safe rectangle adds pixel_area. The SDK never
decodes alpha at runtime — it consumes this precomputed geometry.
6. Asset upload — the two-step flow
Publishable key, scope artwork_upload. The save pipeline runs 6a–6c twice: role: source (the
original photo) and role: rendered (the print composite). The two ast_ ids flow into
orders.buildPayload.
6a. POST /v1/assets → pending asset (201)
Request (the client computes sha256 + size_bytes before calling):
{ "role": "source", "content_type": "image/png", "size_bytes": 2456789,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }
Response:
{ "id": "ast_0000000000000000000000000000b101", "role": "source", "status": "pending",
"content_type": "image/png", "size_bytes": 2456789, "sha256": "e3b0…b855",
"created_at": "2026-07-20T12:00:00Z", "pending_expires_at": "2026-07-20T12:15:00Z",
"upload": { "method": "PUT", "url": "https://storage.treatink.com/…?X-signed=…",
"expires_at": "2026-07-20T12:10:00Z",
"headers": { "Content-Type": "image/png" } } }
role ∈ source|rendered. size_bytes ≤ 50,000,000 (the SDK enforces a stricter 25 MB
client-side).
6b. PUT the bytes to upload.url
Send the raw bytes to upload.url with exactly upload.headers. This is object storage, not the
Treatink API host. On network/CORS failure the SDK surfaces upload_failed. In fixtures mode this
step is skipped — the "upload" becomes a local object URL.
6c. POST /v1/assets/{ast_id}/finalize → final asset
Empty request body.
{ "id": "ast_…b101", "role": "source", "status": "final",
"content_type": "image/png", "size_bytes": 2456789, "width": 2048, "height": 1536,
"sha256": "e3b0…b855", "created_at": "2026-07-20T12:00:00Z",
"finalized_at": "2026-07-20T12:01:00Z" }
No URL is returned (asset reads are secret-key only) — the SDK builds previewUrl locally
from the in-browser composite. Finalize errors: 409 upload_expired, 409 upload_incomplete,
422 upload_validation_failed.
7. POST /v1/orders
Secret key, scope order_manage — server-side only, via submitOrder. The schema is strict:
unknown fields are rejected, and nullable fields must be present as explicit null
(orders.buildPayload() emits exactly this). Requires an Idempotency-Key header (1–255
visible-ASCII; scoped per partner+mode; same key + different body → 409 idempotency_conflict);
external_order_id also has its own per-scope uniqueness constraint.
{ "external_order_id": "partner-1001", "display_order_number": "#1001",
"currency": "USD",
"destination": { "address_line_1": "1 St", "address_line_2": null, "city": "X",
"region": "CA", "postal_code": "90000", "country_code": "US" },
"fulfillment": { "delivery_method": "ship_to_recipient", "instructions": null },
"amounts": { "subtotal_cents": 999, "discount_cents": 0, "shipping_cents": 295,
"tax_cents": 0, "total_cents": 1294 },
"line_items": [
{ "external_line_item_id": "li-1", "variant_id": "var_…", "quantity": 1,
"unit_price_cents": 999, "subtotal_cents": 999,
"personalization": { "source_asset_id": "ast_…b101", "rendered_asset_id": "ast_…b102",
"cutout_label_id": "cut_…aa01", "pet_name": "Milo" } }
] }
Wire rules: currency is "USD" only; the recipient needs ≥ 1 of email/phone;
external_line_item_id is required and unique; 1–100 lines; delivery_method is always
"ship_to_recipient". The wire carries no transform, pet-name position, label zone, or SKU —
the print pipeline uses the rendered asset directly; that context stays client-side in the
draft. Order status vocabulary: received | in_production | shipped | rejected | cancelled
(creation returns received).
The response carries the essentials:
{ "id": "ord_…", "status": "received", "external_order_id": "partner-1001",
"display_order_number": "#1001", "created_at": "…",
"line_items": [ { "id": "…", "external_line_item_id": "li-1",
"variant_id": "var_…", "quantity": 1 } ] }
In fixtures mode the order fixture stays idempotent on external_order_id.
8. Error envelope
Every failure has this shape:
{ "error": { "type": "invalid_request_error", "code": "upload_too_large",
"message": "…", "param": "size_bytes",
"request_id": "req_fx_000001" } }
type varies by class: 4xx request → invalid_request_error; 401 →
authentication_error; 403 → permission_error; 5xx → api_error (e.g. with code
service_unavailable). In fixtures mode, tk.fixtures.failNext(op, { status, code }) produces
this exact envelope on demand (including a request_id). See Errors for how
the SDK surfaces these.