Aller au contenu principal
Produits imprimés dès 1 pièce

API revendeurs

Vendez les produits de Printen.eu dans votre propre boutique. Votre commande devient automatiquement une commande de production, est payée avec votre solde ou votre carte enregistrée et part en marque blanche chez votre client : votre nom, aucune marque Printen.eu et aucun prix.

Démarrage rapide

  1. 1. Créez une clé API

    Une fois votre compte revendeur approuvé, créez une clé dans votre tableau de bord. Une clé prk_live_ passe de vraies commandes ; une clé prk_test_ est un bac à sable qui ne facture rien et ne produit rien. La clé complète n’est affichée qu’une seule fois.

  2. 2. Demandez un prix

    POST /quotes avec les lignes et le pays de livraison renvoie votre prix, remise, TVA et livraison comprises.

  3. 3. Passez une commande

    POST /orders avec un en-tête Idempotency-Key (par exemple votre propre numéro de commande). Une nouvelle tentative avec la même clé ne crée jamais une deuxième commande.

  4. 4. Recevez des webhooks

    Enregistrez un point de terminaison https et recevez les changements de statut, par exemple « expédiée » avec le suivi.

# 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"]}'

Authentification

Envoyez votre clé sous la forme Authorization: Bearer prk_live_... Les clés peuvent avoir des scopes ; une clé sans scope peut tout faire.

Avec une clé prk_test_, les commandes sont créées mais ni facturées ni envoyées en production.

Au maximum 120 requêtes par minute et par clé, plus des quotas par compte pour les devis et les commandes. Chaque réponse contient RateLimit-Limit, RateLimit-Remaining et RateLimit-Reset (secondes) pour la limite la plus stricte ; au-delà, vous recevez 429 avec un en-tête Retry-After.

Base URL: https://www.printen.eu/api/reseller/v1

Points de terminaison

MéthodeCheminDescriptionScope
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

Fonctionnement de l’API

Options et valeurs par défaut

GET /products/{id} renvoie exactement les options qu’un client voit sur la page produit, chacune avec sa valeur par défaut et ses valeurs autorisées. N’envoyez que ces clés dans options. Une option omise reçoit la valeur par défaut indiquée, c’est-à-dire la configuration de départ de la page produit. Pour quelques produits (comme les affiches), la page ajuste ce départ après des contrôles de disponibilité en direct ; envoyez donc chaque option qui compte pour vous.

Les options que le site définit automatiquement (procédé d’impression, format de feuille, regroupement, emballage, etc.) sont définies par nous de la même manière et ne peuvent pas être envoyées : une clé inconnue ou automatique renvoie 422 invalid_options avec la liste des clés autorisées. Pour les mêmes choix, le prix de l’API est égal au prix du site.

Une ligne de devis renvoie options (les options client utilisées, y compris les valeurs par défaut ajoutées ; à renvoyer telles quelles dans POST /orders) et derivedOptions (en lecture seule, pour information).

Quantité et tirage

quantity est toujours le nombre de pièces. Les produits avec quantity.pricedBy = "copies" sont tarifés par tirage : 25 000 flyers correspondent à quantity 25000 et le prix suit le tirage. N’envoyez pas options.copies ; si vous le faites, il doit être égal à quantity, sinon vous recevez 422.

Si un tirage n’est pas réalisable pour une configuration, vous recevez 422 avec la quantité disponible la plus proche, jamais un prix pour un autre tirage.

Validation et erreurs

Un corps mal formé renvoie 400 invalid_request. Une requête valide qui ne peut pas être exécutée renvoie 422 avec error.details : une entrée par problème avec line, field, code et, si utile, allowedValues ou allowedKeys. Une combinaison que notre partenaire d’impression ne propose pas (par exemple un vernis UV vert sur 300 g silk) indique les options en conflit et les valeurs compatibles. Les messages sont en anglais.

Identifiants et slugs des produits

Utilisez l’identifiant du produit comme clé stable dans les devis, les commandes et vos propres systèmes. Les slugs varient selon la langue et peuvent changer. GET /products/{id} accepte l’identifiant ou le slug dans n’importe quelle langue ; le champ slugs les liste tous.

Prix et arrondis

Tous les montants sont en centimes d’euro et les totaux de ligne font foi. Les prix unitaires inférieurs à un centime ne sont pas arrondis : unitNetPrice sur une ligne de devis et unitPrice sur une ligne de commande sont des chaînes décimales exactes ; unitPriceCents est arrondi au centime.

discountCents est votre remise revendeur sur le prix public, hors TVA, par ligne de commande et dans les totaux de la commande.

Fichiers d’impression

fileUrl doit être une URL https publique d’un PDF, PNG ou JPG prêt à imprimer. Lors de la commande, nous lisons les premiers octets du fichier (en suivant les redirections) et vérifions son type réel. Les fichiers EPS, AI, PS et SVG sont aussi acceptés si l’URL se termine par l’extension correspondante ; nous les convertissons en PDF avant la production, ce qui peut ajouter une courte étape manuelle. Un autre type, comme un .ico ou une page HTML, renvoie 422 invalid_file_url.

Coordonnées du destinataire et transporteurs

Les transporteurs ont besoin d’une adresse e-mail et d’un numéro de téléphone pour les messages de livraison. Envoyez recipient.email et recipient.phone pour qu’ils atteignent votre client. S’il en manque un, nous utilisons celui de votre compte revendeur, et le transporteur vous contacte. Chaque commande indique carrierContact avec emailSource et phoneSource (recipient ou reseller).

User-Agent

Envoyez votre propre en-tête User-Agent, par exemple MaBoutique-Integration/1.0. Certaines valeurs par défaut génériques de bibliothèques, comme Python-urllib/3.x de Python, sont bloquées par notre CDN (erreur 1010) avant d’atteindre l’API.

Webhooks

Nous envoyons un POST JSON à votre point de terminaison, avec les en-têtes X-Printen-Event, X-Printen-Delivery, X-Printen-Timestamp et X-Printen-Signature. Répondez avec un statut 2xx dans les 10 secondes. En cas d’échec, nous réessayons pendant environ 24 heures. Un événement peut arriver plusieurs fois : dédupliquez sur le champ id.

Événements

  • order.created
  • order.submitted
  • order.awaiting_funds
  • order.payment_failed
  • order.in_production
  • order.shipped
  • order.delivered
  • order.cancelled
  • order.file_problem

Vérifier la signature

X-Printen-Signature vaut v1= suivi du HMAC-SHA256 hexadécimal de « horodatage.corps brut » avec le secret de votre point de terminaison. Refusez les messages dont l’horodatage date de plus de 5 minutes.

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)

Erreurs

Chaque erreur a la forme {"error": {"code": "...", "message": "...", "details": [...]}}. 400 = requête mal formée ; 422 = requête valide qui ne peut pas être exécutée (voir details) ; 401, 403, 404, 409 et 429 comme d’habitude ; 500 uniquement en cas d’erreur de notre côté. Les montants sont toujours en centimes d’euro.

Spécification OpenAPI

Importez la spécification complète (OpenAPI 3.1) dans Postman, Insomnia ou un générateur de code.

Télécharger openapi.json