TPOP API

Version 1.0.0. Base URL https://api.tpop.pro. Read the catalog, quote, upload print files, create and pay orders, follow production and tracking, receive signed webhooks.

Get started

1. In your TPOP account, open API and create a key. Start with a test key: orders are never produced nor charged.

2. Call the API from your server, never from a web page (calls carrying a browser Origin header are refused):

curl https://api.tpop.pro/v1/me \
  -H "Authorization: Bearer $TPOP_API_KEY"

3. Pick a catalog variant (GET /v1/catalog/products, then /variants) and read its print areas: each has a face code, a size in cm and a minimum resolution.

4. Upload your design (POST /v1/files with an HTTPS link), then create the order:

curl -X POST https://api.tpop.pro/v1/orders \
  -H "Authorization: Bearer $TPOP_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-ERP-1042" \
  -d '{
    "external_id": "ERP-1042",
    "shipping_address": {"name": "Jane Doe", "line1": "1 rue de la Paix", "postal_code": "75002", "city": "Paris", "country": "FR"},
    "items": [{"sku": "TPO-TS-UNI-CHAN-WHI-M", "quantity": 1, "files": {"front": "<file id>"}}]
  }'

5. Pay it with "confirm": true or POST /v1/orders/{id}/confirm, then follow it with GET /v1/orders/{id} or webhooks.

Keys and permissions

Send Authorization: Bearer tpop_live_… on every call. A key is shown once at creation; TPOP keeps only its fingerprint. Each key has its own permissions:

PermissionAllows
catalog:readCatalog, countries, quotes and shipping rates
orders:readList and read orders, shipments, tracking and history
orders:writeCreate draft orders, quote, cancel
orders:confirmPay orders with your TPOP wallet (spending limits required)
files:writeUpload print files
webhooks:manageManage the webhook endpoints of your account

You can restrict a key to your servers' IP addresses and give it an expiry date. A key stops working at once when you revoke it, when the person who created it leaves your account, or when it is found in public code on GitHub (it is revoked automatically and you are alerted).

Limits: 600 requests per minute per account (headers X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset; 429 with Retry-After beyond). Request bodies up to 1 MB, 100 items per order.

Payments and limits

The API moves no money except one thing: paying an order, from your TPOP wallet (offered credit first). Top-ups, withdrawals, refunds and invoices stay in your account.

A key that can pay has two limits you set: per order and per rolling 24 hours (default €150 and €300). Beyond them the order stays a draft, the call returns 402 spending_limit_reached and you are alerted. Unusual activity (many more paid orders than usual, or a burst of new destination countries) freezes the key's payments (402 payments_frozen) until you restore them in your account. Prices are always computed by TPOP: never sent by you.

An order can be cancelled until production starts (POST /v1/orders/{id}/cancel); what the wallet paid goes back to the wallet.

Test mode

Keys starting with tpop_test_ behave exactly like live keys, with the same checks, but nothing is ever charged nor produced: a confirmed test order gets the status test_confirmed. Every order says "test": true|false.

Conventions

  • JSON only, field names in snake_case; unknown fields are refused.
  • Identifiers are UUIDs; catalog variants are identified by their sku.
  • Amounts are integers in euro cents (*_cents) with currency: "EUR"; dates are ISO 8601 in UTC.
  • Lists return {"data": [...], "has_more": true, "next_cursor": "…"}; pass cursor to get the next page, limit from 1 to 100.
  • Idempotency-Key is required to create an order: retrying with the same key and body returns the same order (header Idempotent-Replayed: true); the same key with another body is refused with 409.
  • Order statuses: draft, confirmed, test_confirmed, in_production, shipped, delivered, on_hold, cancelled, refunded.
  • Files given per face are placed on the whole print area, centered horizontally and aligned to the top, at the largest size that fits. Their pixels must reach the area's min_dpi (else 422 file_resolution_too_low with the required resolution).

Errors

Every error has the same shape and a stable code:

