Zum Hauptinhalt springen
Druckprodukte ab 1 Stück

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. 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. 2. Preis abfragen

    POST /quotes mit Positionen und Lieferland liefert Ihren Preis inklusive Rabatt, MwSt. und Versand.

  3. 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. 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

MethodePfadBeschreibungScope
GET/productsList catalogue productscatalog:read
GET/products/{id}Get a product with options and file requirementscatalog:read
POST/quotesPrice lines for a destination countryquotes:write
GET/reseller-productsList your own productsreseller-products:read
POST/reseller-productsCreate 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 productreseller-products:write
DELETE/reseller-products/{id}Delete your own productreseller-products:write
GET/ordersList your orders (newest first)orders:read
POST/ordersPlace an orderorders:write
GET/orders/{id}Get an orderorders:read
POST/orders/{id}/cancelCancel an order while it has not reached productionorders:write
GET/walletBalance and ledger entrieswallet:read
GET/webhooksList webhook endpointswebhooks:read
POST/webhooksCreate a webhook endpoint (the secret is only returned here)webhooks:write
DELETE/webhooks/{id}Delete a webhook endpointwebhooks: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.

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