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 — in the body, or as an Idempotency-Key header, the body's winning where both are sent — so a retried request lands on the same object: the same path and read link, with a fresh upload_url.

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.

Some fields carry a person: the recording a voice clone copies, the picture or clip a lip-sync or an avatar makes speak or move, a person an edit puts into a clip, a reference clip whose own sound becomes a voice. An upload there is taken once your account has accepted, one time, that you have the right to the faces and voices you use — in the Auray app (Account → Privacy), or by sending acceptance: {"real_people": "<words_version>"} with the request that first needs it — and is refused 403 acceptance_required until then, before anything is charged. That field's entry says so in asks: the acceptance it asks and its words_version, the other fields whose presence beside it makes the ask (when_also_sent, where an empty list means whatever else the request sends), and the message the address's reference prints on the field. While using uploads that way is paused, the entry also carries refused, and the request is answered 403 voice_sample_not_made_here or 403 likeness_not_made_here instead. A file one of your jobs made is always taken there.

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 at most 7 days, which is longer than any job takes, and never past the upload itself, which is deleted 7 days after it landed — so for what the upload has left: expires_at says when. Ask again for a fresh one while the upload is kept, and 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-13T04:22:05.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 composer does exactly these three calls when you add a file from your computer, and its Use in your code marks where the request reads what your own upload answers.

Start typing to search every page of the documentation.

Up arrow Down arrow to moveEnter to openEscape to close