Documentation

Brancher une caisse, une borne ou un scanner

Une API REST, du JSON, et rien à installer. De la clé au premier passage crédité, comptez trente minutes.

Spécification OpenAPI 3.1

Un fichier destiné aux outils, pas une page à lire. Collez cette adresse dans Postman, Insomnia ou Bruno pour importer tous les appels d'un coup, ou donnez-la à un générateur de client pour obtenir une bibliothèque dans votre langage.

À coller dans votre outil
https://fidapp.fr/api/v1/openapi.json

Elle est produite à partir des schémas qui valident réellement les requêtes : elle ne peut pas être en retard sur le serveur.

Premier crédit en trente minutes

  1. 1 · Obtenir un jeton

    Deux chemins, selon ce que vous branchez. Une borne ou une tablette s'appaire avec le code à six chiffres de l'établissement, comme le ferait un employé. Un système déjà en place — caisse, CRM — utilise une clé d'organisation, créée par le commerçant dans Paramètres → Développeurs.

    Appairage
    curl -X POST https://fidapp.fr/api/v1/devices/pair \
      -H 'content-type: application/json' \
      -d '{"accessCode":"482913","deviceName":"Borne entrée"}'
    
    # → { "deviceToken": "fid_dev_…", "expiresAt": "…", "scopes": [...] }
  2. 2 · Vérifier qui parle

    Premier réflexe de toute intégration. La réponse dit l'enseigne, l'établissement, les droits et le débit accordé — de quoi savoir tout de suite si vous tenez la bonne clé.

    Identité
    curl https://fidapp.fr/api/v1/me \
      -H 'authorization: Bearer fid_dev_…'
  3. 3 · Identifier le client

    Par le QR de sa carte, par son identifiant dicté au comptoir (K7QM-3XA9), ou par son téléphone. Le téléphone et l'email passent par POST : un numéro dans une URL finit dans les journaux d'accès de chaque intermédiaire, et s'y conserve des mois.

    Scan
    curl "https://fidapp.fr/api/v1/members/lookup?qr=${QR}" \
      -H "authorization: Bearer ${TOKEN}"
  4. 4 · Créditer le passage

    La clé d'idempotence se génère avant l'envoi et se réutilise telle quelle à chaque réessai. C'est elle qui transforme une reprise réseau en non-événement plutôt qu'en double crédit. La réponse contient le nouveau solde, la promotion appliquée et la récompense éventuellement débloquée : rien d'autre à demander pour afficher l'écran de confirmation.

    Crédit
    curl -X POST https://fidapp.fr/api/v1/members/${ID}/earn \
      -H "authorization: Bearer ${TOKEN}" \
      -H "idempotency-key: $(uuidgen)" \
      -H 'content-type: application/json' \
      -d '{"amountCents": 1250}'

Trois règles qui décident de la qualité d'une intégration

Générez la clé d'idempotence avant l'envoi

La générer au moment du réessai revient à ne pas en avoir. Rejouée avec la même clé, une écriture rend la réponse d'origine — entière, récompense débloquée comprise — et l'en-tête Idempotency-Replayed: true.

Déclarez occurredAt dès que l'envoi est différé

Une caisse qui resynchronise après une coupure doit dire quand le client était au comptoir. Sans cela, un passage de 18 h 55 reçu à 19 h 10 perd sa happy hour, et les statistiques du commerçant décrivent vos synchronisations plutôt que sa fréquentation. Tolérance : 48 heures dans le passé, cinq minutes dans le futur.

Ne mettez jamais redeem dans une file de rejeu

Un crédit peut attendre le retour du réseau ; l'utilisation d'une récompense, non. Elle se décide sur un solde possiblement périmé, et une double consommation coûte de l'argent réel au commerçant. Hors ligne, refusez l'opération et dites-le à l'employé.

Les 14 routes

Générées depuis la spécification, elle-même tirée des schémas qui valident les requêtes. Cette liste ne peut pas être en retard sur le serveur.

