Customer Orders API

Submit print, photobook, and photo-box orders to Digitalab. One OrderItem is one product. Put every page or print for that product in its ImageUrls list, and describe cover, paper, foil, and similar choices in Options.

Catalog: JSON Orders: XML Auth: x-api-key header Multi-image: Sequence + Copies Config: Options
Confirm your API key with GET /api/me (sandbox: true on the sandbox host). Orders are submitted once with POST /api/customerorders/submit. Integrate against the sandbox base URL first: submit runs full validation and downloads images, but orders are saved to a separate sandbox area and never reach production. Test the same XML with POST /api/customerorders/validate for a faster dry run: it checks every file URL is reachable and the right type without downloading the files. On production, a duplicate CustomerOrderId is rejected. Look up an order afterwards with GET /api/customerorders/{customerOrderId}, or register a status callback URL so Digitalab POSTs each status change (received, dispatched, completed, failed…). The product catalog is read-only JSON (GET /api/catalog). Shipping methods and image types are GET /api/reference.

Base URLs

EnvironmentURL
Production https://api.digitalab.co.uk
Sandbox (integration testing) https://sandbox.api.digitalab.co.uk

All paths below are relative to the base URL. Use HTTPS in production and on sandbox. Run the app locally with the Sandbox launch profile (ASPNETCORE_ENVIRONMENT=Sandbox) to exercise the same behaviour on http://localhost:5052.

Sandbox

The sandbox host is a read-only integration environment. Every endpoint behaves like production except that orders are saved to a separate sandbox area, never to the production lab folders. Use it to wire your shop, exercise error handling, and confirm image URLs are reachable without creating real jobs.

What is the same as production

  • Authentication, rate limits, catalog, and reference data
  • XML parsing and validation rules (including SKU availability for your API key)
  • POST /api/customerorders/submit checks and downloads each file over HTTPS (JPEG/TIFF/PDF), including background downloads for larger orders, and returns the same JSON shape as production
  • POST /api/customerorders/validate — same dry run as production, including the file URL check

What is different

  • The order folder, images and order XML are saved to a sandbox area that production never picks up
  • Status callbacks are not sent
  • GET /api/customerorders/{customerOrderId} finds sandbox orders you have submitted and reports them as received
  • Duplicate CustomerOrderId checks are skipped so you can replay the same test id (a replay replaces the earlier test order)
  • Successful submit responses include "sandbox": true and a message that the order went to the sandbox
  • GET /api/ping and GET /api/me include "sandbox": true

curl — sandbox submit

curl.exe -v --max-time 120 -X POST "https://sandbox.api.digitalab.co.uk/api/customerorders/submit" -H "Content-Type: application/xml" -H "x-api-key: YOUR-SANDBOX-API-KEY" --data-binary "@customer-order-sample.xml"

Digitalab issues separate sandbox API keys. Use production keys only on api.digitalab.co.uk.

Authentication

Every catalog, submit, validate, status, and /me endpoint requires an API key. Ping does not. Send the key as an HTTP header:

x-api-key: YOUR-API-KEY

In curl that is the -H header flag:

curl -H "x-api-key: YOUR-API-KEY" ...

How keys work

  • Digitalab issues one API key per partner account. The full key is shown once, when it is issued or rotated.
  • If Digitalab rotates your key, the previous key keeps working for 24 hours so you can swap without downtime. A revoked key stops immediately.
  • The key is mapped server-side to your AccountCode.
  • The XML must include OrderInfo.AccountCode, and it must match the account linked to the key.
  • Do not put the API key in the XML body. The server writes it into saved order files itself.
  • Digitalab can optionally restrict a key to office IPs. If that is set, calls from any other address return 403. Empty allow-list means any IP.
Missing key

Returns 401 Unauthorized.

{
  "responseId": "...",
  "status": 401,
  "message": "API Key is missing",
  "title": "Error 401 - Unauthorized",
  "errors": ["API Key is missing"]
}
Invalid key

Returns 403 Forbidden.

{
  "responseId": "...",
  "status": 403,
  "message": "Invalid API Key",
  "title": "Error 403 - Forbidden",
  "errors": ["Invalid API Key"]
}
If the key is valid but OrderInfo.AccountCode does not match the account mapped to that key, the request is rejected with 403 and no order folder is created.

Customer Orders API

Submit a print order as XML. The API checks every file URL you supply (JPEG, TIFF or PDF over HTTPS), downloads the files — straight away for small orders, in the background for larger ones — and saves them with a generated order XML into an order folder for the lab. It returns a JSON receipt with orderStatus received or pending. On any validation or file-check failure it returns 4xx and creates nothing.

GET /api/ping No API key required

Health check. Use this to confirm the service is reachable before submitting orders.

curl — production

curl -v "https://api.digitalab.co.uk/api/ping"

curl — sandbox

curl -v "https://sandbox.api.digitalab.co.uk/api/ping"

On Windows PowerShell use curl.exe instead of curl:

curl.exe -v "https://api.digitalab.co.uk/api/ping"

Success response — 200

{
  "message": "CustomerOrders API is reachable",
  "status": 200,
  "utc": "2026-08-16T19:50:00.0000000Z",
  "sandbox": false
}

On the sandbox host, sandbox is true and the message notes that orders go to the sandbox order folder.

GET /api/me Requires x-api-key

Confirm the API key works. This is the first call to make after Digitalab issues a key. It returns your accountCode, rate limits, whether you can submit orders, and the status callback URL if one is registered. It does not return the full API key or the callback signing secret.

curl — production

curl -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/me"

curl — Windows PowerShell

curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/me"

curl — sandbox

curl.exe -H "x-api-key: YOUR-SANDBOX-API-KEY" "https://sandbox.api.digitalab.co.uk/api/me"

Success response — 200

{
  "responseId": "3f2a1c8e-9b14-4d2a-8c11-0a7e4b91d2aa",
  "message": "API key is valid",
  "status": 200,
  "utc": "2026-08-16T20:30:00.0000000Z",
  "accountCode": "YOURACCOUNT",
  "name": "Acme Photos",
  "maskedApiKey": "7c9e...0ae7",
  "usingPreviousKey": false,
  "canSubmit": true,
  "sandbox": false,
  "maxImagesPerOrder": 80,
  "maxImageBytes": 52428800,
  "clientIp": "203.0.113.10",
  "allowedIps": [],
  "ipRestricted": false,
  "statusCallbackUrl": "https://partner.example/webhooks/digitalab",
  "rateLimits": {
    "submitPerMinute": 20,
    "submitPerHour": 100,
    "catalogPerMinute": 60,
    "catalogPerHour": 600
  }
}

usingPreviousKey is true during a rotate overlap if you are still sending the old key. Switch to the new key before previousKeyValidUntilUtc. clientIp is the address Digitalab saw for this call. If ipRestricted is true, only IPs in allowedIps may use the key. statusCallbackUrl is the HTTPS URL Digitalab POSTs to when an order is accepted, completed, or failed (null if none is registered). Missing key returns 401. Invalid, revoked, or disallowed IP returns 403.

Product catalog

Read the products you can sell: groups, then SKUs with price, description, and available options. The catalog is filtered by your API key's pricing profile (assigned in LMIS). SKUs with price 0 are not listed. Use the option name and choice code values when you submit an order. JSON responses. Same x-api-key as submit.

GET /api/catalog/groups Requires x-api-key

List product groups (Prints, Photobooks, Photo boxes, …).

curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/catalog/groups"
GET /api/catalog/groups/{id}/products Requires x-api-key

