Reseller API
Verkoop producten van Printen.eu in je eigen winkel. Je bestelling wordt automatisch een productieorder, wordt betaald uit je saldo of met je opgeslagen kaart en gaat white label naar je klant: jouw naam, geen Printen.eu-branding en geen prijzen.
Snel aan de slag
1. Maak een API-sleutel
Na goedkeuring van je wederverkopersaccount maak je in je dashboard een sleutel. Een prk_live_-sleutel plaatst echte orders, een prk_test_-sleutel is een sandbox die niets afrekent en niets laat produceren. De volledige sleutel zie je maar één keer.
2. Vraag een prijs op
POST /quotes met regels en het afleverland geeft je inkoopprijs inclusief korting, btw en verzending.
3. Plaats een order
POST /orders met een Idempotency-Key header (bijvoorbeeld je eigen ordernummer). Een herhaling met dezelfde sleutel maakt nooit een tweede order.
4. Ontvang webhooks
Registreer een https-endpoint en ontvang statuswijzigingen, zoals verzonden met tracking.
# Product: customer options with defaults and allowed values (id or any slug)
curl https://www.printen.eu/api/reseller/v1/products/flyers?lang=en \
-H "Authorization: Bearer prk_test_..." -H "User-Agent: MyShop-Integration/1.0"
# Prijs / quote: only customer options; omitted options get the product-page default.
# quantity = number of pieces (for run-priced products the run: here 1000 flyers)
curl -X POST https://www.printen.eu/api/reseller/v1/quotes \
-H "Authorization: Bearer prk_test_..." -H "Content-Type: application/json" \
-H "User-Agent: MyShop-Integration/1.0" \
-d '{"country":"BE","lines":[{"productId":"<product id>","quantity":1000,"options":{"size":"a5","material":"135gr_gesatineerd_mc","printtype":"44"}},{"resellerSku":"POSTER-A2","quantity":1}]}'
# Order
curl -X POST https://www.printen.eu/api/reseller/v1/orders \
-H "Authorization: Bearer prk_test_..." -H "Content-Type: application/json" \
-H "Idempotency-Key: shop-order-1001" \
-d '{"externalOrderId":"1001","lines":[{"resellerSku":"POSTER-A2","quantity":1,"fileUrl":"https://example.com/design.pdf"}],
"recipient":{"name":"Anna de Vries","email":"anna@example.com","phone":"+31612345678","address1":"Dorpsstraat 1","postalCode":"1234 AB","city":"Utrecht","country":"NL"}}'
# Webhook
curl -X POST https://www.printen.eu/api/reseller/v1/webhooks \
-H "Authorization: Bearer prk_live_..." -H "Content-Type: application/json" \
-d '{"url":"https://example.com/printen-webhook","events":["order.shipped","order.delivered"]}'Authenticatie
Stuur je sleutel mee als Authorization: Bearer prk_live_... Sleutels kunnen scopes hebben; een sleutel zonder scopes mag alles.
Met een prk_test_-sleutel worden orders aangemaakt maar niet afgerekend en niet doorgezet naar productie.
Per sleutel maximaal 120 verzoeken per minuut, plus accountlimieten voor offertes en orders. Elk antwoord bevat RateLimit-Limit, RateLimit-Remaining en RateLimit-Reset (seconden) voor de strengste limiet die geldt; daarboven volgt 429 met een Retry-After-header.
Base URL: https://www.printen.eu/api/reseller/v1
Endpoints
| Methode | Pad | Omschrijving | Scope |
|---|---|---|---|
| GET | /products | List catalogue products | catalog:read |
| GET | /products/{id} | Get a product with options and file requirements | catalog:read |
| POST | /quotes | Price lines for a destination country | quotes:write |
| GET | /reseller-products | List your own products | reseller-products:read |
| POST | /reseller-products | Create your own product (SKU with fixed options) | reseller-products:write |
| GET | /reseller-products/{id} | Get your own product (id or SKU) | reseller-products:read |
| PATCH | /reseller-products/{id} | Update your own product | reseller-products:write |
| DELETE | /reseller-products/{id} | Delete your own product | reseller-products:write |
| GET | /orders | List your orders (newest first) | orders:read |
| POST | /orders | Place an order | orders:write |
| GET | /orders/{id} | Get an order | orders:read |
| POST | /orders/{id}/cancel | Cancel an order while it has not reached production | orders:write |
| GET | /wallet | Balance and ledger entries | wallet:read |
| GET | /webhooks | List webhook endpoints | webhooks:read |
| POST | /webhooks | Create a webhook endpoint (the secret is only returned here) | webhooks:write |
| DELETE | /webhooks/{id} | Delete a webhook endpoint | webhooks:write |
Zo werkt de API
Opties en standaardwaarden
GET /products/{id} geeft precies de opties die een klant op de productpagina ziet, elk met de standaardwaarde en de toegestane waarden. Stuur alleen die sleutels mee in options. Laat je een optie weg, dan krijgt hij de standaard die daar staat: de startconfiguratie van de productpagina. Bij een paar producten (zoals posters) past de pagina die start aan na live beschikbaarheidscontroles; stuur dus elke optie mee die voor jou belangrijk is.
Opties die de website zelf invult (productiemethode, velformaat, bundeling, verpakking en dergelijke) vullen wij op dezelfde manier in en kun je niet meesturen: een onbekende of automatische sleutel geeft 422 invalid_options met de lijst toegestane sleutels. Voor dezelfde keuzes is de API-prijs gelijk aan de websiteprijs.
Een offerteregel geeft options terug (de gebruikte klantopties, inclusief de standaarden die wij invulden; ongewijzigd terug te sturen in POST /orders) en derivedOptions (alleen ter informatie).
Aantal en oplage
quantity is altijd het aantal stuks. Producten met quantity.pricedBy = "copies" worden per oplage geprijsd: 25.000 flyers is quantity 25000 en de prijs volgt de oplage. Stuur geen options.copies mee; doe je dat toch, dan moet het gelijk zijn aan quantity, anders volgt 422.
Is een oplage voor een samenstelling niet te maken, dan krijg je 422 met het dichtstbijzijnde beschikbare aantal, nooit een prijs voor een andere oplage.
Validatie en fouten
Een kapotte body geeft 400 invalid_request. Een geldige aanvraag die niet uit te voeren is geeft 422 met error.details: per probleem een regel met line, field, code en waar nuttig allowedValues of allowedKeys. Een combinatie die onze drukpartner niet levert (bijvoorbeeld groene uv-lak op 300 g silk) noemt de botsende opties en de passende waarden. Meldingen zijn in het Engels.
Product-id’s en slugs
Gebruik het product-id als vaste sleutel in offertes, orders en je eigen systemen. Slugs verschillen per taal en kunnen veranderen. GET /products/{id} accepteert het id of de slug in elke taal; het veld slugs toont ze allemaal.
Paginering
limit is 1 tot 100 (standaard 25). Een hogere waarde wordt 100; het antwoord bevat dan limit: 100 en limitCapped: true. Volg nextCursor voor de volgende pagina.
Prijzen en afronding
Alle bedragen zijn in eurocenten en regeltotalen zijn leidend. Stukprijzen onder een cent worden niet afgerond: unitNetPrice op een offerteregel en unitPrice op een orderregel zijn exacte decimale strings; unitPriceCents is afgerond op hele centen.
discountCents is je wederverkoperskorting op de consumentenprijs, exclusief btw, per orderregel en in de ordertotalen.
Drukbestanden
fileUrl moet een openbare https-url zijn van een drukklare PDF, PNG of JPG. Bij het plaatsen van de order lezen we de eerste bytes van het bestand (doorverwijzingen worden gevolgd) en controleren we het echte bestandstype. EPS, AI, PS en SVG mogen ook als de url op de bijbehorende extensie eindigt; wij zetten die voor productie om naar PDF, wat een korte handmatige stap kan toevoegen. Een ander type, zoals een .ico of een HTML-pagina, geeft 422 invalid_file_url.
Contactgegevens ontvanger en vervoerder
Vervoerders hebben een e-mailadres en telefoonnummer nodig voor bezorgberichten. Stuur recipient.email en recipient.phone mee zodat die je klant bereiken. Ontbreekt een van beide, dan gebruiken we die van je wederverkopersaccount en neemt de vervoerder dus contact op met jou. Elke order toont carrierContact met emailSource en phoneSource (recipient of reseller).
User-Agent
Stuur een eigen User-Agent-header mee, bijvoorbeeld MijnWinkel-Koppeling/1.0. Sommige standaardwaarden van bibliotheken, zoals Python-urllib/3.x van Python, worden door ons CDN geblokkeerd (fout 1010) voordat ze de API bereiken.
Webhooks
We sturen een POST met JSON naar je endpoint, met de headers X-Printen-Event, X-Printen-Delivery, X-Printen-Timestamp en X-Printen-Signature. Antwoord binnen 10 seconden met een 2xx-status. Mislukt dat, dan proberen we het ongeveer 24 uur lang opnieuw. Een event kan vaker aankomen: ontdubbel op het veld id.
Events
- order.created
- order.submitted
- order.awaiting_funds
- order.payment_failed
- order.in_production
- order.shipped
- order.delivered
- order.cancelled
- order.file_problem
Handtekening controleren
X-Printen-Signature is v1= gevolgd door de hex HMAC-SHA256 van "timestamp.ruwe body" met het geheim van je endpoint. Weiger berichten met een tijdstempel ouder dan 5 minuten.
Node.js
import crypto from 'node:crypto';
export function verifyPrintenWebhook(rawBody, headers, secret) {
const timestamp = headers['x-printen-timestamp'];
const signature = headers['x-printen-signature'] || '';
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = 'v1=' + crypto.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`).digest('hex');
return signature.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}PHP
<?php
function verify_printen_webhook(string $rawBody, string $timestamp, string $signature, string $secret): bool {
if (abs(time() - (int) $timestamp) > 300) return false;
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $signature);
}Python
import hashlib, hmac, time
def verify_printen_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
expected = "v1=" + hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)Fouten
Elke fout heeft de vorm {"error": {"code": "...", "message": "...", "details": [...]}}. 400 = kapotte aanvraag; 422 = geldige aanvraag die niet uit te voeren is (zie details); 401, 403, 404, 409 en 429 zoals gebruikelijk; 500 alleen bij een fout aan onze kant. Bedragen zijn altijd in eurocenten.
OpenAPI-specificatie
De volledige specificatie (OpenAPI 3.1) kun je importeren in Postman, Insomnia of een codegenerator.
openapi.json downloaden