{
  "error": {
    "code": "spending_limit_reached",
    "message": "This order would exceed the spending limit of this API key. It stays a draft.",
    "details": {"total_cents": 18990, "per_order_limit_cents": 15000, "currency": "EUR"}
  }
}
StatusMeaning
400Malformed request: invalid JSON, unknown field, bad parameter, missing Idempotency-Key
401Missing, invalid, expired or revoked key
402Payment refused: limit reached, payments frozen, balance too low; the order stays a draft
403Missing permission, address not allowed, call from a browser
404Unknown identifier (or not yours)
409Idempotency conflict, duplicate external_id, order already in production
413 / 415Body too large / not JSON
422Content not acceptable: unknown SKU, file too small, incomplete address, country not served
429Too many requests
5xxOur side: retry later with the same Idempotency-Key

Webhooks

Register an HTTPS endpoint (POST /v1/webhooks, permission webhooks:manage) and the events you want: order.created, order.in_production, order.shipped (carrier, tracking number and link), order.delivered, order.issue, order.cancelled. The signing secret is shown once.

Each delivery is a POST with TPOP-Event-Id, TPOP-Event-Type and TPOP-Signature: t=<timestamp>,v1=<hex HMAC-SHA256 of "<timestamp>.<body>">. Answer 2xx within 10 seconds; failed deliveries are retried with increasing delays for 24 hours, then the endpoint is paused and you are alerted. The same event can arrive twice: use TPOP-Event-Id to ignore repeats. Verify the signature:

<?php
$secret = getenv('TPOP_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_TPOP_SIGNATURE'] ?? ''), $sig);
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, $secret);
$fresh = abs(time() - (int) ($sig['t'] ?? 0)) < 300;
if (!$fresh || !hash_equals($expected, $sig['v1'] ?? '')) { http_response_code(400); exit; }
$event = json_decode($body, true); // $event['type'], $event['data']
http_response_code(200);

Versions

This is version 1. Within v1 we only add things (new fields, new endpoints, new event types, new error codes): ignore what you do not know. A breaking change comes only with v2, and v1 stays available 12 months after v2 is released.

Account

The account and API key in use.

GET /v1/me

Account and API key in use

Any valid key.

Responses

  • 200 The account and key
  • 401 Missing, invalid, expired or revoked key

Catalog

Products you can print on, their variants, print areas and prices.

GET /v1/catalog/products

List catalog products

Permission catalog:read. Only products currently offered to your account.

ParameterInTypeDescription
limitqueryinteger
cursorquerystringnext_cursor of the previous page.
langqueryen | fr | it | es | de | nl | pl | pt
categoryquerystringCategory code (sub-categories included).
qquerystringSearch in titles and SKU.
tierqueryessential | premium | eco
genderquerystring
fitquerystring
labelquerystringCertification code, e.g. gots.

Responses

  • 200 A page of products
  • 400 Malformed request (unknown field, invalid JSON, bad parameter)
  • 401 Missing, invalid, expired or revoked key
  • 403 The key lacks the permission, the address is not allowed, or the call comes from a browser

GET /v1/catalog/products/{id}

Catalog product with colors, sizes, print areas and techniques

Permission catalog:read.

ParameterInTypeDescription
id requiredpathstring (uuid)
langqueryen | fr | it | es | de | nl | pl | pt

Responses

  • 200 The product
  • 401 Missing, invalid, expired or revoked key
  • 403 The key lacks the permission, the address is not allowed, or the call comes from a browser
  • 404 Unknown identifier (or not yours)

GET /v1/catalog/products/{id}/variants

Variants of a catalog product (color × size, SKU, price, availability)

Permission catalog:read. Order a variant by its sku.

ParameterInTypeDescription
id requiredpathstring (uuid)
limitqueryinteger
cursorquerystringnext_cursor of the previous page.
langqueryen | fr | it | es | de | nl | pl | pt

Responses

  • 200 A page of variants
  • 401 Missing, invalid, expired or revoked key
  • 403 The key lacks the permission, the address is not allowed, or the call comes from a browser
  • 404 Unknown identifier (or not yours)

GET /v1/countries

Countries we ship to

Permission catalog:read.

ParameterInTypeDescription
langqueryen | fr | it | es | de | nl | pl | pt

Responses

  • 200 All countries served
  • 401 Missing, invalid, expired or revoked key
  • 403 The key lacks the permission, the address is not allowed, or the call comes from a browser

