Getting Started
This guide shows how to integrate a shop, ERP, or MIS with radixSubmit: create an order programmatically, hand your customer an upload link, react to the completion webhook, and download the finished files.
For the interactive endpoint reference, see the API Reference. The companion machine-readable spec is shop-openapi.yaml — import it into Postman, Insomnia, or your codegen tool.
Prerequisites
A tenant admin issues you a clientId + clientSecret pair, and (optionally) configures the order-completion webhook so you can react to confirmations. All examples use https://api.example.com — replace it with the radixSubmit API base URL provided with your credentials.
End-to-end flow
1. POST /auth/token → { token: "app_…", expireAt }
2. POST /orders → { order: { id, products[…] } }
3. POST /orders/{id}/share → { sharedKey }
4. Send PORTAL_URL/s/{key} → customer uploads, assigns, preflights, confirms
5. Webhook: order.confirmed → POST to your endpoint (signed)
6. GET /products/{id}/resources → download preflighted files (presigned URLs)1. Authenticate
Exchange your client credentials for a Bearer token. The token is valid 24 hours — cache it until expireAt and re-authenticate on any 401.
curl -X POST https://api.example.com/auth/token \
-H "Content-Type: application/json" \
-d '{ "clientId": "YOUR_CLIENT_ID", "clientSecret": "YOUR_CLIENT_SECRET" }'{ "token": "app_eyJhbGciOi...", "type": "Bearer", "expireAt": 1715200000 }Pass it on every subsequent call:
Authorization: Bearer app_eyJhbGciOi...Simpler: static API key (no token exchange)
Prefer one step? Authenticate with HTTP Basic using your clientId as the username and clientSecret as the password — no /auth/token call, no expiry:
curl -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-X POST https://api.example.com/orders/simple \
-H "Content-Type: application/json" -d @order.jsonBoth methods yield the same application identity. Always send over HTTPS.
2. Create an order
There are two ways to create an order:
POST /orders/simple— a flat, template-free payload for the common cases (flat work, folded leaflets, saddle-stitch booklets). Recommended for most shops.POST /orders— the full XJDF-based nested model (perfect binding, hardcover, multi-variant). Documented further below.
Quick order — POST /orders/simple
Describe each line item flatly; the backend expands it into the full hierarchy. Set options.createShareLink to get the portal URL straight back.
curl -X POST https://api.example.com/orders/simple \
-H "Authorization: Bearer app_..." \
-H "Content-Type: application/json" \
-d @order.json{
"order": {
"name": "Webshop order #10249",
"externalOrderId": "SHOP-10249",
"customer": { "name": "Acme Retail AG", "email": "max@acme.example" },
"items": [
{ "type": "flat", "name": "A5 Flyer", "quantity": 500, "format": "A5", "pages": 2, "colors": "4/4" },
{
"type": "booklet", "name": "Summer catalogue", "quantity": 1000,
"format": "A4", "bodyPages": 32, "coverPages": 4, "binding": "saddle",
"cover": { "name": "Cover", "colors": "4/4" },
"body": { "name": "Body", "colors": "4/4" }
}
]
},
"options": { "createShareLink": true }
}The response returns the new order plus a ready-to-use shareUrl (when requested):
{
"id": "9b2f8c1a-…",
"orderNumber": 10249,
"status": "READY",
"milestone": "10_CREATED",
"shareUrl": "https://portal.example.com/portal/3f9a…",
"products": [
{ "id": "2c1d…", "name": "A5 Flyer", "type": "FlatWork" },
{ "id": "7e4b…", "name": "Summer catalogue", "type": "Booklet", "displayType": "Booklet" }
]
}Conventions
- Format: named (
A6/A5/A4/A3/DL) or explicitwidth/heightin mm. colors: front/back ink count —4/4(CMYK both sides),4/0(front only),1/0(black front).material:{ weight, type }—weightin g/m².- Booklets: saddle-stitch only (Phase 1),
bodyPagesa multiple of 4; colours are set percover/body; usecover: nullfor a self-cover booklet.
For perfect binding, hardcover or multi-variant, use the full form below.
Full order — POST /orders
Orders use an XJDF-based order model — a nested hierarchy:
order
└── products[] (1..n) what's being produced
└── parts[] (1..n) sub-components (cover, body, insert)
└── pageRanges[] (1..n) page buckets within a part
└── variants[] (0..n) per-run versions (A/B, language)A minimal multi-part booklet (saddle-stitched, cover + body):
curl -X POST https://api.example.com/orders \
-H "Authorization: Bearer app_..." \
-H "Content-Type: application/json" \
-d @order.json{
"order": {
"name": "Summer catalogue — A4 8 pages 4/4",
"externalOrderId": "SHOP-12345",
"jobCustomer": { "name": "Acme Retail AG" },
"jobContacts": [
{ "contactType": "Buyer", "name": "Max Mustermann", "email": "max@acme.example" }
],
"products": [
{
"name": "Summer catalogue",
"type": "Booklet",
"amount": 500,
"parts": [
{
"name": "Body",
"type": "Body",
"intent": {
"layoutIntent": {
"pages": 4,
"sides": "TwoSidedHeadToHead",
"dimensions": { "width": 595.276, "height": 793.701 },
"numberUp": { "width": 1, "height": 1 }
}
},
"pageRanges": [
{ "value": "1-4", "variants": [{ "name": "1", "amount": 500 }] }
]
},
{
"name": "Cover",
"type": "Cover",
"intent": {
"layoutIntent": {
"pages": 4,
"sides": "TwoSidedHeadToHead",
"dimensions": { "width": 595.276, "height": 793.701 },
"numberUp": { "width": 1, "height": 1 }
}
},
"pageRanges": [
{ "value": "1-4", "variants": [{ "name": "1", "amount": 500 }] }
]
}
]
}
]
}
}The response includes the created order with IDs you'll need later (order.id, each product.id, each variant.id) and a standardOrder XJDF export:
{
"order": {
"id": "a7e12b7a-4c3d-4b1a-9e8f-1d2c3b4a5f6e",
"orderNumber": 1042,
"status": "READY",
"milestone": "10_CREATED",
"products": [{ "id": "c3f3…", "parts": [ /* … */ ] }],
"standardOrder": { "...": "XJDF-compliant export" }
}
}Field requirements
layoutIntent.pages, sides, dimensions, and numberUp are required for the portal to render page slots. Including colorIntent and mediaIntent improves preflight routing (otherwise FOGRA defaults are used). Dimensions are in points (A4 = 595.276 × 841.890). The API Reference carries the complete schema and richer examples.
Common errors
| HTTP | code | Cause |
|---|---|---|
| 400 | invalid_argument | Missing required field, unknown enum, or inconsistent variants[].amount sum vs product.amount |
| 401 | unauthenticated | Missing / expired / malformed Bearer token |
| 403 | permission_denied | Token belongs to a different tenant |
3. Create the upload link
After the order exists, create a share link — this is the URL your customer uses to upload. Optionally attach metadata to identify the customer.
curl -X POST https://api.example.com/orders/{ORDER_ID}/share \
-H "Authorization: Bearer app_..." \
-H "Content-Type: application/json" \
-d '{ "metadata": { "externalUserId": "shop-user-998", "externalUserEmail": "buyer@example.com" } }'{ "sharedKey": "sk_…" }Build the URL you email or redirect the customer to:
${PORTAL_URL}/s/${sharedKey}${PORTAL_URL} is your tenant's portal domain (e.g. https://portal.your-company.com). The link is valid 7 days. Creating a new share revokes the previous one; you can also revoke explicitly with DELETE /orders/{ORDER_ID}/share.
4. "Redirect URL" — react to completion
How completion is delivered today
The portal does not take a per-order browser redirect URL. Instead, when the customer confirms, the backend delivers a signed order.confirmed webhook to the URL configured in your tenant settings. Treat this webhook as your "redirect" / return signal. (If you need a literal post-confirmation browser redirect, that's a possible future enhancement — talk to us.)
Configure the webhook
In the tenant dashboard, go to Integration → Webhooks, click Add endpoint, and:
- Enter your HTTPS endpoint URL.
- Subscribe to the
order.confirmedevent. - Copy the auto-generated signing secret shown on creation (it won't be shown again).
If no webhook endpoint is configured, nothing is sent — poll GET /orders/{id} as a fallback.
Payload
{
"event": "order.confirmed",
"timestamp": "2026-04-18T14:32:09.117Z",
"order": {
"id": "a7e12b7a-...",
"name": "Summer catalogue 2026",
"reference": "SHOP-12345",
"confirmedAt": "2026-04-18T14:31:55.000Z",
"products": [
{
"id": "c3f…",
"name": "32-page booklet A4",
"resources": [
{
"id": "r01…",
"name": "booklet-preflighted.pdf",
"mimeType": "application/pdf",
"downloadUrl": "https://s3.../preflighted.pdf?X-Amz-Signature=…"
}
]
}
]
}
}Delivery semantics
- Retries: up to 4 attempts with backoff
0s, 1s, 3s, 9s. - Success: any HTTP
2xx. - No dedup header — make your handler idempotent on
order.id.
Verify the signature
Each request carries X-Webhook-Signature: <hex> where <hex> = HMAC-SHA256(rawBody, secret). Verify against the raw body before JSON-parsing — re-serializing changes the digest.
import crypto from 'node:crypto'
import express from 'express'
const app = express()
app.post(
'/webhooks/radix-submit',
express.raw({ type: 'application/json' }),
(req, res) => {
const expected = crypto
.createHmac('sha256', process.env.RADIX_SUBMIT_WEBHOOK_SECRET)
.update(req.body) // raw Buffer
.digest('hex')
const got = req.header('X-Webhook-Signature')
if (!got || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got))) {
return res.status(401).send('bad signature')
}
const payload = JSON.parse(req.body.toString('utf8'))
// …handle payload.order (idempotent on payload.order.id)…
res.status(200).end()
},
)5. Download preflighted files
downloadUrls in the webhook are 7-day presigned S3 URLs — prefer downloading immediately. To fetch a product's resources later (or after a link expired), list them by product ID (order.products[].id). Download URLs are included by default:
curl "https://api.example.com/products/{PRODUCT_ID}/resources" \
-H "Authorization: Bearer app_..."{
"resources": [
{
"id": "r01…",
"name": "booklet-preflighted.pdf",
"mimeType": "application/pdf",
"pageCount": 32,
"type": "Preflighted",
"downloadUrl": "https://s3.../preflighted.pdf?X-Amz-Signature=…"
}
]
}This returns the product-level resources — the merged, preflighted output you forward to production. Add ?type=Preflighted to filter, or ?withDownloadUrls=false to omit the presigned URLs. (Per-variant customer inputs live under order.products[].parts[].pageRanges[].variants[].resources[] in the order response.)
Endpoint reference
| Method | Path | Purpose |
|---|---|---|
| POST | /auth/token | Exchange credentials → 24h Bearer token |
| POST | /orders | Create an order (XJDF-based order) |
| POST | /orders/simple | Create a simple order (flat/folded/booklet) |
| GET | /orders/{id} | Fetch order + hierarchy + status |
| POST | /orders/{id}/share | Create a 7-day customer share link |
| DELETE | /orders/{id}/share | Revoke active shares |
| GET | /products/{id}/resources | List a product's resources + presigned downloads |
| Outbound | POST {webhookEndpointUrl} | order.confirmed webhook on confirmation |
See the API Reference for full request/response schemas and try-it-out.