API et intégrations

Votre caisse, votre ERP ou votre boutique en ligne créent des tickets, changent leur état et reçoivent chaque étape par webhook. Les clients reçoivent les mêmes notifications que depuis l’app.

Sur demande pendant le lancement : écrivez-nous et nous activons l’API pour votre boutique.

BASEhttps://api.tfa9adni.com/v1

Démarrer

Toutes les requêtes partent de cette adresse, en HTTPS, avec du JSON dans les deux sens.

Chaque réponse a la même enveloppe : success et data, ou success à false et une error avec un code. Les montants sont en unités mineures (millimes, centimes) avec la devise et le nombre de décimales. Les dates sont en ISO 8601, en UTC.

GET/v1/shop

Tester la connexion

Renvoie la boutique liée à la clé, sa devise, son fuseau et les droits de la clé.

Créez une clé dans l’app : Boutique › Intégrations. Seul le propriétaire de la boutique peut le faire. Puis testez la connexion :

Authentification

Envoyez la clé dans l’en-tête Authorization. Une clé ne donne accès qu’à sa boutique.

La clé s’affiche une seule fois à sa création. Gardez-la sur votre serveur ou dans votre caisse, jamais dans une page web ni dans un message. Une clé révoquée cesse de marcher en moins d’une minute. Cinq clés actives au plus par boutique.

Droits d’une clé

  • tickets:readLire les tickets.
  • tickets:writeCréer des tickets et changer leur état.
  • clients:readRetrouver un client et lire sa carte de fidélité.
  • loyalty:writeAjouter des tampons et utiliser une récompense.

« Accès complet » donne les quatre droits, « Lecture seule » donne tickets:read et clients:read.

Tickets

États d’un ticket

  • workshopÀ l’atelier, en cours.
  • readyPrêt, en attente du client.
  • impossiblePas réparable, l’article attend son client.
  • collectedRécupéré. outcome dit s’il était prêt ou impossible.
  • abandonedClôturé : jamais récupéré, ou sans nouvelles de la boutique pendant 60 jours. abandon_reason dit lequel (uncollected, no_news).

POST/v1/tickets

Créer un ticket

Crée un ticket à l’atelier. Son numéro suit ceux de l’app.

L’en-tête Idempotency-Key est obligatoire : si le réseau coupe, renvoyez la même requête avec la même clé, vous recevez le même ticket et replayed à true, jamais un doublon. Le numéro suit ceux de l’app.

En-têtes

  • Idempotency-Key string requis

    Une clé unique par tentative : la même requête renvoyée donne le même ticket.

Corps JSON

  • service string requis

    Le service, comme dans l’app.

  • type string optionnel

    repair (réparation) ou prepare (commande). repair par défaut.

  • item string optionnel

    L’article déposé.

  • description string optionnel

    Une note pour l’atelier.

  • items array optionnel

    Plusieurs articles (2 à 20) : label, service, price_minor.

  • client_id uuid optionnel

    Un client qui connaît déjà la boutique.

  • client_phone string optionnel

    Le numéro du client : le ticket lui est proposé.

  • price_minor integer optionnel

    Le prix, en unités mineures.

  • deposit_minor integer optionnel

    L’acompte, en unités mineures.

  • promised_at datetime optionnel

    Le jour promis, en ISO 8601.

  • external_ref string optionnel

    Votre référence, 64 caractères au plus.

client_phone ne relie personne : le ticket est proposé au compte qui porte ce numéro, qui l’accepte ou bloque la boutique. La réponse est la même qu’un compte existe ou non.

GET/v1/tickets

Lister les tickets

Filtre, pagine et synchronise les tickets de la boutique.

Filtres : state (plusieurs, séparés par des virgules), number, client_id, phone, external_ref, created_after, created_before, updated_since. limit de 1 à 100 (50 par défaut). Si has_more est vrai, renvoyez next_cursor dans cursor. Avec updated_since, l’ordre suit la date de mise à jour : pratique pour synchroniser.

Paramètres d’URL

  • state string optionnel

    Un ou plusieurs états, séparés par des virgules.

  • number integer optionnel

    Le numéro du ticket.

  • phone string optionnel

    Le numéro saisi sur le ticket.

  • external_ref string optionnel

    Votre référence.

  • updated_since datetime optionnel

    Seulement ce qui a changé depuis cette date.

  • limit integer optionnel

    De 1 à 100, 50 par défaut.

  • cursor string optionnel

    Le next_cursor de la page précédente.

GET/v1/tickets/{id}

Lire un ticket

Un ticket par son id. Celui d’une autre boutique répond 404.

GET/v1/tickets/by-number/{n}

Trouver par numéro

Le ticket ouvert le plus récent qui porte ce numéro.

Les numéros repartent à 1 après 9999 : by-number renvoie le ticket ouvert le plus récent, sinon le plus récent, et meta.others liste les autres.

POST/v1/tickets/{id}/status

Changer l’état

Prêt, impossible, retour à l’atelier ou récupéré.

Les mêmes règles que dans l’app : prêt ou impossible depuis l’atelier, récupéré depuis prêt ou impossible, retour à l’atelier depuis prêt ou impossible. Un ticket récupéré peut être rouvert pendant 24 h, ensuite la réponse est 409 avec reason REOPEN_WINDOW. Le client reçoit la même notification, et la fidélité compte comme dans l’app.