RouteCe qu'elle faitDroit
POST /devices/pairAppairer un appareilpublic
POST /devices/unpairDésappairer l'appareil courantpos:read
GET /meIdentité du porteur—
GET /programsProgrammes de l'enseigneprograms:read
GET /programs/{id}Détail d'un programmeprograms:read
GET /locationsÉtablissements visiblespos:read
GET /members/lookupIdentifier par QR ou identifiant dictépos:read
POST /members/lookupIdentifier par téléphone ou emailpos:read
GET /members/searchRechercher un clientpos:read
GET /members/{membershipId}État d'une adhésionpos:read
POST /members/{membershipId}/earnCréditer un passagepos:write
POST /members/{membershipId}/redeemUtiliser une récompensepos:write
GET /transactionsJournal des transactionspos:read
POST /transactions/{id}/cancelAnnuler une transaction récentepos:write

Droits

Une clé porte les droits qu'on lui donne, et rien de plus. Une borne posée en salle reçoit les trois premiers : elle doit pouvoir créditer, pas reconfigurer le programme.

pos:read
Identifier un client et lire son solde
pos:write
Créditer un passage et utiliser une récompense
members:read
Lire la base clients
members:write
Inscrire et modifier des clients
programs:read
Lire les programmes, récompenses et paliers
programs:write
Configurer les programmes
analytics:read
Lire les analyses et exporter
webhooks:manage
Gérer les notifications sortantes

Erreurs

Branchez votre logique sur code, jamais sur message : le premier est stable pour toute la durée de la version, le second est du français destiné à un écran, que nous nous réservons de réécrire. Chaque réponse porte un requestId — c'est ce que le support vous demandera.

Forme d'un refus
{
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Solde insuffisant : 3 tampons sur les 10 requis.",
    "details": { "required": 10, "available": 3 },
    "requestId": "01J9X2K…"
  }
}
UNAUTHORIZED401Aucun jeton fourni. Envoyez votre clé dans l'en-tête « Authorization: Bearer … ».
INVALID_TOKEN401Jeton inconnu. Vérifiez la clé utilisée, ou créez-en une nouvelle.
TOKEN_EXPIRED401Ce jeton a expiré. Réappairez l'appareil pour en obtenir un nouveau.
TOKEN_REVOKED401Ce jeton a été révoqué depuis le back-office.
OAUTH_NOT_AVAILABLE401Les jetons OAuth ne sont pas encore acceptés. Utilisez une clé d'organisation.
FORBIDDEN_SCOPE403Cette clé ne porte pas les droits nécessaires à cette opération.
PLAN_REQUIRED403L'accès à l'API fait partie des offres Pro et Business.
ORGANIZATION_SUSPENDED403Ce compte est suspendu. Contactez-nous pour le réactiver.
VALIDATION_FAILED400La requête est mal formée.
NOT_FOUND404Ressource introuvable.
METHOD_NOT_ALLOWED405Méthode non autorisée sur cette ressource.
UNSUPPORTED_MEDIA_TYPE415Envoyez un corps JSON avec « Content-Type: application/json ».
PAYLOAD_TOO_LARGE413Corps de requête trop volumineux.
IDEMPOTENCY_KEY_REQUIRED400En-tête « Idempotency-Key » obligatoire sur cette écriture : il protège du double-crédit en cas de réessai.
IDEMPOTENCY_KEY_REUSED409Cette clé d'idempotence a déjà servi pour une requête différente. Générez-en une nouvelle.
REQUEST_IN_FLIGHT409Une requête portant cette clé d'idempotence est en cours de traitement. Réessayez dans un instant.
RATE_LIMITED429Trop de requêtes. Réessayez dans quelques secondes.
INVALID_ACCESS_CODE401Ce code ne correspond à aucun établissement.
BLOCKED422Cette carte est désactivée.
PROGRAM_INACTIVE422Ce programme n'accepte plus d'opération.
COOLDOWN429Ce client vient d'être crédité. Patientez avant de recommencer.
DAILY_CAP422Le plafond journalier de ce programme est atteint pour ce client.
NOTHING_TO_EARN422Ce montant ne donne droit à rien.
INSUFFICIENT422Solde insuffisant.
ALREADY_USED422Cette récompense a déjà été utilisée.
UNAVAILABLE422Cette récompense n'est plus disponible.
EXPIRED422Cette récompense a expiré.
OUT_OF_STOCK422Cette récompense est épuisée.
TIER_REQUIRED422Cette récompense est réservée à un palier supérieur.
TOO_LATE422Passé cinq minutes, l'annulation se fait depuis le back-office.
ALREADY_CANCELLED422Cette transaction est déjà annulée.
AMOUNT_REQUIRED400Ce programme crédite au montant : indiquez « amountCents ».
INVALID_AMOUNT400Montant invalide.
INVALID_QUANTITY400Quantité invalide.
INVALID_COMBINATION400Cette combinaison de paramètres n'est pas acceptée par ce programme.
INVALID_COST400Coût de récompense invalide.
MISSING_CONFIG422Ce programme est incomplètement configuré.
OCCURRED_AT_OUT_OF_RANGE422« occurredAt » est trop éloigné de l'heure actuelle. Une opération se déclare dans les 48 heures.
INTERNAL500Erreur interne. Réessayez, puis contactez-nous avec l'identifiant de requête.

