Intégrer Shede à votre marketplace
L’API Shede permet à une marketplace de livraison de récupérer le menu d’un restaurant, de lui transmettre des commandes et de suivre leur préparation en temps réel. Toutes les requêtes et réponses sont en JSON, sur HTTPS.
Adresse de base
https://shede.tacynt.com/api/v1
- 1Le restaurant vous transmet sa clé d’API.
- 2Vous synchronisez son menu avec GET /menu.
- 3Vous envoyez chaque commande avec POST /orders.
- 4Le restaurant l’accepte ou la refuse dans Shede ; vous êtes prévenu par webhook.
- 5Votre livreur la récupère : vous l’indiquez avec POST /orders/{id}/delivery-events.
Authentification
Chaque point de vente possède sa propre clé (shd_live_…), générée par l’administrateur de l’organisation dans Shede (Points & licence → fiche du point → API). La clé identifie le point : aucun autre identifiant n’est nécessaire. Une clé de test (shd_test_…) permet d’essayer l’API sans rien enregistrer (voir Mode test).
Envoyez-la dans l’en-tête Authorization (l’en-tête X-Api-Key est aussi accepté) :
Authorization: Bearer shd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Gardez la clé côté serveur : ne l’intégrez jamais dans une application mobile ou une page web. Une clé régénérée ou révoquée cesse immédiatement de fonctionner.
Réponses et erreurs
Succès : le résultat est dans data.
{
"data": {
"…": "…"
}
}Erreur : un code stable et un message lisible (en français ou en anglais selon l’en-tête Accept-Language).
{
"error": {
"code": "validation_error",
"message": "…",
"details": [
{
"field": "items.0.product_id",
"code": "product_unavailable"
}
]
}
}| HTTP | code | Signification |
|---|---|---|
| 401 | missing_key, invalid_key | Clé absente, inconnue ou révoquée |
| 403 | module_disabled, license_inactive, point_inactive | Module API absent de la licence, licence expirée ou point désactivé |
| 403 | quota_exceeded | Quota mensuel de commandes de l’organisation atteint |
| 404 | not_found | Ressource introuvable |
| 409 | point_paused | Le restaurant a mis les commandes marketplace en pause |
| 409 | invalid_state | Action impossible dans l’état de la commande |
| 422 | validation_error | Requête invalide : details précise les champs en cause |
| 429 | rate_limited | Trop de requêtes (voir Retry-After) |
| 500 | internal_error | Erreur interne, réessayez plus tard |
Montants en francs CFA (XAF), entiers. price est le prix saisi par le restaurant ; price_with_tax le prix TTC payé par le client, selon le régime de TVA du point (tax.prices_include_tax).
Limites
60 requêtes par minute et par point. Chaque réponse indique X-RateLimit-Limit et X-RateLimit-Remaining ; au-delà, réponse 429 avec l’en-tête Retry-After (en secondes).
Le nombre de commandes reçues par l’API chaque mois dépend de la licence du restaurant. Une fois le quota atteint, POST /orders répond 403 quota_exceeded jusqu’au mois suivant.
Mode test
Pour développer et tester votre intégration sans toucher aux données du restaurant, utilisez une clé de test (shd_test_…), générée au même endroit que la clé de production. Mêmes adresses, mêmes réponses : seul le préfixe de la clé change.
Authorization: Bearer shd_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- Rien n’est enregistré : aucune commande, facture, sortie de stock, écriture comptable ni consommation du quota. Le restaurant ne voit rien.
- GET /point et GET /menu renvoient les vraies données du point ; GET /point indique livemode: false.
- POST /orders applique exactement les mêmes contrôles (produits, disponibilité, accompagnements, pause) et calcule les mêmes montants (promotions, TVA), puis renvoie une commande simulée avec livemode: false.
- L’id d’une commande de test commence par test_ et contient la commande : conservez-le, c’est lui qu’il faut utiliser avec GET /orders/{id} (la recherche par external_id n’est pas disponible en test). Il reste valable 7 jours.
- GET /orders renvoie toujours une liste vide.
- Annulation et événements du livreur suivent les mêmes règles et renvoient le résultat simulé, sans le conserver : un GET suivant renvoie la commande telle que la fait évoluer le temps.
Évolution automatique du statut
| Depuis la création | status |
|---|---|
| 0 – 30 s | pending_acceptance |
| 30 – 60 s | accepted (prep_minutes: 15) |
| 60 – 120 s | preparing |
| après 2 min | ready |
Pour tester un refus, terminez external_id par -reject : la commande passe à rejected au bout de 30 secondes. Avec delivery_by: restaurant, la course est attribuée à 2 min, en route à 3 min (picked_up) et livrée à 5 min (delivered).
Webhooks de test
POST/api/v1/sandbox/webhooks
Aucun webhook n’est émis automatiquement en mode test. Déclenchez-en un à la demande : il est envoyé tout de suite à l’adresse de webhook du point, signé avec le même secret, marqué livemode: false, et la réponse indique le résultat de l’envoi. type : order.status_changed (avec order_id, et status pour forcer le statut annoncé), menu.updated, point.paused, point.resumed ou ping.
curl -X POST https://shede.tacynt.com/api/v1/sandbox/webhooks \
-H "Authorization: Bearer $SHEDE_TEST_KEY" -H "Content-Type: application/json" \
-d '{ "type": "order.status_changed", "order_id": "test_…", "status": "ready" }'{
"data": {
"configured": true,
"id": "evt_test_…",
"delivered": true,
"status_code": 200,
"error": null
}
}Le point de vente
GET/api/v1/point
Informations du restaurant associé à la clé, dont son régime de TVA. accepting_orders indique s’il accepte des commandes en ce moment (false si le restaurant a mis les commandes marketplace en pause — paused — ou si le point est désactivé) : vérifiez-le au démarrage et chaque fois qu’un webhook point.paused / point.resumed a pu être manqué.
curl https://shede.tacynt.com/api/v1/point \ -H "Authorization: Bearer $SHEDE_KEY"
{
"data": {
"id": "3f9d78e3-…",
"livemode": true,
"name": "Restaurant Le Wouri",
"type": "RESTAURANT",
"phone": "+237671234567",
"address": "Rue Joss",
"city": "Douala",
"country": "Cameroun",
"currency": "XAF",
"tax": {
"rate": 19.25,
"prices_include_tax": true
},
"takeaway_fee": 200,
"logo_url": null,
"paused": false,
"accepting_orders": true,
"restaurant_delivery": true
}
}Les catégories
GET/api/v1/categories
Catégories du menu, dans l’ordre choisi par le restaurant : chaque catégorie principale (parent_id: null) suivie de ses sous-catégories (parent_id = id de la catégorie). Un seul niveau de sous-catégories. position donne l’ordre parmi les catégories de même niveau. Un produit apparaît dans chacune de ses catégories ou sous-catégories ; pour afficher une catégorie principale, incluez aussi les produits de ses sous-catégories. Les catégories masquées par le restaurant (et les sous-catégories d’une catégorie masquée) ne sont pas renvoyées. Elles changent avec le webhook menu.updated.
curl https://shede.tacynt.com/api/v1/categories \ -H "Authorization: Bearer $SHEDE_KEY"
{
"data": [
{
"id": "1c7d…",
"name": "Plats",
"parent_id": null,
"position": 1
},
{
"id": "4fa2…",
"name": "Poulet",
"parent_id": "1c7d…",
"position": 1
},
{
"id": "7b51…",
"name": "Poisson",
"parent_id": "1c7d…",
"position": 2
},
{
"id": "9e03…",
"name": "Boissons",
"parent_id": null,
"position": 2
}
]
}Zones de livraison du restaurant
GET/api/v1/delivery-zones
Si le restaurant livre lui-même (GET /point → restaurant_delivery: true), voici les quartiers ou secteurs qu’il dessert et les frais de chacun. fee est le montant saisi par le restaurant, fee_with_tax celui payé par le client. Liste vide si le restaurant ne livre pas. Relisez-la à réception du webhook delivery_zones.updated.
curl https://shede.tacynt.com/api/v1/delivery-zones \ -H "Authorization: Bearer $SHEDE_KEY"
{
"data": [
{
"id": "5b20…",
"name": "Bonamoussadi",
"fee": 1000,
"fee_with_tax": 1000
},
{
"id": "8e41…",
"name": "Akwa",
"fee": 1500,
"fee_with_tax": 1500
}
]
}Deux façons de livrer une commande (champ delivery_by de POST /orders) :
| delivery_by | |
|---|---|
| marketplace | Par défaut. Votre livreur récupère la commande au restaurant ; vous suivez la course avec POST /orders/{id}/delivery-events. |
| restaurant | Un livreur du restaurant livre le client dans la zone choisie (delivery.zone_id). Les frais de la zone s’ajoutent au total. Le suivi se fait dans Shede : delivery.status passe de to_assign à assigned, in_transit puis delivered (ou failed). La vente est clôturée à la livraison. |
{
"external_id": "GLV-48214",
"customer": {
"name": "Aïcha N.",
"phone": "699123456"
},
"items": [
{
"product_id": "9b1e…",
"quantity": 1
}
],
"delivery_by": "restaurant",
"delivery": {
"zone_id": "5b20…",
"landmark": "Face pharmacie du Rond-point",
"district": "Bonamoussadi",
"lat": 4.0912,
"lng": 9.7405
}
}Cycle de vie d’une commande
- Le restaurant accepte ou refuse chaque commande dans Shede, et annonce un temps de préparation (prep_minutes, estimated_ready_at).
- Shede fait foi pour les prix : le total (TVA incluse) est calculé à partir des prix du restaurant et renvoyé dans amounts.
- Vous encaissez le client et reversez le montant au restaurant : la vente est clôturée dans Shede quand votre livreur récupère la commande (PICKED_UP).
| Statuts | |
|---|---|
| pending_acceptance | En attente de la décision du restaurant |
| accepted | Acceptée, préparation pas encore commencée |
| preparing | En préparation |
| ready | Prête à être récupérée |
| picked_up | Récupérée par le livreur (vente clôturée) |
| delivered | Livrée au client |
| delivery_failed | Livraison par le restaurant échouée (delivery.note) |
| rejected | Refusée par le restaurant (rejection_reason) |
| cancelled | Annulée (cancel_reason) |
Créer une commande
POST/api/v1/orders
curl -X POST https://shede.tacynt.com/api/v1/orders \
-H "Authorization: Bearer $SHEDE_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "GLV-48213",
"partner": "glovo",
"customer": {
"name": "Aïcha N.",
"phone": "699123456"
},
"items": [
{
"product_id": "9b1e…",
"quantity": 2,
"notes": "Bien pimenté",
"accompaniments": [
{
"id": "a71c…",
"quantity": 1
}
]
}
],
"notes": "Sonner au portail",
"delivery_address": "Bonamoussadi, face Total"
}'Champs
| Champ | Obligatoire | Description |
|---|---|---|
| external_id | oui | Numéro de la commande chez vous (unique par point) |
| partner | non | Nom de votre marketplace (glovo, yango…) |
| customer.name | non | Nom du client |
| customer.phone | oui | Téléphone du client |
| items[].product_id | oui | Id d’un produit de GET /menu |
| items[].quantity | oui | 1 à 99 |
| items[].notes | non | Instruction pour la cuisine |
| items[].accompaniments[] | non | id d’un accompagnement du produit ; quantity ≤ max_quantity (1 par défaut) |
| notes | non | Note sur la commande |
| delivery_address | non | Adresse de livraison (information pour le restaurant) |
| delivery_by | non | marketplace (par défaut) ou restaurant |
| delivery.zone_id | non | Obligatoire si delivery_by = restaurant : id d’une zone de GET /delivery-zones |
| delivery.landmark | non | Obligatoire si delivery_by = restaurant : point de repère (3 caractères min.) |
| delivery.district / city | non | Quartier (par défaut le nom de la zone) et ville |
| delivery.lat / lng | non | Position GPS du client |
Réponse 201 : la commande créée, au statut pending_acceptance.
Idempotence : renvoyer la même requête (même external_id) ne crée pas de doublon ; la commande existante est renvoyée avec le code 200. En cas de coupure réseau, renvoyez simplement la requête.
Erreurs de validation (422) — details[].code :
product_not_foundproduct_unavailableproduct_not_deliverableaccompaniment_not_offeredaccompaniment_unavailableaccompaniment_quantity_exceededrestaurant_delivery_not_offereddelivery_zone_unavailable
Consulter une commande
GET/api/v1/orders/{id}
{id} est l’id Shede ou votre external_id.
curl https://shede.tacynt.com/api/v1/orders/GLV-48213 \ -H "Authorization: Bearer $SHEDE_KEY"
{
"data": {
"id": "c42f…",
"livemode": true,
"external_id": "GLV-48213",
"partner": "glovo",
"status": "accepted",
"created_at": "2026-10-01T12:15:00.000Z",
"updated_at": "2026-10-01T12:16:10.000Z",
"accepted_at": "2026-10-01T12:16:10.000Z",
"prep_minutes": 20,
"estimated_ready_at": "2026-10-01T12:36:10.000Z",
"rejection_reason": null,
"cancel_reason": null,
"invoice_number": null,
"customer": {
"name": "Aïcha N.",
"phone": "+237699123456"
},
"notes": "Sonner au portail",
"items": [
{
"id": "e10a…",
"product_id": "9b1e…",
"name": "Poulet DG",
"quantity": 2,
"unit_price": 4500,
"total_price": 9000,
"notes": "Bien pimenté",
"accompaniments": [
{
"id": "a71c…",
"name": "Plantains mûrs",
"quantity": 2,
"unit_price": 0,
"total_price": 0
}
]
}
],
"amounts": {
"currency": "XAF",
"subtotal": 9000,
"discount": 0,
"tax": 1453,
"total": 9000
},
"courier": {
"status": null,
"name": null,
"phone": null
}
}
}Lister les commandes
GET/api/v1/orders
Deux usages : se resynchroniser après une coupure (updated_since) et rapprocher vos reversements (période de création et statut). Les filtres se combinent.
| Paramètre | Description |
|---|---|
| updated_since | Commandes modifiées après cette date-heure ISO, triées par date de modification. Rappelez avec le updated_at de la dernière commande reçue. |
| created_from | Créées à partir de cette date : YYYY-MM-DD (journée à Douala, UTC+1) ou date-heure ISO. |
| created_to | Créées jusqu’à cette date incluse (YYYY-MM-DD), ou avant cette date-heure ISO. |
| status | Un ou plusieurs statuts séparés par des virgules, ex. picked_up,delivered. |
| limit | 1 à 100 (50 par défaut). |
| offset | Nombre de commandes à sauter (pagination). |
Sans updated_since, les commandes sont triées par date de création. La réponse contient has_more : true s’il reste des commandes ; rappelez avec offset augmenté de limit.
curl "https://shede.tacynt.com/api/v1/orders?updated_since=2026-10-01T12:00:00Z&limit=50" \ -H "Authorization: Bearer $SHEDE_KEY"
curl "https://shede.tacynt.com/api/v1/orders?created_from=2026-09-01&created_to=2026-09-30&status=picked_up,delivered&limit=100" \ -H "Authorization: Bearer $SHEDE_KEY"
{
"data": [
{
"id": "c42f…",
"status": "delivered",
"…": "…"
}
],
"has_more": false
}Annuler une commande
POST/api/v1/orders/{id}/cancel
Possible tant que la préparation n’a pas commencé (pending_acceptance ou accepted) ; sinon 409 invalid_state.
curl -X POST https://shede.tacynt.com/api/v1/orders/GLV-48213/cancel \
-H "Authorization: Bearer $SHEDE_KEY" -H "Content-Type: application/json" \
-d '{ "reason": "Client injoignable" }'Suivi de la livraison
POST/api/v1/orders/{id}/delivery-events
status : ASSIGNED → PICKED_UP → DELIVERED, ou FAILED avec reason. Uniquement pour une commande acceptée livrée par la marketplace (sinon 409 restaurant_delivery). PICKED_UP (ou DELIVERED) clôture la vente côté restaurant : facture, stock et comptabilité.
curl -X POST https://shede.tacynt.com/api/v1/orders/GLV-48213/delivery-events \
-H "Authorization: Bearer $SHEDE_KEY" -H "Content-Type: application/json" \
-d '{ "status": "PICKED_UP", "courier_name": "Paul", "courier_phone": "677000000" }'Webhooks
Shede prévient votre serveur dès qu’il se passe quelque chose sur le point. L’adresse (HTTPS) et le secret de signature sont configurés par le restaurant dans Shede : demandez-lui de vous transmettre le secret.
Requête envoyée
POST https://votre-serveur/webhooks/shede
Content-Type: application/json
Shede-Event: order.status_changed
Shede-Delivery: 6f1c…
Shede-Signature: t=1759312345,v1=5d41402abc4b2a76b9719d911017c592…
{
"id": "6f1c…",
"type": "order.status_changed",
"created_at": "2026-10-01T12:16:10.000Z",
"livemode": true,
"data": {
"previous_status": "pending_acceptance",
"order": {
"…": "…"
}
}
}Événements
| type | Quand | data |
|---|---|---|
| order.status_changed | La commande change de statut (acceptée, refusée, en préparation, prête, récupérée, livrée, annulée) | previous_status, order complète |
| menu.updated | Un produit ou un accompagnement est créé, modifié, désactivé ou supprimé | product_id ou accompaniment_id, change → relisez GET /menu |
| delivery_zones.updated | Une zone de livraison est créée, modifiée, activée ou désactivée | — → relisez GET /delivery-zones |
| point.paused / point.resumed | Le restaurant suspend ou reprend les commandes | paused |
| ping | Test envoyé depuis Shede | — |
Répondre
Répondez 2xx en moins de 8 secondes et traitez l’événement ensuite. Sinon, l’envoi est retenté après 1 min, 5 min, 30 min, 2 h, 12 h et 24 h, puis abandonné. Un même événement peut arriver plusieurs fois : ignorez un Shede-Delivery déjà traité. L’ordre d’arrivée n’est pas garanti : fiez-vous à order.updated_at.
Vérifier la signature
v1 est le HMAC-SHA256 du texte « {t}.{corps brut} » avec votre secret. Refusez les requêtes dont la signature ne correspond pas ou dont t a plus de 5 minutes. Utilisez le corps brut reçu, avant tout JSON.parse.
import { createHmac, timingSafeEqual } from 'crypto';
function verifyShedeSignature(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return expected.length === parts.v1.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}function verifyShedeSignature(string $secret, string $header, string $rawBody): bool {
parse_str(str_replace(',', '&', $header), $parts);
$t = (int) ($parts['t'] ?? 0);
if (!$t || abs(time() - $t) > 300) return false;
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1'] ?? '');
}