Skip to main content

Personalise and Order

A visitor fills in a product with the Form Widget, puts it in a cart, and orders it. The shop has no cart. The cart lives in the visitor's browser until checkout, and nothing is sent to the shop before the order is placed: a personalisation is only worth storing once someone buys it.

 product page          cart (browser)          checkout
┌──────────────┐ ┌────────────────┐ ┌──────────────────────────────────────┐
│ form widget │ ──▶ │ lines + files │ ──▶ │ 1. POST /v1/shop/uploads │
│ fills in the │ │ in IndexedDB │ │ 2. PUT every picture to its address │
│ design │ │ │ │ 3. POST /v1/shop/orders, with keys │
└──────────────┘ └────────────────┘ └──────────────────────────────────────┘

The order is the moment the shop writes the personalised design down. From then on it is fixed: a later change to the design in Typograph does not touch an order already placed.

There are two ways to build this. The shop serves two JavaScript modules that do all of it, and our own storefront uses them. Or you use the form widget yourself and send the same calls.

With the shop's modules​

The shop serves two ES modules from its own address, which any site may load:

ModuleWhat it does
/js/personalize.jsmount() puts the form widget on a product and saves the result to the cart
/js/cart-store.jsThe cart in the browser: listLines(), setQuantity(), removeLine(), countItems(), clear(), and orderLines() for checkout

1. Show the form​

On the product page, read the product with GET /v1/shop/products/{slug} and mount the form:

<script src="https://cdn.typograph.nl/form/latest/form.js"></script>

<div id="personalize"></div>

<script type="module">
import { mount } from 'https://shop-api.typograph.nl/js/personalize.js';

// product = (await GET /v1/shop/products/{slug}).data
await mount({
host: document.getElementById('personalize'),
product: {
slug: product.slug,
name: product.name,
formUrl: product.form_url,
templateUrl: product.template_url,
manifestUrl: product.manifest_url,
assetBaseUrl: product.asset_base_url,
},
variantId: product.variants[0].id,
canvasUrl: 'https://cdn.typograph.nl/editor/latest/viewer.js',
locale: 'en',
translationsUrl: '',
onSaved: () => { location.href = '/cart'; },
});
</script>

When the visitor submits the form, mount() renders a small preview of the result in the browser and stores the line, its pictures and the preview in the cart. Without translationsUrl the widget's texts are the shop's Dutch ones; '' keeps the widget's own English.

2. Place the order​

At checkout, orderLines() uploads the pictures and previews of every line and answers with the lines in the shape the order wants:

import { orderLines, clear } from 'https://shop-api.typograph.nl/js/cart-store.js';

const SHOP = 'https://shop-api.typograph.nl';
const SHOP_KEY = 'shop_pk_…';

const response = await fetch(`${SHOP}/v1/shop/orders`, {
method: 'POST',
headers: { 'X-Shop-Key': SHOP_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
shipping_method: 'standard',
shipping_address,
lines: await orderLines({ shopUrl: SHOP, shopKey: SHOP_KEY }),
}),
});

const { number, access_token } = (await response.json()).data;
await clear();

Keep number and access_token: they are how the visitor reads the order back (see Read an order back).

orderLines() throws before it uploads anything when the cart holds more than the shop takes in one order (see the limits).

With your own cart​

Use the form widget as described in Form Integration, with the four addresses of the product: form_url as the form, template_url as the template, manifest_url as the manifest and asset_base_url as the asset base.

On submit, the widget hands you { name, data, files }: the filled-in design as a JSON string, and every picture the visitor chose as { elementId, name, data } with data a base64 data URL. Keep them until checkout, then:

  1. Ask for an upload address per picture: POST /v1/shop/uploads.
  2. PUT every picture to its address.
  3. Place the order with the design as template_data and the keys of the pictures: POST /v1/shop/orders.

Leave the data URLs in template_data as the widget wrote them. The shop puts the address of the stored picture in their place, matched on the picture's element_uuid in the order (the widget's elementId).

Upload the pictures​

curl -X POST https://shop-api.typograph.nl/v1/shop/uploads \
-H "X-Shop-Key: shop_pk_…" \
-H "Content-Type: application/json" \
-d '{
"files": [
{ "content_type": "image/jpeg", "size": 482113 },
{ "content_type": "image/webp", "size": 35120 }
]
}'

201, with one upload address per file, in the order asked:

