Data model
This page is written for machines as well as people: plain Markdown, no components. The same text is served raw
at /data-model.md. The field tables are generated from the
OpenAPI document, so they match the API.
Conventions
Section titled “Conventions”- IDs: UUID strings.
- Weights: integer milligrams, field names end in
_mg. 1 kg = 1000000 mg. - Money: integer cents, field names end in
_cents, pluscurrency(ISO 4217, e.g.EUR). - Countries: ISO 3166-1 alpha-2 (
NL,BE,DE,FR,AT). - Dates: ISO 8601 (
2026-10-03); timestamps with time zone. - Validity:
valid_fromis the first day (inclusive),valid_tothe end day (exclusive);nullmeans open-ended. - Packaging quantities: decimal strings (
"1","0.5"), parts per sales unit. - Codes from the knowledge pack (materials, layers, regimes, countries) are short text. Their lists come from
GET /reference/{materials|layers|regimes|countries}. - Tenancy: every record belongs to exactly one account. An API key belongs to one account.
- Errors:
{detail, code, params?}; see errors.
Account and companies
Section titled “Account and companies”GET /account returns the account of the key, the key’s name and prefix, and the scopes in force now.
Fields of AccountOut:
| Field | Type | Notes |
|---|---|---|
id |
uuid | The account (brand) this API key belongs to. |
name |
string | Account name. |
plan |
string | Subscription plan. |
token_name |
string | Name of the API key making this call. |
token_prefix |
string | Public prefix of the API key (ep_<prefix>_…). |
scopes |
string[] | Scopes in force for this key now: its own scopes limited by its creator’s current role. |
A legal entity is a company inside the account. Obligations, periods and sales are per legal entity.
Fields of LegalEntityOut:
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
name |
string | Registered company name. |
legal_form |
string | Legal form. |
country |
string | Country of registration (ISO 3166-1 alpha-2). |
registry_type |
string | Company registry (kvk, kbo, …). |
registry_id |
string | Number in that registry. |
vat_number |
string | VAT number. |
is_default |
boolean | The company used when a sale names none. |
archived_at |
date-time | null | Set when the company was archived. |
Team roles, low to high: readonly < accountant < member < admin < owner. Owners, admins and members
can use every API scope; accountants and readers only the :read scopes. Keys: ep_<prefix8>_<secret32>,
shown once, effective rights = scopes ∩ the creator’s current role.
Products and groups
Section titled “Products and groups”A product is one SKU. confirmation.state is none, confirmed or changed (confirmed before, packaging
differs now).
Fields of ProductOut:
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
sku |
string | Your stock keeping unit; unique per account. |
title |
string | Product name. |
brand_name |
string | Brand on the product. |
gtin |
string | null | GTIN/EAN (8-14 digits, check digit validated). |
hs_code |
string | Customs tariff code. |
country_of_origin |
string | ISO 3166-1 alpha-2. |
net_weight_mg |
integer | null | Net product weight in milligrams. |
status |
“active” | “archived” | Archived products are kept for history. |
is_kit |
boolean | A bundle of other products (bill of materials). |
is_electrical |
boolean | Electrical/electronic equipment (WEEE). |
is_textile |
boolean | Textile product (textiles EPR). |
packaging_confirmed |
boolean | Gate 1: a person confirmed this product’s packaging. Changing packaging (also through a proposal) clears it until someone confirms again in the portal. |
confirmed_at |
date-time | null | When the packaging was confirmed. |
confirmation |
ConfirmationOut | Gate 1 in detail (complements packaging_confirmed, which equals state confirmed when the packaging is unchanged). |
group_id |
uuid | null | The product group whose packaging this product inherits (per layer), if any. |
archived_at |
date-time | null | |
created_at |
date-time | |
updated_at |
date-time |
Fields of ConfirmationOut:
| Field | Type | Notes |
|---|---|---|
state |
“none” | “confirmed” | “changed” | none = never confirmed; confirmed = the packaging is what a person confirmed; changed = it was confirmed, but the packaging (own or inherited) differs now. |
at |
date-time | null | When the current confirmation was given. |
by |
string | null | Who confirmed (display name). |
PUT /products/by-sku/{sku} creates or updates a product directly (201 / 200):
Fields of ProductUpsertIn:
| Field | Type | Notes |
|---|---|---|
title |
string | Required. Product name. |
brand_name |
string | null | Brand on the product. |
gtin |
string | null | GTIN/EAN; null clears it. |
hs_code |
string | null | Customs tariff code. |
country_of_origin |
string | null | ISO 3166-1 alpha-2. |
net_weight_mg |
integer | null | Net weight in milligrams. |
is_electrical |
boolean | null | Electrical/electronic equipment (WEEE). |
is_textile |
boolean | null | Textile product. |
A product group holds variants that share packaging, e.g. one Shopify product with several variants.
GET /product-groups/{id} adds the members (with the layers they override) and the group’s packaging.
Fields of ProductGroupOut:
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
name |
string | |
code |
string | Unique per account: a Shopify product gid or a SKU stem. |
source |
“manual” | “csv” | “odoo” | “shopify” | “picqer” | “supplier_spec” | “photo” | “api” | “ai” | |
source_ref |
string | |
member_count |
integer | Products in the group. |
packaging_count |
integer | Packaging lines of the group (all versions). |
archived_at |
date-time | null | |
created_at |
date-time | |
updated_at |
date-time |
Packaging
Section titled “Packaging”Layers: sales (Dutch app: Productverpakking; what the customer receives), grouped (Omverpakking; around
several sales units), transport (Transportverpakking; pallets, stretch film). Packaging of web-shop parcels is
kept apart, as parcel profiles per size class (lt40, 40to90, gt90 litres); it is not in the public API.
Effective packaging, per layer: if the product has its own lines on a layer they apply; otherwise the
group’s lines apply (inherited: true, group_id).
GET /products/{id}/packaging returns the tree layers[] > lines[] > item > materials[]:
Fields of PackagingTreeOut:
| Field | Type | Notes |
|---|---|---|
product_id |
uuid | |
sku |
string | |
group_id |
uuid | null | The product’s group, if any. |
on |
date | The day the totals are for (query parameter on, default today). |
packaging_confirmed |
boolean | Gate 1 passed (see ProductOut.packaging_confirmed). |
confirmed_at |
date-time | null | |
confirmation |
ConfirmationOut | |
layers |
PackagingLayerOut[] | |
totals_by_material |
MaterialMassOut[] | Packaging per sales unit by material on on (lines with in_totals), heaviest first. |
total_mass_mg |
integer | Sum of totals_by_material. |
findings |
FindingOut[] | Automatic plausibility checks on today’s lines. |
Fields of PackagingLayerOut:
| Field | Type | Notes |
|---|---|---|
layer |
“sales” | “grouped” | “transport” | sales (primary), grouped (secondary) or transport (tertiary). |
lines |
PackagingLineOut[] | |
mass_mg |
integer | Mass per sales unit of this layer’s lines that count in the totals. |
Fields of PackagingLineOut:
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
state |
“proposed” | “confirmed” | proposed = not yet checked by a person; confirmed = part of a gate-1 confirmation. |
quantity |
string | Parts per sales unit (may be a fraction, e.g. 1 film per 1440). |
household |
boolean | Ends up with households (counts as household packaging). |
scope_channel |
string | null | Only for sales in this channel; null = all. |
scope_fulfilment |
string | null | Only for this fulfilment; null = all. |
valid_from |
date | null | First day (inclusive); null = from the start. |
valid_to |
date | null | End day (exclusive); null = open-ended. |
mass_mg |
integer | Packaging mass per sales unit for this line (item mass x quantity). |
in_totals |
boolean | Counted in totals_by_material: valid on on and not limited to a channel/fulfilment. |
source |
“manual” | “csv” | “odoo” | “shopify” | “picqer” | “supplier_spec” | “photo” | “api” | “ai” | Where the line came from (provenance). |
source_ref |
string | Which record of that source, e.g. api:token:<prefix>. |
inherited |
boolean | The line is the product group’s: the product has no own line on that layer. |
group_id |
uuid | null | The group the line belongs to (inherited lines). |
item |
PackagingItemOut |
Fields of PackagingItemOut:
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
name |
string | Name of the packaging part. |
code |
string | Your code for the part (unique per account when set). |
role_default |
string | Usual role: sales, grouped, transport, shipping or filler. |
household_default |
boolean | Usually ends up with households (vs. businesses). |
separable |
boolean | Can be separated from the other parts by hand. |
rigidity |
“rigid” | “flexible” | |
recycled_content_pct |
string | null | Recycled content in percent. |
recyclability_grade |
string | null | A-E, when known. |
custom_for_product_id |
uuid | null | Set for a part made for one product only (not in the shared library). |
materials |
MaterialMassOut[] | Material breakdown of ONE part. |
total_mass_mg |
integer | Sum of the materials, milligrams. |
archived_at |
date-time | null | |
created_at |
date-time | |
updated_at |
date-time |
Fields of MaterialMassOut:
| Field | Type | Notes |
|---|---|---|
material |
string | Material code from the knowledge pack (see /reference/materials). |
mass_mg |
integer | Mass in milligrams. |
Fields of FindingOut:
| Field | Type | Notes |
|---|---|---|
code |
string | Stable check code. |
severity |
string | info, warning or error. |
params |
object | Values for the message, per code. |
line_id |
uuid | null | The packaging line the finding is about, if any. |
dismissed |
boolean | A person marked this finding as right (‘klopt zo’). |
POST /products/{id}/packaging/proposals adds lines with state: proposed; a person confirms them in the app.
Each line names an existing item_id or a new_item:
Fields of ProposalLineIn:
| Field | Type | Notes |
|---|---|---|
item_id |
uuid | null | An existing packaging item. |
new_item |
NewPartIn | null | A new part for this product only. |
layer |
“sales” | “grouped” | “transport” | |
quantity |
number | string | Parts per per_units sales units. |
per_units |
integer | null | E.g. 1 film per 1440 jars: quantity 1, per_units 1440. |
household |
boolean | |
scope_channel |
“webshop” | “bol” | “amazon” | “odoo” | “other” | null | Only for this sales channel. |
scope_fulfilment |
“own” | “fba” | “lvb” | “3pl” | null | Only for this fulfilment. |
valid_from |
date | null | First day (inclusive); null = from the start. |
valid_to |
date | null | End (exclusive); null = open-ended. |
Fields of NewPartIn:
| Field | Type | Notes |
|---|---|---|
name |
string | Required. |
materials |
MaterialMassIn[] | Required. Material breakdown of one part. |
rigidity |
“rigid” | “flexible” | |
separable |
boolean |
Evidence
Section titled “Evidence”Files that back up packaging data (photos, spec sheets): jpeg/png/webp/heic/pdf, max 15 MB, uploaded in the app.
GET /products/{id}/evidence lists them with a short-lived signed download_url (GET it without the
Authorization header).
Fields of EvidenceOut:
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
kind |
“photo” | “spec_sheet” | “invoice” | “other” | |
note |
string | |
target |
“product” | “item” | “packaging_line” | What the file is about. |
product_id |
uuid | null | |
item_id |
uuid | null | |
packaging_line_id |
uuid | null | |
filename |
string | |
mime |
string | |
size_bytes |
integer | |
sha256 |
string | |
created_at |
date-time | |
download_url |
string | Short-lived signed URL; GET it without the Authorization header. |
Regime profiles
Section titled “Regime profiles”Product data for a regime other than packaging, validated by the knowledge pack’s JSON Schema. epr.weee data:
in_scope, category (1-6), weight_mg, has_battery, battery_weight_mg, notes. epr.textiles data:
in_scope, weight_mg, notes. GET /products/{id}/profiles lists own and inherited profiles, all versions,
with in_force per regime today. PUT /products/{id}/profiles/{regime} stores a proposal
(state: proposed, source: api).
Fields of RegimeProfileOut:
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
regime |
string | |
schema_version |
string | |
data |
object | |
state |
“proposed” | “confirmed” | proposed (e.g. from the API) or confirmed by a person. |
inherited |
boolean | The group’s profile (the product has no own one for the regime). |
group_id |
uuid | null | |
valid_from |
date | null | |
valid_to |
date | null | Exclusive; null = open-ended. |
source |
“manual” | “csv” | “odoo” | “shopify” | “picqer” | “supplier_spec” | “photo” | “api” | “ai” | |
source_ref |
string | |
confirmed_at |
date-time | null |
Fields of ProfileProposalIn:
| Field | Type | Notes |
|---|---|---|
data |
object | Required. Profile data; must match the regime’s JSON Schema. |
valid_from |
date | null | Start a new version on this day; omit to change the version valid today. |
Reference codes
Section titled “Reference codes”Fields of ReferenceOut:
| Field | Type | Notes |
|---|---|---|
code |
string | |
name |
object | Name per language (nl, en). |
verified |
boolean | False until checked against the cited sources. |
sources |
string[] | URLs of the sources the fact comes from. |
parent |
string | null | Parent material code (materials only). |
scheme_categories |
object | Scheme code -> that scheme’s category. |
POST /sales takes up to 1000 lines and writes them directly, idempotent on external_id. No consumer
personal data is ever stored: a country is all EuroPlaybook needs.
Fields of SalesLineIn:
| Field | Type | Notes |
|---|---|---|
external_id |
string | Required. Your unique id for this line; sending it again updates the line. |
sku |
string | Required. Your SKU; matched to a product. |
sold_on |
date | Required. Sale (or return) date. |
quantity |
integer | Required. Units (positive; use kind=return for returns). |
kind |
“sale” | “return” | |
ship_from_country |
string | Required. ISO 3166-1 alpha-2. |
ship_to_country |
string | Required. ISO 3166-1 alpha-2. |
legal_entity_id |
uuid | null | Selling company; default: the account’s default. |
channel |
“webshop” | “bol” | “amazon” | “odoo” | “other” | |
marketplace |
string | null | Marketplace code, e.g. amazon.de. |
buyer_type |
“consumer” | “business_end_user” | “reseller” | “unknown” | |
fulfilment |
“own” | “fba” | “lvb” | “3pl” | |
net_amount_cents |
integer | null | Net line amount in cents (optional). |
currency |
string |
Fields of SalesPushOut:
| Field | Type | Notes |
|---|---|---|
created |
integer | New lines. |
updated |
integer | Lines whose values changed. |
unchanged |
integer | Lines sent before with the same values. |
held |
string[] | external_ids not stored because a closed or filed period covers them (reopen it first). |
unknown_skus |
string[] | SKUs without a product (stored; map or create the product). |
GET /sales lists stored lines, from every source:
Fields of SalesLineOut:
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
external_id |
string | null | Your id (lines pushed through the API). |
source |
string | Where the line came from: api, csv, odoo, shopify, picqer. |
sku |
string | |
product_id |
uuid | null | Matched product; null = unknown SKU. |
legal_entity_id |
uuid | |
sold_on |
date | |
kind |
“sale” | “return” | |
quantity |
integer | |
ship_from_country |
string | |
ship_to_country |
string | |
channel |
string | |
marketplace |
string | null | |
buyer_type |
string | |
fulfilment |
string |
Obligations
Section titled “Obligations”Per legal entity × jurisdiction (country) × regime (× scope), derived by the knowledge pack’s rules unless a person overrode the status.
Fields of ObligationOut:
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
legal_entity_id |
uuid | |
jurisdiction |
string | |
regime |
string | |
scope |
string | |
status |
string | Shown status (see years for every derived year). |
status_source |
string | rule = derived; user = overridden by a person. |
status_reason |
string | |
status_year |
integer | null | |
since |
date | null | |
pack_version |
string | |
years |
ObligationYearOut[] |
Fields of ObligationYearOut:
| Field | Type | Notes |
|---|---|---|
year |
integer | |
status |
string | needed, not_needed, unknown, … for that year. |
reason_code |
string | Stable code of the rule outcome. |
reason_params |
object | |
status_reason |
string | The reason in English. |
rule_key |
string | Knowledge-pack rule the status rests on. |
verified |
boolean | The rule and its figures are verified against sources. |
Reporting periods and totals
Section titled “Reporting periods and totals”A period is built from sales (and parcel) facts × effective packaging into ledger entries per material. A
person locks it (closed); later uploads do not change a locked period.
Fields of PeriodOut:
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
legal_entity_id |
uuid | |
jurisdiction |
string | |
scheme |
string | |
report_type |
string | |
cadence |
string | |
starts_on |
date | |
ends_on |
date | |
status |
string | open, review, closed, filed or reopened. |
closed_at |
date-time | null |
Fields of PeriodTotalsOut:
| Field | Type | Notes |
|---|---|---|
period |
PeriodOut | |
built_at |
date-time | null | When the ledger was last built; null = never. |
stale |
boolean | Facts or packaging changed since the last build. |
blockers |
string[] | Why the period could not be closed right now (codes). |
unknown_skus |
integer | |
unconfirmed_products |
integer | |
totals |
TotalOut[] | |
declared |
any | What a pack made from this build would declare (scheme lines). |
Fields of TotalOut:
| Field | Type | Notes |
|---|---|---|
kind |
string | product, packaging, parcel_packaging or adjustment. |
material |
string | null | |
household |
boolean | |
obligated_party |
string | |
exclusion |
string | |
fact_kind |
string | |
quantity |
integer | |
mass_mg |
integer | |
size_class |
string | null | |
parcels |
integer |
Lineage. GET /periods/{id}/totals/{key}/explain (MCP explain_total; key from declared.lines[].key
of the totals) traces one figure back to its ledger entries, the products with the packaging lines used (own or
inherited, with source and evidence count), and the sales per month and channel. traced: false means the
period was built before lineage was recorded: rebuild it.
Fields of ExplainTotalOut:
| Field | Type | Notes |
|---|---|---|
period_id |
uuid | |
period_status |
“open” | “review” | “closed” | “filed” | “reopened” | |
line |
ExplainLineOut | |
entry_match |
string | How entries were matched to the line: indexes (by the calculator) or material. |
traced |
boolean | Lineage was recorded at the build (false: built before; rebuild the period). |
entries |
ExplainEntryOut[] | |
products |
ExplainProductOut[] | Products behind the line, largest share first. |
parcels |
ExplainParcelsOut[] | Parcel facts behind parcel packaging entries. |
Filing packs
Section titled “Filing packs”The filing for a period and scheme, approved by a person.
Fields of PackOut:
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
period_id |
uuid | |
scheme |
string | |
report_type |
string | |
kind |
string | original or amendment. |
status |
string | draft, ready, approved or filed. |
format |
string | |
has_file |
boolean | |
approved_at |
date-time | null | |
filed_at |
date-time | null | |
filing_ref |
string | |
created_at |
date-time |
Fields of PackLineOut:
| Field | Type | Notes |
|---|---|---|
line_no |
integer | |
key |
string | |
label |
string | |
value |
string | kg with three decimals; counts as whole numbers. |
unit |
“kg” | “units” | “parcels” |
GET /packs/{id}/download returns a short-lived URL to the file:
Fields of DownloadOut:
| Field | Type | Notes |
|---|---|---|
url |
string | Short-lived signed URL; GET it without the Authorization header. |
filename |
string | |
expires_in |
integer | Seconds until the URL stops working. |
- Sources (Shopify, Picqer, Odoo, CSV uploads, the API) produce sales facts and parcel counts.
- Products have effective packaging (own or inherited lines), confirmed by a person.
- The knowledge pack derives obligations per legal entity.
- A period build multiplies facts by effective packaging into ledger entries, per material.
- A person locks the period, an owner or admin approves the pack, someone marks it filed with the scheme’s reference.
What the API never does
Section titled “What the API never does”Confirm packaging or regime profiles, lock a period, approve a pack or mark it filed. Those are human gates.