Notifications sortantes

Pour la caisse, la réponse de earn suffit : elle contient déjà le nouveau solde et la récompense débloquée. Les notifications servent au reste — tenir votre propre fichier client à jour, réagir à une montée de palier survenue entre deux visites, sans nous interroger en boucle. Le commerçant déclare son adresse dans Paramètres → Développeurs.

member.joined
Un client s'inscrit à un programme
transaction.created
Un passage est crédité
transaction.cancelled
Une opération est annulée
reward.earned
Une récompense est débloquée
reward.redeemed
Une récompense est utilisée
tier.upgraded
Un client atteint un palier supérieur
membership.updated
Le solde ou l'état d'une adhésion change

Livraison au moins une fois. En cas de doute — un délai d'attente sur une requête qui a peut-être abouti — nous réessayons. Dédupliquez sur id, qui identifie le fait et non la livraison. L'ordre n'est pas garanti.

Réessais après 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h, 24 h. Au-delà, la livraison est abandonnée — mais votre adresse continue de recevoir les événements suivants, jusqu'à trois jours d'échecs consécutifs. Un serveur réparé le mardi soir reprend donc le fil tout seul.

En-têtes reçus
POST /votre-adresse
content-type:    application/json
fid-signature:   t=1758724800,v1=<hex>
fid-event-id:    01J9X2K…       ← dédupliquez là-dessus
fid-event-type:  transaction.created
fid-attempt:     1

Vérifiez la signature. Sans cette étape, n'importe qui connaissant votre adresse peut vous faire croire à un passage. L'horodatage fait partie de ce qui est signé : une requête interceptée cesse d'être valable au bout de cinq minutes.

Vérification (Node)
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.trim().split("=")),
  );

  // Fenêtre anti-rejeu.
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && timingSafeEqual(a, b);
}

Le corps doit être lu brut, avant tout analyseur JSON : une re-sérialisation change les espaces et l'ordre des clés, et l'empreinte ne correspond plus.

Il n'existe pas d'événement de descente de palier, et il n'en existera pas. Une montée se célèbre, une descente se constate en silence : prévenir quelqu'un qu'il perd son statut ne le fait pas revenir, ça lui fait désinstaller sa carte.

Débit et offres

L'accès par clé fait partie des offres Pro et Business. Les appareils appairés, eux, fonctionnent sur toutes les offres — c'est la console caisse, elle est incluse partout.

OffrePar minutePar moisClésAdresses
Pro60100 00032
Business3001 000 0001010

Un forfait épuisé n'arrête jamais le comptoir. Créditer un passage et utiliser une récompense passent quoi qu'il arrive : le client d'un commerçant n'a pas à payer un différend d'abonnement entre ce commerçant et nous. Ce sont les lectures qui ralentissent.

Une question sur une intégration ?

Écrivez-nous avec l'identifiant de requête que vous a rendu l'API : il nous mène directement à ce qui s'est passé.

Nous écrire