← Retour à l'accueil

API Scaniha

Branchez votre caisse, votre POS ou vos outils internes sur le programme de fidélité de votre établissement. L'API publique vous permet de créditer des points à l'achat, de valider les codes gagnants et de consulter le solde et l'historique d'un client par numéro de téléphone.

Base URL : https://scaniha.com/api/v1

1. Introduction

L'API Scaniha lit et écrit les données de fidélité de votreétablissement, et de lui seul. Chaque clé est rattachée à un café : une requête ne peut jamais lire ou modifier les données d'un autre établissement.

Concrètement, vous pouvez :

  • Créditer des points lors d'un achat, directement depuis votre caisse (le bonus de bienvenue est attribué automatiquement à la première visite).
  • Valider les codes gagnants (roue) et les codes de récompense présentés par vos clients.
  • Consulter le solde et l'historique d'un client à partir de son numéro de téléphone.

Base URL : https://scaniha.com/api/v1

HTTPS obligatoire.Toutes les requêtes doivent passer par HTTPS. Le trafic HTTP est redirigé vers HTTPS en périphérie ; n'envoyez jamais une clé API sur une connexion non chiffrée.

2. Authentification

L'API s'authentifie par clé API, passée dans l'en-têteAuthorizationau format Bearer :

Authorization: Bearer sk_live_VOTRE_CLE_API

Générez et gérez vos clés depuis votre tableau de bord : /admin/api.

La clé n'est affichée qu'une seule fois, à sa création. Copiez-la immédiatement : elle ne pourra plus être récupérée ensuite (seul son préfixe reste visible). En cas de perte ou de fuite, révoquez-la et créez-en une nouvelle.

Gardez la clé côté serveur.Une clé donne accès en lecture et en écriture aux données de fidélité de votre établissement : ne l'exposez jamais dans un navigateur, une application mobile ou un dépôt de code public. Utilisez-la uniquement depuis votre caisse / vos serveurs.

Chaque clé porte des scopesqui déterminent ce qu'elle peut faire : loyalty:read (lecture) et loyalty:write (écriture). Une requête sans le scope requis renvoie insufficient_scope (403).

Numéros de téléphone — une seule forme canonique

Envoyez toujours le numéro sous une seule et même forme. Forme recommandée : le format international complet, par exemple +216XXXXXXXX. Attention :"+216…" et "216…"sont traités comme deux clients distincts (le solde de points est indexé sur la chaîne exacte du téléphone). Normalisez vos numéros avant chaque appel pour éviter de scinder un même client.

3. Format des erreurs

Toutes les erreurs renvoient le même enveloppe JSON, avec le statut HTTP correspondant :

{
  "error": {
    "code": "snake_case_code",
    "message": "Message en français."
  }
}

Lorsqu'un détail est disponible, il est fusionné dans l'objet error. Par exemple, un solde insuffisant précise le nombre de points manquants :

{
  "error": {
    "code": "insufficient_points",
    "message": "Solde de points insuffisant.",
    "missing": 12
  }
}

Toutes les réponses (succès comme erreur) portent l'en-tête Scaniha-API-Version: 1. Les réponses en succès sont des objets JSON simples (pas d'enveloppe success: true).

codestatutquand
unauthorized401En-tête Bearer absent ou mal formé.
invalid_key401Clé inconnue (hash introuvable).
key_revoked401Clé révoquée.
business_inactive403Établissement suspendu, expiré ou introuvable.
insufficient_scope403La clé ne possède pas le scope requis.
validation_error400Téléphone, montant, reward_id ou code invalide / manquant.
not_found404Récompense, code ou programme introuvable.
program_inactive409Le programme de fidélité est désactivé.
insufficient_points409Solde insuffisant (error.missing = points manquants).
rate_limited429Limite d’usage dépassée (en-tête Retry-After en secondes).
method_not_allowed405Verbe HTTP non supporté pour cette route.
server_error500Erreur inattendue / service momentanément indisponible.

Cette enveloppe structurée s'applique uniquement aux routes /api/v1/*. Les routes internes du tableau de bord utilisent un format différent.

4. Endpoints