Products in that group: sku, name, category (a sub-grouping within the group, e.g. “Elegance Frame” in Wall Art; may be empty), description, price, options, and sizes in inches: widthInches/heightInches (the finished product, e.g. a 16×16 frame) and imageWidthInches/imageHeightInches (the image you supply, e.g. 12×12 inside that frame, or a canvas including its wrap). Prepare each image at the image size and its aspect ratio. Photobooks, albums and boxes also have unitType, coverImage and a unitPrices table of the page, spread or print counts you can order: see Photobooks, albums and boxes.

curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/catalog/groups/photobooks/products"
GET /api/catalog/products/{sku} Requires x-api-key

One product by SKU.

curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/catalog/products/PHOTOBOOK_A4_20"
GET /api/catalog Requires x-api-key

Full catalog in one response: groups with nested products. Useful if you cache the list locally.

curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/catalog"

Option types

TypeUIOn submit
choice Dropdown. Use a values[].code. <Option Name="CoverColour">Navy</Option>
text Free text, honour maxLength. <Option Name="Personalisation">The Smith Family 2026</Option>

priceDelta on a choice value is an extra on top of the product price (for example leather cover +£8.00). Inactive products are omitted.

Sandbox: replace the host with https://sandbox.api.digitalab.co.uk.

Reference lists

Shipping methods and allowed image types. Use these instead of hardcoding RM48 or JPEG in your shop. Same x-api-key as submit. Cache the response; it changes rarely.

GET /api/reference Requires x-api-key

Both lists in one response.

curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/reference"

Success — 200

{
  "version": 1,
  "shippingMethods": [
    {
      "code": "RM48",
      "label": "Royal Mail 48",
      "description": "UK two-working-day.",
      "ukOnly": true,
      "active": true,
      "sortOrder": 2
    }
  ],
  "imageTypes": [
    {
      "code": "jpeg",
      "label": "JPEG",
      "extensions": [".jpg", ".jpeg"],
      "mimeTypes": ["image/jpeg"],
      "active": true,
      "sortOrder": 1
    }
  ]
}

Put shippingMethods[].code in OrderInfo.ShippingMethod. Image URLs must use an extension from imageTypes[].extensions (or omit the extension; Digitalab detects JPEG/TIFF/PDF from the file bytes). ukOnly is a hint for your checkout UI — the API still accepts COURIER for UK addresses.

GET /api/reference/shipping-methods Requires x-api-key

Shipping methods only: { "shippingMethods": [ … ] }.

curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/reference/shipping-methods"
GET /api/reference/image-types Requires x-api-key

Image types only: { "imageTypes": [ … ] }.

curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/reference/image-types"

Sandbox: replace the host with https://sandbox.api.digitalab.co.uk.

POST /api/customerorders/submit Requires x-api-key

Submit one order. The body must be XML with root element <Order>. Set Content-Type: application/xml.

Headers

HeaderRequiredValue
x-api-key Required The API key issued to your account
Content-Type Required application/xml

Request body — example

A complete UK print order. Optional elements are shown empty so you can see every field. See the Order XML schema for which fields are required, and more samples for international, photobook and photo box orders. Download this XML.

<?xml version="1.0" encoding="utf-8"?>
<Order>
  <OrderInfo>
    <CustomerOrderId>101027</CustomerOrderId>
    <UniqueOrderId>PHOTO-2026-004812</UniqueOrderId>
    <OrderDate>2026-07-02T20:06:15Z</OrderDate>
    <Reference>SCHOOL AUTUMN SITTING</Reference>
    <ShippingMethod>RM48</ShippingMethod>
    <AccountCode>YOURACCOUNT</AccountCode>
  </OrderInfo>
  <DeliveryAddress>
    <FirstName>Jane</FirstName>
    <LastName>Smith</LastName>
    <Email>jane.smith@example.com</Email>
    <Telephone>07700900123</Telephone>
    <ChildName>Alex</ChildName>
    <ClassName>Year 2</ClassName>
    <Address>
      <BusinessName></BusinessName>
      <Line1>10 High Street</Line1>
      <Line2></Line2>
      <Line3></Line3>
      <Town>Newcastle</Town>
      <County>Tyne and Wear</County>
      <StateProvince></StateProvince>
      <PostCode>NE1 1AA</PostCode>
      <Country>United Kingdom</Country>
      <CountryCode>GB</CountryCode>
    </Address>
  </DeliveryAddress>
  <OrderItems>
    <OrderItem>
      <LineId>1</LineId>
      <Sku>GP8X6</Sku>
      <Name>8" x 6" Print Only</Name>
      <Quantity>1</Quantity>
      <UnitPrice>10.00</UnitPrice>
      <PrintColourOption>Colour</PrintColourOption>
      <ImageUrls>
        <Image>
          <Url>https://picsum.photos/id/1015/600/800.jpg</Url>
        </Image>
      </ImageUrls>
    </OrderItem>
  </OrderItems>
  <Postage>
    <PostageCharge>4.95</PostageCharge>
  </Postage>
  <Metadata>
    <SourceSystem>CustomerPortal</SourceSystem>
    <SourceVersion>1.0</SourceVersion>
    <ExternalReference>PO-99821</ExternalReference>
    <Notes>Sample UK order</Notes>
    <CustomFields>
      <Field Name="EventId">10039</Field>
      <Field Name="PhotographerName">Example Photographer</Field>
    </CustomFields>
  </Metadata>
</Order>

Request body — multi-image products

Photobooks, albums and multi-aperture frames need several images for one product. Send them as one OrderItem with one <Image> per page or aperture, and give every image a <Sequence>: the page number, or the aperture position. Sequence is required here — on every image of an item with more than one image, and on every image of a multi-image product even if you send only one. See Multi-image products. Download this XML.

This order has a 4-page photobook with a printed cover (line 1) and two copies of a 3-aperture frame (line 2). The cover is marked <Role>cover</Role> and has no Sequence; the pages are numbered 1 to 4. Photobooks, albums and boxes can only be ordered with the page counts in their price table: see Photobooks, albums and boxes. Files can be JPEG, TIFF or PDF; each is named from its line and Sequence: 1_cover.jpg, 1_0001.jpg … 1_0004.jpg (page 3 is saved as 1_0003.pdf) and 2_0001.jpg, 2_0002.jpg, 2_0003.jpg. Quantity is copies of the whole product, never the number of images. The SKUs are examples: use SKUs from your catalog.

