Skip to content
Registrieren

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.

bash
curl -X POST https://api.example.com/auth/token \
  -H "Content-Type: application/json" \
  -d '{ "clientId": "YOUR_CLIENT_ID", "clientSecret": "YOUR_CLIENT_SECRET" }'
json
{ "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:

bash
curl -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -X POST https://api.example.com/orders/simple \
  -H "Content-Type: application/json" -d @order.json

Both 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.

bash
curl -X POST https://api.example.com/orders/simple \
  -H "Authorization: Bearer app_..." \
  -H "Content-Type: application/json" \
  -d @order.json
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):

json
{
  "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 explicit width/height in mm.
  • colors: front/back ink count — 4/4 (CMYK both sides), 4/0 (front only), 1/0 (black front).
  • material: { weight, type }weight in g/m².
  • Booklets: saddle-stitch only (Phase 1), bodyPages a multiple of 4; colours are set per cover/body; use cover: null for 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):

bash
curl -X POST https://api.example.com/orders \
  -H "Authorization: Bearer app_..." \
  -H "Content-Type: application/json" \
  -d @order.json
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:

json
{
  "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

HTTPcodeCause
400invalid_argumentMissing required field, unknown enum, or inconsistent variants[].amount sum vs product.amount
401unauthenticatedMissing / expired / malformed Bearer token
403permission_deniedToken belongs to a different tenant

After the order exists, create a share link — this is the URL your customer uses to upload. Optionally attach metadata to identify the customer.

bash
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" } }'
json
{ "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.confirmed event.
  • 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

json
{
  "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.

js
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:

bash
curl "https://api.example.com/products/{PRODUCT_ID}/resources" \
  -H "Authorization: Bearer app_..."
json
{
  "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

MethodPathPurpose
POST/auth/tokenExchange credentials → 24h Bearer token
POST/ordersCreate an order (XJDF-based order)
POST/orders/simpleCreate a simple order (flat/folded/booklet)
GET/orders/{id}Fetch order + hierarchy + status
POST/orders/{id}/shareCreate a 7-day customer share link
DELETE/orders/{id}/shareRevoke active shares
GET/products/{id}/resourcesList a product's resources + presigned downloads
OutboundPOST {webhookEndpointUrl}order.confirmed webhook on confirmation

See the API Reference for full request/response schemas and try-it-out.