Docs

Working with the API

Uploads

Getting an image, a track or a mesh into a request, in three calls, with the bytes never passing through the API.

Many addresses take a file: an image to turn into a mesh, a mesh to split, a reference track, a clip to extend. This API sends JSON, so a file travels as a URL — one you already have, or one you make here — or, for our own 3D and video addresses, as the path of an upload.

A field's contract says which it accepts. In the address's OpenAPI document an asset field carries format: "uri" when it takes a URL, contentEncoding: "base64" when it takes the bytes inline, x-auray-encoding: "upload-path" when it takes the path an upload answered and nothing else, and x-auray-encoding: "multipart" when the model only takes a file part — which this API does not send, and refuses with unsupported_encoding rather than pretend.

#Three calls

Ask for somewhere to put it, naming the address the file is for. Everything else follows from the address: which bucket holds it, which kinds of file its fields take, how large one may be, and which key may put it there — the same scopes that may submit to that address. idempotency_key is required, so a retried request lands on the same object.

curl -X POST https://api.auray.ai/v1/uploads \
  -H "Authorization: Bearer $AURAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "address": "hitem3d/hi3d/split", "content_type": "model/gltf-binary", "idempotency_key": "chair-scan-3" }'
200 OK
{
  "object": "upload",
  "upload_url": "https://…/upload/…",
  "path": "86e97633-…/chair-scan-3.glb",
  "bucket": "external-inputs",
  "address": "hitem3d/hi3d/split",
  "max_bytes": 209715200,
  "expires_at": "2026-09-06T06:21:58.000Z",
  "read": "https://api.auray.ai/v1/uploads/86e97633-…/chair-scan-3.glb?address=hitem3d%2Fhi3d%2Fsplit",
  "send_as": [{ "field": "mesh_url", "kind": "mesh", "list": false, "value": "read_url" }],
  "request_id": "…"
}

send_as is every field of that address this file can go in. value says what to send there: read_url is the url the third call answers, path is the path above. A file of a kind no field of the address takes is refused with unknown_content_type, listing the types it does take; an address none of whose fields takes a file is refused with upload_not_accepted.

Put the bytes there. Straight to storage, with the content type you declared — the API never sees them, so there is no body-size limit on this side but the bucket's max_bytes.

curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: model/gltf-binary" \
  --data-binary @chair-scan.glb

Ask for the read URL, once the bytes are there — at the read address the first response handed back. It names the address, which picks the bucket to sign from, and any scope that may upload for that address — the kind it makes, or the older product name — reads back through it the account's files of the kinds that address takes, which are often more than the kind it makes: a language model takes images, video, audio and documents. assets:read alone does not: it fetches what a job made, and an upload is something you sent. It is signed for 24 hours, which is longer than any job takes. The upload itself is deleted 3 days after it landed; upload again for a job you run after that.

curl "https://api.auray.ai/v1/uploads/86e97633-…/chair-scan-3.glb?address=hitem3d%2Fhi3d%2Fsplit" \
  -H "Authorization: Bearer $AURAY_API_KEY"
200 OK
{
  "object": "upload",
  "path": "86e97633-…/chair-scan-3.glb",
  "bucket": "external-inputs",
  "url": "https://…/sign/…",
  "expires_at": "2026-09-07T04:22:11.000Z",
  "request_id": "…"
}

Asking before the bytes have landed answers 404 not_found; storage will not sign an object that does not exist yet, and a read through an address signs no file of a kind that address does not take. A read that names neither address nor product looks only in the buckets whose own scope your key holds — the catalogue models' with external:write, then 3D's with 3d:write or threed:write. A key without external:write, which is every key Account mints, is refused 403 insufficient_scope there and told to name the address, unless the file is in 3D's bucket and the key holds 3d:write or threed:write; so keep the query the read link carries.

Then put url in the field send_as named:

curl -X POST https://queue.auray.run/hitem3d/hi3d/split \
  -H "Authorization: Bearer $AURAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "mesh_url": "https://…/sign/…" }'

#A path, not a URL: our 3D and video addresses

auray-ai/hunyuan-3d/image-to-3d takes its photograph as image_path, and that field takes the path the first call answered — as it is, with no read step; its send_as says "value": "path". It never takes a URL: we do not fetch one you give us, and a path in another account's folder is refused with image_required.

auray-ai/minimax-h3/text-to-video takes pictures the same way: first_frame_path, the picture a clip opens on, or reference_image_paths, a list of up to nine that hold a subject — never both in one request. Its uploads are stored as api-<idempotency_key>, and its read link answers only those. A path in another account's folder, or one with nothing there, is refused with reference_must_be_uploaded before any charge.

curl -X POST https://queue.auray.run/auray-ai/hunyuan-3d/image-to-3d \
  -H "Authorization: Bearer $AURAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "image_path": "86e97633-…/chair-photo-1.png", "tier": "textured" }'

#Buckets and limits

The address picks the row; you never name a bucket.

productForTakesUp to
threedOur 3D address's photograph, sent as image_pathimage/jpeg, image/png, image/webp20 MB
externalAny input to an address outside auray-ai/ — only the kinds its fields takeimages, video, audio, meshes, archives200 MB
—Our video address's pictures, sent as first_frame_path or reference_image_paths; reached through its address onlyimage/jpeg, image/png, image/webp25 MB

The last column is the bucket's own ceiling, and it is the max_bytes the first call answers.

Uploads are yours: the path begins with your account id, and no other account can read or sign it.

#Address, not product

The first call used to name a product — threed, video or external — and the caller had to know which bucket an address reads. product still works for every key it worked for, and every response to it carries a Deprecation header and a Link to this section; send address instead. A key minted with the kinds uses it too, which is what a client that can name only the product needs: external takes such a key's files of the kinds some address of its kinds takes — an image with any of them, a mesh with 3d:write — and so does its read link; a file of another kind is refused 403 insufficient_scope, naming the scopes that would take it. video is no longer offered: a picture for our video goes through its address, which stores it under a name of its own and reads back only those. The product spelling answers only the keys that used it — one holding auray-ai-video:write, or video:write beside external:write — for an upload there and for a read back with ?product=video. video:write without external:write — the video kind as Account mints it — is refused 403 insufficient_scope there, and so is external:write alone.

#Or skip all of this

A public https:// URL works in any URL field as it is. Uploading is for files that are not on the internet yet, or that you would rather not leave there. A path field is the exception: image_path, first_frame_path and reference_image_paths take only an upload's path.

The desktop's Playground app does exactly these three calls when you press Upload a file next to a URL field, and prints the resulting request on its API page.

Start typing to search every page of the documentation.

Up arrow Down arrow to moveEnter to openEscape to close