ReluAutomate
Guides

Create your first order

End-to-end walkthrough from token creation to downloading results.

This guide walks through a full order lifecycle against the Relu Automate API: authenticate, pick a service, upload input files, create the order, wait for completion, and fetch outputs. Python and JavaScript examples throughout.

1. Create an API token

Log in to automate.relu.ai, open Settings → Develop, and create a token with the scopes you need. See API keys for detail on scopes and rotation.

If you don't see the API token settings, your organization needs API access enabled, email support@relu.ai.

2. Discover the service you want to run

import requests

response = requests.get(
    "https://api.automate.relu.ai/services",
    headers={"Authorization": "ApiKey <your_api_token>"},
)
services = response.json()
for s in services:
    print(s["id"], s["name"])
const res = await fetch('https://api.automate.relu.ai/services', {
  headers: { Authorization: `ApiKey ${process.env.RELU_API_TOKEN}` },
});
const services = await res.json();
services.forEach((s) => console.log(s.id, s.name));

Pick the service you want to run. Note its id and inspect the input/output/parameter schemas to see what it expects.

3. Get input files in

Two options. See Upload files for the full comparison.

Option A, Presigned download URL. You already have the file hosted somewhere reachable. Pass external_download_url per input file when creating the order; Relu downloads it.

Option B, Upload to the platform. Get a presigned upload URL from Relu, PUT your file, then reference the returned file_id in the order:

