Webshop API
A Typograph webshop sells personalised print products: a visitor fills in a design with the
Form Widget, and orders it as a printed product. The shop itself —
its catalogue, its orders and its back office — runs as its own service, and every
storefront reaches it over one API, /v1/shop.
The storefront is yours to build. Our own demo storefront, Stamped at shop.typograph.nl, uses exactly the calls on these pages and nothing else, so whatever it can do, yours can too.
| Part | What it is | Page |
|---|---|---|
| Catalogue | Products, categories, filters, search, delivery options | Catalogue |
| Personalisation and orders | The form widget, picture uploads, placing an order and reading it back | Personalise and Order |
| Customers | Optional accounts, a shortlist and order history | Customers |
The full specification is in the Webshop API Reference.
Not the platform API
The webshop is a separate service from the Typograph API. It does not
sit behind api.typograph.nl, it takes no OAuth token, and the gateway's rate limits and
scopes do not apply to it.
Each shop has its own address. The examples on these pages use the demo shop:
https://shop-api.typograph.nl
Replace that with your own shop's address. The paths are the same for every shop.
Authentication
Every call carries the shop's publishable key in X-Shop-Key:
curl https://shop-api.typograph.nl/v1/shop/products \
-H "X-Shop-Key: shop_pk_…"
The shop owner finds the key in the back office under Settings → Connection, in the Shop key card (see Connection).
Publishable means it belongs in your page. The visitor's own browser places the order, so the key has to be readable there. It names the shop; it does not prove who is asking.
What stops someone from copying the key into their own site is the Allowed websites list
under it. A browser always says which site a request comes from (the Origin header) and
cannot lie about it, so the shop answers only the sites on that list:
| Request | Answer |
|---|---|
No X-Shop-Key, or the wrong one | 401 |
| Right key, from a site on the list | Served, with Access-Control-Allow-Origin for that site |
| Right key, from a site not on the list | 401 |
Right key, no Origin at all (your own server) | Served |
The list is empty when a shop starts, which means no website may use the key yet. Add every
origin your storefront runs on, one per line, as scheme and host without a path:
https://kaarten.nl, and https://www.kaarten.nl separately if you use both.
Creating a new key in the back office stops the old one at once: every site that uses it gets
401 until it has the new one.
What a browser may send
A page on a site on the list may make every call from the browser. The shop answers
cross-origin GET, POST, PUT and DELETE requests with the headers Content-Type,
X-Shop-Key, X-Customer-Token, X-Order-Token and Accept-Language, and lets the browser
read the Location and Retry-After headers of a response. That covers the catalogue, the
picture uploads, placing and reading back an order, and the
customer calls.
Your own server may make the same calls; without an Origin the list does not apply.
The product documents
The form widget loads a product's form, template, manifest, fonts and pictures from the shop
itself (see Personalise and Order). Those addresses take no key,
but they answer the same list: a page on a site that is not on it gets 403. A request
without Origin, such as a plain <img> or your own server, is served.
Picture uploads
At checkout, the visitor's browser uploads pictures straight to the shop's file storage, not to the API. That storage keeps its own list of sites that may upload. Ask Typograph support to add your storefront's origin before you go live; until then the uploads from your site are refused by the browser.
Response shapes
The shapes are the ones the platform's own services use.
A single resource sits under data:
{
"data": { "slug": "welcome", "name": "Welcome" }
}
A collection is data plus a meta:
{
"data": [{ "slug": "birth", "name": "Birth" }],
"meta": { "total": 1, "limit": 100, "offset": 0, "has_more": false }
}
- Paging. The product listing and search take
limitandoffset:limitis 25 when you send none, and at most 100. Categories and delivery options come back whole; a customer's order history comes back in one list of at most 100 orders. - Prices are whole cents, excluding VAT:
2450is € 24,50. The shop adds VAT at checkout, over the goods and the delivery together. - JSON is snake_case.
- Language. Send
Accept-Languagewith the visitor's language. The shop's own words — product, variant, category and filter names, badges and descriptions — come back in any language the shop lists atGET /v1/shop/languagesand has translated; a text that is not translated yet comes in the shop's main language. Texts the shop writes for people — the names of the sort orders and the delivery options, and most error descriptions — come inenornl; anything else gets English. Filter values in a request stay in the main language, whatever language the labels are shown in.
Errors
A refusal has the same shape as on the platform:
{
"error": "not_found",
"error_description": "This product does not exist."
}
Match on error, which does not change. error_description is written to be shown to a
visitor as it is.
| Status | error | When |
|---|---|---|
401 | unauthorized | No key, the wrong key, or a site not on the allowed list |
401 | not_signed_in | A customer call without a valid X-Customer-Token |
404 | not_found, or the code its page names | An unknown product, category or order, or an order read with the wrong token. A product saved to the shortlist that does not exist answers unknown_product (see Customers) |
422 | see the page of the call | The request was understood and refused; nothing was stored |
429 | too_many_requests | Too many uploads or feedback messages from one visitor, or too many sign-in attempts for one e-mail address, see below |
The 422 codes are listed with the call that returns them: uploads,
orders and customers.
Rate limits
Three calls are limited. Over a limit they answer 429 too_many_requests with a Retry-After
header in seconds.
| Call | Limit | Counted per |
|---|---|---|
POST /v1/shop/uploads | 30 requests per hour | Visitor, by IP address |
POST /v1/shop/sessions | 5 attempts per 15 minutes | E-mail address, known or not; a successful sign-in clears it |
POST /v1/shop/feedback | 10 per hour | Visitor, by IP address |
A checkout normally asks for uploads once. Sign-in is counted per e-mail address rather than per visitor because a storefront that signs customers in from its own server would otherwise make them all share one budget. The price is that anyone who knows an address can keep its owner out for a while by spending the five tries.
The other calls have no limit of the shop's own.