GET /v1/me
Account and API key in use
Any valid key.
Responses
200The account and key401Missing, invalid, expired or revoked key
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.
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.
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:
| Permission | Allows |
|---|---|
catalog:read | Catalog, countries, quotes and shipping rates |
orders:read | List and read orders, shipments, tracking and history |
orders:write | Create draft orders, quote, cancel |
orders:confirm | Pay orders with your TPOP wallet (spending limits required) |
files:write | Upload print files |
webhooks:manage | Manage 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.
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.
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.
snake_case; unknown fields are refused.sku.*_cents) with currency: "EUR"; dates are ISO 8601 in UTC.{"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.draft, confirmed, test_confirmed, in_production, shipped, delivered, on_hold, cancelled, refunded.min_dpi (else 422 file_resolution_too_low with the required resolution).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"}
}
}
| Status | Meaning |
|---|---|
400 | Malformed request: invalid JSON, unknown field, bad parameter, missing Idempotency-Key |
401 | Missing, invalid, expired or revoked key |
402 | Payment refused: limit reached, payments frozen, balance too low; the order stays a draft |
403 | Missing permission, address not allowed, call from a browser |
404 | Unknown identifier (or not yours) |
409 | Idempotency conflict, duplicate external_id, order already in production |
413 / 415 | Body too large / not JSON |
422 | Content not acceptable: unknown SKU, file too small, incomplete address, country not served |
429 | Too many requests |
5xx | Our side: retry later with the same Idempotency-Key |
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);
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.
The account and API key in use.
/v1/meAccount and API key in use
Any valid key.
Responses
200 The account and key401 Missing, invalid, expired or revoked keyProducts you can print on, their variants, print areas and prices.
/v1/catalog/productsList catalog products
Permission catalog:read. Only products currently offered to your account.
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | |
cursor | query | string | next_cursor of the previous page. |
lang | query | en | fr | it | es | de | nl | pl | pt | |
category | query | string | Category code (sub-categories included). |
q | query | string | Search in titles and SKU. |
tier | query | essential | premium | eco | |
gender | query | string | |
fit | query | string | |
label | query | string | Certification code, e.g. gots. |
Responses
200 A page of products400 Malformed request (unknown field, invalid JSON, bad parameter)401 Missing, invalid, expired or revoked key403 The key lacks the permission, the address is not allowed, or the call comes from a browser/v1/catalog/products/{id}Catalog product with colors, sizes, print areas and techniques
Permission catalog:read.
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string (uuid) | |
lang | query | en | fr | it | es | de | nl | pl | pt |
Responses
200 The product401 Missing, invalid, expired or revoked key403 The key lacks the permission, the address is not allowed, or the call comes from a browser404 Unknown identifier (or not yours)/v1/catalog/products/{id}/variantsVariants of a catalog product (color × size, SKU, price, availability)
Permission catalog:read. Order a variant by its sku.
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string (uuid) | |
limit | query | integer | |
cursor | query | string | next_cursor of the previous page. |
lang | query | en | fr | it | es | de | nl | pl | pt |
Responses
200 A page of variants401 Missing, invalid, expired or revoked key403 The key lacks the permission, the address is not allowed, or the call comes from a browser404 Unknown identifier (or not yours)/v1/countriesCountries we ship to
Permission catalog:read.
| Parameter | In | Type | Description |
|---|---|---|---|
lang | query | en | fr | it | es | de | nl | pl | pt |
Responses
200 All countries served401 Missing, invalid, expired or revoked key403 The key lacks the permission, the address is not allowed, or the call comes from a browserPrice an order, with shipping and VAT, without creating it.
/v1/orders/estimateQuote 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 quote400 Malformed request (unknown field, invalid JSON, bad parameter)401 Missing, invalid, expired or revoked key403 The key lacks the permission, the address is not allowed, or the call comes from a browser422 The content is not acceptable (unknown SKU, file too small for its print area, incomplete address…)/v1/shipping/ratesShipping 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 cost422 The content is not acceptable (unknown SKU, file too small for its print area, incomplete address…)Print files TPOP downloads from your HTTPS link.
/v1/filesUpload 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.
| Parameter | In | Type | Description |
|---|---|---|---|
Idempotency-Key | header | string |
Body
| Field | Type | Description |
|---|---|---|
url required | string (uri) | Example: https://example.com/designs/front.png |
filename | string |
Responses
201 The stored file200 Replay of the same request (Idempotent-Replayed: true)409 Idempotency conflict, duplicate external_id, or the order can no longer change422 The content is not acceptable (unknown SKU, file too small for its print area, incomplete address…)/v1/files/{id}A file you uploaded
Permission files:write or orders:read.
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string (uuid) |
Responses
200 The file404 Unknown identifier (or not yours)Create, pay, cancel, list and track orders.
/v1/ordersList orders created through the API
Permission orders:read. Most recent first.
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | |
cursor | query | string | next_cursor of the previous page. |
status | query | OrderStatus | |
external_id | query | string | |
created_after | query | string (date-time) | |
created_before | query | string (date-time) |
Responses
200 A page of orders400 Malformed request (unknown field, invalid JSON, bad parameter)/v1/ordersCreate 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.
| Parameter | In | Type | Description |
|---|---|---|---|
Idempotency-Key required | header | string | Unique 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 order200 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 change422 The content is not acceptable (unknown SKU, file too small for its print area, incomplete address…)/v1/orders/{id}An order, with shipments, tracking and history
Permission orders:read.
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string (uuid) |
Responses
200 The order404 Unknown identifier (or not yours)/v1/orders/{id}/confirmPay 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.
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string (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/v1/orders/{id}/cancelCancel an order before production
Permission orders:write. What the wallet paid goes back to the wallet. Refused once production has started.
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string (uuid) |
Responses
200 The cancelled order, with refunded_cents404 Unknown identifier (or not yours)409 Idempotency conflict, duplicate external_id, or the order can no longer changeSigned alerts sent to your server when an order changes.
/v1/webhooksList webhook endpoints
Permission webhooks:manage.
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | |
cursor | query | string | next_cursor of the previous page. |
Responses
200 A page of endpoints/v1/webhooksRegister 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
| Field | Type | Description |
|---|---|---|
url required | string (uri) | |
events required | array of EventType |
Responses
201 The endpoint, with its secret409 Idempotency conflict, duplicate external_id, or the order can no longer change422 The content is not acceptable (unknown SKU, file too small for its print area, incomplete address…)/v1/webhooks/{id}A webhook endpoint
Permission webhooks:manage.
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string (uuid) |
Responses
200 The endpoint404 Unknown identifier (or not yours)/v1/webhooks/{id}Change the URL, events or status (active or paused)
Permission webhooks:manage.
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string (uuid) |
Body
| Field | Type | Description |
|---|---|---|
url | string (uri) | |
events | array of EventType | |
status | active | paused |
Responses
200 The endpoint404 Unknown identifier (or not yours)422 The content is not acceptable (unknown SKU, file too small for its print area, incomplete address…)/v1/webhooks/{id}Stop an endpoint
Permission webhooks:manage. Pending deliveries are dropped.
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string (uuid) |
Responses
200 The stopped endpoint404 Unknown identifier (or not yours)/v1/webhooks/{id}/testSend a signed `ping` now
Permission webhooks:manage. One attempt, no retry.
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string (uuid) |
Responses
200 Result of the attempt404 Unknown identifier (or not yours)| Field | Type | Description |
|---|---|---|
error required | object |
| Field | Type | Description |
|---|---|---|
data required | array of any | |
has_more required | boolean | |
next_cursor required | string | null |
| Field | Type | Description |
|---|---|---|
account | object | |
api_key | object |
catalog:read | orders:read | orders:write | orders:confirm | files:write | webhooks:manage
Amount in euro cents.
integer
| Field | Type | Description |
|---|---|---|
id | string (uuid) | |
sku_prefix | string | |
title | string | |
subtitle | string | null | |
category | object | |
main_image_url | string | null | |
brand | string | null | |
quality_tier | string | null | |
gender | string | null | |
fit | string | null | |
variants_count | integer | |
colors_count | integer | |
sizes_count | integer | |
base_price_from_cents | Money | |
price_from_cents | Money | |
currency | string | Example: EUR |
production_days | integer | null | |
availability | available | discontinued | |
discontinued_at | string | null (date-time) | |
replacement | object | null |
| Field | Type | Description |
|---|---|---|
All fields of CatalogProduct | ||
description_html | string | null | |
details | string | null | |
weight_grams | integer | null | |
pricing | object | |
lead_time | object | |
attributes | object | Composition, fabric weight, certifications, care codes, origin and print country. |
colors | array of object | |
sizes | array of object | |
print_areas | array of PrintArea | |
techniques | array of object | |
facts | object | Product facts: specifications, certifications, care, size guide in cm. |
| Field | Type | Description |
|---|---|---|
face | string | Use this code as key of files when ordering. |
name | string | |
is_neck_label | boolean | |
width_cm | number | |
height_cm | number | |
zones | array of object | |
min_dpi | integer | |
min_pixels | object | |
techniques | array of string | |
extra_cost_cents | Money | |
currency | string |
| Field | Type | Description |
|---|---|---|
sku | string | Example: TPO-TS-UNI-CHAN-WHI-S |
product_id | string (uuid) | |
color | object | |
size | object | |
availability | available | discontinued | |
discontinued_at | string | null (date-time) | |
replacement | object | null | |
base_price_cents | Money | |
price_cents | Money | |
currency | string | |
weight_grams | integer | null | |
image_url | string | null |
| Field | Type | Description |
|---|---|---|
code | string | Example: FR |
name | string | |
is_eu | boolean | |
requires_customs | boolean |
| Field | Type | Description |
|---|---|---|
name required | string | |
company | string | null | |
line1 required | string | |
line2 | string | null | |
postal_code | string | null | |
city required | string | |
country required | string | Example: FR |
phone | string | null | |
email | string | null (email) |
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).
| Field | Type | Description |
|---|---|---|
sku required | string | |
quantity required | integer | |
product_id | string (uuid) | |
files | object | Example: {"front":"3f0b\u2026","back":"9a1c\u2026"} |
| Field | Type | Description |
|---|---|---|
items required | array of OrderItemRequest | |
shipping_address | Address | |
country | string |
| Field | Type | Description |
|---|---|---|
external_id | string | Your reference, unique in your account. |
confirm | boolean | true = pay now with the wallet (needs orders:confirm). |
shipping_address required | Address | |
items required | array of OrderItemRequest |
| Field | Type | Description |
|---|---|---|
items_cents | Money | |
discount_cents | Money | |
options_cents | Money | |
shipping_cents | Money | |
tax_cents | Money | |
total_cents | Money | |
tax_rate | number | Example: 20 |
vat_regime | domestic | eu_vat | eu_reverse_charge | export | |
currency | string |
| Field | Type | Description |
|---|---|---|
items | array of object | |
options | array of object | |
costs | Costs | |
shipping_country | string |
draft | confirmed | test_confirmed | in_production | shipped | delivered | on_hold | cancelled | refunded
| Field | Type | Description |
|---|---|---|
id | string (uuid) | |
number | string | Example: P261017-E02B86 |
external_id | string | null | |
status | OrderStatus | |
test | boolean | |
items | array of object | |
shipping_address | Address | |
costs | Costs | |
shipments | array of Shipment | |
history | array of object | |
api_key_prefix | string | null | |
refunded_cents | integer | Only on the cancel response. |
created_at | string (date-time) | |
confirmed_at | string | null (date-time) | |
cancelled_at | string | null (date-time) | |
updated_at | string (date-time) |
| Field | Type | Description |
|---|---|---|
id | string (uuid) | |
carrier | string | Example: Colissimo |
tracking_number | string | |
tracking_url | string | null | |
status | label_created | shipped | in_transit | delivered | returned | lost | cancelled | |
shipped_at | string | null (date-time) | |
delivered_at | string | null (date-time) |
| Field | Type | Description |
|---|---|---|
id | string (uuid) | |
status | ready | |
filename | string | null | |
mime_type | image/png | image/jpeg | |
size_bytes | integer | |
width_px | integer | |
height_px | integer | |
dpi | integer | null | Resolution declared by the file. The order check compares pixels with the print area, not this value. |
sha256 | string | null | |
url | string | |
created_at | string (date-time) |
order.created | order.in_production | order.shipped | order.delivered | order.issue | order.cancelled | catalog.variant_discontinued
| Field | Type | Description |
|---|---|---|
id | string (uuid) | |
url | string | |
events | array of EventType | |
status | active | paused | disabled | |
last_delivery | object | null | |
created_at | string (date-time) | |
updated_at | string (date-time) | |
paused_at | string | null (date-time) | |
deleted_at | string | null (date-time) |
Body of every webhook delivery. Headers: TPOP-Event-Id, TPOP-Event-Type, TPOP-Signature: t=<timestamp>,v1=<hex HMAC-SHA256 of "<timestamp>.<body>">.
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Same for every endpoint and every retry: use it to ignore repeats. |
type | EventType | |
created_at | string (date-time) | |
data | object | order (id, number, external_id, status, test); shipments for order.shipped and order.delivered; issue.code for order.issue; variant for catalog.variant_discontinued. |