ReluAutomate
Guides

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.

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_url is 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: volume via presigned URL, upper_jaw via 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-Key or Authorization only to api.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 as ConnectionResetError, connect or read timeouts, and HTTP 408, 429, and 5xx responses.
  • 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 storagePresigned download URL (Option A)
You control storage lifecycle, don't want double egressPresigned download URL
No storage yet, prototypingPlatform upload (Option B)
Files only exist transientlyPlatform upload
Want outputs mirrored to your storageexternal_upload_url
Don't want to manage output lifecycleDefault platform storage

On this page