Programme de fidélité

GET/api/v1/programScope : loyalty:read

Configuration publique du programme de fidélité de l'établissement lié à la clé. Si le programme est désactivé, la réponse reste 200 avec "active": false(c'est une lecture de configuration, pas une erreur).

Réponse 200

{
  "active": true,
  "businessName": "Café Central",
  "pointsPerTnd": 1,
  "rewardsCount": 4
}

Exemple

curl https://scaniha.com/api/v1/program \
  -H "Authorization: Bearer sk_live_xxx"

Erreurs possibles : unauthorized, invalid_key, key_revoked, business_inactive, insufficient_scope, rate_limited, server_error.

Liste des récompenses

GET/api/v1/rewardsScope : loyalty:read

Liste les récompenses actives du programme. Si le programme est désactivé : program_inactive (409).

Réponse 200

{
  "rewards": [
    { "id": "uuid", "label": "Café offert", "points_cost": 50 }
  ]
}

Exemple

curl https://scaniha.com/api/v1/rewards \
  -H "Authorization: Bearer sk_live_xxx"

Erreurs possibles : erreurs d'authentification ci-dessus, program_inactive.

Solde & historique d'un client

GET/api/v1/customers/{phone}Scope : loyalty:read

Solde, historique récent et codes actifs pour un numéro de téléphone. Le {phone} doit être encodé pour l'URL (le + devient %2B). Un numéro inconnu renvoie balance: 0 avec des tableaux vides — ce n'est pas une erreur 404.

Réponse 200

{
  "phone": "+21655555555",
  "balance": 85,
  "recent": [
    { "delta": 75, "reason": "purchase", "note": "Achat de 75 TND", "created_at": "2026-06-17T10:00:00Z" },
    { "delta": 10, "reason": "welcome",  "note": "Bienvenue",       "created_at": "2026-06-17T09:59:00Z" }
  ],
  "activeWins": [
    { "code": "K7F-3QZ", "label": "Café offert", "expires_at": "2026-06-18T...", "created_at": "..." }
  ],
  "activeRedemptions": [
    { "code": "A2B-CDE", "label": "Dessert", "expires_at": "2026-06-19T...", "created_at": "..." }
  ]
}

Exemple

curl https://scaniha.com/api/v1/customers/%2B21655555555 \
  -H "Authorization: Bearer sk_live_xxx"

Erreurs possibles : erreurs d'authentification, validation_error (téléphone invalide).

Créditer des points (achat)

POST/api/v1/points/awardScope : loyalty:write

Crédite un achat. amountest le montant dépensé en TND ; les points ajoutés valent round(amount × pointsPerTnd). Le bonus de bienvenue est ajouté automatiquement lors de la première écriture au grand-livre du client.

Corps de la requête

{
  "phone": "+21655555555",
  "amount": 75,
  "note": "Optionnel, ≤ 120 caractères"
}

amount doit être un nombre fini strictement supérieur à 0. Si noteest omis, une note lisible est générée automatiquement (ex. : "Achat de 75 TND").

Réponse 200

{
  "phone": "+21655555555",
  "pointsAdded": 75,
  "welcomeAdded": 10,
  "balance": 85
}

Exemple

curl -X POST https://scaniha.com/api/v1/points/award \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+21655555555","amount":75,"note":"Table 4"}'

Erreurs possibles : erreurs d'authentification, insufficient_scope, validation_error (téléphone ou montant invalide), program_inactive.

Échanger une récompense

POST/api/v1/rewards/redeemScope : loyalty:write

Débite les points et émet un code de récompense à présenter en caisse. Si le solde est insuffisant : insufficient_points (409) avec error.missing = points manquants.

Corps de la requête

{
  "phone": "+21655555555",
  "reward_id": "uuid"
}

Réponse 200

{
  "code": "A2B-CDE",
  "rewardLabel": "Café offert",
  "expiresAt": "2026-06-19T...",
  "balance": 35
}

Exemple

curl -X POST https://scaniha.com/api/v1/rewards/redeem \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+21655555555","reward_id":"<uuid>"}'

