Docs

Start here

Somewhere to PUT a file for an address

POST/v1/uploadson https://api.auray.ai/v1

Returns a signed URL to upload to and where the file goes afterwards.

Returns a signed URL to upload to and where the file goes afterwards. Bytes never come through this API: the platform's request limit is smaller than the files 3D takes, and two orders of magnitude smaller than the meshes a catalogue model takes.

address names the model the file is for, and everything else follows from it: the bucket, the kinds of file its fields take (an image model takes no audio), the ceiling (max_bytes), and the scope — any scope that may submit to that address. send_as lists every field of the address this file can go in, each saying whether to send the path this call answers (our 3D, auray-ai/hunyuan-3d/image-to-3d, and our video, auray-ai/minimax-h3/text-to-video, neither of which fetches a URL) or the signed url that GET /uploads/{path} answers once the bytes are there (every catalogue model's _url field). An address none of whose fields takes a file is refused with upload_not_accepted.

product is the older spelling and still answers every key it answered, with a Deprecation header and a Link to the guide; send address instead. For the catalogue models' bucket, external, it also takes a kind's scope, for the kinds of file some address of that kind takes — an image with any kind's scope, a mesh with 3d:write — so a key minted today still uploads through a client that can name only the product; a file of a kind outside them is refused 403 insufficient_scope, naming the scopes that would reach it. video is no longer offered — a picture for our video goes through its address, which stores it under a name of its own (api-<idempotency_key>) and reads back only those — and answers only a key that used it: one holding auray-ai-video:write, or video:write beside external:write, never the video kind alone. The upload itself is deleted seven days after it landed.

What one account uploads is held under a daily figure of its plan, counted over the last 24 hours: past it, this answers 429 rate_limited with reason upload_bytes, bucket uploads and a Retry-After for when room comes back, and signs nothing.

#Authentication

Authorization: Bearer auray_sk_… — A scope that may submit to the address the file is for — the kind it makes (3d:write, image:write, …) or the older product name. product is deprecated: threed takes 3d:write or threed:write, external takes external:write or a kind's scope for the kinds of file that kind's addresses take.

#Request

#Headers

NameTypeRequiredDescription
Idempotency-KeystringnoRead when the body names no idempotency_key, and the same thing: it names the object. A header outside the pattern is folded into a key inside it (h- and its SHA-256) rather than refused, so the same header names the same object every time.

#Body

application/json, required.

FieldTypeRequiredDefaultDescription
addressstringno—The model the file is for, as its address (hitem3d/hi3d/split) or its slug. Send this or product.
productstringno—Deprecated: name the address instead. Which bucket: threed takes 3d:write or threed:write; external takes external:write for any file, or a kind's scope for the kinds of file some address of that kind takes. One of "threed", "external".
content_typestringyes—The file's type. An address accepts the kinds its fields take; the refusal lists the types. One of "application/json", "application/octet-stream", "application/pdf", "application/zip", "audio/flac", "audio/mp4", "audio/mpeg", "audio/ogg", "audio/wav", "audio/x-wav", "image/jpeg", "image/png", "image/webp", "model/gltf+json", "model/gltf-binary", "model/obj", "model/stl", "text/plain", "video/mp4", "video/quicktime", "video/webm".
idempotency_keystringno—Required here, unlike on a submit — in the body or as the Idempotency-Key header, and the body's wins when both are sent; with neither the request is 400 invalid_idempotency_key. It names the object, so a retry lands on the same one instead of failing as already-existing: a resend with the same key answers the same path and read link with a fresh upload_url, and a second PUT replaces the bytes.

#Example request

curl -X POST https://api.auray.ai/v1/uploads \
  -H "Authorization: Bearer $AURAY_API_KEY" \
  -H "content-type: application/json" \
  -d '{
  "address": "auray-ai/hunyuan-3d/image-to-3d",
  "content_type": "image/png",
  "idempotency_key": "0b6f3c52-8f7e-4c1d-9a3b-5e2f1d7c4a90"
}'

#Responses

#200 OK

OK