Quotes

Price an order, with shipping and VAT, without creating it.

POST /v1/orders/estimate

Quote an order without creating it

Permission catalog:read or orders:write. Prices are always computed by TPOP from the catalog and your plan.

Body

Object EstimateRequest

Responses

  • 200 The quote
  • 400 Malformed request (unknown field, invalid JSON, bad parameter)
  • 401 Missing, invalid, expired or revoked key
  • 403 The key lacks the permission, the address is not allowed, or the call comes from a browser
  • 422 The content is not acceptable (unknown SKU, file too small for its print area, incomplete address…)

POST /v1/shipping/rates

Shipping cost of a set of items to a country

Permission catalog:read or orders:write. Same body as the quote.

Body

Object EstimateRequest

Responses

  • 200 Shipping cost
  • 422 The content is not acceptable (unknown SKU, file too small for its print area, incomplete address…)

Files

Print files TPOP downloads from your HTTPS link.

POST /v1/files

Upload a print file from an HTTPS link

Permission files:write. TPOP downloads the file once: HTTPS only, public address only, no redirect followed, PNG or JPEG. Send Idempotency-Key to make retries safe.

ParameterInTypeDescription
Idempotency-Keyheaderstring

Body

FieldTypeDescription
url requiredstring (uri)Example: https://example.com/designs/front.png
filenamestring

Responses

  • 201 The stored file
  • 200 Replay of the same request (Idempotent-Replayed: true)
  • 409 Idempotency conflict, duplicate external_id, or the order can no longer change
  • 422 The content is not acceptable (unknown SKU, file too small for its print area, incomplete address…)

GET /v1/files/{id}

A file you uploaded

Permission files:write or orders:read.

ParameterInTypeDescription
id requiredpathstring (uuid)

Responses

  • 200 The file
  • 404 Unknown identifier (or not yours)

Orders

Create, pay, cancel, list and track orders.

GET /v1/orders

List orders created through the API

Permission orders:read. Most recent first.

ParameterInTypeDescription
limitqueryinteger
cursorquerystringnext_cursor of the previous page.
statusqueryOrderStatus
external_idquerystring
created_afterquerystring (date-time)
created_beforequerystring (date-time)

Responses

  • 200 A page of orders
  • 400 Malformed request (unknown field, invalid JSON, bad parameter)

POST /v1/orders

Create an order (draft, or paid with `confirm: true`)

Permission orders:write; confirm: true also needs orders:confirm. Idempotency-Key is required. A draft costs nothing. Paying debits your TPOP wallet, within the spending limits of the key; if a check fails the order stays a draft and the error says why.

ParameterInTypeDescription
Idempotency-Key requiredheaderstringUnique per order you create. The same key with the same body returns the same order; with a different body it is refused (409).

Body

Object OrderRequest

Responses

  • 201 The new order
  • 200 Replay of the same request (Idempotent-Replayed: true)
  • 400 Malformed request (unknown field, invalid JSON, bad parameter)
  • 402 spending_limit_reached, payments_frozen, insufficient_balance, wallet_not_allowed or wallet_disabled: the order stays a draft (error.details.order when it was just created)
  • 409 Idempotency conflict, duplicate external_id, or the order can no longer change
  • 422 The content is not acceptable (unknown SKU, file too small for its print area, incomplete address…)

GET /v1/orders/{id}

An order, with shipments, tracking and history

Permission orders:read.

ParameterInTypeDescription
id requiredpathstring (uuid)

Responses

  • 200 The order
  • 404 Unknown identifier (or not yours)

POST /v1/orders/{id}/confirm

Pay a draft order with the wallet

Permission orders:confirm. Refused beyond the per-order or 24-hour limit of the key, when payments are frozen, or when the balance is too low: the order stays a draft. Calling it again on a paid order changes nothing.

ParameterInTypeDescription
id requiredpathstring (uuid)

Responses

  • 200 The order, now confirmed (or test_confirmed with a test key)
  • 402 spending_limit_reached, payments_frozen, insufficient_balance, wallet_not_allowed or wallet_disabled: the order stays a draft (error.details.order when it was just created)
  • 404 Unknown identifier (or not yours)
  • 409 Idempotency conflict, duplicate external_id, or the order can no longer change