Erreurs possibles : erreurs d'authentification, insufficient_scope, validation_error, not_found (récompense introuvable), insufficient_points, program_inactive.

Valider un code

POST/api/v1/codes/validateScope : loyalty:write

Valide un code gagnant (roue) ou un code de récompense. redeem vaut false par défautsur l'API : par défaut, le code est seulement consulté (« peek ») sans être consommé. Passez "redeem": truepour le consommer (un code ne peut être consommé qu'une seule fois).

Corps de la requête

{
  "code": "A2B-CDE",
  "redeem": false
}

Réponse 200 — code introuvable

{ "found": false }

Un code appartenant à un autre établissement renvoie également { "found": false } — chaque clé ne voit que ses propres codes.

Réponse 200 — code trouvé

{
  "found": true,
  "kind": "win",
  "status": "valid",
  "label": "Café offert",
  "customerPhone": "+21655555555",
  "expiresAt": "2026-06-18T...",
  "redeemedAt": null,
  "pointsCost": null
}

Sémantique des champs

  • kindwin | reward.
  • status valid | redeemed | expired | already | cancelled. valid n'apparaît qu'en mode peek (redeem = false). cancelled ne concerne que les codes de récompense.
  • customerPhone peut être null(ex. : un gain de la roue obtenu sans téléphone). Votre intégration doit tolérer la valeur null.
  • redeemedAt n'est présent que pour status: "already" (code déjà collecté).
  • pointsCost n'est présent que pour kind: "reward" (null/absent pour les gains).
  • expiresAt n'est présent que pour status: "valid" (mode peek).

Exemple

curl -X POST https://scaniha.com/api/v1/codes/validate \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"code":"A2B-CDE","redeem":true}'

Erreurs possibles : erreurs d'authentification, insufficient_scope, validation_error (code trop court ou manquant).

5. Limites & usage équitable

L'API applique une politique d'usage équitable : 60 requêtes par minute et 5 000 requêtes par jour par clé. Au-delà, les requêtes renvoient rate_limited (429) avec un en-tête Retry-After indiquant le nombre de secondes à attendre.

Cette limite est best-effort : elle protège contre un emballement accidentel (une boucle de caisse mal configurée, par exemple) mais n'est pas un quota strict et distribué. Concevez votre intégration pour respecter Retry-After et éviter les rafales.

Révocation & suspension — cohérence éventuelle

Une clé révoquée, ou un établissement suspendu, prend effet à la prochaine requête— la validation est refaite à chaque appel, mais n'est pas appliquée en cours de requête. Au pire, une requête déjà en vol au moment exact de la révocation peut aboutir.

En cas d'usage abusif et persistant, révoquez la clé concernée depuis /admin/api (la dernière utilisation de chaque clé y est visible).

6. Versionnement

L'API est versionnée dans l'URL, sous /api/v1/. Les changements cassants seront publiés sous /api/v2/ ; la v1 reste stable.

Chaque réponse porte l'en-tête Scaniha-API-Version: 1 (succès comme erreur).

L'ajout de nouveaux champs optionnelsdans les réponses n'est pas considéré comme cassant : votre client doit tolérer les champs inconnus.

7. Tarifs & renouvellement

L'accès à l'API est inclus avec le programme de fidélité de votre établissement. Trois formules sont proposées, en dinars tunisiens :

Menu QR — 1 an

150 TND /an

Menu numérique + QR, renouvelable annuellement.

Menu QR — À vie

250 TND /unique

Paiement unique — ne se renouvelle pas.

Fidélité + API — 1 an

50 TND /an

Programme de fidélité & accès API, abonnement annuel.

Le paiement se fait manuellement (D17, virement RIB ou Flouci), avec envoi du reçu depuis votre tableau de bord.

Renouvellement

  • Le renouvellement annuel est manuel — il n'y a aucun prélèvement automatique.
  • Une facture / un rappel est envoyé avant l'échéance.
  • Une période de grâce de 30 jourssuit l'échéance : la clé continue de fonctionner. Passé ce délai, l'API renvoie business_inactive jusqu'au paiement.
  • L'option À vie ne se renouvelle pas.

Une question sur l'intégration ?

Contactez-nous →