ReluAutomate
Guides

Draft order flow

Create an order, invite your end user, and let them finalize the parameters in Relu's UI, our recommended approach for products with end users.

Iframe + draft orders

The draft-order flow is the recommended way to expose Relu services to your product's end users. Your backend creates an order with partial data, invites the user, and opens Relu's UI in an iframe for the user to finalize and submit. You keep control of the overall flow and get Relu's latest parameter UI for free.

When to use

Use draft orders when:

  • Your product exposes Relu services to end users (dentists, lab technicians, etc.)
  • You don't want to re-implement Relu's parameter UI

Files can come from anywhere: a common pattern is that users upload files in your product and you pass presigned URLs to Relu, but you can also let users upload directly to the platform from the iframe. Pick whatever fits your architecture.

If you're automating server-to-server without end-user interaction, use the simpler API integration flow without drafts.

Prerequisites

  1. API keys with assumed-user support, confirm via automate.relu.ai/settings/developer that the "Show shared tokens" option appears.
  2. Iframe integration enabled, confirm via automate.relu.ai/settings/developer.

Missing either? Contact support@relu.ai.

1. Create the draft order

Call POST /orders with service_id and is_draft: true. Pre-fill what you know; leave the rest for the user.

import requests

response = requests.post(
    "https://api.automate.relu.ai/orders",
    json={
        "service_id": "<service-uuid from GET /services>",
        "order_name": "Patient #1234",
        "is_draft": True,
        "input_data": {
            "lower_jaw": {
                "external_download_url": "https://your-storage.com/lower.stl",
                "extension": ".stl",
            }
        },
        "parameter_values": {
            # pre-fills the iframe; user can still override before submit
        },
        "callback_url": "https://your-app.com/relu-webhook",
    },
    headers={"X-API-Key": "<your_api_token>"},
)
order = response.json()
order_id = 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 GET /services>',
    order_name: 'Patient #1234',
    is_draft: true,
    input_data: {
      lower_jaw: {
        external_download_url: 'https://your-storage.com/lower.stl',
        extension: '.stl',
      },
    },
    parameter_values: {
      // pre-fills the iframe; user can still override before submit
    },
    callback_url: 'https://your-app.com/relu-webhook',
  }),
});
const order = await res.json();
const orderId = order.id;

Fields:

  • input_data, partial is fine; the user can upload missing files in the iframe. Use external_download_url so files stay in your storage.
  • parameter_values, anything you set here pre-fills the iframe — the user can still change it before submitting. Leave fields out to surface them as the service's default.
  • preset_id, optional. Reference a saved preset to pre-fill the iframe from a named configuration instead of listing every parameter. parameter_values you also send override the preset for those keys.
  • callback_url, you'll receive webhook events on this URL. See Callbacks.
  • output_data, optional upload URLs per output. Currently unstable for draft orders.

The response contains an id, this is your order_id for the remaining steps.

Creating a draft can return 403 with a coded detail when the organization that owns the API key must first add a payment method (payment_verification_required) or is blocked (account_blocked). See Coded refusals.

2. Invite the end user

Share the draft so the user has permission to edit and submit.

requests.post(
    f"https://api.automate.relu.ai/orders/{order_id}/share/invite",
    params={"send_invitation_email": False},
    json={
        "user_email": "enduser@example.com",
        "role_name": "editor",
        "country": "NL",
        "first_name": "Alex",
        "last_name": "Dupont",
    },
    headers={"X-API-Key": "<your_api_token>"},
)
await fetch(
  `https://api.automate.relu.ai/orders/${orderId}/share/invite?send_invitation_email=false`,
  {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.RELU_API_TOKEN,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      user_email: 'enduser@example.com',
      role_name: 'editor',
      country: 'NL',
      first_name: 'Alex',
      last_name: 'Dupont',
    }),
  },
);

If the invitation creates a Relu account, you can pass the optional language query parameter to set its initial interface language. For example, pass language=en. Existing users keep their saved language.

Use editor, it allows viewing, editing, and submitting. viewer is read-only and can't submit. See Sharing & permissions for the role matrix.

Set send_invitation_email=false when you're using user tokens (step 3) for a smooth flow, the user never needs to log in.

Legacy roles

doctor, doctor_in_house_printing, and technician are deprecated. Use editor + optional granted_permissions.

3. Create a user token

Mint a short-lived API token on behalf of the user, scoped to this specific order.

token_res = requests.post(
    "https://api.automate.relu.ai/connections/api-token",
    json={
        "name": f"enduser-{order_id}",
        "expires_delta": 3600,  # seconds; max is 86400 (24 hours)
        "resources": [f"order:{order_id}"],
        "scopes": [
            "read:users", "update:users",
            "read:roles", "read:implant-library",
            "create:orders", "read:orders", "update:orders", "delete:orders",
            "create:input-files", "read:input-files", "update:input-files", "delete:input-files",
            "create:output-files", "read:output-files", "update:output-files", "delete:output-files",
            "create:comments", "read:comments", "update:comments", "delete:comments",
            "read:order-permissions", "create:order-permissions", "update:order-permissions", "delete:order-permissions",
            "read:presets", "create:presets", "update:presets", "delete:presets",
        ],
        "assume_user_email": "enduser@example.com",
    },
    headers={"X-API-Key": "<your_api_token>"},
).json()
user_token = token_res["token"]
const tokenRes = await (
  await fetch('https://api.automate.relu.ai/connections/api-token', {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.RELU_API_TOKEN,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: `enduser-${orderId}`,
      expires_delta: 3600, // seconds; max is 86400 (24 hours)
      resources: [`order:${orderId}`],
      scopes: [
        'read:users',
        'update:users',
        'read:roles',
        'read:implant-library',
        'create:orders',
        'read:orders',
        'update:orders',
        'delete:orders',
        'create:input-files',
        'read:input-files',
        'update:input-files',
        'delete:input-files',
        'create:output-files',
        'read:output-files',
        'update:output-files',
        'delete:output-files',
        'create:comments',
        'read:comments',
        'update:comments',
        'delete:comments',
        'read:order-permissions',
        'create:order-permissions',
        'update:order-permissions',
        'delete:order-permissions',
        'read:presets',
        'create:presets',
        'update:presets',
        'delete:presets',
      ],
      assume_user_email: 'enduser@example.com',
    }),
  })
).json();
const userToken = tokenRes.token;

