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é.
- Dans votre espace vendeur, ouvrez API et webhooks.
- 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.
- 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"]}}
| Statut | Signification |
|---|---|
400 | La requête est mal formée (un paramètre, le JSON). |
401 | Clé absente, fausse ou révoquée. |
403 | La clé est en lecture seule. |
404 | Aucune commande de ce type dans votre compte. |
409 | La commande ne peut plus changer : déjà annulée, partie avec le transporteur, ou en cours d'appel. |
422 | Un champ est absent ou faux, ou un produit est inconnu. |
429 | Plus 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ètre | Signification |
|---|---|
status | Seulement les commandes dans ce statut (voir Statuts des commandes). |
updated_since | Seulement les commandes modifiées depuis cette date : le plus simple pour rester synchronisé. |
created_since, created_before | Les commandes reçues sur cette période. |
reference | La commande avec votre propre référence. |
phone | Les commandes de ce numéro client. |
page, limit | La 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
}'
| Champ | Signification |
|---|---|
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.phone2 | Un second numéro. |
address.line * | Rue, immeuble, repères. |
address.city, address.governorate | Ville et gouvernorat, en français, arabe ou anglais. |
items * | Les produits : sku (un produit ou un pack de GET /products), qty et price. |
total | Le 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
| status | Signification |
|---|---|
to_confirm | En attente de notre centre d'appel (nouvelle, à rappeler, injoignable). |
confirmed | Confirmée par le client, pas encore préparée. |
to_ship | En préparation dans notre entrepôt. |
shipped | Remise au transporteur. |
in_transit | En route vers le client. |
delivered | Livrée et payée par le client. |
returning | Refusée ou non livrée, en retour. |
returned | De retour dans notre entrepôt. |
cancelled | Annulé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énement | Quand |
|---|---|
order.created | Une nouvelle commande est arrivée sur votre compte (boutique, API ou à la main). |
order.confirmed | Le client l'a confirmée au téléphone. |
order.cancelled | Elle a été annulée. |
order.shipped | Le colis a été remis au transporteur. |
order.in_transit | Le colis est en route. |
order.delivered | Livrée et payée. |
order.returning | Refusée ou non livrée : elle revient. |
order.returned | De retour dans notre entrepôt. |
ping | Le 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.