Se rendre au contenu

Développeurs

API et webhooks Fulfiller

Connectez votre boutique, votre logiciel de gestion ou votre outil d'automatisation à Fulfiller : envoyez des commandes, suivez leur statut et votre stock, et recevez un événement dès qu'une commande change.

Démarrer

L'API est ouverte aux vendeurs dont le compte est validé.

  1. Dans votre espace vendeur, ouvrez API et webhooks.
  2. Créez une clé : « lecture seule » pour lire les commandes et les produits, ou « lecture et écriture » pour aussi créer et annuler des commandes.
  3. Copiez la clé : elle n'est affichée qu'une fois. Gardez-la sur votre serveur, jamais dans une page web ou une application mobile.

Adresse de l'API : https://fulfiller.net/api/v1

Requêtes et réponses sont en JSON (UTF-8). Les dates sont au format ISO 8601 en UTC, les montants en dinars tunisiens (TND).

curl https://fulfiller.net/api/v1/me \
  -H "Authorization: Bearer ffk_your_key"

{"account": {"id": 42, "name": "Najma Store"},
 "key": {"name": "My ERP", "scope": "write", "ends_with": "x9Qa"},
 "webhooks": 1}

Authentification

Envoyez la clé dans l'en-tête Authorization, après le mot Bearer. Une clé absente, fausse ou révoquée reçoit le statut 401 ; une clé en lecture seule qui tente de modifier une commande reçoit 403.

Authorization: Bearer ffk_your_key

Révoquez une clé à tout moment dans API et webhooks : elle cesse de fonctionner aussitôt.

Erreurs et limites

Une erreur répond avec un statut HTTP et un corps JSON :

{"error": {"code": "unknown_product", "message": "Unknown product(s): KETO-90. Use the SKUs of GET /api/v1/products.", "skus": ["KETO-90"]}}
StatutSignification
400La requête est mal formée (un paramètre, le JSON).
401Clé absente, fausse ou révoquée.
403La clé est en lecture seule.
404Aucune commande de ce type dans votre compte.
409La commande ne peut plus changer : déjà annulée, partie avec le transporteur, ou en cours d'appel.
422Un champ est absent ou faux, ou un produit est inconnu.
429Plus de 120 requêtes en une minute : attendez les secondes indiquées dans l'en-tête Retry-After.

Lire les commandes

GET /orders

Vos commandes, les plus récentes d'abord, 50 par page (jusqu'à 100 avec limit).

ParamètreSignification
statusSeulement les commandes dans ce statut (voir Statuts des commandes).
updated_sinceSeulement les commandes modifiées depuis cette date : le plus simple pour rester synchronisé.
created_since, created_beforeLes commandes reçues sur cette période.
referenceLa commande avec votre propre référence.
phoneLes commandes de ce numéro client.
page, limitLa page voulue et sa taille.
curl "https://fulfiller.net/api/v1/orders?status=delivered&updated_since=2026-10-01T00:00:00Z" \
  -H "Authorization: Bearer ffk_your_key"

{"data": [{
   "id": 1234, "number": "FF-001234", "reference": "1042", "source": "api", "store": null,
   "status": "delivered", "confirmation_status": "confirmed", "delivery_status": "delivered",
   "customer": {"name": "Amina B.", "phone": "+21698123456", "phone2": null},
   "address": {"line": "Route de Tunis km 5", "city": "Sakiet Ezzit", "governorate": "sfax", "governorate_name": "Sfax"},
   "total": 89.0, "currency": "TND",
   "items": [{"sku": "KETO-60", "name": "Keto 60 caps", "qty": 2, "price": 44.5}],
   "carrier": "First Delivery", "tracking_number": "FD123456789", "return_reason": null,
   "recovered_cart": false,
   "created_at": "2026-10-28T09:12:03Z", "confirmed_at": "2026-10-28T10:01:44Z", "updated_at": "2026-10-30T15:20:11Z"}],
 "page": 1, "limit": 50, "total": 1, "has_more": false}

GET /orders/{number}

