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 notificationType they 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. Its detail is generic, and a reference identifies 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 /webhooks answers 422, and stores nothing, unless url is an absolute https:// URL of at most 200 characters, with no user name, password or fragment, whose host is public: not localhost, *.localhost, *.internal, *.local or 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 an https:// URL again.
  • Behaviour change: a {channelId} that does not exist answers 401, with the same body as a missing or invalid token, instead of 404 “The channel ID … does not exist”. A valid token addressing a sibling channel in its own team that it does not own still answers that 404. GET /token with an unknown path channel is also a 401, 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 / and GET /token included, with 403 and problem type https://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}/confirm and POST /webhooks answer 403. Confirm refuses before anything is changed, so the order keeps its fulfilments and holds no new stock; confirming an already-confirmed order still answers 202. 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 403 on 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, the channel-inactive 403 is answered.)
  • Behaviour change: request size limits, each a 422 naming the field (a request over a limit that was accepted before is now refused): orderNumber and printTrailOrderNumber at most 255 characters, orderLines[].product.sku at most 255 characters (on POST /orders and PUT /orders/{orderUuid}/lines), and at most 500 orderLines per request, refused on the count alone. The spec states each limit as maxLength / maxItems, and the ShippingDetailsInput lengths now match what the API has always enforced (companyName 150, address1–address4 250, postalCode 25, state.name 100, state.code 30, country.name 100, email 100).
  • Behaviour change: GET /orders and POST /webhooks/resend see 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, while GET /orders/{orderUuid} answered 404 for 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 same 422 as an unknown orderUuid.
  • Behaviour change: a 500 has a generic detail and a reference member identifying the logged error; quote the reference to Print Trail support. Deliberate 4xx messages are unchanged.
  • Behaviour change: webhook deliveries do not follow redirects. A 3xx answer counts as a failed delivery and is retried like any other non-2xx answer. 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 answers 429 until the minute is up. A client with a working token never spends this allowance.

Fixed

  • A 429 response has a problem body with "status": 429 and "title": "Too Many Requests", and carries Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. It previously had a body saying 500 “Internal Server Error” and no Retry-After. More generally, a 4xx error body’s status and title now match the response status.
  • An orderNumber or printTrailOrderNumber too long for its column answered 500; it is now a 422.
  • Registering a webhook as the Standard channel answered 500; such a token is now refused with 403 before the request is handled.

Docs

  • ApiProblem documents the reference member. Every operation documents its 403 (a Forbidden response: inactive channel or Standard channel token; team inactive where it applies), 429 and 500 responses, and the 401 for 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: currentTrackingDetails on fulfilment-tracking-created and products on fulfilment-status-updated are 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’s trackingDetails was not affected.

Docs

  • methodCode in currentTrackingDetails is webhook-only; the order resource’s trackingDetails does not carry it.

[1.1.2] - 2026-10-19

Fixed

  • GET /orders/{orderUuid}, PUT /orders/{orderUuid}/cancel, DELETE /orders/{orderUuid} and PUT /orders/{orderUuid}/confirm now 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, returns 404 with 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 /orders and PUT /orders/{orderUuid}/lines already worked this way.

[1.1.1] - 2026-10-19

Fixed

  • Replaying POST /orders for an order that was confirmed and has since finished (complete, cancelled, failed, returned, partial) returns order-not-updatable; since 1.1.0 it wrongly returned order-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 /orders replay 409 table: a finished or cancelled order answers order-not-updatable even if it was once confirmed, and integrations should read orderStatus / data.status for the order’s actual state.

[1.1.0] - 2026-10-19

Added

  • POST /webhooks now returns the channel’s current webhook signing secret as data.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 /orders for an order that can no longer be replaced still answers 409, now with a distinct problem type (…/problems/order-already-confirmed or …/problems/order-not-updatable) and orderUuid, orderStatus and the full order under data.
  • confirmedAt on the order resource and on order and fulfilment webhooks: when the order was confirmed for processing, null for a draft.
  • A Print-Trail-Api-Version header on every response from a Channel API route (including errors) and on every webhook request, carrying the contract version.

Fixed

  • A replayed POST /orders for an order that is finished or otherwise not updatable answered 500; it now answers 409 with the order-not-updatable problem type.

Docs

  • GET /stock-levels returns every non-deleted product, whatever its active state; sold-out products appear with stock: 0. Print-on-demand products report warehouse stock plus a 10000 sentinel, meaning effectively unlimited. Order status partial is defined.
  • Product activeState values and the active-state filter are lowercase: active, inactive, pre-order. podEnabled and podApproved are 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 in data. The spec showed them unwrapped. (POST /orders really does return the order unwrapped.)
    • ChannelResource.id is the composite channel ID string (e.g. riverside-std), not an integer. The example code is corrected.
    • 401 and the channel 404 are documented on every operation, and every 404/428 has its problem body. PUT /orders/{orderUuid}/confirm documents its order-not-found 404. GET /products documents its 422. POST /webhooks documents its validation 422, which today is only returned when url is missing: an invalid URL is wrongly accepted.
    • ProductResource.dimensions is an object keyed by weight/length/width/depth, each {measurement, unit}; it is [] when a product has no dimensions. The spec said “array of objects”.
    • FulfilmentResource.supplierLocation carries code and countryCode. The spec listed name, which is never sent, and omitted countryCode.
    • Pagination meta includes Laravel’s from, to, path and links; GET /products also returns top-level links.
    • The PUT /orders/{orderUuid}/lines 404 and 409 bodies are anyOf, not oneOf, the problem schemas: they overlap, so no body could match exactly one.
    • Webhooks: fulfilment-status-updated documents originalFulfilmentStatus, currentFulfilmentStatus, orderStatus and products (a list of SKU strings there). fulfilment-tracking-created documents currentTrackingDetails.
  • 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.