API REMYZ

Reliez un logiciel de caisse ou un site e-commerce au programme de fidélité : retrouver un client (nom, téléphone, carte QR), lire son solde, enregistrer un achat qui crédite les points, rembourser, utiliser une récompense — et être prévenu de chaque événement par webhook.

Spécification machine : OpenAPI 3.1 · URL de base : https://remyz.fr/api/v1

Démarrer

  1. Un propriétaire ou administrateur du commerce crée une clé dans Paramètres › API & webhooks (lecture seule, ou lecture et écriture). La clé n'est affichée qu'une fois.
  2. Votre logiciel l'envoie à chaque appel ; la clé désigne le commerce : aucun identifiant d'organisation n'est à fournir.
  3. Pour être prévenu des achats, remboursements, etc. faits en boutique, ajoutez un webhook (adresse HTTPS).
curl "https://remyz.fr/api/v1/customers?q=06%2096%2012%2034%2056" \
  -H "Authorization: Bearer rmz_k7f3q9xz_…"

Authentification et limites

En-tête Authorization: Bearer <clé> sur chaque requête, en HTTPS. Une clé révoquée est refusée immédiatement (401 api_key_revoked). Portées :

  • lecture : clients, soldes et historiques, achats, récompenses, établissements ;
  • écriture (inclut la lecture) : créer un client, enregistrer un achat, rembourser, utiliser une récompense. Une clé en lecture seule reçoit 403 insufficient_scope sur une écriture.

Appelez l'API depuis votre serveur : une clé ne doit jamais figurer dans le code d'une page web ou d'une application mobile (l'API n'autorise pas les appels directs des navigateurs).

Limite : 120 requêtes par minute et par clé. Au-delà : 429 rate_limited avec l'en-tête Retry-After (secondes). Les opérations faites par l'API apparaissent dans le journal d'activité du commerce avec le nom de la clé.

Conventions

Chaque réponse authentifiée réussie inclut unit : points ou tampons, selon le programme du commerce. Les noms existants (pointsBalance, pointsEarned, pointsCost, missingPoints…) restent inchangés ; leurs nombres utilisent cette unité. Les webhooks conservent leur format actuel.

  • JSON en entrée et en sortie ; montants en centimes entiers (5250 = 52,50 €) ; dates ISO 8601 (UTC).
  • Les champs inconnus sont refusés (une faute de frappe ne passe pas inaperçue).
  • Listes paginées par curseur : ?limit=25 (100 au plus), réponse { "data": [...], "hasMore": true, "nextCursor": "…" }, page suivante avec ?cursor=<nextCursor>. Tri : plus récent d'abord.
  • Chaque réponse porte un en-tête X-Request-Id à communiquer au support.

Idempotence

Les écritures d'argent ou de points (POST /purchases, POST /purchases/{id}/refunds, POST /rewards/{id}/redemptions) exigent un en-tête Idempotency-Key : 8 à 100 caractères (lettres, chiffres, _ . : -), unique par opération — par exemple le numéro de ticket. En cas de coupure réseau, renvoyez exactement la même requête avec la même clé : l'opération n'est faite qu'une fois et la réponse initiale revient (200 et Idempotent-Replayed: true). La même clé avec un contenu différent donne 409 idempotency_conflict.

Erreurs

Format unique, message en français lisible par un humain, détail par champ pour la validation :

HTTP/1.1 400 Bad Request
{
  "error": {
    "code": "validation_error",
    "message": "Certains paramètres sont invalides.",
    "details": [{ "path": "amountCents", "message": "Le montant : nombre entier de centimes attendu" }],
    "requestId": "req_Xb2…"
  }
}
HTTPcodeSignification
400validation_errorParamètres invalides (détail champ par champ dans « details »).
400invalid_jsonCorps de requête JSON illisible.
400idempotency_key_requiredEn-tête Idempotency-Key absent ou invalide.
401unauthorizedAuthentification absente.
401invalid_api_keyClé d'API inconnue ou mal formée.
401api_key_revokedClé d'API révoquée.
403insufficient_scopeLa clé n'a pas la portée nécessaire (lecture / écriture).
403organization_suspendedCompte du commerce suspendu.
403forbiddenOpération non autorisée.
404not_foundRessource introuvable (ou appartenant à une autre organisation).
409conflictConflit avec l'état actuel (déjà remboursé, e-mail déjà utilisé…).
409idempotency_conflictIdempotency-Key déjà utilisée pour une requête différente.
413payload_too_largeCorps de requête trop volumineux (64 Ko maximum).
422unprocessableRègle métier non respectée (solde insuffisant, montant restant dépassé…).
429rate_limitedTrop de requêtes : réessayez après le délai indiqué par Retry-After.
500internal_errorErreur interne.