Corps JSON

  • status string requis

    ready, impossible, workshop ou collected.

POST/v1/tickets/{id}/notified

Client prévenu

Vous avez prévenu le client vous-même.

Vous avez prévenu le client vous-même, par SMS ou par téléphone ? Dites-le, la page du client l’affiche.

Corps JSON

  • channel string requis

    sms, whatsapp ou call.

Clients et fidélité

Une boutique ne voit que les clients qui la connaissent déjà : un ticket accepté chez elle ou un passage au comptoir. Chercher un numéro inconnu, ou le numéro d’un client qui a bloqué la boutique, donne la même réponse 404.

Le nom affiché respecte le choix du client : s’il cache son nom, vous voyez « Sana B. ».

GET/v1/clients

Chercher un client

Par téléphone, parmi les clients qui connaissent déjà la boutique.

Paramètres d’URL

  • phone string requis

    Le numéro, au format international.

GET/v1/clients/{id}

Lire un client

Nom affiché, tickets ouverts et au total, carte de fidélité.

GET/v1/clients/{id}/loyalty

Carte de fidélité

Le programme de la boutique et la carte du client.

POST/v1/clients/{id}/loyalty/stamps

Ajouter des tampons

Ajoute de 1 à 10 tampons sur la carte.

Tampons et récompenses exigent une Idempotency-Key. 20 tampons au plus par client et par jour et par clé. Si la fidélité est éteinte, la réponse est 409 LOYALTY_OFF.

En-têtes

  • Idempotency-Key string requis

    Obligatoire.

Corps JSON

  • count integer requis

    De 1 à 10.

POST/v1/clients/{id}/loyalty/redeem

Utiliser une récompense

Consomme une récompense disponible sur la carte.

En-têtes

  • Idempotency-Key string requis

    Obligatoire.

Webhooks

Donnez une adresse https dans l’app : Tfa9adni y envoie un POST à chaque étape d’un ticket. Un seul webhook par boutique.

  • ticket.createdUn ticket est créé, dans l’app ou par l’API.
  • ticket.readyLe ticket est prêt.
  • ticket.impossibleLe ticket est marqué impossible.
  • ticket.reopenedLe ticket revient à l’atelier.
  • client.notifiedLe client a été prévenu (WhatsApp, app, SMS, appel).
  • ticket.collectedLe client a récupéré son article.
  • ticket.abandonedLe ticket est clôturé : prêt et jamais récupéré (60 jours), ou resté à l’atelier sans nouvelles (60 jours). reason vaut uncollected ou no_news.
  • quote.acceptedLe client accepte un nouveau prix.
  • client.linkedUn client est relié au ticket.
  • pingLe bouton « Envoyer un test » de l’app.

Vérifier la signature

Chaque envoi porte l’en-tête Tfa9adni-Signature : t est l’heure d’envoi en secondes, v1 le HMAC-SHA256 de « t.corps » avec le secret du webhook, en hexadécimal. Calculez-le sur le corps brut, comparez en temps constant, et refusez un t à plus de 5 minutes. Pendant 24 h après un changement de secret, deux v1 sont envoyés.

Bonnes pratiques

  • Répondez 2xx en moins de 10 secondes, puis traitez plus tard.
  • Un événement peut arriver deux fois : dédoublonnez sur id.
  • Ignorez les types que vous ne connaissez pas : de nouveaux arriveront.
  • Pour rattraper un trou, relisez GET /v1/tickets?updated_since=.

Sans réponse 2xx, nouvel essai après 1 min, 5 min, 30 min, 2 h, 6 h, 12 h et 24 h, puis l’envoi est abandonné. Après 20 échecs d’affilée sur 72 h, ou une réponse 410, le webhook s’éteint et le propriétaire est prévenu. Les redirections ne sont pas suivies, et les adresses privées sont refusées.

Erreurs

En cas d’erreur, success est à false et error.code dit pourquoi. details.reason précise un 409.

400 VALIDATION_ERROR Corps ou paramètre invalide, details dit lequel.
401 UNAUTHENTICATED Clé absente, fausse ou révoquée.
403 FORBIDDEN La clé n’a pas ce droit (details.scope).
403 SHOP_NOT_APPROVED La boutique n’est pas encore validée.
403 ACCOUNT_DEACTIVATED La boutique est désactivée.
403 API_DISABLED L’API n’est pas encore ouverte pour cette boutique.
404 NOT_FOUND Introuvable pour cette boutique.
409 CONFLICT Action impossible dans cet état (details.reason).
422 PROFANITY Le texte contient un mot interdit.
429 TOO_MANY_ATTEMPTS Trop de requêtes ou de tickets, réessayez plus tard.

Limites

  • 120 par minuteRequêtes par clé
  • 30 par minuteÉcritures par clé
  • 150 par heure, 600 par jourTickets par boutique (app et API ensemble)
  • 60 par heure, 300 par jourRecherches de client par téléphone

Chaque réponse porte les en-têtes RateLimit-Limit, RateLimit-Remaining et RateLimit-Reset. Au-delà, la réponse est 429 : attendez RateLimit-Reset secondes.