Skip to main content

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_typecat|dog|horse. categoryhealth|treats. product_typebiscuits|dental_treats|soft_treats|treats. statusactive|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. availabilityavailable|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 }

categorystandard|holidays|birthdays|occasions (drives the designer tab bar). themelight|dark. pet_name_positiondefault|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" } } }

rolesource|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",
"recipient": { "name": "A B", "email": "a@b.com", "phone": null },
"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 requestinvalid_request_error; 401authentication_error; 403permission_error; 5xxapi_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.