Start here
Somewhere to PUT a file for an address
/v1/uploadson https://api.auray.ai/v1Returns 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 three days after it landed.
#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
#Body
application/json, required.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
address | string | no | — | The model the file is for, as its address (hitem3d/hi3d/split) or its slug. Send this or product. |
product | string | no | — | 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_type | string | yes | — | 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_key | string | yes | — | Required here, unlike on a submit. It names the object, so a retry lands on the same one instead of failing as already-existing. |
#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"
}'import { readFile } from "node:fs/promises";
import { createClient } from "@auray/client";
const auray = createClient(); // reads AURAY_API_KEY
const upload = await auray.storage.upload({
address: "auray-ai/hunyuan-3d/image-to-3d",
contentType: "image/png",
data: await readFile("photo.png"),
idempotencyKey: "0b6f3c52-8f7e-4c1d-9a3b-5e2f1d7c4a90",
});
// send_as names the field that takes the file, and whether it wants the path or the signed url
for (const { field, value } of upload.send_as ?? []) {
console.log(field, value === "read_url" ? upload.url : upload.path);
}from auray_client import Client
auray = Client() # reads AURAY_API_KEY
upload = auray.storage.upload_file("photo.png", address="auray-ai/hunyuan-3d/image-to-3d", idempotency_key="0b6f3c52-8f7e-4c1d-9a3b-5e2f1d7c4a90")
# send_as names the field that takes the file, and whether it wants the path or the signed url
for target in upload["send_as"] or []:
print(target["field"], upload["url"] if target["value"] == "read_url" else upload["path"]){
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "upload_file",
"arguments": {
"address": "auray-ai/hunyuan-3d/image-to-3d",
"content_type": "image/png",
"bytes_base64": "<base64>",
"idempotency_key": "0b6f3c52-8f7e-4c1d-9a3b-5e2f1d7c4a90"
}
}
}const res = await fetch("https://api.auray.ai/v1/uploads", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AURAY_API_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
"address": "auray-ai/hunyuan-3d/image-to-3d",
"content_type": "image/png",
"idempotency_key": "0b6f3c52-8f7e-4c1d-9a3b-5e2f1d7c4a90"
}),
});
console.log(await res.json());import os, requests
r = requests.post(
"https://api.auray.ai/v1/uploads",
headers={"Authorization": f"Bearer {os.environ['AURAY_API_KEY']}"},
json={
"address": "auray-ai/hunyuan-3d/image-to-3d",
"content_type": "image/png",
"idempotency_key": "0b6f3c52-8f7e-4c1d-9a3b-5e2f1d7c4a90"
},
)
r.raise_for_status()
print(r.json())package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
)
func main() {
body := []byte(`{
"address": "auray-ai/hunyuan-3d/image-to-3d",
"content_type": "image/png",
"idempotency_key": "0b6f3c52-8f7e-4c1d-9a3b-5e2f1d7c4a90"
}`)
req, err := http.NewRequest("POST", "https://api.auray.ai/v1/uploads", bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("AURAY_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.Status, string(out))
}<?php
$ch = curl_init("https://api.auray.ai/v1/uploads");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("AURAY_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"address" => "auray-ai/hunyuan-3d/image-to-3d",
"content_type" => "image/png",
"idempotency_key" => "0b6f3c52-8f7e-4c1d-9a3b-5e2f1d7c4a90",
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response, PHP_EOL;require "net/http"
require "json"
uri = URI("https://api.auray.ai/v1/uploads")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch("AURAY_API_KEY")}"
req["Content-Type"] = "application/json"
req.body = {
"address" => "auray-ai/hunyuan-3d/image-to-3d",
"content_type" => "image/png",
"idempotency_key" => "0b6f3c52-8f7e-4c1d-9a3b-5e2f1d7c4a90"
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code, res.bodyimport java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
String body = """
{
"address": "auray-ai/hunyuan-3d/image-to-3d",
"content_type": "image/png",
"idempotency_key": "0b6f3c52-8f7e-4c1d-9a3b-5e2f1d7c4a90"
}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.auray.ai/v1/uploads"))
.header("Authorization", "Bearer " + System.getenv("AURAY_API_KEY"))
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}using System.Net.Http.Headers;
using System.Text;
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AURAY_API_KEY"));
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.auray.ai/v1/uploads");
var body = """
{
"address": "auray-ai/hunyuan-3d/image-to-3d",
"content_type": "image/png",
"idempotency_key": "0b6f3c52-8f7e-4c1d-9a3b-5e2f1d7c4a90"
}
""";
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");#Responses
#200 OK
OK
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
object | string | no | — | One of "upload". |
upload_url | string | no | — | 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. |
path | string | no | — | 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. |
bucket | string | no | — | |
address | string | no | — | The address's published path, when the request named one. |
max_bytes | integer | no | — | The bucket's ceiling; a larger PUT is refused by storage. |
expires_at | string | no | — | Two hours. Supabase fixes this and exposes no option. |
read | string | no | — | 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_as | object[] | no | — | Every field of the address this file can go in. Null when only the deprecated product named an unknown address. May be null. |
send_as[].field | string | yes | — | The field's name, as GET /models/{address} lists it. |
send_as[].kind | string | yes | — | One of "image", "video", "audio", "file", "mesh". |
send_as[].list | boolean | yes | — | True when the field takes a list, of which this file is one element. |
send_as[].value | string | yes | — | path: send the path above. read_url: send the url that read answers. One of "path", "read_url". |
Headers:
| Header | Description |
|---|---|
RateLimit-Limit | Burst capacity. |
RateLimit-Remaining | |
RateLimit-Reset | Seconds until refill. |
X-RateLimit-Limit | |
X-RateLimit-Remaining | |
X-RateLimit-Reset | Unix seconds. The X- spelling is what client libraries read. |
X-Request-Id | |
Deprecation | Present only when the request named product rather than address: the day that spelling was deprecated, as @<unix seconds> (RFC 9745). |
Link | Beside 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
| Status | Meaning |
|---|---|
200 | OK |
400 | Bad Request |
401 | Unauthorized |
403 | Forbidden |
413 | Payload Too Large |
429 | Too Many Requests |
503 | Service Unavailable |
Every refusal names its code, and every code is explained at Errors.