<?xml version="1.0" encoding="utf-8"?>
<Order>
  <OrderInfo>
    <CustomerOrderId>MULTI-1001</CustomerOrderId>
    <UniqueOrderId>PHOTO-2026-005020</UniqueOrderId>
    <OrderDate>2026-10-06T18:00:00Z</OrderDate>
    <Reference>FAMILY BOOK AND FRAME</Reference>
    <ShippingMethod>RM48</ShippingMethod>
    <AccountCode>YOURACCOUNT</AccountCode>
  </OrderInfo>
  <DeliveryAddress>
    <FirstName>Jane</FirstName>
    <LastName>Smith</LastName>
    <Email>jane.smith@example.com</Email>
    <Telephone>07700900123</Telephone>
    <Address>
      <Line1>10 High Street</Line1>
      <Town>Newcastle</Town>
      <PostCode>NE1 1AA</PostCode>
      <Country>United Kingdom</Country>
      <CountryCode>GB</CountryCode>
    </Address>
  </DeliveryAddress>
  <OrderItems>
    <!-- Photobook: one OrderItem, one Image per page plus the cover. Sequence = page number (required). -->
    <OrderItem>
      <LineId>1</LineId>
      <Sku>PHOTOBOOK_A4</Sku>
      <Name>A4 Photobook</Name>
      <Quantity>1</Quantity>
      <UnitPrice>29.95</UnitPrice>
      <PrintColourOption>Colour</PrintColourOption>
      <Options>
        <Option Name="CoverColour">Navy</Option>
        <Option Name="Personalisation">The Smith Family 2026</Option>
      </Options>
      <ImageUrls>
        <Image>
          <Url>https://cdn.example.com/orders/1001/cover.jpg</Url>
          <Role>cover</Role>
        </Image>
        <Image>
          <Url>https://cdn.example.com/orders/1001/page-01.jpg</Url>
          <Sequence>1</Sequence>
        </Image>
        <Image>
          <Url>https://cdn.example.com/orders/1001/page-02.jpg</Url>
          <Sequence>2</Sequence>
        </Image>
        <Image>
          <Url>https://cdn.example.com/orders/1001/page-03.pdf</Url>
          <Sequence>3</Sequence>
        </Image>
        <Image>
          <Url>https://cdn.example.com/orders/1001/page-04.jpg</Url>
          <Sequence>4</Sequence>
        </Image>
      </ImageUrls>
    </OrderItem>
    <!-- Multi-aperture frame: one Image per aperture. Sequence = aperture position (required). -->
    <OrderItem>
      <LineId>2</LineId>
      <Sku>GP10X8MULTI</Sku>
      <Name>10x8" Multi Print in Frame</Name>
      <Quantity>2</Quantity>
      <UnitPrice>24.95</UnitPrice>
      <PrintColourOption>Colour</PrintColourOption>
      <ImageUrls>
        <Image>
          <Url>https://cdn.example.com/orders/1001/frame-left.jpg</Url>
          <Sequence>1</Sequence>
        </Image>
        <Image>
          <Url>https://cdn.example.com/orders/1001/frame-centre.jpg</Url>
          <Sequence>2</Sequence>
        </Image>
        <Image>
          <Url>https://cdn.example.com/orders/1001/frame-right.jpg</Url>
          <Sequence>3</Sequence>
        </Image>
      </ImageUrls>
    </OrderItem>
  </OrderItems>
  <Postage>
    <PostageCharge>4.95</PostageCharge>
  </Postage>
</Order>

What happens on success

  1. The XML is parsed and validated.
  2. The API key is mapped to your AccountCode.
  3. Each file URL is checked without downloading the whole file: it must be reachable over HTTPS and contain a JPEG, TIFF or PDF within the size limit. Any problem returns 400 “File check failed” listing each failing file.
  4. Small orders (files totalling up to 5 MB) are downloaded straight away and the response says "orderStatus": "received".
  5. Larger orders — or small ones that take longer than a few seconds — are accepted immediately with "orderStatus": "pending" and their files are downloaded in the background.
  6. A JSON success body is returned, including a responseId you can quote in support and the saved name of every file in files.

Both received and pending mean the order is accepted — do not resubmit it. A pending order becomes received once its files are in, or failed (with the reason in statusDetail) if they still cannot be downloaded after several retries over about two hours. You find out through your status callback or order status lookup. An order that failed this way may be resubmitted with the same CustomerOrderId.

Keep every file URL working for at least 24 hours after submitting. Signed or expiring links that stop working before the background download runs will make the order fail.

On the sandbox host, orders are written to a separate sandbox area instead of the production order folder. The JSON response includes "sandbox": true.

ApiKey and ResponseId in the saved XML are set by the server. The stored ApiKey is masked (e.g. 7c9e...0ae7), not the full key. Any values you send for those fields, or for each image FileName, are ignored. Sequence and Copies on each image are kept (a single image on a single-image product defaults to Sequence 1; Copies defaults to 1).

curl — production

curl -v --max-time 120 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" \
  -H "Content-Type: application/xml" \
  -H "x-api-key: YOUR-API-KEY" \
  --data-binary "@customer-order-sample.xml"

curl — Windows PowerShell

curl.exe -v --max-time 120 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" -H "Content-Type: application/xml" -H "x-api-key: YOUR-API-KEY" --data-binary "@customer-order-sample.xml"
POST /api/customerorders/validate Requires x-api-key

Dry run. Send the same XML as /submit. The API runs the same authentication, XML, and field checks, then returns whether the order would be accepted. On production, duplicate-CustomerOrderId checks run as well; on sandbox they are skipped.

Every file URL is checked the same way as on submit, without downloading the file: the API requests only the first 1 KB to confirm the URL works, detect the real file type (JPEG, TIFF or PDF) and read the file size. Any problem returns 400 “File check failed”, listing each failing file. Nothing is written and no order is created.

The response lists each file with its saved name, detected type and sizeBytes (null if the host did not report a size), the totalBytes, and expectedOrderStatus: whether submit would most likely return received (small order, downloaded during the call) or pending (downloaded in the background). A 200 here can still fail on submit if a file URL stops working in between.

curl — production

curl -v --max-time 30 -X POST "https://api.digitalab.co.uk/api/customerorders/validate" \
  -H "Content-Type: application/xml" \
  -H "x-api-key: YOUR-API-KEY" \
  --data-binary "@customer-order-sample.xml"

curl — Windows PowerShell

curl.exe -v --max-time 30 -X POST "https://api.digitalab.co.uk/api/customerorders/validate" -H "Content-Type: application/xml" -H "x-api-key: YOUR-API-KEY" --data-binary "@customer-order-sample.xml"

Success response — 200

{
  "responseId": "3f2a1c8e-9b14-4d2a-8c11-0a7e4b91d2aa",
  "message": "Order is valid",
  "status": 200,
  "dryRun": true,
  "accountCode": "YOURACCOUNT",
  "customerOrderId": "101027",
  "uniqueOrderId": "PHOTO-2026-004812",
  "itemCount": 1,
  "imageCount": 3,
  "totalBytes": 412873,
  "expectedOrderStatus": "received",
  "plannedImageFiles": ["1_0001.jpg", "1_0002.jpg", "1_0003.pdf"],
  "files": [
    { "fileName": "1_0001.jpg", "url": "https://cdn.example.com/a.jpg", "type": "jpeg", "sizeBytes": 186943 },
    { "fileName": "1_0002.jpg", "url": "https://cdn.example.com/b.jpg", "type": "jpeg", "sizeBytes": 212666 },
    { "fileName": "1_0003.pdf", "url": "https://cdn.example.com/c.pdf", "type": "pdf",  "sizeBytes": 13264 }
  ],
  "items": [
    { "lineId": "1", "sku": "GP10X8", "quantity": 1, "unitType": null, "units": null, "price": 4.50, "lineTotal": 4.50 }
  ]
}

Failures use the same 4xx JSON as submit (errors[]). Quote responseId if you contact Digitalab.

GET /api/customerorders/{customerOrderId} Requires x-api-key

Look up an order you have already submitted. The lookup is scoped to the account on your API key — you cannot see another partner’s orders.

Status is updated by Digitalab as the order moves through the lab:

orderStatusMeaning
pendingAccepted; files are still being fetched.
receivedAll files received; waiting in the lab queue.
printingIn production.
dispatchedSent. carrier, trackingNumber and trackingUrl are filled in when available.
completedThe lab has finished the order.
failedThe order could not be completed. statusDetail explains why; contact Digitalab with the customerOrderId.
cancelledThe order was cancelled.

Treat unknown values as “in progress” — new statuses may be added. history lists every status change, oldest first.

curl — production

curl -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/customerorders/101027"

curl — Windows PowerShell

curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/customerorders/101027"

curl — sandbox

curl.exe -H "x-api-key: YOUR-SANDBOX-API-KEY" "https://sandbox.api.digitalab.co.uk/api/customerorders/101027"

Success response — 200

{
  "responseId": "3f2a1c8e-9b14-4d2a-8c11-0a7e4b91d2aa",
  "message": "Order found",
  "status": 200,
  "accountCode": "YOURACCOUNT",
  "customerOrderId": "101027",
  "uniqueOrderId": "PHOTO-2026-004812",
  "orderStatus": "dispatched",
  "statusDetail": null,
  "sandbox": false,
  "carrier": "Royal Mail",
  "trackingNumber": "AB123456789GB",
  "trackingUrl": null,
  "receivedUtc": "2026-08-16T19:50:00.0000000Z",
  "updatedUtc": "2026-08-18T09:12:41.0000000Z",
  "fileCount": 3,
  "history": [
    { "orderStatus": "pending",    "statusDetail": null, "utc": "2026-08-16T19:50:00.0000000Z" },
    { "orderStatus": "received",   "statusDetail": null, "utc": "2026-08-16T19:50:02.0000000Z" },
    { "orderStatus": "dispatched", "statusDetail": null, "utc": "2026-08-18T09:12:41.0000000Z" }
  ]
}

HTTP 200 means the lookup succeeded. A failed order still returns 200 with orderStatus: "failed". Unknown ids return 404. Sandbox orders are looked up with your sandbox key and return "sandbox": true. To avoid polling this endpoint, register a status callback URL (production only).

Status callback

Digitalab POSTs to your HTTPS URL when an order is accepted, completed, or failed. Register the URL with Digitalab at onboarding (or store it on the partner record). Leave it empty if you prefer to poll GET /api/customerorders/{customerOrderId}. Callbacks are not sent from the sandbox host.

received is POSTed once every file of the order is in: straight after submit for small orders, or when the background download finishes for orders submitted as pending. failed is POSTed if the files still cannot be downloaded after the retries (the reason is in the order status statusDetail), or if the lab cannot complete the order. Later lab updates — printing, dispatched, completed, cancelled — are POSTed as they happen. No callback is sent for pending; the submit response already tells you.

Digitalab POSTs once per order id + status. Failed deliveries are retried for a few hours. Treat the callback as idempotent, and if callbacks arrive out of order, trust GET /api/customerorders/{customerOrderId} for the latest status.

Request

POST your callback URL. Content-Type: application/json. No API key header — verify the signature instead.

HeaderValue
x-digitalab-event order.status
x-digitalab-signature sha256= followed by the lowercase hex HMAC-SHA256 of the raw JSON body, using the signing secret Digitalab showed once when the URL was set (or rotated).

Body

{
  "event": "order.status",
  "utc": "2026-08-16T20:45:00.0000000Z",
  "accountCode": "YOURACCOUNT",
  "customerOrderId": "101027",
  "uniqueOrderId": "PHOTO-2026-004812",
  "orderStatus": "completed",
  "location": "Done",
  "test": false
}

orderStatus is one of the order statuses (received, printing, dispatched, completed, failed, cancelled). location is ORDERS, Done, ERRORS, or LAB for other lab updates. Prefer orderStatus; location is kept for older integrations. A test callback from Digitalab uses customerOrderId: "TEST" and test: true — ignore it in your order pipeline.

Verify the signature

Compute HMAC-SHA256 over the exact request body bytes with your callback secret. Hex-encode the digest in lowercase and compare it to the value after sha256= using a constant-time comparison. Reject the POST if it does not match.

expected = HMAC_SHA256(secret, rawBody)
header   = "sha256=" + hex(expected).toLowerCase()

Respond with HTTP 2xx when you accept the event. Any other status is treated as a failure and retried.

GET /api/me returns statusCallbackUrl (not the secret) so you can confirm what Digitalab has stored.

Order XML schema

Root element is <Order>. Field names are case-sensitive.

OrderInfo

FieldNotes
CustomerOrderId Required Your unique order id. Letters, numbers, underscore or hyphen. Must start with a letter or number. Max 64 characters. Re-submitting the same id is rejected.
AccountCode Required Must match the account linked to your API key.
ShippingMethod Required Must be a code from GET /api/reference/shipping-methods (currently RM24, RM48, RM24TRACKED, RM48TRACKED, RMSPEC, COURIER, COLLECT).
UniqueOrderId Optional Secondary reference from your system.
OrderDate Optional ISO-8601 date/time recommended, e.g. 2026-07-02T20:06:15Z.
Reference Optional Job or sitting name.
ApiKey / ResponseId Server-set Do not send these. They are written when the order XML is generated. ApiKey is stored masked (first 4 and last 4 characters only).

DeliveryAddress

The recipient and delivery address. Older integrations may send this element as <Customer>; that name is still accepted.

FieldNotes
FirstNameRequired
LastNameRequired
EmailRequiredMust contain @.
TelephoneConditionalRequired when CountryCode is not GB.
ChildNameOptionalSchool / nursery orders.
ClassNameOptionalSchool / nursery orders.

DeliveryAddress.Address

FieldNotes
Line1RequiredFirst address line.
TownRequired
PostCodeRequired
Country or CountryCodeRequiredAt least one must be present.
CountryCodeRecommended2-letter ISO code, e.g. GB, DE, US. Used to decide if customs fields are required.
BusinessName, Line2, Line3, County, StateProvinceOptional

OrderItems / OrderItem

At least one OrderItem is required.

FieldNotes
SkuRequiredProduct SKU agreed with Digitalab.
QuantityRequiredCopies of the finished product (the whole print, photobook, or photo box). Integer greater than 0. Not the number of images.
ImageUrlsRequiredOne or more <Image> entries. A single print usually has one; a photobook has one per page; a photo box lists every distinct shot.
LineIdRecommendedUsed as the filename prefix. Must be unique per item. If omitted, the 1-based item index is used.
OptionsOptionalName/value configuration for this product (cover colour, paper, personalisation). See Product options.
Name, UnitPrice, PrintColourOptionOptionalPassed through to the saved XML. PrintColourOption remains for simple prints.

Image URLs

FieldNotes
UrlRequiredAbsolute HTTPS URL to a file whose extension is listed on GET /api/reference/image-types (currently .jpg, .jpeg, .tif, .tiff, .pdf), or no extension; the server detects JPEG/TIFF/PDF from the bytes.
SequenceConditionalPage, aperture or print position (1–9999), unique within the item. Required on every image when the item has more than one image, or when the product is a multi-image product (photobook, album, multi-aperture frame — maxImages above 1 in the catalog). Optional for a single image on a single-image product, where it defaults to 1.
RoleOptionalcover for the cover image of a photobook or album whose catalog entry has coverImage optional or required. A cover has no Sequence. Leave Role out (or use page, spread or print) for every other image. See Photobooks, albums and boxes.
CopiesOptionalHow many times this image is printed inside one unit of the product. Defaults to 1. Use this for a photo box (10 of shot A, 5 of shot B). Must be 1 on photobooks, albums and boxes priced by count.
FileNameServer-setDo not send this. Assigned after download as {LineId}_{Sequence:0000} plus .jpg, .tif or .pdf to match the file contents, e.g. 1_0001.jpg, 1_0002.pdf. A cover is saved as {LineId}_cover, e.g. 1_cover.jpg.
  • HTTP, relative URLs, and other image types are rejected.
  • Default maximum size is 50 MB per image.
  • Default maximum is 80 images per order. Pages, spreads and prints of photobooks, albums and boxes priced by count are not included in this limit; their price table sets how many they take.