Un identifiant appartenant à un autre commerce répond 404, exactement comme un identifiant inexistant.

Clients

GET/customers?q=…portée : lecture

Recherche par nom, prénom, e-mail, téléphone, numéro de membre ; une URL de carte (…/c/<jeton>, lue à la douchette) ou le jeton seul renvoie le client exact. Sans q : tous les clients actifs.

POST/customersportée : écriture

Crée un client. firstName obligatoire. marketingEmailConsent: true seulement si le client a donné son accord explicite (RGPD) ; il exige un e-mail.
curl -X POST https://remyz.fr/api/v1/customers \
  -H "Authorization: Bearer rmz_k7f3q9xz_…" -H "Content-Type: application/json" \
  -d '{ "firstName": "Hélène", "lastName": "Joseph", "phone": "0696 12 34 56" }'

GET/customers/{id}portée : lecture

Fiche, solde (pointsBalance), cumul, visites, lien de carte (cardUrl).

GET/customers/{id}/pointsportée : lecture

Solde et historique des points (grand livre : achats, remboursements, récompenses, ajustements…), paginé.

Achats et remboursements

POST/purchasesportée : écriture

Enregistre un achat et crédite les points selon les règles du programme (taux, minimum, arrondi), parrainage compris. reference (numéro de ticket ou de commande) est obligatoire. establishmentId est facultatif si le commerce n'a qu'un établissement actif (sinon voir GET /establishments). eligibleAmountCents : part ouvrant droit aux points. Une clé agit pour toute l'organisation : les restrictions d'établissements et les droits personnalisés des employés (écran Équipe) ne s'appliquent pas à l'API.
curl -X POST https://remyz.fr/api/v1/purchases \
  -H "Authorization: Bearer rmz_k7f3q9xz_…" -H "Content-Type: application/json" \
  -H "Idempotency-Key: ticket-2026-000123" \
  -d '{ "customerId": "4f0c…", "amountCents": 5200, "reference": "T-000123" }'

HTTP/1.1 201 Created
{ "purchase": { "id": "…", "amountCents": 5200, "pointsEarned": 52, "status": "COMPLETED", … },
  "customer": { "id": "4f0c…", "pointsBalance": 214 }, "belowMinimum": false, "referral": { "qualified": false } }

GET/purchases?customerId=…&reference=…portée : lecture

Achats (rapprochement de caisse par reference).

GET/purchases/{id}portée : lecture

Détail d'un achat et de ses remboursements.

POST/purchases/{id}/refundsportée : écriture

Remboursement partiel (amountCents) ou total (corps vide : tout le restant). Les points sont retirés au prorata, sans dérive d'arrondi ; si le client les a déjà dépensés, son solde devient négatif (negativeBalance: true). Idempotency-Key obligatoire.
curl -X POST https://remyz.fr/api/v1/purchases/9a1e…/refunds \
  -H "Authorization: Bearer rmz_k7f3q9xz_…" -H "Content-Type: application/json" \
  -H "Idempotency-Key: avoir-2026-000045" \
  -d '{ "amountCents": 1500, "reason": "Article retourné" }'

Récompenses et établissements

GET/rewards?customerId=…portée : lecture

Récompenses du programme ; avec customerId, chaque récompense indique affordable et missingPoints.

POST/rewards/{id}/redemptionsportée : écriture