Une commande, par son numéro Fulfiller (FF-001234) ou son id, avec l'historique de suivi du transporteur dans history.

Créer une commande

POST /orders lecture et écriture

Envoyez la commande telle que votre boutique l'a reçue. Notre centre d'appel appelle le client pour la confirmer, puis nous emballons, expédions et encaissons, comme pour vos autres commandes.

curl -X POST https://fulfiller.net/api/v1/orders \
  -H "Authorization: Bearer ffk_your_key" -H "Content-Type: application/json" \
  -d '{
    "reference": "1042",
    "customer": {"name": "Amina B.", "phone": "98 123 456"},
    "address": {"line": "Route de Tunis km 5", "city": "Sakiet Ezzit", "governorate": "Sfax"},
    "items": [{"sku": "KETO-60", "qty": 2, "price": 44.5}],
    "total": 89
  }'
ChampSignification
reference *Votre propre numéro unique pour la commande. Renvoyé, il redonne la commande déjà créée : rien n'est créé deux fois.
customer.name *Le nom du client.
customer.phone *Un numéro tunisien, écrit de n'importe quelle façon.
customer.phone2Un second numéro.
address.line *Rue, immeuble, repères.
address.city, address.governorateVille et gouvernorat, en français, arabe ou anglais.
items *Les produits : sku (un produit ou un pack de GET /products), qty et price.
totalLe montant à encaisser ; sans lui, la somme des articles.
type« order », ou « abandoned_cart » pour un paiement que le client n'a pas fini : notre centre d'appel le rappelle 30 minutes plus tard pour lui proposer de confirmer.

La réponse est 201 avec la commande quand elle est créée, 200 avec la commande déjà connue si la référence a déjà été envoyée. Un SKU inconnu reçoit 422 avec la liste des produits inconnus.

Annuler une commande

POST /orders/{number}/cancel lecture et écriture

Pour un client qui ne veut plus la commande. Avant confirmation, elle est annulée aussitôt ; après confirmation, seulement tant que le colis est encore dans notre entrepôt. Indiquez la raison.

curl -X POST https://fulfiller.net/api/v1/orders/FF-001234/cancel \
  -H "Authorization: Bearer ffk_your_key" -H "Content-Type: application/json" \
  -d '{"reason": "The customer ordered twice"}'

Produits et stock

GET /products

Ce que vous pouvez vendre, avec le SKU à mettre dans vos commandes : vos propres produits, les produits marketplace de votre catalogue (à votre prix de vente) et vos packs. stock.available est le nombre d'unités disponibles à la vente dans notre entrepôt ; stock.status vaut ok, low, out ou order (fourni sur commande).

{"data": [
  {"type": "marketplace", "sku": "KETO-60", "name": "Keto 60 caps", "price": 44.5,
   "stock": {"available": 120, "status": "ok"}},
  {"type": "pack", "sku": "PACK-DUO", "name": "Duo pack", "price": 79.0,
   "items": [{"sku": "KETO-60", "qty": 2}], "stock": null}
]}

Statuts des commandes

statusSignification
to_confirmEn attente de notre centre d'appel (nouvelle, à rappeler, injoignable).
confirmedConfirmée par le client, pas encore préparée.
to_shipEn préparation dans notre entrepôt.
shippedRemise au transporteur.
in_transitEn route vers le client.
deliveredLivrée et payée par le client.
returningRefusée ou non livrée, en retour.
returnedDe retour dans notre entrepôt.
cancelledAnnulée.

confirmation_status et delivery_status donnent le détail de chaque étape.

Webhooks

Un webhook envoie un événement à votre adresse dès qu'une commande change, sans avoir à interroger l'API sans cesse. Ajoutez jusqu'à 5 adresses https dans API et webhooks et choisissez les événements de chacune.

