Start here
A signed read URL for a file you uploaded
/v1/uploads/{path}on https://api.auray.ai/v1The second step of an upload, and a separate one by measurement: storage refuses to sign a read for an object that does not exist, so the read URL cannot come back beside the upload URL without pro…
The second step of an upload, and a separate one by measurement: storage refuses to sign a read for an object that does not exist, so the read URL cannot come back beside the upload URL without promising a file nobody has sent. PUT the bytes to upload_url, then ask here. The URL is good for seven days at most — long enough for any model's fetch and any queue — and never past the upload, which is deleted seven days after it landed: it is signed for what the upload has left, and expires_at says when; a fresh one can be asked for while the upload is kept. An upload kept past its seven days — while something of the account's is held after a report, or while a 3D draft of yours still uses it — is signed for seven days. It is what a catalogue model's _url field takes. An upload past its seven days that nothing keeps is 410 expired with reason: retention_ended, and nothing is signed, until the retention sweep deletes it, normally within minutes; it is 404 not_found after that.
path is the <user_id>/<file> the upload answered; another account's folder, a file that was never PUT, or one deleted after its seven days is 404 not_found. address picks the bucket the way it did on the upload, and any scope that may upload for that address reads back through it the account's files of the kinds that address takes — the kind it makes (image:write, 3d:write, …) or the older product name; the upload's read link names it, and a path of another kind is 404 not_found through it. The deprecated product picks a bucket by name and takes the scopes the upload by that product took — for external, external:write, or a kind's scope for the kinds of file some address of that kind takes, a path of another kind refused 403 insufficient_scope naming the scopes that would reach it. Naming neither looks only in the buckets whose own scope the key holds — the catalogue models' with external:write, then 3D's with 3d:write or threed:write. A key without external:write, as every key Account mints is, is refused 403 insufficient_scope there, with the address as the way out, unless our 3D's bucket holds the file and the key holds 3d:write or threed:write; so keep the query the read link carries. assets:read alone reads nothing back: it fetches what a job made, and an upload is something the caller sent.
#Authentication
Authorization: Bearer auray_sk_… — Through address, a scope that may upload for it — the kind it makes or the older product name — for a file of a kind it takes. Through product, the scopes the upload by that product takes. Naming neither address nor product, the bucket's own: external:write for the catalogue models' inputs, 3d:write or threed:write for 3D's. assets:read alone does not.
#Request
#Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
path | string | yes | <user_id>/<file>, as the upload answered it. |
#Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
address | string | no | The address the file was uploaded for — what the upload's read link carries. |
product | string | no | Deprecated: name the address. Which product's bucket the path is in. One of "threed", "external". |
#Example request
curl "https://api.auray.ai/v1/uploads/86e97633-…/chair-scan-3.glb?address=hitem3d%2Fhi3d%2Fsplit" \
-H "Authorization: Bearer $AURAY_API_KEY"const res = await fetch("https://api.auray.ai/v1/uploads/86e97633-…/chair-scan-3.glb?address=hitem3d%2Fhi3d%2Fsplit", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.AURAY_API_KEY}` },
});
console.log(await res.json());import os, requests
r = requests.get(
"https://api.auray.ai/v1/uploads/86e97633-…/chair-scan-3.glb?address=hitem3d%2Fhi3d%2Fsplit",
headers={"Authorization": f"Bearer {os.environ['AURAY_API_KEY']}"},
)
r.raise_for_status()
print(r.json())package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.auray.ai/v1/uploads/86e97633-…/chair-scan-3.glb?address=hitem3d%2Fhi3d%2Fsplit", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("AURAY_API_KEY"))
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/86e97633-…/chair-scan-3.glb?address=hitem3d%2Fhi3d%2Fsplit");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("AURAY_API_KEY"),
],
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response, PHP_EOL;require "net/http"
uri = URI("https://api.auray.ai/v1/uploads/86e97633-…/chair-scan-3.glb?address=hitem3d%2Fhi3d%2Fsplit")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch("AURAY_API_KEY")}"
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 {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.auray.ai/v1/uploads/86e97633-…/chair-scan-3.glb?address=hitem3d%2Fhi3d%2Fsplit"))
.header("Authorization", "Bearer " + System.getenv("AURAY_API_KEY"))
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}using System.Net.Http.Headers;
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AURAY_API_KEY"));
using var request = new HttpRequestMessage(HttpMethod.Get, "https://api.auray.ai/v1/uploads/86e97633-…/chair-scan-3.glb?address=hitem3d%2Fhi3d%2Fsplit");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");#Responses
#200 OK
A signed read URL.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
object | string | yes | — | One of "upload". |
path | string | yes | — | |
bucket | string | yes | — | |
url | string | yes | — | Signed; fetchable by anyone who holds it until expires_at: at most seven days, never past the upload. A URL. |
expires_at | string | yes | — | |
request_id | string | yes | — |
#400 Bad Request
A refusal.
The error body: error, message, docs (the entry that explains it) and request_id.
#401 Unauthorized
A refusal.
The error body: error, message, docs (the entry that explains it) and request_id.
#403 Forbidden
A refusal.
The error body: error, message, docs (the entry that explains it) and request_id.
#404 Not Found
A refusal.
The error body: error, message, docs (the entry that explains it) and request_id.
#410 Gone
A refusal.
The error body: error, message, docs (the entry that explains it) and request_id.
#429 Too Many Requests
A refusal.
The error body: error, message, docs (the entry that explains it) and request_id.
#503 Service Unavailable
A refusal.
The error body: error, message, docs (the entry that explains it) and request_id.
#Status codes
| Status | Meaning |
|---|---|
200 | A signed read URL. |
400 | A refusal. |
401 | A refusal. |
403 | A refusal. |
404 | A refusal. |
410 | A refusal. |
429 | A refusal. |
503 | A refusal. |
Every refusal names its code, and every code is explained at Errors.