FieldTypeRequiredDefaultDescription
objectstringno—One of "upload".
upload_urlstringno—PUT the bytes here with Content-Type set to what you declared. Send no Auray credential: the signature in the URL is the authorisation. A URL.
pathstringno—The object's key, <your account id>/<idempotency_key>.<ext> — <your account id>/api-<idempotency_key>.<ext> for our video's address. Where send_as says path, send it as it is.
bucketstringno—
addressstringno—The address's published path, when the request named one.
max_bytesintegerno—The bucket's ceiling; a larger PUT is refused by storage.
expires_atstringno—Two hours. Supabase fixes this and exposes no option.
readstringno—Where to ask for the read URL once the bytes are there: GET /uploads/{path}?address=… (or ?product=… for the deprecated spelling), absolute. Use this address as given. A URL.
send_asobject[]no—Every field of the address this file can go in. Where the model would make the person in the file speak or move, copy the voice in it, put them into a clip or keep a clip's own sound as a voice, the entry also carries asks: an upload in that field is taken once your account has accepted, one time, that you have the right to the faces and voices you use, and refused 403 acceptance_required until then, before anything is charged — and, only while using uploads that way is paused, refused, when it is refused 403 instead. A file one of your jobs made is taken there. Null when only the deprecated product named an unknown address. May be null.
send_as[].fieldstringyes—The field's name, as GET /models/{address} lists it.
send_as[].kindstringyes—One of "image", "video", "audio", "file", "mesh".
send_as[].listbooleanyes—True when the field takes a list, of which this file is one element.
send_as[].valuestringyes—path: send the path above. read_url: send the url that read answers. One of "path", "read_url".
send_as[].asksobject[]no—Present only on a field where this model would make the person in the file speak or move, copy the voice in it, put them into a clip or keep a clip's own sound as a voice: each way an upload there asks your account's one acceptance that you have the right to the faces and voices you use — taken once accepted, and refused 403 acceptance_required until then, before anything is charged. Accept in the Auray app (Account → Privacy), or send acceptance with the request; an assistant cannot accept for you. A file one of your jobs made asks nothing, and nor does a clip you upload with no sound track where only its sound is asked.
send_as[].asks[].acceptancestringyes—What is asked: the key to send in the request's acceptance field — real_people, the faces and voices of real people. One of "real_people".
send_as[].asks[].words_versionstringyes—The version of the words in force: the value to send under that key, else acceptance_invalid.
send_as[].asks[].when_also_sentstring[][]yes—The other fields, named as field names them, whose presence beside this one makes the ask: any one list, every field in it. A list that is empty means whatever else the request sends.
send_as[].asks[].messagestringyes—The same sentence the field's description says on the address's own reference.
send_as[].refusedobject[]no—Present only while using an upload this way is paused, on a field whose entry asks: each way an upload there is refused instead, 403, before anything is charged, whatever your account accepted. A file one of your jobs made is taken there.
send_as[].refused[].errorstringyes—The error the request is answered with while this is paused, 403. One of "likeness_not_made_here", "voice_sample_not_made_here".
send_as[].refused[].when_also_sentstring[][]yes—The other fields, named as field names them, whose presence beside this one makes the refusal: any one list, every field in it. A list that is empty means whatever else the request sends.
send_as[].refused[].messagestringyes—The same sentence the field's description says on the address's own reference.

Headers:

HeaderDescription
RateLimit-LimitBurst capacity.
RateLimit-Remaining
RateLimit-ResetSeconds until refill.
X-RateLimit-Limit
X-RateLimit-Remaining
X-RateLimit-ResetUnix seconds. The X- spelling is what client libraries read.
X-Request-Id
DeprecationPresent only when the request named product rather than address: the day that spelling was deprecated, as @<unix seconds> (RFC 9745).
LinkBeside Deprecation: the guide section naming the replacement, rel="deprecation".

#400 Bad Request

#401 Unauthorized

#403 Forbidden

#413 Payload Too Large

#429 Too Many Requests

#503 Service Unavailable

#Status codes

StatusMeaning
200OK
400Bad Request
401Unauthorized
403Forbidden
413Payload Too Large
429Too Many Requests
503Service Unavailable

Every refusal names its code, and every code is explained at Errors.

Start typing to search every page of the documentation.

Up arrow Down arrow to moveEnter to openEscape to close