API vendeur v1

Introduction

L'API vous permet d'initialiser des ventes sécurisées depuis votre site, de suivre vos ventes et vos soldes, de stocker les images de vos produits et d'être notifié par webhook. L'acceptation, le paiement, la validation de réception et les litiges restent des actions de l'acheteur dans l'application. Aucun retrait n'est possible via l'API.

Base URL : https://escrow.middlefolio.online/api/v1

Démarrage rapide

  1. Créez un compte, puis une clé de test dans le tableau de bord (API > Gérer).
  2. Faites votre premier appel : listez vos boutiques (exemple ci-dessous, avec votre langage).
  3. Créez une vente de test, ouvrez son lien d’invitation et payez-la avec un numéro de test.
  4. Passez sur une clé réelle une fois votre intégration validée.
curl 'https://escrow.middlefolio.online/api/v1/shops' \
  -H 'Authorization: Bearer <votre_clé>' \
  -H 'Accept: application/json'

Le choix du langage est mémorisé et appliqué à tous les exemples de la page.

Authentification

Créez une clé dans le tableau de bord (API > Gérer) ; elle reste consultable et copiable depuis la modale « Clé & secret ». Envoyez-la dans l'en-tête Authorization. Une clé réelle exige que le module API soit activé ; une clé de test fonctionne toujours et ne manipule que des données fictives.

curl 'https://escrow.middlefolio.online/api/v1/shops' \
  -H 'Authorization: Bearer <votre_clé>' \
  -H 'Accept: application/json'

Droits d’une clé : shops:read, sales:read, balance:read, sales:create, media:read, media:write (les images appartiennent au compte, pas à une boutique). Une clé ne peut jamais faire plus que son propriétaire sur la boutique : le droit effectif est l’intersection des droits de la clé et du rôle du propriétaire, évalué à chaque appel.

Boutiques

Lister les boutiques

GET/shopsshops:read

Boutiques rattachées à la clé et encore accessibles à son propriétaire.

curl 'https://escrow.middlefolio.online/api/v1/shops' \
  -H 'Authorization: Bearer <votre_clé>' \
  -H 'Accept: application/json'

Réponses