{
"data": {
"uploads": [
{
"key": "incoming/019f7f9f-12c1-7f74-a9c6-539064ddeb24",
"upload_url": "https://…",
"content_type": "image/jpeg",
"size": 482113
},
{
"key": "incoming/019f7f9f-12c1-7f74-a9c6-539064ddeb25",
"upload_url": "https://…",
"content_type": "image/webp",
"size": 35120
}
],
"expires_in": 900,
"max_bytes": 20971520
}
}

Then PUT each file to its upload_url, with exactly that Content-Type:

await fetch(upload.upload_url, {
method: 'PUT',
headers: { 'Content-Type': upload.content_type },
body: blob,
});
  • The type and the size are part of the address's signature. The storage refuses a file of another type or another size than the one you asked for.
  • An address works for expires_in seconds: 15 minutes.
  • Pictures are JPEG, PNG, WebP or GIF, at most 20 MB each, and at most 40 per request. A cart with more asks more than once.
  • A picture nobody orders is removed after a day.

Your storefront's website has to be allowed to upload; see Picture uploads.

Upload errors​

StatuserrorMeaning
422no_filesfiles is missing or empty
422too_many_filesMore than 40 files in one request
422unsupported_typeA type other than JPEG, PNG, WebP or GIF
422upload_too_largeA file without a size, or over 20 MB
429too_many_requestsOver 30 requests in an hour from this visitor; Retry-After says when to try again

Place the order​

