Conventions
The shapes every endpoint shares — base URLs and versioning, ids, timestamps, pagination and the error envelope.
Base URL and versioning
| Environment | Base URL |
|---|---|
| Production | https://api.veeton.com/v1 |
| Development | https://api.veeton.net/v1 |
Versioning is single-axis and lives in the path. Pin to /v1. Within a major version only additive changes ship: new fields, new optional parameters, new enum values where the docs say a set is open. Clients must ignore fields they do not know.
Identifiers
Every resource is addressed by an id: a 26-character Crockford base32 ULID such as 01HX5K2MZ7A3Q4FBNDC0EVDXY1. Ids are opaque and non-enumerable. The one exception is model ids: Veeton-curated system models carry shorter legacy ids ("19274"), so treat model_id as an opaque string rather than validating its shape.
Webhook event ids are prefixed: evt_….
Timestamps
All timestamps are RFC 3339 in UTC, 2026-09-03T14:30:09Z. Query parameters that take a timestamp (since= on exports) accept an offset as well.
Pagination
Every list endpoint returns the same envelope and takes the same parameters.
{ "data": [ … ], "next_cursor": "eyJpZCI6MTIzNDV9", "has_more": true}limit: 1 to 100, default 25.cursor: thenext_cursorfrom the previous page. Opaque; do not construct it.
Pagination is keyset-based, so adding or deleting rows while you page never skips or repeats an item. An unusable cursor is refused with 400 invalid_cursor.
Errors
Every error, from any endpoint, is the same envelope.
{ "error": { "type": "not_found", "code": "product_not_found", "message": "Product 01HX5K2MZ7A3Q4FBNDC0EVDXY1 was not found.", "param": "inputs.product_id" }}| Field | Meaning |
|---|---|
type |
The class of failure: invalid_request, not_found, unauthorized, forbidden, conflict, rate_limited, internal, not_implemented. Switch on this for behaviour. |
code |
A stable, specific identifier such as reference_already_exists. Switch on this for messaging. |
message |
Human-readable, safe to show an operator. Not stable; do not parse it. |
param |
When present, the request field the error is about. |
Two endpoints validate many things at once and return a list instead of a single error, so you never retry-and-bisect: POST /v1/imports returns { "errors": [{ "line", "code", "message", "param?" }] } and POST /v1/tasks/tryon returns { "errors": [{ "input_index", "type", "code", "message", "param?" }] }.
A 5xx never echoes internal detail. The response carries a generic message and the X-Request-Id to quote to support; the detail is on our side, keyed by that id.
Request ids
Every response carries X-Request-Id. Send your own value in the same header and it is echoed back (up to 128 characters of letters, digits, ., _, : and -), so you can correlate a call across your logs and ours.
Images
Images are typed by their bytes, never by a declared Content-Type. JPEG, PNG and WebP are accepted, up to 10 MB each. Signed URLs to image bytes last one hour; the image id is permanent and GET /v1/images/{id} returns a fresh URL.
CORS
The API answers CORS preflights permissively so it can be called from any backend. It is designed for server-to-server use: a machine key in a browser is a machine key in public.