RELU_API_KEY = "<your_api_token>"

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()

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()
# Use upload["file_id"] in the order body below.
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();

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}`);
}
// Use upload.file_id in the order body below.

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. Keep extension when you use an external_download_url; it is required.

3Shape jaw scans may legitimately use .dcm, so a .dcm file is not necessarily a CBCT volume.

4. Create the order

Call POST /orders with the service's UUID as service_id. Input keys are snake_case (e.g. lower_jaw, upper_jaw, volume), not the human-readable titles you see in the UI. Check the API reference or use the View API Example snippet from the platform's three-dots menu on the service card.

You configure the service's parameters in one of two ways. Pick whichever fits how you work:

  • Pass parameters explicitly in parameter_values (shown first below). Best when options vary per order and you want to set them flexibly from your side.
  • Reference a preset by preset_id (shown second). Save a named configuration once in the platform UI, then reuse it — best when you have a stable set of settings per service. You can still override individual parameters per order. See Presets for the full story.

Option A — pass parameters explicitly

The example below uses Model Builder; the parameter keys are service-specific.

response = requests.post(
    "https://api.automate.relu.ai/orders",
    json={
        "service_id": "<service-uuid from step 2>",
        "order_name": "My first order",
        "input_data": {
            "lower_jaw": {
                "external_download_url": "https://.../lower.stl",
                "extension": ".stl",
            },
            "upper_jaw": {
                "file_id": "<file_id from step 3>",
            },
        },
        "parameter_values": {
            # service-specific, see https://api.automate.relu.ai/redoc
            # or View API Example
        },
        "callback_url": "https://your-app.com/relu-webhook",
    },
    headers={"X-API-Key": "<your_api_token>"},
)
order = response.json()
print(order["id"])
const res = await fetch('https://api.automate.relu.ai/orders', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.RELU_API_TOKEN,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    service_id: '<service-uuid from step 2>',
    order_name: 'My first order',
    input_data: {
      lower_jaw: {
        external_download_url: 'https://.../lower.stl',
        extension: '.stl',
      },
      upper_jaw: {
        file_id: '<file_id from step 3>',
      },
    },
    parameter_values: {
      // service-specific, see https://api.automate.relu.ai/redoc
      // or View API Example
    },
    callback_url: 'https://your-app.com/relu-webhook',
  }),
});
const order = await res.json();
console.log(order.id);

Option B — use a preset

If you saved a preset for this service in the platform UI, reference it by preset_id instead of listing every parameter. List your presets with GET /presets (filter by service_id) to find its ID.

response = requests.post(
    "https://api.automate.relu.ai/orders",
    json={
        "service_id": "<service-uuid from step 2>",
        "preset_id": "<preset-uuid from GET /presets>",
        "order_name": "My first order",
        "input_data": {
            "lower_jaw": {
                "external_download_url": "https://.../lower.stl",
                "extension": ".stl",
            },
            "upper_jaw": {
                "file_id": "<file_id from step 3>",
            },
        },
        # Optional: override individual parameters from the preset.
        # Anything you set here wins over the preset's value for that key.
        "parameter_values": {},
        "callback_url": "https://your-app.com/relu-webhook",
    },
    headers={"X-API-Key": "<your_api_token>"},
)
order = response.json()
print(order["id"])
const res = await fetch('https://api.automate.relu.ai/orders', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.RELU_API_TOKEN,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    service_id: '<service-uuid from step 2>',
    preset_id: '<preset-uuid from GET /presets>',
    order_name: 'My first order',
    input_data: {
      lower_jaw: {
        external_download_url: 'https://.../lower.stl',
        extension: '.stl',
      },
      upper_jaw: {
        file_id: '<file_id from step 3>',
      },
    },
    // Optional: override individual parameters from the preset.
    // Anything you set here wins over the preset's value for that key.
    parameter_values: {},
    callback_url: 'https://your-app.com/relu-webhook',
  }),
});
const order = await res.json();
console.log(order.id);

When you send both preset_id and parameter_values, parameters resolve in priority order: your parameter_values first, then the preset's values, then the service defaults.

You get back an order ID immediately, the order is queued and will run asynchronously.

5. Wait for completion

Two ways. See Poll vs webhook for which to choose.

Webhook (recommended). Because you passed callback_url, Relu will POST to it when the order reaches a terminal state. See Callbacks for event types and payload shape.

Polling (fallback). Useful during development or as a safety net:

import time

while True:
    response = requests.post(
        "https://api.automate.relu.ai/orders/job-statuses",
        headers={"X-API-Key": "<your_api_token>"},
        json={"order_ids": [order["id"]]},
    )
    response.raise_for_status()
    body = response.json()
    order_status = body["statuses"].get(order["id"])
    if order_status is None:
        error = next(
            (item for item in body["errors"] if item["order_id"] == order["id"]),
            None,
        )
        code = error["code"] if error else "unknown_error"
        raise RuntimeError(f"Order status is unavailable: {code}")

    main_job = order_status["main_job"]
    if main_job is None:
        time.sleep(30)
        continue

    job_status = main_job["job_status"]
    if job_status == "Done":
        break
    if job_status in ("Failed", "Cancelled"):
        raise RuntimeError(f"Order ended with status {job_status}")
    time.sleep(30)
let status;
while (true) {
  const res = await fetch('https://api.automate.relu.ai/orders/job-statuses', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': process.env.RELU_API_TOKEN,
    },
    body: JSON.stringify({ order_ids: [order.id] }),
  });
  if (!res.ok) throw new Error(`Relu API returned HTTP ${res.status}`);
  const body = await res.json();
  status = body.statuses[order.id];
  if (!status) {
    const error = body.errors.find((item) => item.order_id === order.id);
    throw new Error(
      `Order status is unavailable: ${error?.code ?? 'unknown_error'}`,
    );
  }
  if (!status.main_job) {
    await new Promise((r) => setTimeout(r, 30_000));
    continue;
  }

  const jobStatus = status.main_job.job_status;
  if (jobStatus === 'Done') {
    break;
  }
  if (['Failed', 'Cancelled'].includes(jobStatus)) {
    throw new Error(`Order ended with status ${jobStatus}`);
  }
  await new Promise((r) => setTimeout(r, 30_000));
}

For production polling, handle temporary request failures as described in API errors and retries.

Polling cadence:

  • Polling only: every 30–60 seconds is reasonable. Most services finish in a few minutes, so you'll see the result quickly.
  • Combined with webhooks: polling becomes a safety net, run it every 10–30 minutes to catch missed callbacks. No need to go faster since the webhook is the primary signal.

6. Download outputs

When job_status is "Done", fetch the full order once with include_output_download_urls=true:

completed_order_response = requests.get(
    f"https://api.automate.relu.ai/orders/{order['id']}",
    headers={"X-API-Key": "<your_api_token>"},
    params={"include_output_download_urls": True},
)
completed_order_response.raise_for_status()
completed_order = completed_order_response.json()
const orderRes = await fetch(
  `https://api.automate.relu.ai/orders/${order.id}?include_output_download_urls=true`,
  { headers: { 'X-API-Key': process.env.RELU_API_TOKEN } },
);
if (!orderRes.ok) {
  throw new Error(`Relu API returned HTTP ${orderRes.status}`);
}
const completedOrder = await orderRes.json();

The response includes each output file with a status and a time-limited download_url. Only files with status == "Available" are ready to download. Files can also be "Missing" when they were not produced, or "Failed".

storage = requests.Session()  # Deliberately has no Relu credentials.

for output in completed_order["output_files"]:
    if output["status"] != "Available":
        continue
    ext = output["options"].get("extension", "")
    destination = f"{output['name']}{ext}"

    with storage.get(
        output["download_url"],
        stream=True,
        timeout=(10, 120),
    ) as response:
        response.raise_for_status()

        with open(destination, "wb") as file:
            for chunk in response.iter_content(chunk_size=1024 * 1024):
                file.write(chunk)
for (const output of completedOrder.output_files) {
  if (output.status !== 'Available') continue;
  const res = await fetch(output.download_url, {
    signal: AbortSignal.timeout(120_000),
  });
  if (!res.ok) throw new Error(`Storage download returned HTTP ${res.status}`);
  const buf = new Uint8Array(await res.arrayBuffer());
  await Deno.writeFile(`${output.name}${output.options.extension ?? ''}`, buf);
}

Download URLs expire; if they do, re-fetch the order to get fresh URLs. See Reliable file transfers for timeout, credential, and retry guidance.

Next steps

On this page