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
- 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.
- Votre logiciel l'envoie à chaque appel ; la clé désigne le commerce : aucun identifiant d'organisation n'est à fournir.
- 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_scopesur 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…"
}
}| HTTP | code | Signification |
|---|---|---|
| 400 | validation_error | Paramètres invalides (détail champ par champ dans « details »). |
| 400 | invalid_json | Corps de requête JSON illisible. |
| 400 | idempotency_key_required | En-tête Idempotency-Key absent ou invalide. |
| 401 | unauthorized | Authentification absente. |
| 401 | invalid_api_key | Clé d'API inconnue ou mal formée. |
| 401 | api_key_revoked | Clé d'API révoquée. |
| 403 | insufficient_scope | La clé n'a pas la portée nécessaire (lecture / écriture). |
| 403 | organization_suspended | Compte du commerce suspendu. |
| 403 | forbidden | Opération non autorisée. |
| 404 | not_found | Ressource introuvable (ou appartenant à une autre organisation). |
| 409 | conflict | Conflit avec l'état actuel (déjà remboursé, e-mail déjà utilisé…). |
| 409 | idempotency_conflict | Idempotency-Key déjà utilisée pour une requête différente. |
| 413 | payload_too_large | Corps de requête trop volumineux (64 Ko maximum). |
| 422 | unprocessable | Règle métier non respectée (solde insuffisant, montant restant dépassé…). |
| 429 | rate_limited | Trop de requêtes : réessayez après le délai indiqué par Retry-After. |
| 500 | internal_error | Erreur interne. |
Un identifiant appartenant à un autre commerce répond 404, exactement comme un identifiant inexistant.
Clients
GET/customers?q=…portée : lecture
…/c/<jeton>, lue à la douchette) ou le jeton seul renvoie le client exact. Sans q : tous les clients actifs.POST/customersportée : écriture
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
pointsBalance), cumul, visites, lien de carte (cardUrl).GET/customers/{id}/pointsportée : lecture
Achats et remboursements
POST/purchasesportée : écriture
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
reference).GET/purchases/{id}portée : lecture
POST/purchases/{id}/refundsportée : écriture
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
customerId, chaque récompense indique affordable et missingPoints.POST/rewards/{id}/redemptionsportée : écriture
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
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
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.created | Client créé |
| transaction.created | Achat enregistré |
| transaction.refunded | Achat remboursé (total ou partiel) |
| reward.redeemed | Récompense utilisée |
| points.adjusted | Points ajustés manuellement |
| reward.redemption_cancelled | Utilisation 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
2xxen 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.