API and MCP server
PPWR-Ready is not a closed system. You can reach your packaging, product and reporting data through a REST interface, through an MCP server for AI assistants, and through bulk import and export in the dashboard. This page describes all three routes in full.
All interfaces use the same personal API key, the same access check, the same rate limit and the same error format. What you understand here once applies to every endpoint.
1. Access
Creating a key
You create your personal API key in the dashboard under Settings → API access. It starts with epr_live_ and belongs to exactly one account. A newly created key replaces the previous one immediately — running integrations have to be switched over.
The key is a secret, like a password. It belongs in your system's configuration — not in the source of an application that runs in the browser, and not in a URL.
Base URL
https://cuugzslijbqurmeuvshh.supabase.co/functions/v1/
Two equivalent ways to send the key
The recommended header is X-Api-Key. It works in every configuration:
X-Api-Key: epr_live_...
Alternatively, every endpoint also accepts the key in the Authorization header:
Authorization: Bearer epr_live_...
Both routes lead to the same check. If your environment already uses the Authorization header for something else, use X-Api-Key.
A first call to try it out
This call writes nothing and is the quickest way to check that key and connection are in order:
curl https://cuugzslijbqurmeuvshh.supabase.co/functions/v1/materials-summary \ -H "X-Api-Key: epr_live_..."
{
"year": 2026,
"used_kg": 607184.6,
"rate_limit_per_minute": 120,
"used_this_minute": 1
}
2. Limits and errors
Rate limit
The limit applies per account across all endpoints together, not per endpoint, and covers the last 60 seconds. MCP calls count towards it. It follows your plan:
| Rate | Calls per minute |
|---|---|
basic | 60 |
starter, standard | 120 |
pro | 300 |
growth | 600 |
scale | 1200 |
| any other plan | 120 |
Every successful response tells you where you stand: rate_limit_per_minute and used_this_minute. That lets you pace your own calls instead of waiting for an error. Once the limit is reached, the endpoint answers with 429; retrying a minute later then succeeds.
Error format
Every error comes back as a JSON object with the field error and a stable error key. That key is machine-readable and will not change — evaluate it rather than the HTTP status alone.
{ "error": "api-schluessel-ungueltig", "rate_limit_per_minute": null }
| HTTP | error | What it means and what to do |
|---|---|---|
401 | missing-api-key | No key was sent. Check the headers. |
401 | api-schluessel-ungueltig | The key does not belong to any account. It may have been regenerated in the dashboard. |
403 | abo-nicht-aktiv | The subscription is dormant or the account is blocked. The key itself is valid. |
403 | poa-not-signed | Volume declarations on the representation plan only: the signed power of attorney is missing. |
429 | ratengrenze-erreicht: … | Rate limit exhausted. Try again after a minute. |
400 | invalid-body, invalid-value-for-… | The body is not a JSON object, or a field carries an invalid value. The field name is part of the error key. |
400 | no-known-fields-provided | The call contained no known field at all. Nothing was written. |
405 | method-not-allowed | Wrong HTTP verb. The permitted verbs are listed with each endpoint. |
500 | db-error: …, auth-error: … | Server-side error. The call may be retried. |
3. REST endpoints
POST /submit-materials — volume declaration
Submits packaging volumes placed on the market. The endpoint writes into its own source api; manual entries in the dashboard and volumes from the packing bench stay untouched. The volume shown for a period is the sum of all sources.
Volumes are added, not replaced. One call is one event — typically an order or a shipment — not a new total. If your system could send the same event twice, it is up to you to make sure it does not (for example: send once per order number).
Volume fields, all in kilograms, all optional — send whatever occurred:
| Field | Type | Meaning |
|---|---|---|
paper_cardboard | Number | Paper and cardboard |
plastic_rigid | Number | Plastic, rigid |
plastic_flexible | Number | Plastic, flexible (films, pouches) |
beverage_cartons | Number | Beverage cartons |
glass | Number | Glass |
metal_aluminum | Number | Aluminum |
metal_steel | Number | Steel and tinplate |
wood | Number | Wood |
other_composite | Number | Other and composites |
sup_pieces | Integer | Number of single-use plastic packaging units. Added as well. Declarable from the very first unit, regardless of any weight threshold. |
period | Text | Reporting period, e.g. "2026 H2". Without it, the current period. |
plastic_pcr | Boolean | Recycled content present. Applies to the whole period and is overwritten, not summed. |
plastic_mono | Boolean | Mono-material. Applies to the whole period. |
plastic_color | Boolean | Colouring per the discount criterion. Applies to the whole period. |
Discountable plastic volume
The three booleans above apply flatly to the entire period. If only part of your plastic packaging meets a discount criterion, state the discountable volume per material class and criterion instead — then only that share is discounted:
"discountable_plastic": {
"plastic_flexible": { "pcr": 1.2, "mono": 3.0 },
"plastic_rigid": {}
}
An empty object ("plastic_rigid": {}) is a statement: calculated, nothing discountable. A missing key means the opposite — the full volume counts as discountable. So whoever uses this field has to send it for every plastic class they declare.
Example
curl -X POST https://cuugzslijbqurmeuvshh.supabase.co/functions/v1/submit-materials \
-H "X-Api-Key: epr_live_..." \
-H "Content-Type: application/json" \
-d '{
"paper_cardboard": 12.5,
"plastic_flexible": 3.2,
"sup_pieces": 40,
"discountable_plastic": { "plastic_flexible": { "pcr": 3.2 } }
}'
{
"ok": true,
"period": "2026 H2",
"amounts": { "paper": 12.5, "flexpl": 3.2 },
"plastic_recycling": { "pcr": false, "mono": false, "color": false },
"total_kg": 15.7,
"sup_pieces": 40,
"orders_synced_count": 1,
"rate_limit_per_minute": 120,
"used_this_minute": 3
}
The response shows the state after the call and across all sources, not just your own row. The fee is calculated in the database alone; it cannot be set through the interface.
GET /materials-summary — query the annual volume
Returns the account's cumulative annual volume across all reporting routes. Read-only, writes nothing.
curl https://cuugzslijbqurmeuvshh.supabase.co/functions/v1/materials-summary \ -H "X-Api-Key: epr_live_..."
{ "year": 2026, "used_kg": 607184.6, "rate_limit_per_minute": 120, "used_this_minute": 1 }
POST /submit-product-passport — product data and product passport
Submits product master data and the preparation fields for the digital product passport. The key is the sku: if it exists in the account, only the fields you send are updated; if it does not, the product is created — which makes name mandatory in that case.
The digital product passport is an upcoming obligation under the ESPR. These fields serve to prepare for it; they do not constitute a declaration of conformity.
| Field | Type | Meaning |
|---|---|---|
sku | Text | Mandatory. Article number, the key for matching and creation. |
name | Text | Product name. Mandatory when the product is created. |
ean | Text | EAN/GTIN |
length_mm, width_mm, height_mm | Number | Dimensions in millimetres |
volume_cm3 | Number | Volume in cubic centimetres |
durability_info | Text | Information on service life |
repairability_score | Text | Repairability score |
repair_instructions_url | Text | Link to the repair instructions |
spare_parts_info | Text | Availability of spare parts |
disassembly_instructions_url | Text | Link to the disassembly instructions |
substances_of_concern | Text | Substances of concern |
substances_status | Text | One of not_started, none_known, contains_listed, unknown |
restricted_substances_list | Text | Restricted substances in plain text |
carbon_footprint_value | Number | Carbon footprint. Without carbon_footprint_methodology this is rejected — a number without a methodology is not evidence. |
carbon_footprint_methodology | Text | Methodology and source, e.g. "GHG Protocol, own calculation" |
recycled_content_pct | Number | Recycled content in percent |
recyclability_grade | Text | Your own recyclability rating |
passport_status | Text | One of not_started, in_progress, ready_for_review |
curl -X POST https://cuugzslijbqurmeuvshh.supabase.co/functions/v1/submit-product-passport \
-H "X-Api-Key: epr_live_..." \
-H "Content-Type: application/json" \
-d '{
"sku": "ART-1234",
"name": "Precision turned part M8",
"carbon_footprint_value": 2.4,
"carbon_footprint_methodology": "GHG Protocol, own calculation"
}'
{ "ok": true, "id": "…", "sku": "ART-1234", "created": false,
"updated_fields": ["name", "carbon_footprint_value", "carbon_footprint_methodology"] }
4. MCP server
The Model Context Protocol (MCP) is the open standard through which AI assistants reach business systems. PPWR-Ready runs its own server for it: you connect it to your assistant once, and from then on you can query your packaging and reporting data in plain language — "Which packaging has no declaration of conformity?", "How much plastic did we declare in 2026?".
Read-only, without exception. The MCP server has no writing tool at all. An assistant can analyse your data, but can neither change nor delete it, nor submit a declaration.
Key facts
| Endpoint | https://cuugzslijbqurmeuvshh.supabase.co/functions/v1/mcp |
| Transport | Streamable HTTP, JSON-RPC 2.0 over POST |
| Protocol version | 2025-06-18 |
| Server name | ppwr-ready |
| Authentication | the same API key, in X-Api-Key or Authorization: Bearer |
Setting it up in Claude Desktop or Claude Code
{
"mcpServers": {
"ppwr-ready": {
"type": "http",
"url": "https://cuugzslijbqurmeuvshh.supabase.co/functions/v1/mcp",
"headers": { "X-Api-Key": "epr_live_..." }
}
}
}
The six tools
| Tool | What it returns |
|---|---|
get_account | Company, plan and the countries this account reports in. A sensible first call. |
list_data_sources | All queryable data areas with their field counts — packaging, products, volume declarations, suppliers, tasks and more. |
list_fields | The fields of a data area with their types. Parameter: source. |
query_data | The query itself: pick fields, filter (where), sort, group (group_by) and aggregate (metrics), up to 1000 rows. |
get_reporting_periods | The volume declarations: period, total weight, fee, time of submission. |
get_open_tasks | Open compliance tasks with severity and due date — a missing declaration of conformity, say, or an expiring document. |
An empty value in a result means not recorded — not zero and not "no". The server makes that distinction explicitly, so an assistant does not present a gap in the data as a statement of fact.
Calling it without an assistant
The server is ordinary JSON-RPC and can be addressed directly:
curl -X POST https://cuugzslijbqurmeuvshh.supabase.co/functions/v1/mcp \
-H "X-Api-Key: epr_live_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
5. Bulk import and export
Not every integration needs programming. For one-off or recurring data exchange there are three routes in the dashboard that need no code at all.
Import
Products, packaging and suppliers can be read in as CSV. The import recognises both semicolon and comma as separators and copes with the character sets Excel produces. Before anything is written you see a preview with the column mapping; every run is logged and can be undone as a whole if a file was wrong.
Export and reports
In the Reporting centre you assemble reports yourself: pick a data source, pick fields, filter, group — and output them as XLSX or as an audit-proof PDF . A layout you have built once can be saved as a template and produced again at any time — half-yearly for the same authority request, for instance.
The same data sources and fields you can choose there are reachable through query_data on the MCP server. It is the same catalogue — whatever may appear in a report may also be queried, and the other way round.
Full data export
Independently of that, under Settings → Privacy you can request a full export of all your account's data. It is produced server-side and provided as an archive.
6. Security
Tenant isolation
Every query through the API and MCP runs in the database with your account's own rights, secured by row level security. The separation does not come from a filter the interface applies, but from the same rule that governs the dashboard. A query that explicitly asks for someone else's data returns nothing — not because it is intercepted, but because those rows do not exist for this account.
Access log
Every access is logged with time, endpoint, outcome, IP address and the calling program's identifier — rejected ones included. Invalid keys and blocked accounts additionally create an entry in the security log. These entries are what the rate limit is built on, and at the same time the record of who retrieved what, and when.
Lost your key
Generate a new key in the dashboard. The old one loses its validity at once; from that moment calls using it are rejected with 401 and recorded in the security log.
Questions about integrating?
If you would like to connect an ERP or merchandise management system and are unsure which route is right, write to us — with a short description of your system and your data, one reply is usually enough.
info@ppwr-ready.app