API
esc

Type to search.

Syncing and exporting

Read every image we hold, filed under its product. Ask for what changed since your last run, deletions included, or freeze a selection into a file.

A PIM or DAM sync is two reads, products and images, each with a delta. It is deliberately not a file list: every image carries the id of the product it belongs to, so your side never parses filenames to work out which product a render belongs to.

The whole sync loop
for product in GET /v1/products?since=T: upsert it, or remove it if deleted_at is set
for each changed product: replace its images with the ones from GET /v1/images?since=T

Read the images

GET/v1/images

Every image we hold for your products: your own photos, Product beautifier renders, photo shoots and their edits. Newest product first.

200 OK (two items)
{
"data": [
{
"id": "01HX5K2MZ7A3Q4FBNDC0EVDXYS",
"mime_type": "image/jpeg",
"width": 2000, "height": 2667,
"url": "https://…signed…",
"url_expires_at": "2026-09-03T15:32:00Z",
"created_at": "2026-09-01T09:12:44Z",
"product_id": "01HX5K2MZ7A3Q4FBNDC0EVDXY1",
"role": "source",
"ai_generated": false,
"task_id": null,
"attributes": { "orientation": "front" },
"also_features": [],
"suggested_filename": "TEE-SAGE-001_front.jpg"
},
{
"id": "01HX5K2MZ7A3Q4FBNDC0EVDXZ9",
"mime_type": "image/webp",
"width": 1536, "height": 2048,
"url": "https://…signed…",
"url_expires_at": "2026-09-03T15:32:00Z",
"created_at": "2026-09-02T16:40:03Z",
"product_id": "01HX5K2MZ7A3Q4FBNDC0EVDXY1",
"role": "tryon",
"ai_generated": true,
"task_id": "01HX5K2MZ7A3Q4FBNDC0EVDXY2",
"attributes": { "angle_shot": "full_body_relaxed_symmetric", "background": "park", "model_id": "01HX5K2MZ7A3Q4FBNDC0EVDXMD1" },
"also_features": ["01HX5K2MZ7A3Q4FBNDC0EVDXPN"],
"suggested_filename": "TEE-SAGE-001_tryon_park.webp"
}
],
"next_cursor": "eyJpZCI6InByb2RfMDFIWCJ9",
"has_more": true
}

What an item tells you

Field Use it for
id, mime_type, width, height, url, url_expires_at, created_at The same Image object every endpoint returns. url points at this exact image and is valid for one hour. A crop or retouch made in the dashboard is not listed. One expired URL is re-fetched with GET /v1/images/{id}, never by re-listing.
product_id The product this image belongs to, the one it was made for.
role What the image is: source (your photo), beautifier, tryon, recoloring. Edits keep the role of the generation they came from; attributes.edit_number says which edit. New roles may appear; treat unknown values as opaque.
ai_generated false only for your own photos (role: source). For EU AI Act labeling.
task_id The task that produced it, for GET /v1/tasks/{id}. Null for your photos and for generations made in the dashboard.
edited_from_image_id For an edit: the image it was made from. Null otherwise. Every version is its own row, so an image nothing was edited from is the end of a chain.
archived true when the result was archived in the dashboard. Archived results are left out of ZIP exports.
attributes Flat, open facts per role: orientation, angle_shot, background, model_id, edit_number; a recoloring carries the orientation it was made from and its hex_color (plus hex_colors when it has several). Ignore keys you do not know.
also_features Other products visible in this image (the styling items of a photo shoot).
suggested_filename A filename, never a path, built from your organization’s export naming template with the real extension of the bytes.

Each image appears exactly once, under the product it was made for. A photo shoot of a top styled with pants lives on the top; the pants are listed in also_features. Whether it also goes on the pants page is your merchandising call.

Filters and paging

  • product_id=… lists one product’s images.
  • role=beautifier,tryon keeps only those roles.
  • limit (1 to 100, default 25) and cursor page through. Pages are cut on product boundaries: a page holds every image of up to limit products, so the number of images per page varies.

Only what changed

Add since=<timestamp> to either list and you get only what changed at or after that instant.

Terminal window
curl "https://api.veeton.com/v1/products?since=2026-09-02T00:00:00Z" \
-H "Authorization: Bearer $VEETON_KEY"
curl "https://api.veeton.com/v1/images?since=2026-09-02T00:00:00Z" \
-H "Authorization: Bearer $VEETON_KEY"
  • GET /v1/products?since= returns the products that changed: a field edited, a photo attached or removed, a generation or edit completed, a result archived or deleted, or the product deleted. Each is its full current state, so upsert it whole. A deleted product appears once with deleted_at set and an empty images array: remove it on your side.
  • GET /v1/images?since= returns every image of those same products, so a changed product’s images are always re-read together. Replace that product’s images with these rows: this is how you learn an image was removed. A product left with no images has no rows, so take the list of changed products from GET /v1/products?since=.

Use the created_at of your previous run, minus a safety margin, as the next since. A nightly job is then a delta, not a re-read.

Freeze a selection into a file

POST/v1/exports

The same image items as a stable artifact: a JSONL manifest (one item per line) or a ZIP of the image files rendered by the same export pipeline the dashboard uses, with your organization’s naming template and per-workflow formats.

Terminal window
curl https://api.veeton.com/v1/exports \
-H "Authorization: Bearer $VEETON_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"format":"zip","product_ids":["01HX5K2MZ7A3Q4FBNDC0EVDXY1"],"role":["beautifier","tryon"]}'
  • Select up to 500 products with product_ids, or omit it for the whole catalog visible to the API (same cap). Narrow with role.
  • format: manifest (default) is written immediately; the response is already completed with a signed url.
  • format: zip is rendered asynchronously. Poll GET /v1/exports/{id}, which carries Retry-After while queued or processing, or subscribe to export.completed and export.failed.
202 Accepted
{
"id": "01HX5K2MZ7A3Q4FBNDC0EVDXEX",
"format": "zip",
"status": "queued",
"url": null,
"url_expires_at": null,
"expires_at": null,
"total_images": 148,
"error": null,
"created_at": "2026-09-03T14:30:09Z",
"started_at": null,
"finished_at": null
}

Image URLs inside a manifest are valid one hour after it was written; the image ids are permanent. The artifact URL on the job is re-signed on every read, so cache the file, not the URL.

A ZIP is kept 48 hours after it was produced. The job carries the deadline in expires_at; past it the job stays completed, url is null, and a new export gives you the files again. Manifests do not expire (expires_at is null).

GET/v1/exports lists your export jobs, newest first, with a status filter. Imports have the same list at GET /v1/imports.