ÉvénementQuand
order.createdUne nouvelle commande est arrivée sur votre compte (boutique, API ou à la main).
order.confirmedLe client l'a confirmée au téléphone.
order.cancelledElle a été annulée.
order.shippedLe colis a été remis au transporteur.
order.in_transitLe colis est en route.
order.deliveredLivrée et payée.
order.returningRefusée ou non livrée : elle revient.
order.returnedDe retour dans notre entrepôt.
pingLe test envoyé par le bouton « Envoyer un test ».
POST https://your-site.example/fulfiller/webhook
Content-Type: application/json; charset=utf-8
X-Fulfiller-Event: order.delivered
X-Fulfiller-Delivery: evt_5f0c2a9d4b7e4f61a2c3d4e5f6a7b8c9
X-Fulfiller-Timestamp: 1793372411
X-Fulfiller-Signature: sha256=4c1f…

{"id": "evt_5f0c2a9d4b7e4f61a2c3d4e5f6a7b8c9", "event": "order.delivered",
 "created_at": "2026-10-30T15:20:11Z",
 "data": {"id": 1234, "number": "FF-001234", "reference": "1042", "status": "delivered", …}}

data est la commande telle que GET /orders/{number} la montre, lue au premier envoi de l'événement.

  • X-Fulfiller-Delivery est l'identifiant propre de l'événement : le même à chaque nouvel essai. Gardez-le pour ignorer un événement déjà traité.
  • Répondez avec un statut 2xx en moins de 10 secondes, puis faites votre traitement. Toute autre réponse, ou aucune, est renvoyée après 1 minute, 5 minutes, 30 minutes, 2 heures, 6 heures et 24 heures.
  • Un webhook qui échoue pendant une journée entière est mis en pause et vous êtes prévenu. Réactivez-le dans API et webhooks : les événements en attente sont envoyés.
  • Les événements peuvent arriver dans le désordre ou en double : fiez-vous à status et updated_at dans data.

Vérifier une signature

Chaque webhook a son propre secret (whsec_…), affiché dans API et webhooks. Calculez le HMAC-SHA256 de l'horodatage, d'un point et du corps brut, avec le secret comme clé, et comparez-le à la valeur après sha256= dans X-Fulfiller-Signature. Refusez un horodatage de plus de 5 minutes.

Python

import hashlib, hmac, time

def is_from_fulfiller(secret, headers, raw_body):
    stamp = headers["X-Fulfiller-Timestamp"]
    if abs(time.time() - int(stamp)) > 300:
        return False
    expected = hmac.new(secret.encode(), stamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    given = headers.get("X-Fulfiller-Signature", "").removeprefix("sha256=")
    return hmac.compare_digest(expected, given)

PHP

$raw = file_get_contents('php://input');
$stamp = $_SERVER['HTTP_X_FULFILLER_TIMESTAMP'] ?? '0';
$given = substr($_SERVER['HTTP_X_FULFILLER_SIGNATURE'] ?? '', 7);
$expected = hash_hmac('sha256', $stamp . '.' . $raw, $secret);
if (abs(time() - (int) $stamp) > 300 || !hash_equals($expected, $given)) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true);   // $event['event'], $event['data']['status']…

Node.js (Express)

const crypto = require('crypto');

app.post('/fulfiller/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const stamp = req.get('X-Fulfiller-Timestamp') || '0';
  const given = (req.get('X-Fulfiller-Signature') || '').replace('sha256=', '');
  const expected = crypto.createHmac('sha256', process.env.FULFILLER_SECRET)
    .update(stamp + '.').update(req.body).digest('hex');
  const same = given.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
  if (!same || Math.abs(Date.now() / 1000 - Number(stamp)) > 300) return res.sendStatus(401);
  res.sendStatus(200);
  const event = JSON.parse(req.body);   // event.event, event.data.status…
});

Sans développeur

Connectez votre boutique dans Boutiques connectées : Shopify, WooCommerce, YouCan, TikTak PRO, Converty ou un Google Sheet. Les commandes, et les paniers abandonnés si vous le souhaitez, arrivent toutes seules.

Cookies

Nous utilisons uniquement les cookies nécessaires au fonctionnement du site : votre connexion, votre langue et vos choix d'affichage. Aucun cookie publicitaire. Avec votre accord, nous pouvons aussi mesurer l'audience pour améliorer la plateforme.