API caisse

Pour les fournisseurs de caisse et les développeurs. Avec cette API, une caisse envoie ses ventes à Frequent : le client reçoit automatiquement des tampons ou des points, un remboursement les reprend, et au comptoir vous voyez la carte du client.

Clé

Le commerce crée une clé par caisse dans son tableau de bord, sous Connexion caisse, Autre caisse ou fournisseur de caisse. Envoyez-la dans chaque requête :

Authorization: Bearer frq_live_…

Au maximum 300 requêtes par minute par clé. Les réponses sont en JSON.

Reconnaître le client

Indiquez l’un de ces trois. card_code est ce que le scanner lit sur le QR code de la carte dans Wallet (une adresse ou les 16 caractères de la carte) ; l’e-mail et le GSM retrouvent un client qui n’a pas sa carte sur lui.

  • card_code
  • email
  • phone (toute écriture : 0470 12 34 56 ou +32 470…)

Une vente

Après chaque ticket payé. Avec une carte à tampons, chaque vente donne un tampon ; avec des points, le montant compte. transaction_id est le numéro du ticket : envoyer deux fois le même ticket ne donne qu’une fois. Mettez reward_redeemed à true si le caissier a appliqué la récompense comme remise.

curl -X POST https://frequent.be/api/v1/sales \
  -H "Authorization: Bearer frq_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "transaction_id": "BON-1043", "amount": 12.50, "card_code": "k7m2qz9xw4hp3trb" }'
{
  "outcome": "awarded",
  "message": { "nl": "Verkoop geteld.", "fr": "Vente comptée." },
  "cards": [{
    "id": "k7m2qz9xw4hp3trb",
    "customer": { "name": "Sofie Claes" },
    "program": { "name": "Koffiekaart", "mechanic": "stamps", "goal": 10, "reward": "Gratis koffie" },
    "balance": 7,
    "reward_ready": false
  }]
}

Résultats possibles : awarded, ignored (carte pleine ou montant trop bas), no_match (pas de carte pour ce client), no_customer (aucun client indiqué). Un ticket répété donne "duplicate": true.

Un remboursement

Reprend ce que la vente d’origine avait donné, jamais en dessous de zéro.

curl -X POST https://frequent.be/api/v1/refunds \
  -H "Authorization: Bearer frq_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "transaction_id": "RET-88", "refund_of": "BON-1043" }'

Consulter la carte

Pour afficher au comptoir si la récompense est prête.

curl "https://frequent.be/api/v1/cards?card_code=k7m2qz9xw4hp3trb" \
  -H "Authorization: Bearer frq_live_…"

Chaque carte a une liste rewards: la récompense de la carte ("id": "main") et d’éventuelles récompenses en plus, avec leur coût et si elles sont possibles maintenant.

Échanger une récompense

Le caissier choisit une récompense disponible ; la caisse applique la remise sur le ticket et nous le signale. Deux caisses en même temps ne peuvent pas échanger deux fois la même récompense.

curl -X POST https://frequent.be/api/v1/redemptions \
  -H "Authorization: Bearer frq_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "transaction_id": "BON-1044", "card_code": "k7m2qz9xw4hp3trb", "reward_id": "main" }'

Réponse : "outcome": "redeemed" avec la récompense et la carte telle qu’elle est maintenant. Trop peu de tampons ou de points donne 409.

Payer avec une carte cadeau

Le scanner lit le QR code de la carte cadeau (ou le code est tapé). D’abord le solde, puis débiter le montant :

curl "https://frequent.be/api/v1/giftcards?code=CB7K2M9QXW" \
  -H "Authorization: Bearer frq_live_…"

curl -X POST https://frequent.be/api/v1/giftcards/spend \
  -H "Authorization: Bearer frq_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "transaction_id": "BON-1045", "code": "CB7K2M9QXW", "amount": 18.50 }'

Jamais plus que le solde (sinon 409), pas après l’échéance, et le même ticket envoyé deux fois ne débite qu’une fois.

Webhooks

Vous voulez savoir tout de suite quand il se passe quelque chose sur une carte, même hors de votre caisse ? Le commerce ajoute votre adresse web dans le tableau de bord (Connexion caisse, Webhooks) et choisit les événements. Nous envoyons alors un POST en JSON par événement.

  • card.created: un client a reçu une carte
  • card.updated: des tampons ou des points en plus ou en moins
  • reward.redeemed: une récompense a été échangée
  • giftcard.used: un paiement a été fait par carte cadeau
POST https://jouw-systeem.be/frequent
Frequent-Event: card.updated
Frequent-Delivery: 5f0c…
Frequent-Signature: t=1762700000,v1=3b1f…

{
  "id": "5f0c…",
  "type": "card.updated",
  "created_at": "2026-11-10T09:14:03Z",
  "data": {
    "card": { "id": "k7m2qz9xw4hp3trb", "program_name": "Koffiekaart", "mechanic": "stamps", "stamps": 7, "points": 0, "goal": 10 },
    "customer": { "id": "…", "name": "Sofie Claes", "email": "sofie@example.com", "phone": "+32470123456" },
    "activity": { "kind": "stamp", "delta": 1, "amount": 4.5, "source": "counter" }
  }
}

Vérifiez la signature à chaque fois : calculez HMAC-SHA256 de <t>.<body> avec le code secret (whsec_…) du tableau de bord et comparez avec v1. Refusez les messages de plus de 5 minutes.

import crypto from "node:crypto";

function fromFrequent(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Répondez en moins de 8 secondes avec un 2xx. Sinon, nous réessayons, au plus tôt après 1 minute, 5 minutes, 30 minutes, 2, 6, 12 et 24 heures (et au moins une fois par jour). Après 25 échecs de suite, nous désactivons le webhook ; le commerce peut le réactiver. Un message peut exceptionnellement arriver deux fois : utilisez id pour ignorer les doublons.

Erreurs

Une erreur revient sous la forme { "error": { "code": "…", "message": { "nl": "…", "fr": "…" } } }: 401 (clé), 403 (abonnement du commerce expiré), 404, 422 (champ), 429 (trop de requêtes).

Questions

Vous construisez une connexion pour votre caisse ? Écrivez-nous à viktor@frequent.be.