# BlotPrints API

All our products can be ordered through our API. Made for developers and for AI agents.

## Introduction

The base URL is:

```
https://www.blotprints.com/api/v1
```

- Requests and responses are JSON (`Content-Type: application/json`).
- No API key is needed. Calls are rate limited per IP address (see [Rate limits](#rate-limits)).
- Dates and times are in ISO 8601, in UTC, like `2026-10-25T12:00:00Z`.
- There's no test mode: a quote is free and changes nothing, and an order only becomes real once it's paid.

## Example order

Ordering a hardcover book from PDFs that are online.

**1. Get a quote**

```bash
curl -X POST https://www.blotprints.com/api/v1/quotes \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{
      "product": "hardcover",
      "size": "a5-portrait",
      "options": { "cover": "matte", "paper": "matte_standard" },
      "quantity": 1,
      "files": {
        "interior": { "url": "https://example.com/book-interior.pdf" },
        "cover": { "url": "https://example.com/book-cover.pdf" }
      }
    }],
    "shipping": { "country": "NL", "method": "standard" }
  }'
```

**2. Place the order**, with the `quoteId` from the response. If the quote has warnings, add `"acceptWarnings": true` (see [3. Get a quote](#3-get-a-quote)).

```bash
curl -X POST https://www.blotprints.com/api/v1/orders \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f1c2d3e-my-unique-key" \
  -d '{
    "quoteId": "quo_8hQ2kV7xLm3pR9tY",
    "acceptTerms": true,
    "email": "jane@example.com",
    "shippingAddress": {
      "name": "Jane Doe",
      "line1": "Keizersgracht 123",
      "postalCode": "1015 CJ",
      "city": "Amsterdam",
      "country": "NL"
    }
  }'
```

**3. Complete payment**: open the `paymentUrl` from the response in a browser.

# Placing an order

## 1. Choose a product

The products tell you what you can order: per product its sizes, options and prices, and the files we need. Use the names from here (`product`, `size`, the option values) in your [quote](#3-get-a-quote).

### All products

```
GET https://www.blotprints.com/api/v1/products
```

Everything you can order, with its sizes, options and prices.

```json
{
  "products": [
    {
      "product": "hardcover",
      "name": "Hardcover book",
      "sizes": [
        {
          "size": "a5-portrait",
          "width": 148,
          "height": 210,
          "pages": { "min": 24, "max": 400, "step": 2 },
          "files": {
            "interior": { "fileTypes": ["pdf"], "width": 154, "height": 216, "bleed": 3 },
            "cover": { "fileTypes": ["pdf", "jpg", "png"], "width": 354, "height": 255, "bleed": 19 }
          },
          "price": { "base": 15.99, "perExtraPage": 0.40 },
          "options": {
            "cover": {
              "values": [
                { "value": "matte", "name": "Hardcover matte", "price": 5.99 },
                { "value": "glossy", "name": "Hardcover glossy", "price": 0 }
              ],
              "default": "matte"
            },
            "paper": {
              "values": [
                { "value": "matte_standard", "name": "Standard matte", "price": 0, "perExtraPage": 0 },
                { "value": "matte_heavy", "name": "Heavy matte", "price": 1.44, "perExtraPage": 0.07 }
              ],
              "default": "matte_standard"
            }
          }
        },
        {
          "size": "a5-landscape",
          "width": 210,
          "height": 148,
          "pages": { "min": 24, "max": 400, "step": 2 },
          "files": {
            "interior": { "fileTypes": ["pdf"], "width": 216, "height": 154, "bleed": 3 },
            "cover": { "fileTypes": ["pdf", "jpg", "png"], "width": 470, "height": 193, "bleed": 19 }
          },
          "price": { "base": 15.99, "perExtraPage": 0.40 },
          "options": {
            "cover": {
              "values": [
                { "value": "matte", "name": "Hardcover matte", "price": 5.99 },
                { "value": "glossy", "name": "Hardcover glossy", "price": 0 }
              ],
              "default": "matte"
            },
            "paper": {
              "values": [
                { "value": "matte_standard", "name": "Standard matte", "price": 0, "perExtraPage": 0 },
                { "value": "matte_heavy", "name": "Heavy matte", "price": 1.44, "perExtraPage": 0.07 }
              ],
              "default": "matte_standard"
            }
          }
        }
      ]
    }
  ]
}
```

| Field | Description |
|---|---|
| `product` | The name to order with, like `hardcover`. |
| `name` | The product's name, to show to a person. |
| `sizes[].size` | The name to order with. Portrait and landscape are separate sizes: `a5-portrait` and `a5-landscape`, `30x40` and `40x30`. A square size has one, like `21x21`. |
| `sizes[].width` | The finished width, in millimetres. |
| `sizes[].height` | The finished height, in millimetres. |
| `sizes[].pages` | Only for products with pages (books, magazines, calendars): the fewest and most pages, and the step (`2` means an even number). |
| `sizes[].files` | The files to send, by name, with their types and their size in millimetres including the bleed. A book has an `interior` and usually a `cover`, a poster or panel has one `design`. |
| `sizes[].price.base` | The price of the size, without options. For products with pages, with the fewest pages. |
| `sizes[].price.perExtraPage` | Only for products with pages: what each page above the fewest adds. |
| `sizes[].options` | The options of the size (like `cover`, `paper` or `hanging`): its values, and which one is the `default`. Sizes can differ: an A0 poster has no frame, and prices depend on the size. |
| `sizes[].options.{option}.values[].value` | The value to order with, like `matte`. |
| `sizes[].options.{option}.values[].name` | The value's name, to show to a person. |
| `sizes[].options.{option}.values[].price` | What the value adds to the price; `0` when it's included. |
| `sizes[].options.{option}.values[].perExtraPage` | Only for values that cost more per page (a book's paper): what they add for each page above the fewest. |

### One product

```
GET https://www.blotprints.com/api/v1/products/{product}
```

The same as one product in the list.

## 2. Add your files

Your files are what we print: the design of a poster or panel, or a book's pages and cover, as PDF, JPG or PNG. Which files a size needs is in its `files` (see [1. Choose a product](#1-choose-a-product)). You can send each file in two ways: as a link we download it from, or uploaded to us.

### Download link

Give a URL we can download, with `https`. Handy for files that are already online.

```json
{ "url": "https://example.com/book-interior.pdf" }
```

The link has to work, without logging in, when you get the quote: that's when we download the file. Links that expire (like presigned storage links) are fine.

### Upload a file

For files on your computer (or an agent's), ask for an upload address and send the file there.

```
POST https://www.blotprints.com/api/v1/uploads
```

No body is needed. Response:

```json
{
  "uploadId": "upl_3vN8qR2xKt5mW7yZ",
  "uploadUrl": "https://…",
  "expiresAt": "2026-10-25T14:30:00Z"
}
```

| Field | Description |
|---|---|
| `uploadId` | The upload, to use in your quote once the file is sent. |
| `uploadUrl` | Where to send the file, with a `PUT`. |
| `expiresAt` | Until when `uploadUrl` works (an hour). |

Then send the file:

```bash
curl -T book-interior.pdf "<uploadUrl>"
```

And use the upload in your quote:

```json
{ "uploadId": "upl_3vN8qR2xKt5mW7yZ" }
```

- File types: PDF, JPG and PNG, as the product allows. We go by what's in the file, not its name or extension: anything else is refused (`unsupported_file_type`).
- Up to 200 MB per file.
- An upload we refuse (another type, or too large) is deleted right away: upload the right file again.
- An upload address works for an hour; an upload is kept for a day.

## 3. Get a quote

A quote is the step before an order: you send what you want to print, with the files, and we check the files and tell you what it costs, including shipping. Nothing is ordered or paid yet. If it looks good, you [place the order](#4-place-the-order) with the quote.

```
POST https://www.blotprints.com/api/v1/quotes
```

```json
{
  "items": [
    {
      "product": "poster",
      "size": "a3-landscape",
      "options": { "paper": "matte", "hanging": "photo_frame_black" },
      "quantity": 2,
      "files": { "design": { "uploadId": "upl_3vN8qR2xKt5mW7yZ" } }
    }
  ],
  "shipping": { "country": "NL", "method": "standard" }
}
```

| Field | Description |
|---|---|
| `items` | **Required.** What to print: one or more items, all in one order. |
| `items[].product` | **Required.** The product, like `hardcover` (see [1. Choose a product](#1-choose-a-product)). |
| `items[].size` | **Required.** The size, like `a5-portrait` or `40x30`. |
| `items[].options` | Optional. Per option of the product a value, like `{ "cover": "matte" }`. An option you leave out gets its `default`. |
| `items[].quantity` | Optional. 1 to 50, the default is 1. |
| `items[].files` | **Required.** The files, by name, like `interior` and `cover`, or `design`: the ones in `files` of the size in [the product](#all-products). |
| `items[].files.{name}.url` | A link to the file. Give this or `uploadId`: see [2. Add your files](#2-add-your-files). |
| `items[].files.{name}.uploadId` | An upload of the file. Give this or `url`: see [2. Add your files](#2-add-your-files). |
| `shipping` | **Required.** Where and how to ship. |
| `shipping.country` | **Required.** Any EU country, as its two-letter code, like `NL`, `DE` or `IT`. |
| `shipping.method` | Optional. `standard` (the default) or `express`. Express isn't available for every product: then the quote ships standard, and says so in `shipping.note`. |

Response:

```json
{
  "quoteId": "quo_8hQ2kV7xLm3pR9tY",
  "expiresAt": "2026-10-26T12:00:00Z",
  "previewUrl": "https://www.blotprints.com/preview/quo_8hQ2kV7xLm3pR9tY",
  "items": [
    {
      "product": "poster",
      "size": "a3-landscape",
      "options": { "paper": "matte", "hanging": "photo_frame_black" },
      "quantity": 2,
      "unitPrice": 25.98,
      "price": 51.96,
      "vat": 9.02,
      "warnings": [
        {
          "code": "design_adjusted",
          "file": "design",
          "message": "The design is a different size or shape than the size needs. We zoom it in to fit, so 9.1% is cut off at the edges. Check the preview.",
          "values": { "cutOffPercent": 9.1 }
        }
      ],
      "previews": [
        {
          "file": "design",
          "url": "https://www.blotprints.com/api/v1/quotes/quo_8hQ2kV7xLm3pR9tY/previews/0-design.jpg",
          "width": 1200,
          "height": 853,
          "description": "The design as it's printed. The red line is where it's cut."
        }
      ]
    }
  ],
  "shipping": {
    "method": "standard",
    "price": 5.00,
    "vat": 0.87,
    "estimatedDelivery": { "from": "2026-10-28", "to": "2026-10-29" },
    "note": null
  },
  "shippingOptions": [
    {
      "method": "standard",
      "price": 5.00,
      "vat": 0.87,
      "estimatedDelivery": { "from": "2026-10-28", "to": "2026-10-29" }
    },
    {
      "method": "express",
      "price": 12.50,
      "vat": 2.17,
      "estimatedDelivery": { "from": "2026-10-27", "to": "2026-10-27" }
    }
  ],
  "total": 56.96,
  "vat": 9.89,
  "currency": "EUR",
  "termsUrl": "https://www.blotprints.com/terms"
}
```

| Field | Description |
|---|---|
| `quoteId` | The quote, to [place the order](#4-place-the-order) with. |
| `expiresAt` | Until when the quote can be ordered (24 hours). |
| `previewUrl` | A page that shows the files as we print them, the same preview as on our website: where it's cut, the spine of a book, every page. Show it to the person before ordering. It's in their language, and only whoever has the link can see it. |
| `items[].pageCount` | Only for products with pages: the pages we counted in your file, which the price is based on. |
| `items[].unitPrice` | The price of one piece, with its options. |
| `items[].price` | The price of all pieces of the item. |
| `items[].vat` | The VAT included in the item's `price`. |
| `items[].warnings` | Things we'll fix while printing, that you should know about, like a design that's a different shape and gets cut off a bit. Empty when there are none. To order anyway, set `acceptWarnings` when you place the order. |
| `items[].warnings[].code` | What kind of warning, like `design_adjusted`, `pages_adjusted` or `cover_adjusted` (a file of another size, zoomed in to fit), or `low_resolution` (an image that may print blurry). There are more: show the `message` to the person. |
| `items[].warnings[].file` | Which file of the item it's about. |
| `items[].warnings[].message` | What's going on, to show to a person. |
| `items[].warnings[].values` | The numbers in the message, so you can say it your own way. `cutOffPercent` on `design_adjusted`, `pages_adjusted`, `cover_adjusted` and `scaled`: how much is cut off (for pages, the most of any page), with `difference`: what differs (`landscape` or `portrait`: it lies the other way; `wider` or `taller`: another shape; `smaller` or `larger`: another size, then also `fileSize` and `printSize`, like `"210 × 297 mm"`). `dpi` and `minimumDpi` on `low_resolution`: what the image prints at, and from what it's sharp. Other warnings can have others, like `pageCount`. |
| `items[].previews` | A picture of each file as we print it: a JPG of 1200 pixels on its long side, with the red line where it's cut. For a book: the whole cover with its spine, and pages 2 and 3 of the interior (the `previewUrl` shows every page). Empty when we couldn't make them; the quote works without. |
| `items[].previews[].file` | Which file of the item it shows. |
| `items[].previews[].url` | The picture. Only whoever has the link can see it. |
| `items[].previews[].description` | What the picture shows, and what its lines mean. |
| `shipping.method` | How it ships: `standard` or `express`. Standard when you asked for express and that isn't available (see `note`). |
| `shipping.price` | The shipping costs. Standard shipping is free (`0`) when the items together cost €50 or more; express keeps its price. |
| `shipping.vat` | The VAT included in the shipping costs. |
| `shipping.estimatedDelivery` | When it should arrive, as a range of dates. Not guaranteed. |
| `shipping.note` | Why it ships another way than you asked, to show to the person: express isn't available for this order, so it ships standard. Otherwise `null`. |
| `shippingOptions` | Every way this order can ship to this country, with its `method`, `price`, `vat` and `estimatedDelivery`, the one of this quote too. To let the person choose: show these, and get a new quote with the `shipping.method` they pick. `total` and `vat` are for the quote's own `shipping`. |
| `total` | Everything together: the items and shipping. |
| `vat` | The VAT included in the `total`. |
| `termsUrl` | Our terms, which apply to the order. Read them before you accept them when you [place the order](#4-place-the-order). |

A file we can't print (wrong type, too few pages, can't be read) is an error: the quote fails with `422` and lists every problem of every file in `problems`, so you can fix them all at once. See [Errors](#errors).

A quote can be ordered once. For another order, get a new quote.

## 4. Place the order

An order is a [quote](#3-get-a-quote) you've decided to buy, with the address to ship it to. You get a payment link: once it's paid, we print and ship it, and you can follow its status until it arrives.

```
POST https://www.blotprints.com/api/v1/orders
```

```json
{
  "quoteId": "quo_8hQ2kV7xLm3pR9tY",
  "acceptTerms": true,
  "acceptWarnings": true,
  "reference": "SHOP-2026-0042",
  "email": "jane@example.com",
  "shippingAddress": {
    "name": "Jane Doe",
    "line1": "Keizersgracht 123",
    "line2": "",
    "postalCode": "1015 CJ",
    "city": "Amsterdam",
    "country": "NL"
  }
}
```

| Field | Description |
|---|---|
| `quoteId` | **Required.** The quote to order, from [3. Get a quote](#3-get-a-quote). A quote can be ordered once. |
| `reference` | Optional. Your own reference for the order, like your order number: up to 100 characters. We return it with the order, so you can match it to yours. |
| `acceptTerms` | **Required.** `true` to accept our terms (the `termsUrl` of the quote). Without it, the order is refused with `terms_not_accepted`. The person who pays also accepts them on the payment page. |
| `acceptWarnings` | Optional. `true` to order a quote with warnings. Without it, such a quote is refused with `warnings_not_accepted`. |
| `email` | **Required.** Where we send the confirmation and the track and trace. |
| `shippingAddress` | **Required.** Where to ship. |
| `shippingAddress.name` | **Required.** The name of the receiver. |
| `shippingAddress.line1` | **Required.** The first address line: usually street and house number. |
| `shippingAddress.line2` | Optional. A second address line, like an apartment or a company name. |
| `shippingAddress.postalCode` | **Required.** The postal code. |
| `shippingAddress.city` | **Required.** The city. |
| `shippingAddress.country` | **Required.** The country of the quote. |

| Header | Description |
|---|---|
| `Idempotency-Key` | Optional, but advised. Any unique text, like a UUID, so a retry doesn't order twice. See [Idempotency](#idempotency). |

Response (`201`):

```json
{
  "orderId": "1042-x7Kq2mV9pL4tR8wN3bY6zA",
  "reference": "SHOP-2026-0042",
  "status": "awaiting_payment",
  "paymentUrl": "https://www.blotprints.com/pay/1042-x7Kq2mV9pL4tR8wN3bY6zA",
  "total": 56.96,
  "currency": "EUR"
}
```

| Field | Description |
|---|---|
| `orderId` | The order. **Keep it**: it's the only way to look the order up, and it's secret, as anyone with it can see the order. |
| `reference` | Your own reference, as you sent it; `null` without one. |
| `status` | `awaiting_payment`: placed, waiting for payment. See [6. Track the order](#6-track-the-order) for all statuses. |
| `paymentUrl` | The page where a person pays for the order. See [5. Complete payment](#5-complete-payment). |

## 5. Complete payment

Open the `paymentUrl` in a browser. It shows the order and asks for payment (iDEAL, card, and the other methods of the country). Once it's paid, we print. An order that isn't paid within 7 days is cancelled.

## 6. Track the order

```
GET https://www.blotprints.com/api/v1/orders/{orderId}
```

```json
{
  "orderId": "1042-x7Kq2mV9pL4tR8wN3bY6zA",
  "reference": "SHOP-2026-0042",
  "status": "shipped",
  "createdAt": "2026-10-25T12:00:00Z",
  "paidAt": "2026-10-25T12:05:00Z",
  "items": [
    {
      "product": "poster",
      "size": "a3-landscape",
      "options": { "paper": "matte", "hanging": "photo_frame_black" },
      "quantity": 2,
      "unitPrice": 25.98,
      "price": 51.96,
      "vat": 9.02
    }
  ],
  "shipping": {
    "method": "standard",
    "price": 5.00,
    "vat": 0.87,
    "trackingUrl": "https://…"
  },
  "shippingAddress": {
    "name": "Jane Doe",
    "line1": "Keizersgracht 123",
    "line2": "",
    "postalCode": "1015 CJ",
    "city": "Amsterdam",
    "country": "NL"
  },
  "total": 56.96,
  "vat": 9.89,
  "currency": "EUR",
  "paymentUrl": null
}
```

| Field | Description |
|---|---|
| `status` | Where the order is, see below. |
| `items`, `shipping`, `total`, `vat` | As in the [quote](#3-get-a-quote) the order was made from. |
| `paidAt` | When it was paid; `null` until then. |
| `shipping.trackingUrl` | Where to follow the parcel; `null` until it's available. It can come a while after the order is marked as shipped. |
| `shippingAddress` | Where it's shipped to, as you sent it when you [placed the order](#4-place-the-order), tidied up (like the spacing of the postal code). |
| `paymentUrl` | Where to pay; `null` once it's paid or cancelled. |

**`status`** is one of:

| Status | Meaning |
|---|---|
| `awaiting_payment` | Placed, waiting for payment. |
| `paid` | Paid. |
| `preparing` | Being prepared for printing. |
| `in_production` | Being printed. |
| `shipped` | On its way. |
| `cancelled` | Not paid in time, or cancelled. |

# Reference

## Errors

An error has an HTTP status and a body like this:

```json
{
  "error": {
    "code": "file_check_failed",
    "message": "The interior has 18 pages. It needs at least 24. The cover isn't a file type we print.",
    "item": 0,
    "file": "interior",
    "problems": [
      {
        "item": 0,
        "file": "interior",
        "code": "too_few_pages",
        "message": "The interior has 18 pages. It needs at least 24.",
        "values": { "pageCount": 18, "min": 24 }
      },
      {
        "item": 0,
        "file": "cover",
        "code": "unsupported_file_type",
        "message": "The cover isn't a file type we print."
      }
    ]
  }
}
```

| Field | Description |
|---|---|
| `error.code` | What went wrong, as a fixed code to check in your code (see the table below). |
| `error.message` | What's wrong and how to fix it, written to be shown to a person or read by an agent. |
| `error.item` | Only for an error about an item of a quote: which one, counting from `0`. |
| `error.file` | Only for an error about a file: which one of the item, like `interior`, `cover` or `design`. With several problems: the first one's. |
| `error.problems` | Only for `file_check_failed`: every problem, of every file and item, each with its `item`, `file`, `code` (like `too_few_pages`, `pdf_only`, `file_too_large` or `file_unreadable`), `message`, and the numbers in it (`values`). |
| `error.docs` | The part of these docs about the error. |

| Status | Code | When |
|---|---|---|
| `400` | `invalid_request` | The JSON is wrong, or a field is missing. |
| `404` | `not_found` | No such product, upload, quote or order. |
| `409` | `quote_already_ordered` | The quote has been ordered already. Get a new one for another order. |
| `409` | `idempotency_key_reused` | The `Idempotency-Key` was used before, for a different request. Use a new key. |
| `410` | `quote_expired` | The quote is older than 24 hours. Get a new one. |
| `422` | `file_check_failed` | Files can't be printed. `problems` lists them all. |
| `422` | `file_unreachable` | We couldn't download a file link. |
| `422` | `country_mismatch` | `shippingAddress.country` isn't the country of the quote. |
| `422` | `terms_not_accepted` | Set `acceptTerms` to accept our terms. |
| `422` | `warnings_not_accepted` | The quote has warnings: set `acceptWarnings`. |
| `429` | `rate_limited` | Too many requests. Wait `Retry-After` seconds. |
| `500` | `server_error` | Something went wrong on our side. Try again later. |

## Idempotency

Networks fail, and scripts and agents try again. To make sure an order is never placed twice, send an `Idempotency-Key` header with `POST /orders`: any unique text, like a UUID.

Sending the same key again within 24 hours gives back the same order, instead of a new one. Use a new key for each new order.

## Rate limits

Per IP address:

| What | Limit |
|---|---|
| All calls | 60 per minute |
| `POST /quotes` | 30 per hour |
| `POST /orders` | 10 per hour |

Over a limit, you get `429` with a `Retry-After` header: the seconds to wait.

## For AI agents

This page is also plain Markdown, at [`/api/docs.md`](/api/docs.md): read that to learn the API. AI tools can also find it through [`/llms.txt`](/llms.txt), with the tips below.

**MCP server.** AI assistants that support the Model Context Protocol (Claude, ChatGPT, Copilot, …) can add BlotPrints as a connector, at:

```
https://www.blotprints.com/mcp
```

No login is needed. The assistant then gets the steps of this page as tools: `list_products`, `get_product`, `create_upload`, `get_quote`, `place_order` and `get_order`.

**OpenAPI.** The API is also described as OpenAPI 3.1, at [`/api/openapi.json`](/api/openapi.json): for tools that read it, like code generators or a custom GPT's actions.

Tips for agents:

- Start with `GET /products` to see what's possible, and pick sizes and options from it: don't guess names.
- For files on the user's computer, use [an upload](#upload-a-file): `curl -T file.pdf "<uploadUrl>"`.
- Show the user the quote's preview (`previewUrl`), price, warnings and terms (`termsUrl`) before you place the order, and ask whether the preview looks right.
- Give the user the `paymentUrl` to pay: you can't pay yourself.
- Keep the `orderId` to check on the order later.

## Frequently asked questions

### How do I change a quote?

A quote can't be changed. Get a new one with what you want now: a quote is free and only takes a moment. The old one simply expires after 24 hours.

### How do I change or replace a file?

Before you place the order: [upload](#upload-a-file) or link the new file, and [get a new quote](#3-get-a-quote) with it.

After you placed it, but before it's paid: the `paymentUrl` opens our checkout, where the order can still be changed. **Change** next to an item opens it on our website, with its files, to pick another one.

Once it's paid, it can't be changed anymore: we print it specially for you.

### How do I change an order, like its address or quantity?

There's no API call to change an order. Until it's paid, the `paymentUrl` opens our checkout, where the person paying can still change the address and the quantities, change files, or remove items. `GET /orders/{orderId}` then shows the order as it is.

You can also let it be and [place a new order](#4-place-the-order) from a new quote: an order that isn't paid is cancelled by itself after 7 days.

Once it's paid, it can't be changed anymore: we print it specially for you.

### How do I cancel an order?

Don't pay it: an unpaid order is cancelled by itself after 7 days, and its `status` becomes `cancelled`. Nothing is printed or charged.

A paid order can't be cancelled or returned: we print it specially for you (see our [terms](/terms)).

### Can I test the API without ordering?

There's no test mode, and it isn't needed: products, uploads and quotes are free and change nothing. A quote checks your files and shows the [previews](#3-get-a-quote), so you can test everything up to ordering.

You can even place an order: as long as it isn't paid, nothing is printed, and it's cancelled after 7 days.

### Who pays, and can I pay by invoice?

Every order is paid on its own `paymentUrl`, with iDEAL, card or another method of the country: by you, or by your customer if you send them the link. Paying by invoice or monthly isn't possible yet.

An AI agent can't pay: it gives the `paymentUrl` to the person.

### Can I ship straight to my customer?

Yes. Put your customer's address in `shippingAddress`. The `email` gets the confirmation and the track and trace: use your customer's, or your own to keep that to yourself.

One order goes to one address. To ship to several addresses, get a quote and place an order per address.

### Which countries do you ship to?

To every country of the EU, with tracking: give its two-letter code in `shipping.country`, like `NL`, `DE` or `IT`. The quote gives the shipping costs and the expected delivery, which depend on how far it goes. Standard shipping is free from €50; `express` is faster, for the products that have it. The quote's `shippingOptions` list every way it can ship, with price and dates.

### How do I know when an order is paid or shipped?

There are no webhooks yet. Ask for the order with [`GET /orders/{orderId}`](#6-track-the-order) now and then, like every few hours: its `status` says where it is, and `shipping.trackingUrl` where the parcel is once it's shipped. The `email` of the order also gets a confirmation and the track and trace.

### My quote has warnings. What should I do?

Look at the `previewUrl` first: it shows how we print the files. A warning means we fix something while printing, like zooming in a design of another shape, so its edges are cut off.

Looks fine? Order with `"acceptWarnings": true`. If not, fix the file and get a new quote. The warning's `values` say how much, like `cutOffPercent` or `dpi`.

### Why was my file refused?

A file we can't print makes the quote fail with `file_check_failed`, and `problems` lists every problem with its file, so you can fix them all at once. Often it's the type (a book's interior must be a PDF), the number of pages, or a file over 200 MB. A link we can't download gives `file_unreachable`: check that it works without logging in.

The sizes, page counts and types each product needs are in [its `files`](#all-products).

### Are the prices the same as on the website?

Yes, and VAT is included. The VAT in it is that of the country it ships to, and the quote shows it per item and for shipping. Prices can change: a quote's prices hold for its 24 hours.

### Do I need an API key? What if the limits are too low?

No key is needed. The [rate limits](#rate-limits) are per IP address, enough for most shops and agents. If you need more, [let us know](/contact).

### What do you do with my files?

We only use them to print your order. Only who has the links can see them: the `previewUrl`, the previews and the `orderId` can't be guessed. Uploads and quotes are deleted after 7 days, the files of an order 90 days after it's shipped.

### Something's wrong with what arrived. Who do I contact?

[Let us know](/support/problem) with the order number (the part of the `orderId` before the dash) and the email of the order, and a photo if it helps. We'll sort it out.
