Quick start
- Create a key under Settings › API keys (Dutch app: Instellingen › API-sleutels).
catalogue:readis enough to start; the write examples needcatalogue:writeandsales:write. - Put it in an environment variable:
export EUROPLAYBOOK_API_KEY='ep_1a2b3c4d_...'export API_BASE='https://api.europlaybook.com/v1'Check the key
Section titled “Check the key”curl -s "$API_BASE/account" \ -H "Authorization: Bearer $EUROPLAYBOOK_API_KEY"import osimport httpx
api = httpx.Client( base_url=os.environ["API_BASE"], headers={"Authorization": f"Bearer {os.environ['EUROPLAYBOOK_API_KEY']}"},)print(api.get("/account").json())const base = process.env.API_BASE;const headers = { Authorization: `Bearer ${process.env.EUROPLAYBOOK_API_KEY}` };
const res = await fetch(`${base}/account`, { headers });console.log(await res.json());List all products
Section titled “List all products”Lists use a cursor. Keep calling with next_cursor until it is null.
curl -s "$API_BASE/products?limit=100" \ -H "Authorization: Bearer $EUROPLAYBOOK_API_KEY"# then: ...&cursor=<next_cursor>products, cursor = [], Nonewhile True: page = api.get("/products", params={"limit": 100, "cursor": cursor}).json() products += page["items"] cursor = page["next_cursor"] if not cursor: breakconst products = [];let cursor = null;do { const url = new URL(`${base}/products`); url.searchParams.set('limit', '100'); if (cursor) url.searchParams.set('cursor', cursor); const page = await (await fetch(url, { headers })).json(); products.push(...page.items); cursor = page.next_cursor;} while (cursor);Read a product’s packaging
Section titled “Read a product’s packaging”curl -s "$API_BASE/products?sku=DG-HOSE-25" -H "Authorization: Bearer $EUROPLAYBOOK_API_KEY"curl -s "$API_BASE/products/<id>/packaging" -H "Authorization: Bearer $EUROPLAYBOOK_API_KEY"The response is a tree: layers[] > lines[] > item > materials[], plus totals_by_material and
confirmation. Weights are integer milligrams. Lines the product inherits from its group have
inherited: true and group_id; every line says where it came from (source, source_ref).
Explain a total
Section titled “Explain a total”Each figure of a period can be traced back. Take a key from declared.lines of the period totals and ask
where it comes from:
curl -s "$API_BASE/periods/<period_id>/totals" -H "Authorization: Bearer $EUROPLAYBOOK_API_KEY"curl -s "$API_BASE/periods/<period_id>/totals/<key>/explain" -H "Authorization: Bearer $EUROPLAYBOOK_API_KEY"You get the ledger entries behind the line, the products (largest share first) with the packaging lines used and their evidence count, and the units sold per month and channel.
Send sales
Section titled “Send sales”Up to 1000 lines per request. external_id is your own id for the line; sending it again does not count it
twice.
curl -s -X POST "$API_BASE/sales" \ -H "Authorization: Bearer $EUROPLAYBOOK_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 2026-10-batch-1" \ -d '{"lines":[{"external_id":"order-1001-1","sku":"DG-HOSE-25","sold_on":"2026-10-03", "quantity":2,"ship_from_country":"NL","ship_to_country":"BE","channel":"webshop"}]}'lines = [{ "external_id": "order-1001-1", "sku": "DG-HOSE-25", "sold_on": "2026-10-03", "quantity": 2, "ship_from_country": "NL", "ship_to_country": "BE", "channel": "webshop",}]r = api.post("/sales", json={"lines": lines}, headers={"Idempotency-Key": "2026-10-batch-1"})print(r.json())const lines = [{ external_id: 'order-1001-1', sku: 'DG-HOSE-25', sold_on: '2026-10-03', quantity: 2, ship_from_country: 'NL', ship_to_country: 'BE', channel: 'webshop',}];const res = await fetch(`${base}/sales`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json', 'Idempotency-Key': '2026-10-batch-1' }, body: JSON.stringify({ lines }),});console.log(await res.json());The answer counts what happened to the lines:
{ "created": 1, "updated": 0, "unchanged": 0, "held": [], "unknown_skus": [] }| Field | Meaning |
|---|---|
created |
New lines |
updated |
Lines sent before (same external_id) whose values changed |
unchanged |
Lines sent before with the same values |
held |
external_ids not stored because a locked or filed period covers them; reopen the period in the app first |
unknown_skus |
SKUs without a product. The lines are stored; create or map the product and they count |
Open periods that overlap the dates are recalculated in the background. Never send personal data of consumers. A country code is all EuroPlaybook needs.
Create or update a product by SKU
Section titled “Create or update a product by SKU”PUT with the SKU in the path creates the product (201) or updates the fields you send (200). title is
required. This writes directly; it does not touch the product’s packaging or its confirmation.
curl -s -X PUT "$API_BASE/products/by-sku/DG-SPRINK-01" \ -H "Authorization: Bearer $EUROPLAYBOOK_API_KEY" \ -H "Content-Type: application/json" \ -d '{"title":"Garden sprinkler","net_weight_mg":350000,"country_of_origin":"NL","is_electrical":false}'Propose a regime profile
Section titled “Propose a regime profile”Regime profiles hold product data for other EPR regimes, such as epr.weee. A PUT stores a proposal
(state: proposed, source: api); a person confirms it in the app. data must match the regime’s schema
(422 otherwise). Send valid_from to start a new version on that day; leave it out to change the version
valid today.
curl -s -X PUT "$API_BASE/products/<id>/profiles/epr.weee" \ -H "Authorization: Bearer $EUROPLAYBOOK_API_KEY" \ -H "Content-Type: application/json" \ -d '{"data":{"in_scope":true,"category":5,"weight_mg":420000,"has_battery":true,"battery_weight_mg":45000}}'epr.weee data: in_scope, category (1-6), weight_mg, has_battery, battery_weight_mg, notes.
epr.textiles: in_scope, weight_mg, notes. Read them back with GET /products/{id}/profiles.
Next: the API reference lists every endpoint and field.