See User tokens for constraints (max 24h lifetime, required resources, allowed scopes).

Only share with the intended user

Actions taken with this token are attributed to assume_user_email. Share the token only with that user, never forward it.

4. Open the draft in an iframe

Embed Relu's order-finalization UI in your product. See Embed the iframe viewer for the full SDK reference.

import { createViewerHost } from '@relu-bv/iframe-api';

const host = createViewerHost({
  integratorId: '<your-integrator-id>',
  container: document.getElementById('viewer')!,
  viewerUrl: 'https://automate.relu.ai',
  auth: { type: 'apiToken', token: userToken },
  initialConfig: {
    ui: {
      showToolbar: false,
      showLeftSidebar: false,
      showRightSidebar: false,
      showBottomBar: false,
      showCustomerSupportChat: false,
    },
    orderCreation: {
      showOrderSummary: false,
      showCreditCost: false,
      showPresets: true,
      showBackButton: false,
      showServiceTitle: false,
      showServiceSwitcher: false,
      flatten: true,
      preferredParameterValues: {
        Manufacturer: 'ETK',
        'Implant Product Line': 'ETK@iBone E',
      },
      completionBehavior: 'notify-host',
    },
  },
  onReady: (api) => {
    api.navigate({ query: { finalize_order_id: orderId } });
  },
  onCloseRequested: ({ reason }) => {
    if (reason === 'completed') {
      host.destroy();
      // Render the next step in your own workflow.
    }
  },
  onError: (e) => console.error(e),
});

This is a good default for an embedded draft flow: it keeps preset selection visible, hides the surrounding Automate navigation, and lets your application handle successful completion.

Order-creation options

All orderCreation options are optional:

OptionTypeDefaultEffect
showOrderSummarybooleantrueShows the order-summary panel.
showCreditCostbooleantrueShows the credit cost.
showPresetsbooleantrueShows preset selection and management controls.
showBackButtonbooleantrueShows back navigation.
showServiceTitlebooleantrueShows the service name in the order header.
showServiceSwitcherbooleantrueLets the user change services. When false, the service name is static. This has no effect when showServiceTitle is false.
flattenbooleanfalseRemoves the bordered card around the form so it sits flush against the host page.
preferredParameterValuesRecord<string, JSON-compatible value>{}Sets editable initial parameter values when no selected preset or explicit draft value supplies the same parameter.
completionBehavior'redirect' | 'notify-host''redirect'Redirects inside Automate, or emits onCloseRequested with reason: 'completed' after successful finalization so the host can advance its UI.

Close the iframe after finalization

By default, completionBehavior: 'redirect' sends the user to the Automate order list after finalization. Set it to notify-host to keep navigation under your control. After the finalization request succeeds, the SDK calls onCloseRequested with reason: 'completed'.

Destroy the host in that callback to remove the iframe and clean up its event listeners:

const host = createViewerHost({
  // ...container, viewerUrl, auth, and other options
  initialConfig: {
    orderCreation: {
      completionBehavior: 'notify-host',
    },
  },
  onCloseRequested: ({ reason }) => {
    if (reason !== 'completed') return;

    host.destroy();
    showNextStep();
  },
});

Check the reason before closing. The same callback can also receive userAction or error, which your application may want to handle differently.

Preferred parameter values

Use preferredParameterValues to choose editable initial values when the draft does not already contain a value and the user has not selected a preset. Keys are the exact, case-sensitive service parameter names. Values must match an option exposed by that service.

For implant planning, Manufacturer uses the implant-library folder name, such as ETK. Implant Product Line uses <manufacturer folder>@<product line name>, such as ETK@iBone E. Call GET /implant-library/lists to read the current supported values instead of hardcoding the catalog.

Preferences do not override presets or draft values

Values resolve in this order: service defaults, host preferences, the selected preset, then explicit values stored on the draft. A preference only wins when neither a preset nor the draft supplies that parameter.

Alternative, open in a new browser tab (no iframe):

https://automate.relu.ai/services/order?finalize_order_id=<order_id>&token=<userToken>

Refresh the user token before opening, expires_delta is short by design.

5. Wait for processing

You'll receive a POST to your callback_url when the order reaches a terminal state:

{
  "event_type": "order.job_status",
  "order_id": "<order_id>",
  "order_name": "Patient #1234"
}

See Callbacks for all event types.

6. Open the result viewer

Once processing is done, open the viewer in the iframe:

host.getApi().navigate({ query: { orderId: orderId } });

URL pattern by service:

  • Non-implant: https://automate.relu.ai/viewer?orderId=<order_id>
  • Implant planning: https://implant.automate.relu.ai/?orderId=<order_id> (or beta.implant.automate.relu.ai for the beta service)

On this page