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|retired — retirement is
in-place: retired products stay readable in the catalog, but ordering a retired offering fails
with 422 variant_not_suitable.
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. availability ∈
available|retired (same in-place retirement semantics as product status).
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).
Accepted content_type values are role-split. role: source accepts image/png, image/jpeg
(including phone-originated JPEGs carrying MPF/gain-map auxiliary frames — the primary image
governs validation and dimensions), image/heic, image/heif, image/webp, and image/avif.
role: rendered accepts exactly image/png and image/jpeg. Animated files, multi-photograph
containers, video, and undecodable bytes are refused at finalize with
422 upload_validation_failed. (The SDK converts HEIC for display, but uploads the ORIGINAL
HEIC/HEIF bytes as the source asset — full fidelity; the transcode is only the fallback when
the original can no longer be read at save time.)
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.