Utilise une récompense pour un client (customerId) : débit des points sous verrou, refus si le solde est insuffisant (422, comme une récompense réservée à un niveau de fidélité que le client n'a pas). Idempotency-Key obligatoire.

POST/redemptions/{id}/cancelportée : écriture

Annule une utilisation (identifiant renvoyé à l'utilisation) : les points débités sont rendus, l'utilisation reste dans l'historique au statut CANCELLED. Corps : { "reason": "Client parti sans son café" } (motif obligatoire). Une seconde annulation répond 409 conflict : rien n'est rendu deux fois.

GET/establishmentsportée : lecture

Établissements actifs (identifiants à indiquer dans les achats).

Webhooks

Pour chaque événement choisi, REMYZ envoie un POST JSON à votre adresse HTTPS. L'événement est enregistré dans la même transaction que l'opération : il n'existe que si elle a réussi.

customer.createdClient créé
transaction.createdAchat enregistré
transaction.refundedAchat remboursé (total ou partiel)
reward.redeemedRécompense utilisée
points.adjustedPoints ajustés manuellement
reward.redemption_cancelledUtilisation de récompense annulée (points rendus)
POST /votre/adresse HTTP/1.1
Content-Type: application/json; charset=utf-8
Remyz-Signature: t=1792072800,v1=5f2b…
Remyz-Event-Id: 0b6f…
Remyz-Event-Type: transaction.created
Remyz-Delivery-Id: 77c1…

{ "id": "0b6f…", "type": "transaction.created", "apiVersion": "v1", "createdAt": "2026-10-15T14:00:00.000Z",
  "organizationId": "…",
  "data": { "purchase": { "id": "…", "reference": "T-000123", "amountCents": 5200, "pointsEarned": 52, … },
            "customer": { "id": "4f0c…", "pointsBalance": 214 } } }
  • Aucune donnée personnelle (nom, e-mail, téléphone, motif) dans les événements : relisez la fiche avec GET /customers/{id} si besoin.
  • Répondez 2xx en moins de 10 s (traitez ensuite en arrière-plan). Toute autre réponse, une redirection ou un délai dépassé déclenche une reprise : 30 s, 1,5 min, 4,5 min, 13,5 min, 40,5 min, 2 h, 6 h — puis abandon après 8 essais. L'historique et le bouton « Renvoyer » sont dans Paramètres › API & webhooks.
  • Livraison « au moins une fois », ordre non garanti : dédupliquez avec Remyz-Event-Id (identique à chaque reprise et à chaque renvoi).
  • Sécurité : adresses HTTPS uniquement, sans identifiants dans l'URL ; les adresses qui désignent ou résolvent vers un réseau privé ou local (127.0.0.1, 10.x, 192.168.x, 169.254.x, fc00::/7…) sont refusées, à l'enregistrement comme à chaque envoi.

Vérifier la signature

Remyz-Signature: t=<horodatage>,v1=<signature> où la signature est le HMAC-SHA256 (hexadécimal) de la chaîne <t>.<corps brut>, avec pour clé le secret du webhook (whsec_…, affiché à sa création). Calculez-la sur le corps brut reçu (avant toute analyse JSON), comparez en temps constant et refusez un horodatage de plus de 5 minutes (rejeu).

// Node.js (Express : app.post("/remyz", express.raw({ type: "application/json" }), handler))
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyRemyz(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const received = Buffer.from(parts.v1 ?? "", "hex");
  return received.length === expected.length && timingSafeEqual(received, expected);
}
# PHP
[$t, $v1] = [null, null];
foreach (explode(',', $_SERVER['HTTP_REMYZ_SIGNATURE']) as $p) { [$k, $v] = explode('=', $p, 2); if ($k === 't') $t = $v; if ($k === 'v1') $v1 = $v; }
$ok = abs(time() - (int)$t) <= 300
   && hash_equals(hash_hmac('sha256', $t . '.' . file_get_contents('php://input'), $secret), $v1);

Une question d'intégration ? Écrivez-nous en indiquant l'en-tête X-Request-Id de la réponse concernée.