Multi-image products

One OrderItem is one sellable product. Send every page or print for that product in that item’s ImageUrls list. Do not split a photobook into one line item per page.

ProductQuantityImage list
Single printCopies of that printOne URL. Sequence and Copies can be omitted (both default to 1).
Photobook / albumCopies of the whole bookOne URL per page (photobook) or finished spread (album), plus the cover if it has one. Sequence (required) is the page order. See Photobooks, albums and boxes.
Presentation box of printsCopies of the whole boxOne URL per print in the box. Sequence (required) is the print order. The box holds a fixed number of prints: see Photobooks, albums and boxes.
Multi-aperture frameCopies of the framed productOne URL per aperture. Sequence (required) is the aperture position, as shown for the product. Required even if you send only one image.
Box of photosCopies of the whole boxOne URL per distinct shot. Copies is how many times that shot is printed in the box.
Sequence is required on every image of an item that has more than one image, and on every image of a multi-image product (maxImages above 1 in GET /api/catalog). It tells the lab which page, aperture or position each image belongs to. Missing a Sequence returns 400, for example “OrderItems[0].ImageUrls[1].Sequence is required”. Sequences must be unique within the item; they do not have to be in XML order.
Quantity is never “number of images”. A book with 20 pages and Quantity 2 is two copies of the same book. A box with three shots (10 + 5 + 1 copies) and Quantity 1 is one box containing 16 prints.

Saved files are sortable by name: line 1 page 1 is 1_0001.jpg, page 2 is 1_0002.jpg. Indexes are always four digits. Give each item a unique LineId so prefixes do not clash.

Photobook — a cover and one image per page

<ImageUrls>
  <Image>
    <Url>https://cdn.example.com/cover.jpg</Url>
    <Role>cover</Role>
  </Image>
  <Image>
    <Url>https://cdn.example.com/page-01.jpg</Url>
    <Sequence>1</Sequence>
  </Image>
  <Image>
    <Url>https://cdn.example.com/page-02.jpg</Url>
    <Sequence>2</Sequence>
  </Image>
  <!-- … one Image per page, Sequence 1 to the page count … -->
</ImageUrls>

Photo box — Copies per shot

<ImageUrls>
  <Image>
    <Url>https://cdn.example.com/shot-a.jpg</Url>
    <Sequence>1</Sequence>
    <Copies>10</Copies>
  </Image>
  <Image>
    <Url>https://cdn.example.com/shot-b.jpg</Url>
    <Sequence>2</Sequence>
    <Copies>5</Copies>
  </Image>
  <Image>
    <Url>https://cdn.example.com/shot-c.jpg</Url>
    <Sequence>3</Sequence>
  </Image>
</ImageUrls>

Photobooks, albums and boxes

Some products are priced by how many pages, spreads or prints they have. Their catalog entry has a unitType and a unitPrices table, and only the counts in that table can be ordered.

unitTypeProductSend one image per
pagePhotobookSingle page, at the product's imageWidthInches × imageHeightInches.
spreadAlbumFinished spread (both facing pages as one image), at the product's image size — e.g. 16 × 8 in for an 8 × 8 album.
printPresentation boxPrint in the box. The box holds a fixed number of prints for a set price.

Catalog

{
  "sku": "SARA-ALBUM-8X8",
  "name": "Sara Album 8x8",
  "price": 73.90,
  "imageWidthInches": 16,
  "imageHeightInches": 8,
  "unitType": "spread",
  "coverImage": "none",
  "unitPrices": [
    { "units": 10, "price": 73.90 },
    { "units": 15, "price": 81.47 },
    { "units": 20, "price": 93.74 },
    { "units": 25, "price": 108.63 }
  ]
}
  • price is the lowest price in the table (a “from” price). Your pricing profile's discount is already applied to every row.
  • coverImage is none, optional or required. When a row has coverWidthInches and coverHeightInches, make the cover image that size: a photobook's printed cover gets wider as the spine thickens with more pages.

Ordering

  • Send one OrderItem per book or box. Quantity is copies of the whole book or box.
  • The number of images, not counting the cover, must be one of the units in the table. Any other count returns 400, e.g. “'SARA-ALBUM-8X8' can be ordered with 10, 15, 20 or 25 spreads; this item has 18.”
  • Number the pages, spreads or prints with Sequence 1 to the count, with no gaps. Copies must be 1.
  • Send the cover, if the product takes one, as an extra image with <Role>cover</Role> and no Sequence. A missing required cover, or a cover on a product without one, returns 400.
  • Validate and submit return each line's price in items: the table row for the count you sent.

Album — 10 finished spreads

<OrderItem>
  <LineId>1</LineId>
  <Sku>SARA-ALBUM-8X8</Sku>
  <Quantity>1</Quantity>
  <ImageUrls>
    <Image><Url>https://cdn.example.com/spread-01.jpg</Url><Sequence>1</Sequence></Image>
    <Image><Url>https://cdn.example.com/spread-02.jpg</Url><Sequence>2</Sequence></Image>
    <!-- … spreads 3 to 9 … -->
    <Image><Url>https://cdn.example.com/spread-10.jpg</Url><Sequence>10</Sequence></Image>
  </ImageUrls>
</OrderItem>

Price in the response

"items": [
  { "lineId": "1", "sku": "SARA-ALBUM-8X8", "quantity": 1, "unitType": "spread", "units": 10, "price": 73.90, "lineTotal": 73.90 },
  { "lineId": "2", "sku": "GP6X4", "quantity": 3, "unitType": null, "units": null, "price": 0.45, "lineTotal": 1.35 }
]

Product options

SKU names the product family. Options describes how to make this unit (cover colour, paper, foil text, and so on). Do not encode those choices in the SKU, and do not put them in order-level Metadata.CustomFields — two items in one order can have different covers.

Each option is <Option Name="CoverColour">Navy</Option>. Get the allowed names and values from the product catalog. Submit still stores unknown option names and passes them through to production.

RuleNotes
OptionalSimple prints can omit Options entirely.
NameRequired. Letters, numbers or underscore, starting with a letter. Max 64. Unique per item (case-insensitive).
ValueRequired. Max 200 characters.
CountAt most 30 options per item.

Suggested names (convention only):

NameTypical valuesUsed for
CoverColourNavy, Grey, BlushPhotobook cover
CoverMaterialLinen, Leather, PhotoPhotobook cover
PaperTypeSilk, Gloss, MattBook / print stock
FinishGloss, Lustre, MattLoose prints and boxes
PersonalisationFree textFoil or printed cover title
FoilColourGold, Silver, BlindCover foil
PrintSize6x4, 8x6Photo box / print packs
BoxColourWhite, KraftPhoto box
BorderNone, WhitePrints

Foil or cover wording is an Option. A photographic cover is still an Image with Sequence 1.

<Options>
  <Option Name="CoverColour">Navy</Option>
  <Option Name="CoverMaterial">Linen</Option>
  <Option Name="PaperType">Silk</Option>
  <Option Name="Personalisation">The Smith Family 2026</Option>
  <Option Name="FoilColour">Gold</Option>
</Options>

Postage, Shipping, Metadata

FieldNotes
Postage.PostageChargeOptionalInformational.
Shipping.DeliveryInstructionsOptional
Shipping.Customs.DeclaredValueConditionalRequired for international orders (CountryCode not GB). Must be a number greater than 0.
Shipping.Customs.*OptionalContentsType, ContentsDescription, Currency, HsTariffCode, OriginCountryCode, RecipientTaxId, EoriNumber.
MetadataOptionalSourceSystem, SourceVersion, ExternalReference, Notes, and CustomFields as <Field Name="...">value</Field>. Use CustomFields for order-level data (EventId, photographer). Product configuration belongs on the item’s Options.

