Intégrer Shede à votre marketplace

API v1

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

URL
https://shede.tacynt.com/api/v1
  1. 1Le restaurant vous transmet sa clé d’API.
  2. 2Vous synchronisez son menu avec GET /menu.
  3. 3Vous envoyez chaque commande avec POST /orders.
  4. 4Le restaurant l’accepte ou la refuse dans Shede ; vous êtes prévenu par webhook.
  5. 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é) :

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

JSON
{
  "data": {
    "…": "…"
  }
}

Erreur : un code stable et un message lisible (en français ou en anglais selon l’en-tête Accept-Language).

JSON
{
  "error": {
    "code": "validation_error",
    "message": "…",
    "details": [
      {
        "field": "items.0.product_id",
        "code": "product_unavailable"
      }
    ]
  }
}
HTTPcodeSignification
401missing_key, invalid_keyClé absente, inconnue ou révoquée
403module_disabled, license_inactive, point_inactiveModule API absent de la licence, licence expirée ou point désactivé
403quota_exceededQuota mensuel de commandes de l’organisation atteint
404not_foundRessource introuvable
409point_pausedLe restaurant a mis les commandes marketplace en pause
409invalid_stateAction impossible dans l’état de la commande
422validation_errorRequête invalide : details précise les champs en cause
429rate_limitedTrop de requêtes (voir Retry-After)
500internal_errorErreur 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.

HTTP
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éationstatus
0 – 30 spending_acceptance
30 – 60 saccepted (prep_minutes: 15)
60 – 120 spreparing
après 2 minready

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.

bash
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" }'
JSON
{
  "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é.

bash
curl https://shede.tacynt.com/api/v1/point \
  -H "Authorization: Bearer $SHEDE_KEY"
JSON
{
  "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.

bash
curl https://shede.tacynt.com/api/v1/categories \
  -H "Authorization: Bearer $SHEDE_KEY"
JSON
{
  "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.

bash
curl https://shede.tacynt.com/api/v1/delivery-zones \
  -H "Authorization: Bearer $SHEDE_KEY"
JSON
{
  "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
marketplacePar défaut. Votre livreur récupère la commande au restaurant ; vous suivez la course avec POST /orders/{id}/delivery-events.
restaurantUn 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.
JSON — POST /orders
{
  "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_acceptanceEn attente de la décision du restaurant
acceptedAcceptée, préparation pas encore commencée
preparingEn préparation
readyPrête à être récupérée
picked_upRécupérée par le livreur (vente clôturée)
deliveredLivrée au client
delivery_failedLivraison par le restaurant échouée (delivery.note)
rejectedRefusée par le restaurant (rejection_reason)
cancelledAnnulée (cancel_reason)

Créer une commande

POST/api/v1/orders

bash
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

ChampObligatoireDescription
external_idouiNuméro de la commande chez vous (unique par point)
partnernonNom de votre marketplace (glovo, yango…)
customer.namenonNom du client
customer.phoneouiTéléphone du client
items[].product_idouiId d’un produit de GET /menu
items[].quantityoui1 à 99
items[].notesnonInstruction pour la cuisine
items[].accompaniments[]nonid d’un accompagnement du produit ; quantity ≤ max_quantity (1 par défaut)
notesnonNote sur la commande
delivery_addressnonAdresse de livraison (information pour le restaurant)
delivery_bynonmarketplace (par défaut) ou restaurant
delivery.zone_idnonObligatoire si delivery_by = restaurant : id d’une zone de GET /delivery-zones
delivery.landmarknonObligatoire si delivery_by = restaurant : point de repère (3 caractères min.)
delivery.district / citynonQuartier (par défaut le nom de la zone) et ville
delivery.lat / lngnonPosition 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.

bash
curl https://shede.tacynt.com/api/v1/orders/GLV-48213 \
  -H "Authorization: Bearer $SHEDE_KEY"
JSON
{
  "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ètreDescription
updated_sinceCommandes 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_fromCréées à partir de cette date : YYYY-MM-DD (journée à Douala, UTC+1) ou date-heure ISO.
created_toCréées jusqu’à cette date incluse (YYYY-MM-DD), ou avant cette date-heure ISO.
statusUn ou plusieurs statuts séparés par des virgules, ex. picked_up,delivered.
limit1 à 100 (50 par défaut).
offsetNombre 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.

bash
curl "https://shede.tacynt.com/api/v1/orders?updated_since=2026-10-01T12:00:00Z&limit=50" \
  -H "Authorization: Bearer $SHEDE_KEY"
bash
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"
JSON
{
  "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.

bash
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é.

bash
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

HTTP
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

typeQuanddata
order.status_changedLa 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.updatedUn produit ou un accompagnement est créé, modifié, désactivé ou suppriméproduct_id ou accompaniment_id, change → relisez GET /menu
delivery_zones.updatedUne zone de livraison est créée, modifiée, activée ou désactivée— → relisez GET /delivery-zones
point.paused / point.resumedLe restaurant suspend ou reprend les commandespaused
pingTest 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.

Node.js
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));
}
PHP
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'] ?? '');
}