Upload files
Two ways to get input files to Relu, presigned download URL, or upload to the platform. Which to pick and why.
When creating an order, every input file must reach Relu somehow. You have two options; pick the one that fits your infrastructure.
Option A, Presigned download URL (recommended)
If your files already live in S3, GCS, Azure Blob, or any storage that can generate a time-limited signed URL, pass the URL directly when creating the order:
"input_data": {
"lower_jaw": {
"external_download_url": "https://s3.amazonaws.com/your-bucket/...",
"extension": ".stl"
}
}The key (lower_jaw here) is the service's input name, always snake_case. Use three-dots menu → View API Example on the service card, or fetch GET /services/{id}, for the exact keys your service accepts.
Relu downloads the file directly from your storage. No intermediate upload, no egress from your side to Relu-side storage.
When to pick this:
- Your files are already hosted (most integrators).
- You want to avoid paying egress twice (once to upload to Relu, once for Relu to pull).
- You control file lifecycle independently.
Gotchas:
- The URL must be publicly reachable from Relu's infrastructure (we don't support IP allowlists).
- Keep the signed-URL expiry generous, at least 3 hours, ideally 24 hours. Orders can queue behind other work; a URL that expires before Relu downloads the file fails the order.
- The URL is not revalidated later.
Option B, Upload to the platform
If you don't have file storage with presigning (e.g., files come from user uploads in your UI and you haven't set up S3), Relu can provide a presigned upload URL:
import os
import requests
RELU_API_KEY = os.environ["RELU_API_KEY"]
api = requests.Session()
api.headers["X-API-Key"] = RELU_API_KEY
# Deliberately has no Relu credentials.
storage = requests.Session()
registration = api.post(
"https://api.automate.relu.ai/input-files",
json={"name": "My upper IOS", "extension": ".stl"},
timeout=(10, 30),
)
registration.raise_for_status()
upload = registration.json()
print(upload["file_id"])
with open("upper.stl", "rb") as file:
response = storage.put(
upload["upload_url"],
data=file,
headers={"Content-Type": "application/octet-stream"},
timeout=(10, 300),
)
response.raise_for_status()const registration = await fetch('https://api.automate.relu.ai/input-files', {
method: 'POST',
headers: {
'X-API-Key': process.env.RELU_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: 'My upper IOS', extension: '.stl' }),
signal: AbortSignal.timeout(30_000),
});
if (!registration.ok) {
throw new Error(`Relu API returned HTTP ${registration.status}`);
}
const upload = await registration.json();
console.log(upload.file_id);
const file = await Deno.readFile('upper.stl');
const response = await fetch(upload.upload_url, {
method: 'PUT',
body: file,
headers: { 'Content-Type': 'application/octet-stream' },
signal: AbortSignal.timeout(300_000),
});
if (!response.ok) {
throw new Error(`Storage upload returned HTTP ${response.status}`);
}You get back a file_id. Reference it in the order body:
Omit extension for registered files
When you use a registered file_id, do not send extension in the order. The
extension recorded by POST /input-files is authoritative.
"input_data": {
"upper_jaw": {
"file_id": "<file_id>"
}
}When you use an external_download_url instead, extension is required because
there is no registered file to provide it.
A jaw scan can use .dcm
3Shape jaw scans may legitimately use .dcm. A .dcm input is therefore not
necessarily a CBCT volume; the input key and the service schema determine how
Relu interprets the file.
When to pick this:
- You're prototyping and don't want to set up storage yet.
- Files only exist transiently (e.g., a user uploads to your UI, you forward to Relu, you never persist them).
Gotchas:
upload_urlis time-limited. PUT soon after you receive it.- Set
Content-Type: application/octet-stream(the presigned URL is signed for it). - You can mix options per file:
volumevia presigned URL,upper_jawvia platform upload, in the same order.
Output uploads
Output files work the same way in reverse. By default, outputs are stored on the Relu platform and you fetch them via download URLs. If you want outputs pushed to your own storage too, pass external_upload_url per output:
"output_data": {
"lower_jaw_model": {
"external_upload_url": "https://s3.amazonaws.com/your-bucket/..."
}
}As with inputs, the output key (lower_jaw_model here) is the service-specific output name — use View API Example or GET /services/{id} to find it.
Relu PUTs the result to your URL when the order completes. The output also stays available through the platform, you get both.
Reliable file transfers
Relu API calls and signed-URL file transfers are separate network requests. A successful Relu API response means the signed URL was issued. It does not mean the upload or download completed. Signed URLs point directly to object storage and can fail independently.
- Use separate HTTP clients or sessions for Relu API and storage requests.
- Send
X-API-KeyorAuthorizationonly toapi.automate.relu.ai. Never attach Relu credentials to a signed URL. - Configure API and file-transfer timeouts separately. Set the transfer read timeout according to the expected file size and connection speed.
- Retry transient storage failures independently instead of repeating the preceding API operation. Retry
ConnectionError, resets such asConnectionResetError, connect or read timeouts, and HTTP408,429, and5xxresponses. - Never log a complete signed URL. Its query string contains temporary credentials.
Use the exponential backoff and jitter pattern from API errors and retries. Three attempts are usually enough for a storage transfer. Do not automatically retry other 4xx responses. For 401 or 403, request a fresh signed URL because the previous URL may have expired.
Restart a failed download from the beginning and write it to a temporary file. Rename the file only after the download finishes. You can retry a PUT to the same signed URL and object key: a completed upload atomically replaces the previous attempt.
Which to pick
| If… | Use |
|---|---|
| Files already in your cloud storage | Presigned download URL (Option A) |
| You control storage lifecycle, don't want double egress | Presigned download URL |
| No storage yet, prototyping | Platform upload (Option B) |
| Files only exist transiently | Platform upload |
| Want outputs mirrored to your storage | external_upload_url |
| Don't want to manage output lifecycle | Default platform storage |
Related
- Input & output files, the underlying model
- Create your first order, where these fields fit in the order body