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.
for product in GET /v1/products?since=T: upsert it, or remove it if deleted_at is setfor each changed product: replace its images with the ones from GET /v1/images?since=TRead 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.
{ "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,tryonkeeps only those roles.limit(1 to 100, default 25) andcursorpage through. Pages are cut on product boundaries: a page holds every image of up tolimitproducts, 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.
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 withdeleted_atset and an emptyimagesarray: 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 fromGET /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.
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 withrole. format: manifest(default) is written immediately; the response is alreadycompletedwith a signedurl.format: zipis rendered asynchronously. PollGET /v1/exports/{id}, which carriesRetry-Afterwhilequeuedorprocessing, or subscribe toexport.completedandexport.failed.
{ "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.