200Liste des boutiques
{
  "data": [
    {
      "id": "01J9ZQ3M8K7Y2E5R6T4W1N0PAB",
      "name": "Ma Boutique",
      "legal_name": null,
      "category": "retail",
      "is_verified": false,
      "logo_url": null,
      "currency": "FCFA",
      "default_release_hours": 24,
      "created_at": "2026-09-01T08:00:00+00:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 15,
    "total": 1
  }
}

Lire une boutique

GET/shops/{shop}shops:read

Informations d'une boutique autorisée pour la clé.

Chemin

shop *Paramètre de chemin — exemple : 01J9ZQ3M8K7Y2E5R6T4W1N0PAB
curl 'https://escrow.middlefolio.online/api/v1/shops/01J9ZQ3M8K7Y2E5R6T4W1N0PAB' \
  -H 'Authorization: Bearer <votre_clé>' \
  -H 'Accept: application/json'

Réponses

200La boutique
{
  "id": "01J9ZQ3M8K7Y2E5R6T4W1N0PAB",
  "name": "Ma Boutique",
  "legal_name": null,
  "category": "retail",
  "is_verified": false,
  "logo_url": null,
  "currency": "FCFA",
  "default_release_hours": 24,
  "created_at": "2026-09-01T08:00:00+00:00"
}
403shop_forbidden : boutique non autorisée pour cette clé
404shop_not_found : boutique inconnue

Lire les soldes

GET/shops/{shop}/balancebalance:read

Soldes de la boutique, en lecture seule. in_escrow : payé, réception non validée. awaiting_release : validé, minuterie de libération en cours. in_dispute : bloqué par un litige. available : montant retirable depuis le tableau de bord. Un solde live n'inclut jamais de donnée de test.

Chemin

shop *Paramètre de chemin — exemple : 01J9ZQ3M8K7Y2E5R6T4W1N0PAB
curl 'https://escrow.middlefolio.online/api/v1/shops/01J9ZQ3M8K7Y2E5R6T4W1N0PAB/balance' \
  -H 'Authorization: Bearer <votre_clé>' \
  -H 'Accept: application/json'

Réponses

200Les soldes
{
  "currency": "FCFA",
  "available": 45000,
  "in_escrow": 25000,
  "awaiting_release": 0,
  "in_dispute": 0,
  "released_total": 100000,
  "paid_out": 50000,
  "pending_payouts": 0,
  "platform_fees": 5000,
  "mode": "live"
}
403insufficient_role : réservé aux rôles owner, admin, finance

Ventes

Redirigez votre client vers invite_url : il se connecte, accepte la vente puis paie en séquestre. Une vente est créée en draft ; elle passe en attente de paiement dès qu'un acheteur l'accepte.

Objet vente
{
  "code": "K7Q-M2X-9AB",
  "shop_id": "01J9ZQ3M8K7Y2E5R6T4W1N0PAB",
  "name": "Commande #4521",
  "description": null,
  "status": "draft",
  "mode": "live",
  "currency": "FCFA",
  "total_amount": 25000,
  "commission_amount": 1250,
  "commission_rate": 0.05,
  "fees_charged_to": "seller",
  "base_amount": 25000,
  "release_timer_hours": 24,
  "conditions": [
    "Livraison à Douala sous 3 jours"
  ],
  "items": [
    {
      "label": "Casque audio",
      "description": null,
      "unit_price": 25000,
      "quantity": 1
    }
  ],
  "buyer": {
    "name": "Awa",
    "email": "[email protected]",
    "has_joined": false
  },
  "invite_url": "https://…/secure-sales/K7Q-M2X-9AB/invite",
  "redirect_url": "https://boutique.example/merci",
  "webhook_url": "https://boutique.example/webhooks/escrow",
  "created_at": "2026-09-21T10:00:00+00:00"
}

Lister les ventes d'une boutique

GET/shops/{shop}/salessales:read

Statuts : draft, awaiting_payment, in_escrow, awaiting_release, disputed, released, refunded, cancelled, expired.

Chemin

shop *Paramètre de chemin — exemple : 01J9ZQ3M8K7Y2E5R6T4W1N0PAB

Paramètres de requête

statusFiltre sur le statut
searchRecherche sur le code ou le nom
per_pageTaille de page (100 max)
curl 'https://escrow.middlefolio.online/api/v1/shops/01J9ZQ3M8K7Y2E5R6T4W1N0PAB/sales?status=in_escrow&per_page=20' \
  -H 'Authorization: Bearer <votre_clé>' \
  -H 'Accept: application/json'

Réponses

200Ventes paginées
{
  "data": [
    {
      "code": "K7Q-M2X-9AB",
      "shop_id": "01J9ZQ3M8K7Y2E5R6T4W1N0PAB",
      "name": "Commande #4521",
      "description": null,
      "status": "draft",
      "mode": "live",
      "currency": "FCFA",
      "total_amount": 25000,
      "commission_amount": 1250,
      "commission_rate": 0.05,
      "fees_charged_to": "seller",
      "base_amount": 25000,
      "release_timer_hours": 24,
      "conditions": [
        "Livraison à Douala sous 3 jours"
      ],
      "items": [
        {
          "label": "Casque audio",
          "description": null,
          "unit_price": 25000,
          "quantity": 1
        }
      ],
      "buyer": {
        "name": "Awa",
        "email": "[email protected]",
        "has_joined": false
      },
      "invite_url": "https://…/secure-sales/K7Q-M2X-9AB/invite",
      "redirect_url": "https://boutique.example/merci",
      "webhook_url": "https://boutique.example/webhooks/escrow",
      "created_at": "2026-09-21T10:00:00+00:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 15,
    "total": 1
  }
}

Initier une vente

POST/shops/{shop}/salessales:create

Crée une vente en draft. Fournissez soit items[], soit total_amount — jamais les deux. redirect_url et webhook_url sont facultatives, absolues, en https en production. L'en-tête Idempotency-Key est recommandé : rejouer la même requête dans les 24 h renvoie la réponse initiale sans créer de doublon. Illustrez la vente avec des images de votre compte (voir Images) via gallery_image_ids. Frais : par défaut (fees_charged_to = seller) la commission de la plateforme est déduite de votre versement. Avec fees_charged_to = buyer, l'acheteur la supporte : total_amount est majoré pour que vous receviez exactement le montant saisi (renvoyé dans base_amount) ; pour une vente itemisée, la différence apparaît en ligne « Frais de transaction » dans items[].

Chemin

shop *Paramètre de chemin — exemple : 01J9ZQ3M8K7Y2E5R6T4W1N0PAB

Corps de la requête

name *Nom de la vente
items[]Articles : label, unit_price, quantity, description?, gallery_image_ids[] (2 max)
total_amountMontant à plat (exclusif avec items)
descriptionDescription libre
conditions[]Conditions de complétion (texte libre)
gallery_image_ids[]Ids d'images de votre compte (3 max)
buyer_emailE-mail de l'acheteur
buyer_nameNom de l'acheteur
release_timer_hoursMinuterie de libération (heures)
fees_charged_toQui supporte la commission : seller (défaut) ou buyer (prix acheteur majoré, vous recevez le montant saisi)
redirect_urlRedirection après paiement (https)
webhook_urlURL des webhooks (https)
curl -X POST 'https://escrow.middlefolio.online/api/v1/shops/01J9ZQ3M8K7Y2E5R6T4W1N0PAB/sales' \
  -H 'Authorization: Bearer <votre_clé>' \
  -H 'Accept: application/json' \
  -H 'Idempotency-Key: commande-4521' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "Commande #4521",
  "items": [
    {
      "label": "Casque audio",
      "unit_price": 25000,
      "quantity": 1,
      "gallery_image_ids": [
        "01J9ZS0K3Q8V4N2M6P7R1T5XYZ"
      ]
    }
  ],
  "conditions": [
    "Livraison à Douala sous 3 jours"
  ],
  "buyer_email": "[email protected]",
  "buyer_name": "Awa",
  "redirect_url": "https://boutique.example/merci",
  "webhook_url": "https://boutique.example/webhooks/escrow"
}'

Réponses

201La vente créée
{
  "code": "K7Q-M2X-9AB",
  "shop_id": "01J9ZQ3M8K7Y2E5R6T4W1N0PAB",
  "name": "Commande #4521",
  "description": null,
  "status": "draft",
  "mode": "live",
  "currency": "FCFA",
  "total_amount": 25000,
  "commission_amount": 1250,
  "commission_rate": 0.05,
  "fees_charged_to": "seller",
  "base_amount": 25000,
  "release_timer_hours": 24,
  "conditions": [
    "Livraison à Douala sous 3 jours"
  ],
  "items": [
    {
      "label": "Casque audio",
      "description": null,
      "unit_price": 25000,
      "quantity": 1
    }
  ],
  "buyer": {
    "name": "Awa",
    "email": "[email protected]",
    "has_joined": false
  },
  "invite_url": "https://…/secure-sales/K7Q-M2X-9AB/invite",
  "redirect_url": "https://boutique.example/merci",
  "webhook_url": "https://boutique.example/webhooks/escrow",
  "created_at": "2026-09-21T10:00:00+00:00"
}
409idempotency_conflict : même Idempotency-Key, corps différent
422validation_failed : détail par champ dans error.details

Lire une vente

GET/sales/{code}sales:read

Détail d'une vente d'une boutique autorisée.

Chemin

code *Paramètre de chemin — exemple : K7Q-M2X-9AB
curl 'https://escrow.middlefolio.online/api/v1/sales/K7Q-M2X-9AB' \
  -H 'Authorization: Bearer <votre_clé>' \
  -H 'Accept: application/json'

Réponses

200La vente
{
  "code": "K7Q-M2X-9AB",
  "shop_id": "01J9ZQ3M8K7Y2E5R6T4W1N0PAB",
  "name": "Commande #4521",
  "description": null,
  "status": "draft",
  "mode": "live",
  "currency": "FCFA",
  "total_amount": 25000,
  "commission_amount": 1250,
  "commission_rate": 0.05,
  "fees_charged_to": "seller",
  "base_amount": 25000,
  "release_timer_hours": 24,
  "conditions": [
    "Livraison à Douala sous 3 jours"
  ],
  "items": [
    {
      "label": "Casque audio",
      "description": null,
      "unit_price": 25000,
      "quantity": 1
    }
  ],
  "buyer": {
    "name": "Awa",
    "email": "[email protected]",
    "has_joined": false
  },
  "invite_url": "https://…/secure-sales/K7Q-M2X-9AB/invite",
  "redirect_url": "https://boutique.example/merci",
  "webhook_url": "https://boutique.example/webhooks/escrow",
  "created_at": "2026-09-21T10:00:00+00:00"
}
404sale_not_found

Images

Nommez vos images avec l'identifiant de vos produits, retrouvez leur id par leur nom (?name=) et réutilisez-les dans vos ventes avec gallery_image_ids. Elles apparaissent aussi dans la galerie du tableau de bord (badge « API ») et peuvent y être supprimées. Seules les images téléversées par l'API peuvent être remplacées ou supprimées par l'API.

Lister les images du compte

GET/mediamedia:read

Toutes les images du compte (téléversées par l'API et depuis le tableau de bord). Retrouvez l'id d'une image par son nom avec ?name=. meta.storage indique l'usage du quota de 25 Mo.

Paramètres de requête

nameNom exact
searchLe nom contient
sourceapi ou dashboard
per_pageTaille de page (100 max)
curl 'https://escrow.middlefolio.online/api/v1/media?name=produit-4521&source=api' \
  -H 'Authorization: Bearer <votre_clé>' \
  -H 'Accept: application/json'

Réponses

200Images et usage du stockage
{
  "data": [
    {
      "id": "01J9ZS0K3Q8V4N2M6P7R1T5XYZ",
      "name": "produit-4521",
      "url": "https://…/storage/media/users/…/01J9ZS0K….jpg",
      "mime_type": "image/jpeg",
      "size": 48213,
      "source": "api",
      "in_use": false,
      "manageable": true,
      "created_at": "2026-09-21T10:00:00+00:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 15,
    "total": 1,
    "storage": {
      "used_bytes": 48213,
      "quota_bytes": 26214400,
      "remaining_bytes": 26166187
    }
  }
}

Lire une image

GET/media/{id}media:read

Une image du compte. Une image d'un autre compte répond 404.

Chemin

id *Paramètre de chemin — exemple : 01J9ZS0K3Q8V4N2M6P7R1T5XYZ
curl 'https://escrow.middlefolio.online/api/v1/media/01J9ZS0K3Q8V4N2M6P7R1T5XYZ' \
  -H 'Authorization: Bearer <votre_clé>' \
  -H 'Accept: application/json'

Réponses

200L'image
{
  "id": "01J9ZS0K3Q8V4N2M6P7R1T5XYZ",
  "name": "produit-4521",
  "url": "https://…/storage/media/users/…/01J9ZS0K….jpg",
  "mime_type": "image/jpeg",
  "size": 48213,
  "source": "api",
  "in_use": false,
  "manageable": true,
  "created_at": "2026-09-21T10:00:00+00:00"
}
404media_not_found

Téléverser une image

POST/mediamedia:write

Chaque compte dispose de 25 Mo pour les images téléversées par l'API. Image ≤ 5 Mo (compressée au-delà de 250 Ko). Nommez-la comme vous le souhaitez, par exemple avec l'identifiant de votre produit. Elle apparaît aussi dans la galerie du tableau de bord (badge « API »).

Corps de la requête

file *Fichier image
nameNom de l’image (255 max) — défaut : nom du fichier
curl -X POST 'https://escrow.middlefolio.online/api/v1/media' \
  -H 'Authorization: Bearer <votre_clé>' \
  -H 'Accept: application/json' \
  -F '[email protected]' \
  -F 'name=produit-4521'

Réponses

201L'image, avec son id
{
  "id": "01J9ZS0K3Q8V4N2M6P7R1T5XYZ",
  "name": "produit-4521",
  "url": "https://…/storage/media/users/…/01J9ZS0K….jpg",
  "mime_type": "image/jpeg",
  "size": 48213,
  "source": "api",
  "in_use": false,
  "manageable": true,
  "created_at": "2026-09-21T10:00:00+00:00"
}
422storage_quota_exceeded (quota dépassé) ou validation_failed (fichier invalide)

Remplacer une image

POST/media/{id}media:write

Remplace le contenu d'une image téléversée par l'API en conservant son id : les ventes qui la référencent affichent le nouveau contenu. POST et non PUT, car PHP ne lit pas un PUT multipart.

Chemin

id *Paramètre de chemin — exemple : 01J9ZS0K3Q8V4N2M6P7R1T5XYZ
curl -X POST 'https://escrow.middlefolio.online/api/v1/media/01J9ZS0K3Q8V4N2M6P7R1T5XYZ' \
  -H 'Authorization: Bearer <votre_clé>' \
  -H 'Accept: application/json' \
  -F '[email protected]'

Réponses

200L'image mise à jour
{
  "id": "01J9ZS0K3Q8V4N2M6P7R1T5XYZ",
  "name": "produit-4521",
  "url": "https://…/storage/media/users/…/01J9ZS0K….jpg",
  "mime_type": "image/jpeg",
  "size": 51200,
  "source": "api",
  "in_use": false,
  "manageable": true,
  "created_at": "2026-09-21T10:00:00+00:00"
}
403media_not_manageable : image non téléversée par l'API
404media_not_found
422storage_quota_exceeded

Supprimer une image

DELETE/media/{id}media:write

Supprime une image téléversée par l'API et libère son espace. Refusé si elle est utilisée par une vente.

Chemin

id *Paramètre de chemin — exemple : 01J9ZS0K3Q8V4N2M6P7R1T5XYZ
curl -X DELETE 'https://escrow.middlefolio.online/api/v1/media/01J9ZS0K3Q8V4N2M6P7R1T5XYZ' \
  -H 'Authorization: Bearer <votre_clé>' \
  -H 'Accept: application/json'

Réponses

204Supprimée (pas de contenu)
403media_not_manageable
409media_in_use : détachez-la d’abord

Redirection après paiement

Si la vente a une redirect_url, l'acheteur y est renvoyé après paiement avec ?sale=<code>&status=<statut>. Cette redirection est indicative (modifiable par le client) : ne livrez jamais sur cette seule redirection, seul le webhook signé fait foi.

Webhooks

Si la vente a une webhook_url, un POST JSON signé est envoyé à chaque changement : secure_sale.paid, .completed, .disputed, .dispute_cancelled, .released, .refunded, .cancelled, .expired. Le secret (whsec_…) est propre à la clé qui a créé la vente ; il s’affiche à la création de la clé, reste consultable et est régénérable.

POST /webhooks/escrow
X-Event-Id: evt_01J9ZR…
X-Event: secure_sale.paid
X-Timestamp: 1790000000
X-Signature: sha256=<hmac>
Content-Type: application/json

{
  "id": "evt_01J9ZR…",
  "event": "secure_sale.paid",
  "created_at": "2026-09-21T10:05:00+00:00",
  "data": "{ …vente… }"
}

Signature : HMAC-SHA256 de « {X-Timestamp}.{corps brut} » avec votre secret. Rejetez si l’horodatage a plus de 5 minutes et dédupliquez sur X-Event-Id. Répondez 2xx sous 10 s ; sinon l’envoi est retenté jusqu’à 6 fois (backoff croissant). Les redirections ne sont pas suivies.

<?php
$secret    = getenv('ESCROW_WEBHOOK_SECRET');           // whsec_...
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$body      = file_get_contents('php://input');           // corps BRUT

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);

if (! hash_equals($expected, $signature) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}
// Dédupliquez sur X-Event-Id (livraison "au moins une fois"), puis répondez 2xx.
http_response_code(200);

Mode test & paiements simulés

Une clé de test manipule des données isolées : les ventes créées sont signalées « TEST » aux utilisateurs, aucun argent réel n’est engagé et elles sont supprimées après 30 jours. Payer une vente de test ouvre une fenêtre de paiement dédiée : le résultat dépend du numéro Mobile Money saisi.

Tout numéro valide (9 chiffres commençant par 6, avec ou sans +237) réussit, sauf ces numéros de test qui simulent chacun une erreur de paiement :

NuméroCasMessage affiché
670000001insufficient_fundsSolde Mobile Money insuffisant pour ce paiement.
670000002timeoutLe paiement n'a pas été confirmé sur votre téléphone dans le délai imparti.
670000003declinedLe paiement a été refusé par l'opérateur.
670000004invalid_accountCe numéro n'est associé à aucun compte Mobile Money.
670000005limit_exceededLe plafond de transaction de ce compte est dépassé.
670000006operator_unavailableLe service de l'opérateur est momentanément indisponible. Réessayez dans quelques minutes.

Le bac à sable du tableau de bord simule les appels de l’API (sans rien enregistrer) : chaque donnée de la requête peut déclencher l’erreur correspondante — clé « expired », boutique « forbidden », quota dépassé, etc. Ouvrir le bac à sable

Réponses et codes

Succès : 200 (lecture), 201 (création), 204 (suppression). Chaque réponse porte un en-tête X-Request-Id, à citer en cas de support.

{
  "error": {
    "code": "validation_failed",
    "message": "Les données envoyées sont invalides.",
    "details": {
      "items.0.unit_price": [
        "Le champ items.0.unit_price est requis."
      ]
    }
  },
  "request_id": "req_01J9ZR…"
}
HTTPcodemessage
401unauthenticatedClé API absente, invalide, révoquée ou expirée.
403api_disabledLe module API est désactivé pour ce compte (clés réelles uniquement).
403forbidden_abilityCette clé API n'a pas le droit « <droit> ».
403insufficient_roleLe propriétaire de cette clé n'a pas les droits nécessaires sur cette boutique pour « <droit> ».
403shop_forbiddenCette clé API n'a pas accès à cette boutique.
403media_not_manageableCette image n'a pas été téléversée par l'API : elle ne peut être modifiée ou supprimée que depuis le tableau de bord.
404shop_not_foundBoutique introuvable.
404sale_not_foundVente introuvable.
404media_not_foundImage introuvable.
409idempotency_conflictCette clé d'idempotence a déjà été utilisée avec une requête différente.
409media_in_useCette image est utilisée par une vente ou une autre ressource : détachez-la avant de la supprimer.
422validation_failedLes données envoyées sont invalides (détail par champ dans « details »).
422storage_quota_exceededLe quota de stockage API de ce compte est dépassé (details : used_bytes, quota_bytes, file_size).
429rate_limitedTrop de requêtes. Réessayez plus tard (en-tête Retry-After).
500server_errorErreur interne du serveur.

Limites et bonnes pratiques

  • Débit : 60 requêtes/minute par clé, 20 créations/minute (429 + Retry-After au-delà).
  • Stockage d’images par l’API : 25 Mo par compte, 5 Mo par fichier.
  • Utilisez Idempotency-Key sur chaque création.
  • Ne placez jamais la clé ni le secret dans du code côté navigateur.
  • Testez d’abord avec une clé de test : les ventes de test sont signalées aux utilisateurs, aucun argent réel n’est engagé et elles sont supprimées après 30 jours.