Reseller API
Verkaufen Sie Produkte von Printen.eu in Ihrem eigenen Shop. Ihre Bestellung wird automatisch zum Produktionsauftrag, wird aus Ihrem Guthaben oder mit Ihrer hinterlegten Karte bezahlt und geht als White Label an Ihre Kunden: Ihr Name, kein Printen.eu-Branding und keine Preise.
Schnellstart
1. API-Schlüssel erstellen
Nach der Freigabe Ihres Wiederverkäuferkontos erstellen Sie im Dashboard einen Schlüssel. Ein prk_live_-Schlüssel erzeugt echte Bestellungen, ein prk_test_-Schlüssel ist eine Sandbox ohne Abrechnung und ohne Produktion. Der vollständige Schlüssel wird nur einmal angezeigt.
2. Preis abfragen
POST /quotes mit Positionen und Lieferland liefert Ihren Preis inklusive Rabatt, MwSt. und Versand.
3. Bestellung aufgeben
POST /orders mit einem Idempotency-Key-Header (zum Beispiel Ihrer eigenen Bestellnummer). Eine Wiederholung mit demselben Schlüssel erzeugt nie eine zweite Bestellung.
4. Webhooks empfangen
Registrieren Sie einen https-Endpunkt und erhalten Sie Statusänderungen, etwa „versendet“ mit Sendungsnummer.
# 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"]}'Authentifizierung
Senden Sie Ihren Schlüssel als Authorization: Bearer prk_live_... Schlüssel können Scopes haben; ein Schlüssel ohne Scopes darf alles.
Mit einem prk_test_-Schlüssel werden Bestellungen angelegt, aber weder abgerechnet noch an die Produktion übergeben.
Pro Schlüssel höchstens 120 Anfragen pro Minute, dazu Kontingente pro Konto für Angebote und Bestellungen. Jede Antwort enthält RateLimit-Limit, RateLimit-Remaining und RateLimit-Reset (Sekunden) für das strengste geltende Limit; darüber folgt 429 mit einem Retry-After-Header.
Base URL: https://www.printen.eu/api/reseller/v1
Endpunkte
| Methode | Pfad | Beschreibung | 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 |
So funktioniert die API
Optionen und Standardwerte
GET /products/{id} liefert genau die Optionen, die Kunden auf der Produktseite sehen, jeweils mit Standardwert und erlaubten Werten. Senden Sie nur diese Schlüssel in options. Lassen Sie eine Option weg, erhält sie den dort angezeigten Standardwert, also die Startkonfiguration der Produktseite. Bei einigen Produkten (etwa Postern) passt die Seite den Start nach Live-Verfügbarkeitsprüfungen an; senden Sie daher jede Option mit, die Ihnen wichtig ist.
Optionen, die die Website automatisch setzt (Druckverfahren, Bogenformat, Bündelung, Verpackung usw.), setzen wir auf dieselbe Weise und können nicht mitgesendet werden: Ein unbekannter oder automatischer Schlüssel ergibt 422 invalid_options mit der Liste der erlaubten Schlüssel. Bei gleicher Auswahl entspricht der API-Preis dem Website-Preis.
Eine Angebotszeile liefert options (die verwendeten Kundenoptionen inklusive der von uns ergänzten Standardwerte; unverändert in POST /orders zurücksendbar) und derivedOptions (nur zur Information).
Menge und Auflage
quantity ist immer die Stückzahl. Produkte mit quantity.pricedBy = "copies" werden pro Auflage berechnet: 25.000 Flyer sind quantity 25000, und der Preis folgt der Auflage. Senden Sie kein options.copies; falls doch, muss es quantity entsprechen, sonst folgt 422.
Ist eine Auflage für eine Konfiguration nicht herstellbar, erhalten Sie 422 mit der nächstmöglichen Menge statt eines Preises für eine andere Auflage.
Validierung und Fehler
Ein fehlerhafter Body ergibt 400 invalid_request. Eine gültige Anfrage, die nicht ausführbar ist, ergibt 422 mit error.details: pro Problem ein Eintrag mit line, field, code und, wo sinnvoll, allowedValues oder allowedKeys. Eine Kombination, die unser Druckpartner nicht anbietet (zum Beispiel grüner UV-Lack auf 300 g Silk), nennt die kollidierenden Optionen und die passenden Werte. Meldungen sind auf Englisch.
Produkt-IDs und Slugs
Verwenden Sie die Produkt-ID als festen Schlüssel in Angeboten, Bestellungen und Ihren eigenen Systemen. Slugs unterscheiden sich je Sprache und können sich ändern. GET /products/{id} akzeptiert die ID oder den Slug in jeder Sprache; das Feld slugs listet alle auf.
Paginierung
limit liegt zwischen 1 und 100 (Standard 25). Ein höherer Wert wird auf 100 begrenzt; die Antwort enthält dann limit: 100 und limitCapped: true. Folgen Sie nextCursor für die nächste Seite.
Preise und Rundung
Alle Beträge sind in Eurocent, und Zeilensummen sind maßgeblich. Stückpreise unter einem Cent werden nicht gerundet: unitNetPrice in einer Angebotszeile und unitPrice in einer Bestellposition sind exakte Dezimal-Strings; unitPriceCents ist auf ganze Cent gerundet.
discountCents ist Ihr Wiederverkäuferrabatt auf den Endkundenpreis, ohne MwSt., je Bestellposition und in den Bestellsummen.
Druckdateien
fileUrl muss eine öffentliche https-URL einer druckfertigen PDF-, PNG- oder JPG-Datei sein. Beim Aufgeben der Bestellung lesen wir die ersten Bytes der Datei (Weiterleitungen werden verfolgt) und prüfen den tatsächlichen Dateityp. EPS, AI, PS und SVG sind ebenfalls erlaubt, wenn die URL auf die passende Endung endet; wir wandeln sie vor der Produktion in PDF um, was einen kurzen manuellen Schritt bedeuten kann. Ein anderer Typ, etwa eine .ico-Datei oder eine HTML-Seite, ergibt 422 invalid_file_url.
Kontaktdaten des Empfängers und Versanddienstleister
Versanddienstleister benötigen eine E-Mail-Adresse und Telefonnummer für Zustellnachrichten. Senden Sie recipient.email und recipient.phone, damit diese Ihre Kunden erreichen. Fehlt eine Angabe, verwenden wir die Ihres Wiederverkäuferkontos, und der Versanddienstleister kontaktiert Sie. Jede Bestellung zeigt carrierContact mit emailSource und phoneSource (recipient oder reseller).
User-Agent
Senden Sie einen eigenen User-Agent-Header, zum Beispiel MeinShop-Integration/1.0. Einige generische Standardwerte von Bibliotheken, etwa Python-urllib/3.x von Python, werden von unserem CDN blockiert (Fehler 1010), bevor sie die API erreichen.
Webhooks
Wir senden einen POST mit JSON an Ihren Endpunkt, mit den Headern X-Printen-Event, X-Printen-Delivery, X-Printen-Timestamp und X-Printen-Signature. Antworten Sie innerhalb von 10 Sekunden mit einem 2xx-Status. Schlägt das fehl, versuchen wir es etwa 24 Stunden lang erneut. Ein Event kann mehrfach ankommen: deduplizieren Sie über das Feld id.
Events
- order.created
- order.submitted
- order.awaiting_funds
- order.payment_failed
- order.in_production
- order.shipped
- order.delivered
- order.cancelled
- order.file_problem
Signatur prüfen
X-Printen-Signature ist v1= gefolgt vom hex HMAC-SHA256 von „Zeitstempel.roher Body“ mit dem Geheimnis Ihres Endpunkts. Lehnen Sie Nachrichten mit einem Zeitstempel älter als 5 Minuten ab.
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)Fehler
Jeder Fehler hat die Form {"error": {"code": "...", "message": "...", "details": [...]}}. 400 = fehlerhafte Anfrage; 422 = gültige Anfrage, die nicht ausführbar ist (siehe details); 401, 403, 404, 409 und 429 wie üblich; 500 nur bei einem Fehler auf unserer Seite. Beträge sind immer in Eurocent.
OpenAPI-Spezifikation
Die vollständige Spezifikation (OpenAPI 3.1) können Sie in Postman, Insomnia oder einen Codegenerator importieren.
openapi.json herunterladen