POST /v1/orders/{id}/cancel

Cancel an order before production

Permission orders:write. What the wallet paid goes back to the wallet. Refused once production has started.

ParameterInTypeDescription
id requiredpathstring (uuid)

Responses

  • 200 The cancelled order, with refunded_cents
  • 404 Unknown identifier (or not yours)
  • 409 Idempotency conflict, duplicate external_id, or the order can no longer change

Webhooks

Signed alerts sent to your server when an order changes.

GET /v1/webhooks

List webhook endpoints

Permission webhooks:manage.

ParameterInTypeDescription
limitqueryinteger
cursorquerystringnext_cursor of the previous page.

Responses

  • 200 A page of endpoints

POST /v1/webhooks

Register an HTTPS endpoint

Permission webhooks:manage. HTTPS on port 443 to a public address only; 10 endpoints at most. The response contains the signing secret, shown only once.

Body

FieldTypeDescription
url requiredstring (uri)
events requiredarray of EventType

Responses

  • 201 The endpoint, with its secret
  • 409 Idempotency conflict, duplicate external_id, or the order can no longer change
  • 422 The content is not acceptable (unknown SKU, file too small for its print area, incomplete address…)

GET /v1/webhooks/{id}

A webhook endpoint

Permission webhooks:manage.

ParameterInTypeDescription
id requiredpathstring (uuid)

Responses

  • 200 The endpoint
  • 404 Unknown identifier (or not yours)

PATCH /v1/webhooks/{id}

Change the URL, events or status (active or paused)

Permission webhooks:manage.

ParameterInTypeDescription
id requiredpathstring (uuid)

Body

FieldTypeDescription
urlstring (uri)
eventsarray of EventType
statusactive | paused

Responses

  • 200 The endpoint
  • 404 Unknown identifier (or not yours)
  • 422 The content is not acceptable (unknown SKU, file too small for its print area, incomplete address…)

DELETE /v1/webhooks/{id}

Stop an endpoint

Permission webhooks:manage. Pending deliveries are dropped.

ParameterInTypeDescription
id requiredpathstring (uuid)

Responses

  • 200 The stopped endpoint
  • 404 Unknown identifier (or not yours)

POST /v1/webhooks/{id}/test

Send a signed `ping` now

Permission webhooks:manage. One attempt, no retry.

ParameterInTypeDescription
id requiredpathstring (uuid)

Responses

  • 200 Result of the attempt
  • 404 Unknown identifier (or not yours)

Objects

Error

FieldTypeDescription
error requiredobject

Page

FieldTypeDescription
data requiredarray of any
has_more requiredboolean
next_cursor requiredstring | null

Me

FieldTypeDescription
accountobject
api_keyobject

Scope

catalog:read | orders:read | orders:write | orders:confirm | files:write | webhooks:manage

Money

Amount in euro cents.

integer

CatalogProduct

FieldTypeDescription
idstring (uuid)
sku_prefixstring
titlestring
subtitlestring | null
categoryobject
main_image_urlstring | null
brandstring | null
quality_tierstring | null
genderstring | null
fitstring | null
variants_countinteger
colors_countinteger
sizes_countinteger
base_price_from_centsMoney
price_from_centsMoney
currencystringExample: EUR
production_daysinteger | null
availabilityavailable | discontinued
discontinued_atstring | null (date-time)
replacementobject | null

CatalogProductDetail

FieldTypeDescription
All fields of CatalogProduct
description_htmlstring | null
detailsstring | null
weight_gramsinteger | null
pricingobject
lead_timeobject
attributesobjectComposition, fabric weight, certifications, care codes, origin and print country.
colorsarray of object
sizesarray of object
print_areasarray of PrintArea
techniquesarray of object
factsobjectProduct facts: specifications, certifications, care, size guide in cm.

PrintArea

FieldTypeDescription
facestringUse this code as key of files when ordering.
namestring
is_neck_labelboolean
width_cmnumber
height_cmnumber
zonesarray of object
min_dpiinteger
min_pixelsobject
techniquesarray of string
extra_cost_centsMoney
currencystring

