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:
| Module | What it does |
|---|---|
/js/personalize.js | mount() puts the form widget on a product and saves the result to the cart |
/js/cart-store.js | The 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:
- Ask for an upload address per picture:
POST /v1/shop/uploads. PUTevery picture to its address.- Place the order with the design as
template_dataand 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_inseconds: 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
| Status | error | Meaning |
|---|---|---|
422 | no_files | files is missing or empty |
422 | too_many_files | More than 40 files in one request |
422 | unsupported_type | A type other than JPEG, PNG, WebP or GIF |
422 | upload_too_large | A file without a size, or over 20 MB |
429 | too_many_requests | Over 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"
}
}
]
}
| Field | Required | Description |
|---|---|---|
lines | Yes | At least one line |
lines[].product_slug, lines[].variant_id | Yes | What is ordered: a product on sale and one of its variants |
lines[].quantity | No | A whole number from 1 to 10,000. Default 1 |
lines[].personalization.template_data | Yes | The filled-in design, as the JSON string the widget handed over |
lines[].personalization.files | No | The 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_key | No | The key of an uploaded picture of the result. The back office shows it with the order |
email | No | The 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 |
note | No | A note from the customer |
shipping_method | No | A value from the delivery options; standard when absent or unknown |
payment_method | No | ideal, invoice, card or direct_debit. An unknown value is left off the order, not refused |
payment_reference | No | The bank for ideal, the purchase order number for invoice, at most 120 characters. Ignored for the other methods |
shipping_address | No | name, 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.
error | Meaning |
|---|---|
unreadable | The body, or a line in it, is not readable JSON |
empty_order | No lines |
unknown_product | No product on sale with this slug |
unknown_variant | The product has no variant with this id |
invalid_quantity | A quantity that is not a whole number from 1 to 10,000 |
missing_personalization | A line without personalization.template_data |
unreadable_personalization | template_data is not a JSON document, or a picture has no element_uuid |
missing_upload | A picture without a key, or a preview sent inline instead of as preview_key: upload it first |
unknown_upload | Nothing was uploaded under a key, or the upload has expired |
upload_too_large | One picture over 20 MB |
order_too_large | The pictures and previews of the order over 200 MB together |
too_many_files | More 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.
status | Meaning |
|---|---|
new | Placed |
paid | Paid |
in_production | Being made |
shipped | Sent |
cancelled | Cancelled |
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.status | Meaning |
|---|---|
queued | Waiting its turn |
rendering | Being made, usually within a few minutes |
ready | Ready to download, see below |
failed | Making 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.
| Status | error | Meaning |
|---|---|---|
404 | not_found | No such order, a wrong token, or no line with this id in the order |
409 | print_file_not_ready | The line has no PDF yet, or making it failed |
409 | awaiting_payment | The shop hands the PDF out once the order is paid |
409 | order_cancelled | The order was cancelled |