API changelog
What changed in each version of the Print Trail Channel API, what may change within a version, and what your integration must tolerate.
Changes to the Channel API contract: the HTTP API under /v1/channels/{channelId}/ and the webhooks it sends. The contract itself is the OpenAPI specification; its info.version is the version listed here, and every response from a Channel API route, and every webhook, carries it in the Print-Trail-Api-Version header.
Compatibility policy
Versioning
The spec is versioned with semantic versioning: MAJOR.MINOR.PATCH.
- PATCH: documentation-only corrections that do not change what the API does (wording, examples, a schema fixed to match long-standing behaviour).
- MINOR: additive changes and behaviour changes, as defined below.
- MAJOR: breaking changes. A new major version is served side by side under a new path prefix (
/v2/channels/{channelId}/…), and the previous major version keeps working until its announced sunset.
The major version is the number in the URL. /v1/ stays 1.x.y for as long as it is served.
What may change within a major version
Within a major version the API only grows. Any of these can ship in a minor release:
- new endpoints;
- new optional request fields and query parameters;
- new fields in responses and webhook payloads;
- new problem
types on existing error status codes; - new enum values (order/fulfilment statuses, notification types, refusal reasons, …);
- new webhook notification types.
What clients must do
So that those changes never break an integration, clients MUST:
- ignore fields they do not recognise, in responses and in webhook payloads;
- tolerate enum values they do not recognise (treat an unknown status as “something else”, not as an error);
- tolerate problem
types they do not recognise, falling back on the HTTP status code; - ignore webhooks with a
notificationTypethey do not handle, and still answer them with a 2xx.
Behaviour changes
A behaviour change keeps the request and response shapes the same but changes what a request means or does: a different outcome for the same input, a different status code for the same situation, a new precondition. It ships as a minor release with a prominent Behaviour change entry below, and is announced in advance.
Breaking changes
These require a new major version:
- removing or renaming a field, endpoint, parameter or enum value;
- changing a field’s type or format, or making an optional request field required;
- changing what a status code means for a request that already works;
- removing a webhook notification type or a payload field.
Deprecation
A field or endpoint due to go is first marked deprecated: true in the spec and announced here. Responses that use it then carry a Deprecation header and, once a date is set, a Sunset header (RFC 8594) naming the date it will be removed. Removal itself only happens in a new major version.
[1.2.0] - 2026-10-20
A security release. Most integrations need no change; check the Behaviour change entries below, in particular webhook URLs (HTTPS on a public host), inactive channels and the Standard channel.
Security
- Webhook URLs are checked so that webhooks can only be sent to public HTTPS hosts. Registration and every delivery refuse non-public destinations, a delivery connects only to the addresses that were checked (so a DNS change after the check cannot redirect it), and redirects are not followed.
- A server error (
500) no longer carries internal details of the failure. Itsdetailis generic, and areferenceidentifies the logged error for Print Trail support. - Channel IDs cannot be discovered by probing: an unknown channel is indistinguishable from a bad token, and repeated failed requests are throttled per client IP address.
- Rate limits are applied per token, and the client IP address used for the failed-request limit is the connecting address; forwarded-for headers are only believed from a configured trusted proxy.
- Deactivating a channel revokes its API access and stops its webhooks. The team’s Standard (default) channel cannot be given an API token, and an existing token for it is refused.
Changed
- Behaviour change: webhook URLs must be HTTPS on a public host.
POST /webhooksanswers422, and stores nothing, unlessurlis an absolutehttps://URL of at most 200 characters, with no user name, password or fragment, whose host is public: notlocalhost,*.localhost,*.internal,*.localor a single-label name, and not, or resolving to, a private, loopback, link-local, unique-local, shared (CGNAT), multicast or reserved address. The host must resolve in DNS and every address must be public. A{notificationType}placeholder is allowed in the path or query only. Previously any string was accepted. - Behaviour change: the same check runs before every webhook delivery, including retries. A webhook whose URL does not meet it, including a URL registered before this release, is not sent. If your registered URL is
http://, register anhttps://URL again. - Behaviour change: a
{channelId}that does not exist answers401, with the same body as a missing or invalid token, instead of404“The channel ID … does not exist”. A valid token addressing a sibling channel in its own team that it does not own still answers that404.GET /tokenwith an unknown path channel is also a401, so it can no longer diagnose a mistyped team code; it still answers for any channel in the token’s own team. - Behaviour change: a channel set inactive in Print Trail is refused on every endpoint,
GET /andGET /tokenincluded, with403and problem typehttps://docs.printtrail.com/channel-api/problems/channel-inactive. No new webhooks of any kind (order, fulfilment or stock-level) are sent to it until it is made active again; a delivery already queued before it was deactivated may still be retried. Previously an inactive channel’s token kept working and it kept receiving webhooks. - Behaviour change: for a team that is inactive,
PUT /orders/{orderUuid}/confirmandPOST /webhooksanswer403. Confirm refuses before anything is changed, so the order keeps its fulfilments and holds no new stock; confirming an already-confirmed order still answers202. Reads and cancelling (PUT …/cancel,DELETE /orders/{orderUuid}) still work, so the merchant can clean up. Creating and amending orders were already refused. - Behaviour change: a token belonging to the team’s Standard (default) channel, which holds orders entered in Print Trail, is refused with
403on every endpoint of that channel, and that channel can no longer be given a token. An integration using such a token needs a dedicated channel and token. (If the Standard channel is also inactive, thechannel-inactive403is answered.) - Behaviour change: request size limits, each a
422naming the field (a request over a limit that was accepted before is now refused):orderNumberandprintTrailOrderNumberat most 255 characters,orderLines[].product.skuat most 255 characters (onPOST /ordersandPUT /orders/{orderUuid}/lines), and at most 500orderLinesper request, refused on the count alone. The spec states each limit asmaxLength/maxItems, and theShippingDetailsInputlengths now match what the API has always enforced (companyName150,address1–address4250,postalCode25,state.name100,state.code30,country.name100,email100). - Behaviour change:
GET /ordersandPOST /webhooks/resendsee exactly the orders the single-order endpoints see: those created through the Channel API on the token’s channel. An imported or manually entered order filed under the channel was listed by the search, whileGET /orders/{orderUuid}answered404for it, and a resend could be asked for it. It is now absent from the search, and a resend for it is refused with the same422as an unknownorderUuid. - Behaviour change: a
500has a genericdetailand areferencemember identifying the logged error; quote thereferenceto Print Trail support. Deliberate4xxmessages are unchanged. - Behaviour change: webhook deliveries do not follow redirects. A
3xxanswer counts as a failed delivery and is retried like any other non-2xxanswer. Register the final URL. - Behaviour change: the rate limit for authenticated requests (default 600 per minute) is per token, not per client IP address. Two shops behind one address no longer share it.
- New rate limit: requests that fail authentication (
401) are limited per client IP address (default 60 per minute), counted before the channel is looked up. Over it, every request from that address answers429until the minute is up. A client with a working token never spends this allowance.
Fixed
- A
429response has a problem body with"status": 429and"title": "Too Many Requests", and carriesRetry-After,X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset. It previously had a body saying500“Internal Server Error” and noRetry-After. More generally, a4xxerror body’sstatusandtitlenow match the response status. - An
orderNumberorprintTrailOrderNumbertoo long for its column answered500; it is now a422. - Registering a webhook as the Standard channel answered
500; such a token is now refused with403before the request is handled.
Docs
ApiProblemdocuments thereferencemember. Every operation documents its403(aForbiddenresponse: inactive channel or Standard channel token; team inactive where it applies),429and500responses, and the401for an unknown channel.- The guide lists the order of checks a request goes through: failed-request rate limit, channel, authentication, channel scope, channel active, Standard channel, per-token rate limit.
[1.1.3] - 2026-10-19
Fixed
- Webhooks:
currentTrackingDetailsonfulfilment-tracking-createdandproductsonfulfilment-status-updatedare always sent as JSON arrays, as documented. When entries were reordered or dropped, the field could arrive as a JSON object keyed"0","1", … instead. Output now matches the schema documented all along, so integrations that follow the spec need no change. The order resource’strackingDetailswas not affected.
Docs
methodCodeincurrentTrackingDetailsis webhook-only; the order resource’strackingDetailsdoes not carry it.
[1.1.2] - 2026-10-19
Fixed
GET /orders/{orderUuid},PUT /orders/{orderUuid}/cancel,DELETE /orders/{orderUuid}andPUT /orders/{orderUuid}/confirmnow only see the channel’s own orders: orders created through the Channel API on that channel. Another channel’s order, or one entered in Print Trail or imported, returns404with the same body as an order uuid that does not exist (“This order could not be found.”), and is left unchanged. These endpoints previously found such an order by its uuid.POST /orders,GET /ordersandPUT /orders/{orderUuid}/linesalready worked this way.
[1.1.1] - 2026-10-19
Fixed
- Replaying
POST /ordersfor an order that was confirmed and has since finished (complete,cancelled,failed,returned,partial) returnsorder-not-updatable; since 1.1.0 it wrongly returnedorder-already-confirmed.
Docs
- Webhook secret rotation: accept both the old and new secret for a few hours after rotating, not a few minutes. Failed deliveries are retried up to 5 times with backoff waits of 10 s, 100 s, 1,000 s and 10,000 s, so an old-signed retry can arrive up to about 3 hours later.
- The
POST /ordersreplay409table: a finished or cancelled order answersorder-not-updatableeven if it was once confirmed, and integrations should readorderStatus/data.statusfor the order’s actual state.
[1.1.0] - 2026-10-19
Added
POST /webhooksnow returns the channel’s current webhook signing secret asdata.webhookDetail.secret. Registering again returns the same secret.POST /webhooks/secret/rotate: rotates the webhook signing secret and returns the new one in the same shape. The old secret stops verifying immediately.- A replayed
POST /ordersfor an order that can no longer be replaced still answers409, now with a distinct problemtype(…/problems/order-already-confirmedor…/problems/order-not-updatable) andorderUuid,orderStatusand the full order underdata. confirmedAton the order resource and on order and fulfilment webhooks: when the order was confirmed for processing,nullfor a draft.- A
Print-Trail-Api-Versionheader on every response from a Channel API route (including errors) and on every webhook request, carrying the contract version.
Fixed
- A replayed
POST /ordersfor an order that is finished or otherwise not updatable answered500; it now answers409with theorder-not-updatableproblem type.
Docs
GET /stock-levelsreturns every non-deleted product, whatever its active state; sold-out products appear withstock: 0. Print-on-demand products report warehouse stock plus a10000sentinel, meaning effectively unlimited. Order statuspartialis defined.- Product
activeStatevalues and theactive-statefilter are lowercase:active,inactive,pre-order.podEnabledandpodApprovedare documented. - The spec now describes what the API has always sent, found by the new contract tests. None of these is an API change:
GET /,GET /team,GET /products/{productUuid},GET /orders/{orderUuid},PUT /orders/{orderUuid}/confirm(202) and the cancel endpoints (PUT …/cancel,DELETE /orders/{orderUuid}) wrap the resource indata. The spec showed them unwrapped. (POST /ordersreally does return the order unwrapped.)ChannelResource.idis the composite channel ID string (e.g.riverside-std), not an integer. The examplecodeis corrected.401and the channel404are documented on every operation, and every404/428has its problem body.PUT /orders/{orderUuid}/confirmdocuments its order-not-found404.GET /productsdocuments its422.POST /webhooksdocuments its validation422, which today is only returned whenurlis missing: an invalid URL is wrongly accepted.ProductResource.dimensionsis an object keyed byweight/length/width/depth, each{measurement, unit}; it is[]when a product has no dimensions. The spec said “array of objects”.FulfilmentResource.supplierLocationcarriescodeandcountryCode. The spec listedname, which is never sent, and omittedcountryCode.- Pagination
metaincludes Laravel’sfrom,to,pathandlinks;GET /productsalso returns top-levellinks. - The
PUT /orders/{orderUuid}/lines404and409bodies areanyOf, notoneOf, the problem schemas: they overlap, so no body could match exactly one. - Webhooks:
fulfilment-status-updateddocumentsoriginalFulfilmentStatus,currentFulfilmentStatus,orderStatusandproducts(a list of SKU strings there).fulfilment-tracking-createddocumentscurrentTrackingDetails.
- This changelog and its compatibility policy.
[1.0.0] - 2026-04-24
Baseline: the Channel API as first published. Changes between April and October 2026 shipped in place without a version bump. 1.1.0 is the first release under the policy above.