CatalogVariant

FieldTypeDescription
skustringExample: TPO-TS-UNI-CHAN-WHI-S
product_idstring (uuid)
colorobject
sizeobject
availabilityavailable | discontinued
discontinued_atstring | null (date-time)
replacementobject | null
base_price_centsMoney
price_centsMoney
currencystring
weight_gramsinteger | null
image_urlstring | null

Country

FieldTypeDescription
codestringExample: FR
namestring
is_euboolean
requires_customsboolean

Address

FieldTypeDescription
name requiredstring
companystring | null
line1 requiredstring
line2string | null
postal_codestring | null
city requiredstring
country requiredstringExample: FR
phonestring | null
emailstring | null (email)

OrderItemRequest

A catalog variant (sku) with either one of your products (product_id: its print files are used) or uploaded files per print face (files: each file is placed on the whole print area, centered horizontally and aligned to the top, at the largest size that fits; its resolution must reach the min_dpi of the area).

FieldTypeDescription
sku requiredstring
quantity requiredinteger
product_idstring (uuid)
filesobjectExample: {"front":"3f0b\u2026","back":"9a1c\u2026"}

EstimateRequest

FieldTypeDescription
items requiredarray of OrderItemRequest
shipping_addressAddress
countrystring

OrderRequest

FieldTypeDescription
external_idstringYour reference, unique in your account.
confirmbooleantrue = pay now with the wallet (needs orders:confirm).
shipping_address requiredAddress
items requiredarray of OrderItemRequest

Costs

FieldTypeDescription
items_centsMoney
discount_centsMoney
options_centsMoney
shipping_centsMoney
tax_centsMoney
total_centsMoney
tax_ratenumberExample: 20
vat_regimedomestic | eu_vat | eu_reverse_charge | export
currencystring

Estimate

FieldTypeDescription
itemsarray of object
optionsarray of object
costsCosts
shipping_countrystring

OrderStatus

draft | confirmed | test_confirmed | in_production | shipped | delivered | on_hold | cancelled | refunded

Order

FieldTypeDescription
idstring (uuid)
numberstringExample: P261017-E02B86
external_idstring | null
statusOrderStatus
testboolean
itemsarray of object
shipping_addressAddress
costsCosts
shipmentsarray of Shipment
historyarray of object
api_key_prefixstring | null
refunded_centsintegerOnly on the cancel response.
created_atstring (date-time)
confirmed_atstring | null (date-time)
cancelled_atstring | null (date-time)
updated_atstring (date-time)

Shipment

FieldTypeDescription
idstring (uuid)
carrierstringExample: Colissimo
tracking_numberstring
tracking_urlstring | null
statuslabel_created | shipped | in_transit | delivered | returned | lost | cancelled
shipped_atstring | null (date-time)
delivered_atstring | null (date-time)

File

FieldTypeDescription
idstring (uuid)
statusready
filenamestring | null
mime_typeimage/png | image/jpeg
size_bytesinteger
width_pxinteger
height_pxinteger
dpiinteger | nullResolution declared by the file. The order check compares pixels with the print area, not this value.
sha256string | null
urlstring
created_atstring (date-time)

EventType

order.created | order.in_production | order.shipped | order.delivered | order.issue | order.cancelled | catalog.variant_discontinued

WebhookEndpoint

FieldTypeDescription
idstring (uuid)
urlstring
eventsarray of EventType
statusactive | paused | disabled
last_deliveryobject | null
created_atstring (date-time)
updated_atstring (date-time)
paused_atstring | null (date-time)
deleted_atstring | null (date-time)

Event

Body of every webhook delivery. Headers: TPOP-Event-Id, TPOP-Event-Type, TPOP-Signature: t=<timestamp>,v1=<hex HMAC-SHA256 of "<timestamp>.<body>">.

FieldTypeDescription
idstring (uuid)Same for every endpoint and every retry: use it to ignore repeats.
typeEventType
created_atstring (date-time)
dataobjectorder (id, number, external_id, status, test); shipments for order.shipped and order.delivered; issue.code for order.issue; variant for catalog.variant_discontinued.