Validation rules

If any rule fails, the API returns 400 with an errors array. No folder is created.

  • Body must be non-empty, well-formed XML that deserializes to Order.
  • OrderInfo, DeliveryAddress, DeliveryAddress.Address and at least one OrderItem are required.
  • CustomerOrderId must match ^[A-Za-z0-9][A-Za-z0-9_\-]{0,63}$.
  • The same CustomerOrderId must not already have been submitted for the account. The one exception: an order that failed because its files could not be downloaded may be resubmitted.
  • ShippingMethod must be a code from GET /api/reference/shipping-methods (case-insensitive; stored in uppercase).
  • Each item needs a SKU, quantity > 0, and at least one HTTPS image URL.
  • Quantity is copies of the finished product, not the image count.
  • LineId must produce a unique filename prefix per item. Recommended for multi-item orders.
  • Sequence on each image must be unique within that item (1–9999). It is required on every image when the item has more than one image or the product is multi-image (maxImages above 1); a single image on a single-image product defaults to 1.
  • Copies on each image defaults to 1 (repeats of that image inside one product unit).
  • An order may contain at most 80 images (configurable).
  • Each file URL must be reachable and point to a JPEG, TIFF or PDF no larger than 50 MB. This is checked on validate and submit by reading only the first 1 KB of each file; failures return 400 “File check failed”.
  • Options on an item are optional. If present, each needs a unique Name and a Value (max 200 characters; max 30 options per item).
  • International orders also need DeliveryAddress.Telephone and Shipping.Customs.DeclaredValue > 0.

Responses

Success — 200

{
  "responseId": "3f2a1c8e-9b14-4d2a-8c11-0a7e4b91d2aa",
  "message": "Order accepted",
  "status": 200,
  "sandbox": false,
  "accountCode": "YOURACCOUNT",
  "customerOrderId": "101027",
  "uniqueOrderId": "PHOTO-2026-004812",
  "orderStatus": "received",
  "orderFolder": "101027",
  "orderXmlFile": "order_101027_20260816195000.xml",
  "files": ["1_0001.jpg", "1_0002.jpg", "1_0003.pdf"],
  "downloadedImages": ["1_0001.jpg", "1_0002.jpg", "1_0003.pdf"],
  "items": [
    { "lineId": "1", "sku": "GP10X8", "quantity": 1, "unitType": null, "units": null, "price": 4.50, "lineTotal": 4.50 }
  ]
}

Success, files downloading — 200

{
  "responseId": "3f2a1c8e-9b14-4d2a-8c11-0a7e4b91d2aa",
  "message": "Order accepted — files are downloading",
  "status": 200,
  "sandbox": false,
  "accountCode": "YOURACCOUNT",
  "customerOrderId": "101028",
  "uniqueOrderId": "PHOTO-2026-004813",
  "orderStatus": "pending",
  "orderFolder": "101028",
  "orderXmlFile": "order_101028_20260816195500.xml",
  "files": ["1_0001.pdf", "2_0001.jpg"],
  "downloadedImages": [],
  "items": [
    { "lineId": "1", "sku": "GP10X8", "quantity": 1, "unitType": null, "units": null, "price": 4.50, "lineTotal": 4.50 }
  ]
}

files always lists every file the order will contain. downloadedImages lists the files already downloaded during the call (empty while pending). On sandbox submit, sandbox is true.

File check failed — 400

{
  "responseId": "3f2a1c8e-9b14-4d2a-8c11-0a7e4b91d2aa",
  "message": "File check failed",
  "status": 400,
  "title": "Error 400 - Bad Request",
  "errors": [
    "File 2 (https://cdn.example.com/missing.pdf): HTTP 404 from the file URL."
  ]
}

Who am I — 200

{
  "responseId": "3f2a1c8e-9b14-4d2a-8c11-0a7e4b91d2aa",
  "message": "API key is valid",
  "status": 200,
  "utc": "2026-08-16T20:30:00.0000000Z",
  "accountCode": "YOURACCOUNT",
  "maskedApiKey": "7c9e...0ae7",
  "usingPreviousKey": false,
  "canSubmit": true,
  "maxImagesPerOrder": 80,
  "maxImageBytes": 52428800,
  "clientIp": "203.0.113.10",
  "allowedIps": [],
  "ipRestricted": false,
  "statusCallbackUrl": "https://partner.example/webhooks/digitalab",
  "rateLimits": {
    "submitPerMinute": 20,
    "submitPerHour": 100,
    "catalogPerMinute": 60,
    "catalogPerHour": 600
  }
}

Validate (dry run) — 200

{
  "responseId": "3f2a1c8e-9b14-4d2a-8c11-0a7e4b91d2aa",
  "message": "Order is valid",
  "status": 200,
  "dryRun": true,
  "accountCode": "YOURACCOUNT",
  "customerOrderId": "101027",
  "uniqueOrderId": "PHOTO-2026-004812",
  "itemCount": 1,
  "imageCount": 3,
  "totalBytes": 412873,
  "expectedOrderStatus": "received",
  "plannedImageFiles": ["1_0001.jpg", "1_0002.jpg", "1_0003.pdf"],
  "files": [
    { "fileName": "1_0001.jpg", "url": "https://cdn.example.com/a.jpg", "type": "jpeg", "sizeBytes": 186943 },
    { "fileName": "1_0002.jpg", "url": "https://cdn.example.com/b.jpg", "type": "jpeg", "sizeBytes": 212666 },
    { "fileName": "1_0003.pdf", "url": "https://cdn.example.com/c.pdf", "type": "pdf",  "sizeBytes": 13264 }
  ],
  "items": [
    { "lineId": "1", "sku": "GP10X8", "quantity": 1, "unitType": null, "units": null, "price": 4.50, "lineTotal": 4.50 }
  ]
}

dryRun is always true. File URLs are checked (first 1 KB only), never downloaded, and no folder is created.

Order status — 200

{
  "responseId": "3f2a1c8e-9b14-4d2a-8c11-0a7e4b91d2aa",
  "message": "Order found",
  "status": 200,
  "accountCode": "YOURACCOUNT",
  "customerOrderId": "101027",
  "uniqueOrderId": "PHOTO-2026-004812",
  "orderStatus": "received",
  "statusDetail": null,
  "sandbox": false,
  "carrier": null,
  "trackingNumber": null,
  "trackingUrl": null,
  "receivedUtc": "2026-08-16T19:50:00.0000000Z",
  "updatedUtc": "2026-08-16T19:50:02.0000000Z",
  "fileCount": 3,
  "history": [
    { "orderStatus": "pending",  "statusDetail": null, "utc": "2026-08-16T19:50:00.0000000Z" },
    { "orderStatus": "received", "statusDetail": null, "utc": "2026-08-16T19:50:02.0000000Z" }
  ]
}

orderStatus is one of pending, received, printing, dispatched, completed, failed or cancelled. See Order status.

Validation failure — 400

{
  "responseId": "3f2a1c8e-9b14-4d2a-8c11-0a7e4b91d2aa",
  "status": 400,
  "message": "Order validation failed",
  "title": "Error 400 - Bad Request",
  "errors": [
    "OrderInfo.ShippingMethod must be one of: RM24, RM48, RM24TRACKED, RM48TRACKED, RMSPEC, COURIER, COLLECT.",
    "OrderItems[0].ImageUrls[0].Url must be an absolute HTTPS URL."
  ]
}