curl -X POST https://shop-api.typograph.nl/v1/shop/orders \
-H "X-Shop-Key: shop_pk_…" \
-H "Content-Type: application/json" \
-d @order.json
{
"email": "emma@example.com",
"note": "Please deliver before Friday",
"shipping_method": "standard",
"payment_method": "invoice",
"payment_reference": "PO-1234",
"shipping_address": {
"name": "Emma Clarke",
"company": "",
"street": "Keizersgracht",
"house_number": "12",
"postal_code": "1015 CX",
"city": "Amsterdam",
"country": "NL"
},
"lines": [
{
"product_slug": "welcome",
"variant_id": "0198a3c4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"quantity": 1,
"personalization": {
"template_data": "{\"pages\":[…]}",
"files": [
{
"element_uuid": "4f6e2a10-8c1b-4d2e-9f3a-5b6c7d8e9f01",
"name": "cat.png",
"key": "incoming/019f7f9f-12c1-7f74-a9c6-539064ddeb24"
}
],
"preview_key": "incoming/019f7f9f-12c1-7f74-a9c6-539064ddeb25"
}
}
]
}
FieldRequiredDescription
linesYesAt least one line
lines[].product_slug, lines[].variant_idYesWhat is ordered: a product on sale and one of its variants
lines[].quantityNoA whole number from 1 to 10,000. Default 1
lines[].personalization.template_dataYesThe filled-in design, as the JSON string the widget handed over
lines[].personalization.filesNoThe pictures of this line: element_uuid (the widget's elementId) and name as the widget gave them, and the key of the upload. A picture without element_uuid is refused
lines[].personalization.preview_keyNoThe key of an uploaded picture of the result. The back office shows it with the order
emailNoThe customer's address, kept on the order. It does not link the order to an account: that is the X-Customer-Token header, see below
noteNoA note from the customer
shipping_methodNoA value from the delivery options; standard when absent or unknown
payment_methodNoideal, invoice, card or direct_debit. An unknown value is left off the order, not refused
payment_referenceNoThe bank for ideal, the purchase order number for invoice, at most 120 characters. Ignored for the other methods
shipping_addressNoname, company, street, house_number, postal_code, city, country (default NL). The billing address is the same

On a customer's account. Send the customer's X-Customer-Token header (optional, next to X-Shop-Key) to place the order on their account, so it shows up under GET /v1/shop/users/me/orders. Without a token, or with one that has expired, it is a guest order. The order is never refused for it: a checkout must not fail because a session ran out halfway.

The shop does not take payment. It records the method the customer chose and the reference, and the order starts as new. Whoever handles the orders in the back office moves it on.

Prices come from the shop. The order is charged at the variant's price and the delivery option's price, plus VAT over both. Anything price-shaped in the request is ignored.

All or nothing. The shop checks every line and every upload before it stores anything. A refused order leaves nothing behind, so it is safe to fix and send again.

201, with the order and a Location header to read it back from:

{
"data": {
"number": "ST-YYYY-0042",
"status": "new",
"email": "emma@example.com",
"shipping_method": "Standard delivery, Thursday",
"payment_method": "On account",
"subtotal_cents": 2450,
"shipping_cents": 0,
"vat_cents": 515,
"total_cents": 2965,
"lines": [
{
"id": "019f7fa0-3b2c-7d1e-8f4a-6b5c4d3e2f11",
"product_name": "Welcome",
"variant_name": "25 cards",
"sku": "WEL-25",
"quantity": 1,
"unit_price_cents": 2450,
"personalization": {
"id": "019f7fa0-3b2c-7d1e-8f4a-6b5c4d3e2f10",
"template_url": "https://shop-api.typograph.nl/personalizations/…",
"preview_image_url": "https://shop-api.typograph.nl/personalizations/…"
},
"print_file": { "status": "queued", "is_downloadable": false }
}
],
"access_token": "…"
}
}

The order number is two letters, the year and a count of at least four digits: ST-YYYY-0042, and ST-YYYY-10000 after the 9,999th. shipping_method and payment_method are the readable names, in the request's language.

access_token is sent this once. Nothing can ask the shop for it again, so keep it with the order number, in your confirmation page or your own records.

Order errors​

All 422. Nothing of the order was stored.

errorMeaning
unreadableThe body, or a line in it, is not readable JSON
empty_orderNo lines
unknown_productNo product on sale with this slug
unknown_variantThe product has no variant with this id
invalid_quantityA quantity that is not a whole number from 1 to 10,000
missing_personalizationA line without personalization.template_data
unreadable_personalizationtemplate_data is not a JSON document, or a picture has no element_uuid
missing_uploadA picture without a key, or a preview sent inline instead of as preview_key: upload it first
unknown_uploadNothing was uploaded under a key, or the upload has expired
upload_too_largeOne picture over 20 MB
order_too_largeThe pictures and previews of the order over 200 MB together
too_many_filesMore than 100 pictures and previews in one order

Read an order back​

curl "https://shop-api.typograph.nl/v1/shop/orders/ST-YYYY-0042" \
-H "X-Shop-Key: shop_pk_…" \
-H "X-Order-Token: …"

The same order as above, without access_token. Order numbers count up and are meant to be read out over the phone, so the number alone proves nothing: the token does. A wrong token answers 404, exactly like a number that does not exist.

Send the token in the X-Order-Token header, from the browser or from your own server. Keep it out of addresses: a URL ends up in browser history, server logs and Referer headers, and the token with it. When your own pages need it after a page load, keep it the way a session is kept, for instance in an HttpOnly cookie. ?token= is still read but deprecated.

statusMeaning
newPlaced
paidPaid
in_productionBeing made
shippedSent
cancelledCancelled

The status changes only when someone moves the order on in the back office.

The print file of a line​

Every personalised line gets a print-ready PDF: the file the printer gets, in CMYK with its bleed. print_file.status on each line says how it is doing; a line without a design has print_file: null.

print_file.statusMeaning
queuedWaiting its turn
renderingBeing made, usually within a few minutes
readyReady to download, see below
failedMaking it went wrong. The shop has been told and sees it in its back office; it can make it again

Read the order again to follow it. print_file.is_downloadable says whether the PDF is handed out now. That depends on one switch in the shop's back office, Customers may download their print files before the order is paid under Settings → Print files. It is on by default: is_downloadable is true as soon as the file is ready. A shop that turns it off hands the PDF out once the order is paid (or in_production, shipped); until then a ready file has is_downloadable: false. Ask for the address of a downloadable PDF with the order's token, as for the order itself:

curl "https://shop-api.typograph.nl/v1/shop/orders/ST-YYYY-0042/lines/019f7fa0-3b2c-7d1e-8f4a-6b5c4d3e2f11/print-file" \
-H "X-Shop-Key: shop_pk_…" \
-H "X-Order-Token: …"
{
"data": {
"url": "https://…/print-files/ST-YYYY-0042/019f7fa0-3b2c-7d1e-8f4a-6b5c4d3e2f11.pdf?…",
"expires_at": "YYYY-MM-DDT14:10:00+02:00"
}
}

The address works for five minutes. A link or a navigation cannot carry the shop key or the token, so fetch the address when somebody clicks, from the page or from your own server, then send the browser to url. The storefront does this from its server. Ask again for a new one rather than keeping it. A cancelled order shows no print file and hands none out.

StatuserrorMeaning
404not_foundNo such order, a wrong token, or no line with this id in the order
409print_file_not_readyThe line has no PDF yet, or making it failed
409awaiting_paymentThe shop hands the PDF out once the order is paid
409order_cancelledThe order was cancelled