Loyalty API
NEWAPI fidélité YaniPay — gestion des programmes partenaires, points, tier et récompenses depuis l'application mobile.
Dernière mise à jour : 2026-04-09
Overview
L'API Loyalty Mobile expose 10 endpoints permettant à l'application React Native YaniPay de gérer entièrement l'expérience fidélité de l'utilisateur : consultation du tier et des points, inscription/désinscription aux programmes partenaires, défis actifs, historique des transactions et échange de récompenses.
Base URL
/api/v1/mobile/loyalty. Chaque requête doit inclure un header Authorization: Bearer <access_token>obtenu via l'endpoint /api/v1/mobile/auth/login./api/v1/mobile/loyalty/statusStatut fidélité, tier et points de l'utilisateur
/api/v1/mobile/loyalty/catalogCatalogue complet des programmes partenaires publiés
/api/v1/mobile/loyalty/programsProgrammes auxquels l'utilisateur est inscrit
/api/v1/mobile/loyalty/enroll/{programId}Inscription à un programme partenaire
/api/v1/mobile/loyalty/enroll/{programId}Désinscription d'un programme partenaire
/api/v1/mobile/loyalty/challengesDéfis actifs et leur progression
/api/v1/mobile/loyalty/historyHistorique des transactions de points
/api/v1/mobile/loyalty/cashbackHistorique et solde cashback
/api/v1/mobile/loyalty/cardsCartes fidélité actives de l'utilisateur
/api/v1/mobile/loyalty/rewardsRécompenses disponibles dans les programmes
Types TypeScript
Ces interfaces correspondent aux types réels utilisés dans le store Zustand mobile/src/store/loyaltyStore.ts et dans les routes backend.
// Tiers disponibles
type LoyaltyTierName = 'BRONZE' | 'SILVER' | 'GOLD' | 'PLATINUM' | 'DIAMOND';
// Statut fidélité global de l'utilisateur
interface LoyaltyStatus {
userId: string;
tier: LoyaltyTierName;
totalPoints: number;
availablePoints: number;
pendingPoints: number;
lifetimePoints: number;
tierProgress: number; // Pourcentage vers le tier suivant (0-100)
pointsToNextTier: number;
nextTier: LoyaltyTierName | null;
cashbackRate: number; // Taux cashback en %
multiplier: number; // Multiplicateur de points actuel
memberSince: string; // ISO 8601
tierExpiresAt: string | null;
}
// Programme partenaire
interface LoyaltyProgram {
id: string;
name: string;
description: string;
merchantName: string;
merchantDomain: string;
logo?: string; // URL CDN Brandfetch
color: string; // Couleur hex
pointsPerEuro: number;
cashbackRate: number;
isEnrolled: boolean;
enrolledAt?: string; // ISO 8601, si inscrit
currentPoints?: number; // Points actuels dans ce programme
tier?: string; // Tier dans ce programme
isPublished: boolean;
category: string; // Secteur (ex: "RETAIL", "FOOD")
}
// Défi actif
interface LoyaltyChallenge {
id: string;
title: string;
description: string;
type: 'TRANSACTIONS' | 'AMOUNT' | 'REFERRAL' | 'CATEGORY';
target: number;
current: number; // Valeur actuelle de progression
progress: number; // Pourcentage (0-100)
reward: number; // Valeur de la récompense
rewardType: 'POINTS' | 'CASHBACK' | 'BADGE';
startDate: string; // ISO 8601
endDate: string; // ISO 8601
status: 'ACTIVE' | 'COMPLETED' | 'EXPIRED';
}
// Transaction de points
interface LoyaltyTransaction {
id: string;
type: 'earn' | 'redeem' | 'expire' | 'bonus';
points: number;
description: string;
programName?: string;
timestamp: string; // ISO 8601
}
// Récompense disponible
interface LoyaltyReward {
id: string;
title: string;
description: string;
thresholdPoints: number; // Points nécessaires pour débloquer
type: 'DISCOUNT' | 'FREE_PRODUCT' | 'CASHBACK' | 'GIFT';
value: number;
programId: string;
programName: string;
merchantName: string;
merchantDomain: string;
logo?: string;
isActive: boolean;
}
// Carte fidélité active
interface MobileLoyaltyCard {
id: string;
programId: string;
programName: string;
pointsBalance: number;
cashbackBalance: number;
tier: string;
status: 'ACTIVE' | 'SUSPENDED' | 'BLOCKED';
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}/api/v1/mobile/loyalty/statusBearer JWTRetourne le statut fidélité complet de l'utilisateur authentifié : tier actuel, points disponibles, progression et taux de cashback.
// Response shape
interface StatusResponse {
success: true;
userId: string;
tier: LoyaltyTierName;
totalPoints: number;
availablePoints: number;
pendingPoints: number;
lifetimePoints: number;
tierProgress: number; // 0-100
pointsToNextTier: number;
nextTier: LoyaltyTierName | null;
cashbackRate: number; // En %
multiplier: number;
memberSince: string; // ISO 8601
tierExpiresAt: string | null;
}import { api } from '@/services/api';
// Depuis le store Zustand
const response = await api.get<LoyaltyStatus>('/loyalty/status');
// Exemple de réponse JSON
// {
// "success": true,
// "userId": "clxyz123",
// "tier": "GOLD",
// "totalPoints": 6840,
// "availablePoints": 5240,
// "pendingPoints": 1600,
// "lifetimePoints": 12500,
// "tierProgress": 37,
// "pointsToNextTier": 8160,
// "nextTier": "PLATINUM",
// "cashbackRate": 2,
// "multiplier": 1.5,
// "memberSince": "2025-01-15T10:30:00.000Z",
// "tierExpiresAt": null
// }Cache côté client
useLoyaltyStore.getState().invalidateCache() pour forcer un rechargement./api/v1/mobile/loyalty/catalogBearer JWTRetourne tous les programmes de fidélité publiés et actifs, avec indication si l'utilisateur y est déjà inscrit.
// Response shape
interface CatalogResponse {
success: true;
programs: LoyaltyProgram[]; // Tous les programmes publiés
totalPrograms: number;
}
// Chaque LoyaltyProgram inclut :
// - isEnrolled: boolean (calculé côté serveur pour cet utilisateur)
// - currentPoints?: number (si inscrit)
// - tier?: string (tier dans ce programme, si inscrit)
// - logo?: string // https://cdn.brandfetch.io/{domain}/w/400/h/400/theme/dark/icon.png
// - rewards?: RewardSummary[] // Les 3 premières récompenses du programmeconst response = await api.get<{
success: boolean;
programs: LoyaltyProgram[];
totalPrograms?: number;
}>('/loyalty/catalog');
const programs = response.data.programs;
const enrolledCount = programs.filter(p => p.isEnrolled).length;
// Filtrer par secteur côté client
const foodPrograms = programs.filter(p => p.category === 'FOOD');/api/v1/mobile/loyalty/programsBearer JWTRetourne tous les programmes disponibles avec le flag isSubscribed pour filtrer les programmes auxquels l'utilisateur est inscrit.
// Response shape
interface ProgramsResponse {
success: true;
programs: Array<LoyaltyProgram & { isSubscribed?: boolean }>;
pagination: {
page: number;
limit: number;
total: number;
pages: number;
};
categories: string[];
}
// Note: le backend utilise 'isSubscribed' ; le store normalise vers 'isEnrolled'
// programs.filter(p => p.isEnrolled ?? p.isSubscribed) → programmes inscrits uniquementconst response = await api.get<{
success: boolean;
programs: Array<LoyaltyProgram & { isSubscribed?: boolean }>;
}>('/loyalty/programs');
// Normalisation isSubscribed → isEnrolled
const allPrograms = response.data.programs.map(p => ({
...p,
isEnrolled: p.isEnrolled ?? p.isSubscribed ?? false,
}));
const myPrograms = allPrograms.filter(p => p.isEnrolled);/api/v1/mobile/loyalty/enroll/{programId}Bearer JWTInscrit l'utilisateur authentifié au programme partenaire spécifié. Crée une nouvelle LoyaltyCard avec un solde de 0 point.
// Paramètre URL
// programId: string — ID du programme (ex: "clxyz456")
// Corps de la requête : aucun body requis
// Response shape (succès)
interface EnrollResponse {
success: true;
card: {
id: string;
programId: string;
userId: string;
pointsBalance: number; // 0 à la création
status: 'ACTIVE';
createdAt: string; // ISO 8601
};
}
// Response shape (erreur 409 — déjà inscrit)
interface EnrollConflictResponse {
success: false;
error: 'ALREADY_ENROLLED';
message: string;
}// Depuis le store Zustand
await api.post(`/loyalty/enroll/${programId}`);
// La mise à jour optimiste dans le store :
set(state => ({
catalogPrograms: state.catalogPrograms.map(p =>
p.id === programId
? { ...p, isEnrolled: true, enrolledAt: new Date().toISOString(), currentPoints: 0 }
: p
),
enrolledPrograms: [
...state.enrolledPrograms,
{ ...state.catalogPrograms.find(p => p.id === programId)!, isEnrolled: true, currentPoints: 0 },
],
}));/api/v1/mobile/loyalty/enroll/{programId}Bearer JWTDésinscrit l'utilisateur du programme spécifié. La carte fidélité est mise en statut SUSPENDED (soft delete — les points ne sont pas supprimés).
// Paramètre URL
// programId: string — ID du programme
// Response shape (succès)
interface UnenrollResponse {
success: true;
message: string;
}
// Response shape (erreur 404 — non inscrit)
interface UnenrollNotFoundResponse {
success: false;
error: 'NOT_ENROLLED';
message: string;
}await api.delete(`/loyalty/enroll/${programId}`);
// Mise à jour optimiste dans le store :
set(state => ({
catalogPrograms: state.catalogPrograms.map(p =>
p.id === programId
? { ...p, isEnrolled: false, currentPoints: undefined }
: p
),
enrolledPrograms: state.enrolledPrograms.filter(p => p.id !== programId),
}));Soft delete
SUSPENDED. Les points accumulés sont conservés et une réinscription ultérieure rétablit la carte./api/v1/mobile/loyalty/challengesBearer JWTRetourne les défis actifs de l'utilisateur avec leur progression en temps réel.
// Response shape
interface ChallengesResponse {
success: true;
challenges: RawChallenge[];
}
// Shape brute retournée par le backend (avant normalisation store)
interface RawChallenge {
id: string;
title: string;
description: string;
type: 'TRANSACTIONS' | 'SPENDING' | 'AMOUNT' | 'REFERRAL' | 'CATEGORY';
target: number;
progress?: number; // Valeur de progression brute (interchangeable avec 'current')
current?: number;
reward: number;
rewardType: 'POINTS' | 'CASHBACK' | 'MULTIPLIER' | 'BADGE';
startDate: string; // ISO 8601
endDate: string; // ISO 8601
status?: 'ACTIVE' | 'COMPLETED' | 'EXPIRED';
isCompleted?: boolean;
}
// Après normalisation dans le store (LoyaltyChallenge) :
// - type 'SPENDING' → 'AMOUNT'
// - rewardType 'MULTIPLIER' → 'BADGE'
// - progress: pourcentage calculé (0-100)
// - current: valeur absolue// Réponse backend brute
{
"id": "ch_123",
"title": "10 paiements ce mois",
"description": "Effectuez 10 paiements pour gagner 500 points",
"type": "TRANSACTIONS",
"target": 10,
"progress": 7, // 7 paiements effectués
"reward": 500,
"rewardType": "POINTS",
"startDate": "2026-04-01T00:00:00.000Z",
"endDate": "2026-04-30T23:59:59.000Z",
"status": "ACTIVE"
}
// Après normalisation dans le store :
// { current: 7, progress: 70, type: 'TRANSACTIONS', rewardType: 'POINTS' }/api/v1/mobile/loyalty/historyBearer JWTRetourne l'historique complet des transactions de points de l'utilisateur (gains, utilisations, expirations, bonus).
// Response shape
interface HistoryResponse {
success: true;
transactions: LoyaltyTransaction[];
}
interface LoyaltyTransaction {
id: string;
type: 'earn' | 'redeem' | 'expire' | 'bonus';
points: number; // Positif (gain) ou négatif (utilisation)
description: string; // ex: "Achat chez Leclerc Saint-Pierre"
programName?: string; // Nom du programme associé
timestamp: string; // ISO 8601
}{
"success": true,
"transactions": [
{
"id": "tx_789",
"type": "earn",
"points": 150,
"description": "Achat 30€ — Leclerc Sainte-Marie",
"programName": "Leclerc Réunion",
"timestamp": "2026-04-08T14:32:00.000Z"
},
{
"id": "tx_788",
"type": "bonus",
"points": 500,
"description": "Bonus bienvenue YaniPay",
"timestamp": "2026-01-15T10:30:00.000Z"
},
{
"id": "tx_790",
"type": "redeem",
"points": -200,
"description": "Échange bon d'achat 2€",
"programName": "YaniPay Rewards",
"timestamp": "2026-04-07T09:15:00.000Z"
}
]
}/api/v1/mobile/loyalty/cashbackBearer JWTRetourne l'historique des transactions cashback (où cashbackEarned > 0 ou cashbackUsed > 0) et le solde total disponible.
// Response shape
interface CashbackResponse {
success: true;
transactions: CashbackTransaction[];
totalCashback: number; // Solde cashback total disponible (en EUR)
pendingCashback: number; // Cashback en cours de validation
currentRate: number; // Taux cashback moyen actuel (en %)
}
interface CashbackTransaction {
id: string;
type: 'earn' | 'redeem';
amount: number; // Montant en EUR
programName: string;
merchantName: string;
date: string; // ISO 8601
}
// Réponse si aucune carte inscrite :
// { success: true, transactions: [], totalCashback: 0, pendingCashback: 0, currentRate: 0 }{
"success": true,
"transactions": [
{
"id": "cb_001",
"type": "earn",
"amount": 0.60,
"programName": "Leclerc Réunion",
"merchantName": "Leclerc",
"date": "2026-04-08T14:32:00.000Z"
}
],
"totalCashback": 4.80,
"pendingCashback": 1.20,
"currentRate": 2
}/api/v1/mobile/loyalty/cardsBearer JWTRetourne toutes les cartes fidélité de l'utilisateur avec leurs détails, soldes et statistiques agrégées.
// Response shape
interface CardsResponse {
success: true;
cards: MobileLoyaltyCard[];
stats: {
totalCards: number;
activeCards: number;
totalPoints: number; // Total points sur toutes les cartes
totalCashback: number; // Total cashback sur toutes les cartes (EUR)
};
}
interface MobileLoyaltyCard {
id: string;
programId: string;
pointsBalance: number;
cashbackBalance: number;
tier: string;
status: 'ACTIVE' | 'SUSPENDED' | 'BLOCKED';
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
program: {
id: string;
name: string;
description: string;
pointsPerEuro: number;
cashbackRate: number;
type: string;
isActive: boolean;
store: {
id: string;
name: string;
sector: string;
};
};
}import { apiGet } from '@/lib/api/client';
import type { MobileLoyaltyCardsResponse } from '@platform/api/contracts/mobile';
const data = await apiGet<MobileLoyaltyCardsResponse>('/api/v1/mobile/loyalty/cards');
console.log(`${data.stats.activeCards} cartes actives, ${data.stats.totalPoints} points`);/api/v1/mobile/loyalty/rewardsBearer JWTRetourne le catalogue des récompenses disponibles dans les programmes actifs, avec filtrage et pagination.
// Query params (tous optionnels)
interface RewardsQueryParams {
category?: string; // Filtre par secteur du store (ex: "FOOD", "RETAIL")
featured?: 'true'; // Si 'true', trie par seuil de points croissant (plus accessibles en premier)
page?: number; // Défaut: 1
limit?: number; // Défaut: 20, max: 50
}
// Response shape
interface RewardsResponse {
success: true;
rewards: LoyaltyReward[];
pagination: {
page: number;
limit: number;
total: number;
pages: number;
};
}
interface LoyaltyReward {
id: string;
title: string;
description: string;
thresholdPoints: number; // Points nécessaires pour débloquer
type: 'DISCOUNT' | 'FREE_PRODUCT' | 'CASHBACK' | 'GIFT';
value: number; // Valeur de la récompense
programId: string;
programName: string;
merchantName: string;
merchantDomain: string;
logo?: string; // URL CDN Brandfetch
isActive: boolean;
}// Récompenses les plus accessibles (featured)
const featured = await api.get('/loyalty/rewards?featured=true&limit=6');
// Récompenses alimentaires
const food = await api.get('/loyalty/rewards?category=FOOD&page=1&limit=20');
// Exemple de réponse d'une récompense
// {
// "id": "rw_001",
// "title": "Bon d'achat 5€",
// "description": "Échangeable chez tous les magasins Leclerc Réunion",
// "thresholdPoints": 500,
// "type": "DISCOUNT",
// "value": 5,
// "programId": "prog_leclerc",
// "programName": "Leclerc Réunion",
// "merchantName": "Leclerc",
// "merchantDomain": "https://www.leclercdrive.fr",
// "logo": "https://cdn.brandfetch.io/leclercdrive.fr/w/400/h/400/theme/dark/icon.png",
// "isActive": true
// }Système de Tiers
Le tier est calculé automatiquement par le backend en fonction des points lifetimede l'utilisateur (total de tous les points jamais gagnés, sans déduction des points utilisés).
| Tier | Points lifetime min. | Multiplicateur | Cashback rate |
|---|---|---|---|
| BRONZE | 0 pts | ×1 | 1% |
| SILVER | 1 000 pts | ×1.25 | 1.5% |
| GOLD | 5 000 pts | ×1.5 | 2% |
| PLATINUM | 15 000 pts | ×2 | 3% |
| DIAMOND | 50 000 pts | ×3 | 5% |
tierProgress et pointsToNextTier
tierProgress est un entier de 0 à 100 représentant la progression vers le tier suivant. pointsToNextTier indique le nombre de points lifetime supplémentaires nécessaires. Au tier DIAMOND (max), tierProgress vaut 100 et pointsToNextTier vaut 0.Spécification OpenAPI 3.0.3 complète
Tous les endpoints /api/v1/ sont documentés dans le fichier YAML — importable dans Postman, Insomnia ou Swagger UI.