Quote responseId if you contact Digitalab about a failed or accepted order.

curl examples

On Windows PowerShell, use curl.exe (not curl, which is an alias for Invoke-WebRequest). Do not use ^ for line continuation. Use one line, or ` as the PowerShell continuation character. On Linux and macOS, curl is fine and you can break lines with \.

1. Health check

curl -v "https://api.digitalab.co.uk/api/ping"
curl.exe -v "https://api.digitalab.co.uk/api/ping"

1a. Who am I

curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/me"
curl.exe -H "x-api-key: YOUR-SANDBOX-API-KEY" "https://sandbox.api.digitalab.co.uk/api/me"

1b. Product catalog

curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/catalog/groups"
curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/catalog/groups/photobooks/products"
curl.exe -H "x-api-key: YOUR-SANDBOX-API-KEY" "https://sandbox.api.digitalab.co.uk/api/catalog"

1b2. Reference lists

curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/reference"
curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/reference/shipping-methods"
curl.exe -H "x-api-key: YOUR-SANDBOX-API-KEY" "https://sandbox.api.digitalab.co.uk/api/reference/image-types"

1c. Order status

curl.exe -H "x-api-key: YOUR-API-KEY" "https://api.digitalab.co.uk/api/customerorders/101027"
curl.exe -H "x-api-key: YOUR-SANDBOX-API-KEY" "https://sandbox.api.digitalab.co.uk/api/customerorders/101027"

1d. Validate (dry run)

curl.exe -v --max-time 30 -X POST "https://api.digitalab.co.uk/api/customerorders/validate" -H "Content-Type: application/xml" -H "x-api-key: YOUR-API-KEY" --data-binary "@customer-order-sample.xml"
curl.exe -v --max-time 30 -X POST "https://sandbox.api.digitalab.co.uk/api/customerorders/validate" -H "Content-Type: application/xml" -H "x-api-key: YOUR-SANDBOX-API-KEY" --data-binary "@customer-order-sample.xml"

2. Submit a UK order (production)

curl -v --max-time 120 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" \
  -H "Content-Type: application/xml" \
  -H "x-api-key: YOUR-API-KEY" \
  --data-binary "@customer-order-sample.xml"
curl.exe -v --max-time 120 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" -H "Content-Type: application/xml" -H "x-api-key: YOUR-API-KEY" --data-binary "@customer-order-sample.xml"

3. Submit an international order

curl -v --max-time 120 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" \
  -H "Content-Type: application/xml" \
  -H "x-api-key: YOUR-API-KEY" \
  --data-binary "@customer-order-international.xml"
curl.exe -v --max-time 120 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" -H "Content-Type: application/xml" -H "x-api-key: YOUR-API-KEY" --data-binary "@customer-order-international.xml"

4. Submit a photobook (several page images)

curl -v --max-time 300 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" \
  -H "Content-Type: application/xml" \
  -H "x-api-key: YOUR-API-KEY" \
  --data-binary "@customer-order-photobook.xml"
curl.exe -v --max-time 300 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" -H "Content-Type: application/xml" -H "x-api-key: YOUR-API-KEY" --data-binary "@customer-order-photobook.xml"

5. Submit a photo box

curl -v --max-time 300 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" \
  -H "Content-Type: application/xml" \
  -H "x-api-key: YOUR-API-KEY" \
  --data-binary "@customer-order-photobox.xml"
curl.exe -v --max-time 300 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" -H "Content-Type: application/xml" -H "x-api-key: YOUR-API-KEY" --data-binary "@customer-order-photobox.xml"

6. Sandbox (integration — saved to the sandbox area, not production)

curl -v --max-time 120 -X POST "https://sandbox.api.digitalab.co.uk/api/customerorders/submit" \
  -H "Content-Type: application/xml" \
  -H "x-api-key: YOUR-SANDBOX-API-KEY" \
  --data-binary "@customer-order-sample.xml"
curl.exe -v --max-time 120 -X POST "https://sandbox.api.digitalab.co.uk/api/customerorders/submit" -H "Content-Type: application/xml" -H "x-api-key: YOUR-SANDBOX-API-KEY" --data-binary "@customer-order-sample.xml"

7. Missing API key (expect 401)

curl -v --max-time 30 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" \
  -H "Content-Type: application/xml" \
  --data-binary "@customer-order-sample.xml"
curl.exe -v --max-time 30 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" -H "Content-Type: application/xml" --data-binary "@customer-order-sample.xml"

8. Invalid API key (expect 403)

curl -v --max-time 30 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" \
  -H "Content-Type: application/xml" \
  -H "x-api-key: wrong-key" \
  --data-binary "@customer-order-sample.xml"
curl.exe -v --max-time 30 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" -H "Content-Type: application/xml" -H "x-api-key: wrong-key" --data-binary "@customer-order-sample.xml"

9. Inline XML (no file)

curl -v --max-time 120 -X POST "https://api.digitalab.co.uk/api/customerorders/submit" \
  -H "Content-Type: application/xml" \
  -H "x-api-key: YOUR-API-KEY" \
  --data-binary @- <<'EOF'
<?xml version="1.0" encoding="utf-8"?>
<Order>
  <OrderInfo>
    <CustomerOrderId>101027</CustomerOrderId>
    <ShippingMethod>RM48</ShippingMethod>
    <AccountCode>YOURACCOUNT</AccountCode>
  </OrderInfo>
  <DeliveryAddress>
    <FirstName>Jane</FirstName>
    <LastName>Smith</LastName>
    <Email>jane.smith@example.com</Email>
    <Address>
      <Line1>10 High Street</Line1>
      <Town>Newcastle</Town>
      <PostCode>NE1 1AA</PostCode>
      <CountryCode>GB</CountryCode>
    </Address>
  </DeliveryAddress>
  <OrderItems>
    <OrderItem>
      <Sku>GP8X6</Sku>
      <Quantity>1</Quantity>
      <ImageUrls>
        <Image>
          <Url>https://picsum.photos/id/1015/600/800.jpg</Url>
        </Image>
      </ImageUrls>
    </OrderItem>
  </OrderItems>
</Order>
EOF

Photobook sample XML

<?xml version="1.0" encoding="utf-8"?>
<Order>
  <OrderInfo>
    <CustomerOrderId>PB-1001</CustomerOrderId>
    <ShippingMethod>RM48</ShippingMethod>
    <AccountCode>YOURACCOUNT</AccountCode>
  </OrderInfo>
  <DeliveryAddress>
    <FirstName>Jane</FirstName>
    <LastName>Smith</LastName>
    <Email>jane.smith@example.com</Email>
    <Address>
      <Line1>10 High Street</Line1>
      <Town>Newcastle</Town>
      <PostCode>NE1 1AA</PostCode>
      <CountryCode>GB</CountryCode>
    </Address>
  </DeliveryAddress>
  <OrderItems>
    <OrderItem>
      <LineId>1</LineId>
      <Sku>PHOTOBOOK_A4_20</Sku>
      <Name>A4 Photobook</Name>
      <Quantity>1</Quantity>
      <Options>
        <Option Name="CoverColour">Navy</Option>
        <Option Name="CoverMaterial">Linen</Option>
        <Option Name="PaperType">Silk</Option>
        <Option Name="Personalisation">The Smith Family 2026</Option>
        <Option Name="FoilColour">Gold</Option>
      </Options>
      <ImageUrls>
        <Image>
          <Url>https://picsum.photos/id/1015/600/800.jpg</Url>
          <Sequence>1</Sequence>
        </Image>
        <Image>
          <Url>https://picsum.photos/id/1016/600/800.jpg</Url>
          <Sequence>2</Sequence>
        </Image>
        <Image>
          <Url>https://picsum.photos/id/1018/600/800.jpg</Url>
          <Sequence>3</Sequence>
        </Image>
      </ImageUrls>
    </OrderItem>
  </OrderItems>
</Order>

Photo box sample XML

<?xml version="1.0" encoding="utf-8"?>
<Order>
  <OrderInfo>
    <CustomerOrderId>BOX-1001</CustomerOrderId>
    <ShippingMethod>RM48</ShippingMethod>
    <AccountCode>YOURACCOUNT</AccountCode>
  </OrderInfo>
  <DeliveryAddress>
    <FirstName>Jane</FirstName>
    <LastName>Smith</LastName>
    <Email>jane.smith@example.com</Email>
    <Address>
      <Line1>10 High Street</Line1>
      <Town>Newcastle</Town>
      <PostCode>NE1 1AA</PostCode>
      <CountryCode>GB</CountryCode>
    </Address>
  </DeliveryAddress>
  <OrderItems>
    <OrderItem>
      <LineId>1</LineId>
      <Sku>PHOTOBOX_50</Sku>
      <Name>Box of photos</Name>
      <Quantity>1</Quantity>
      <Options>
        <Option Name="PrintSize">6x4</Option>
        <Option Name="Finish">Lustre</Option>
        <Option Name="BoxColour">White</Option>
      </Options>
      <ImageUrls>
        <Image>
          <Url>https://picsum.photos/id/1015/600/800.jpg</Url>
          <Sequence>1</Sequence>
          <Copies>10</Copies>
        </Image>
        <Image>
          <Url>https://picsum.photos/id/1016/600/800.jpg</Url>
          <Sequence>2</Sequence>
          <Copies>5</Copies>
        </Image>
        <Image>
          <Url>https://picsum.photos/id/1018/600/800.jpg</Url>
          <Sequence>3</Sequence>
        </Image>
      </ImageUrls>
    </OrderItem>
  </OrderItems>
</Order>

UK print sample XML

<?xml version="1.0" encoding="utf-8"?>
<Order>
  <OrderInfo>
    <CustomerOrderId>101027</CustomerOrderId>
    <UniqueOrderId>PHOTO-2026-004812</UniqueOrderId>
    <OrderDate>2026-07-02T20:06:15Z</OrderDate>
    <Reference>SCHOOL AUTUMN SITTING</Reference>
    <ShippingMethod>RM48</ShippingMethod>
    <AccountCode>YOURACCOUNT</AccountCode>
  </OrderInfo>
  <DeliveryAddress>
    <FirstName>Jane</FirstName>
    <LastName>Smith</LastName>
    <Email>jane.smith@example.com</Email>
    <Telephone>07700900123</Telephone>
    <ChildName>Alex</ChildName>
    <ClassName>Year 2</ClassName>
    <Address>
      <Line1>10 High Street</Line1>
      <Town>Newcastle</Town>
      <PostCode>NE1 1AA</PostCode>
      <Country>United Kingdom</Country>
      <CountryCode>GB</CountryCode>
    </Address>
  </DeliveryAddress>
  <OrderItems>
    <OrderItem>
      <LineId>1</LineId>
      <Sku>GP8X6</Sku>
      <Name>8" x 6" Print Only</Name>
      <Quantity>1</Quantity>
      <UnitPrice>10.00</UnitPrice>
      <PrintColourOption>Colour</PrintColourOption>
      <ImageUrls>
        <Image>
          <Url>https://picsum.photos/id/1015/600/800.jpg</Url>
        </Image>
      </ImageUrls>
    </OrderItem>
  </OrderItems>
  <Postage>
    <PostageCharge>4.95</PostageCharge>
  </Postage>
</Order>

C# example

using var client = new HttpClient();
using var request = new HttpRequestMessage(
    HttpMethod.Post,
    "https://api.digitalab.co.uk/api/customerorders/submit");

request.Headers.Add("x-api-key", "YOUR-API-KEY");
request.Content = new StringContent(xml, Encoding.UTF8, "application/xml");

using var response = await client.SendAsync(request);
string body = await response.Content.ReadAsStringAsync();
Console.WriteLine((int)response.StatusCode);
Console.WriteLine(body);

HTTP status codes

StatusWhen
200Order accepted, catalog returned, order validated (dry run), or order status found. For status lookups, check orderStatus in the JSON — failed still returns HTTP 200.
400Invalid XML, failed validation, a file URL that is unreachable or not a JPEG/TIFF/PDF (“File check failed”), duplicate CustomerOrderId, missing required fields, or an invalid order id on a status lookup.
401x-api-key header is missing.
403API key is invalid, AccountCode does not match the key, the client IP is not on the account’s allow-list, or the client is blocked after repeated rate-limit violations.
404No order with that CustomerOrderId for this API key.
429Too many requests. Wait for Retry-After seconds (also in the JSON retryAfterSeconds field) and retry.
500Server error (for example the account is misconfigured, or an image download failed after validation). The API attempts to delete a partially created folder.

Rate limits

Catalog, order-status, validate, and submit calls are limited per API key (or per IP if no key is sent). Ping is not limited. Digitalab can raise a partner’s submit cap in their API client config.

EndpointDefaultWhy
POST /api/customerorders/submit20 / minute and 100 / hourEach submit downloads images. A school sitting or checkout burst fits; a tight retry loop does not.
GET /api/catalog…, GET /api/reference…, GET /api/me, GET /api/customerorders/{id}, and POST /api/customerorders/validate60 / minute and 600 / hourShared budget for cheap reads and dry-runs. Enough for a product picker, status polling, and shop wiring; stops scraping.

After 5 rate-limit responses in 15 minutes the key (or IP) is blocked and further calls return 403 until Digitalab unblocks it. A 429 includes Retry-After. A block does not expire on its own unless Digitalab sets AutoUnblockMinutes.

{
  "message": "Rate limit exceeded (20 minute). Try again in 12 seconds.",
  "status": 429,
  "title": "Error 429 - Too Many Requests",
  "retryAfterSeconds": 12,
  "limit": 20,
  "window": "minute"
}

Notes

  • After you receive an API key, call GET /api/me first. A 200 with your accountCode means the key works. The response includes clientIp — use that if Digitalab restricts the key to office addresses.
  • Shipping methods and allowed image types come from GET /api/reference. Do not hardcode RM48 or JPEG extensions in your shop.
  • Image hosts must be publicly reachable over HTTPS from the Digitalab server.
  • Downloaded files are named {LineId}_{Sequence:0000} with .jpg, .tif or .pdf to match the file contents (four-digit indexes: 1_0001.jpg, 1_0002.pdf).
  • Product configuration (cover colour, paper, personalisation) goes on the item’s Options, not in order-level CustomFields. Allowed values come from GET /api/catalog.
  • Use a new CustomerOrderId for every order. The same id cannot be reused, except after a failed file download. A submit that returns pending is accepted — do not resubmit it. Register a status callback URL to be notified as the order moves through received, dispatched, completed or failed, or poll GET /api/customerorders/{customerOrderId}.
  • Wire your shop against the sandbox base URL first. POST /api/customerorders/submit there runs full validation and file downloads, but orders go to a separate sandbox area and never reach production. Use POST /api/customerorders/validate for a faster dry run: it checks every file URL without downloading the files.
  • Replace YOUR-API-KEY and YOURACCOUNT with the values Digitalab issued to you.