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.
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
- API keys with assumed-user support, confirm via automate.relu.ai/settings/developer that the "Show shared tokens" option appears.
- 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. Useexternal_download_urlso 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_valuesyou 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:
| Option | Type | Default | Effect |
|---|---|---|---|
showOrderSummary | boolean | true | Shows the order-summary panel. |
showCreditCost | boolean | true | Shows the credit cost. |
showPresets | boolean | true | Shows preset selection and management controls. |
showBackButton | boolean | true | Shows back navigation. |
showServiceTitle | boolean | true | Shows the service name in the order header. |
showServiceSwitcher | boolean | true | Lets the user change services. When false, the service name is static. This has no effect when showServiceTitle is false. |
flatten | boolean | false | Removes the bordered card around the form so it sits flush against the host page. |
preferredParameterValues | Record<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>(orbeta.implant.automate.relu.aifor the beta service)