Écritures : contacts, passages, codes promo et brouillons

Alimenter la base clients depuis une caisse ou un CRM, enregistrer des passages, valider des codes promo et déposer des brouillons de réponse.

Documentation de l'APIGuides · Écritures

Avant d'écrire

Les écritures sont en version bêta. Elles suivent les mêmes règles que les actions faites dans l'application, et chacune demande un droit dédié :

DroitOpérations
contacts:writePOST /v1/contacts, PATCH /v1/contacts/{contactId}
visits:writePOST /v1/visits
promo_codes:redeemPOST /v1/promo-codes/redeem
replies:writePUT /v1/reviews/{reviewId}/reply-draft
ai:generatePOST /v1/reviews/{reviewId}/reply-drafts

Le modèle Caisse, proposé à la création d'une clé, réunit establishments:read, contacts:write, visits:write et promo_codes:redeem. La publication d'une réponse sur Google a son propre guide : publication sur Google.

  • Toutes les écritures sont refusées en 403 tenant_read_only sur un compte en lecture seule, par exemple après la fin de l'essai.
  • Toutes acceptent l'en-tête Idempotency-Key, pour être renvoyées sans risque de doublon : voir le guide d'idempotence.
  • Les corps s'envoient en JSON, avec Content-Type: application/json. Un champ inconnu répond 400 invalid_request.
  • Les écritures produisent les mêmes événements que l'application, avec l'origine api : un contact créé par l'API donne un contact.created d'origine api.

Contacts et consentement

POST /v1/contacts ajoute un client à la base d'un établissement, ou complète le contact qui a déjà le même email ou le même téléphone dans cet établissement :

curl https://api.mabellenote.com/v1/contacts \
  -H "Authorization: Bearer $MBN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-contact-58213" \
  -d '{
    "establishmentId": "kh2n8p5r3t6v9b1c4d7f0g2j5k8m1n3p",
    "email": "[email protected]",
    "phone": "+33612345678",
    "firstName": "Camille",
    "lastName": "Martin",
    "smsConsent": {
      "collectedAt": "2026-10-20T18:30:00.000Z",
      "proofText": "J’accepte de recevoir les offres du Bistrot par SMS. Désinscription à tout moment par STOP."
    }
  }'

L'API répond 201 quand elle crée le contact et 200 quand elle complète un contact existant, avec le contact dans sa forme publique (Contact). Un contact existant n'est que complété : un champ déjà renseigné n'est pas écrasé. Pour modifier un champ, utilisez PATCH /v1/contacts/{contactId}, où un champ texte à null est effacé et un champ absent reste inchangé. L'email et le téléphone ne se modifient pas par l'API.

L'email ou le téléphone est obligatoire. Le téléphone s'écrit au format E.164 (+33612345678) ou au format national français (0612345678).

La preuve de consentement

Un consentement marketing transmis, emailConsent ou smsConsent, porte toujours sa preuve :

ChampContenu
collectedAtDate à laquelle la personne a donné son accord, au format ISO 8601, jamais dans le futur.
proofTextTexte exact qui lui a été présenté au moment de l'accord, 2 000 caractères au plus.

Sans l'un de ces deux champs, l'API répond 400 invalid_request. Un consentement SMS exige un téléphone sur le contact, un consentement email exige un email. L'origine enregistrée du consentement est api.

Créer une clé qui porte contacts:write demande en plus, dans l'application, que le propriétaire ou l'administrateur certifie que les clients envoyés par l'outil ont donné leur consentement explicite et préalable. Cette attestation est rattachée à chaque consentement enregistré avec la clé.

L'identifiant de votre CRM

Transmettez externalId, l'identifiant du client dans votre CRM ou votre caisse (1 à 128 caractères, unique dans un établissement). POST /v1/contacts reconnaît d'abord le contact par cet identifiant, puis par son email ou son téléphone : un client dont l'email a changé dans votre CRM reste le même contact. PATCH /v1/contacts/{contactId} le modifie ou l'efface (null), et GET /v1/contacts?externalId=crm-48213 retrouve le contact.

L'identifiant revient dans chaque réponse d'enquête (survey_response.completed), dans le champ contactExternalId, avec visitExternalId quand l'enquête suit un passage : votre CRM range la réponse dans la bonne fiche sans conserver les identifiants Ma Belle Note. Si l'enquête après passage de l'établissement a l'option « Envoyer aussi à chaque nouveau client reçu par l'API », un contact créé avec un consentement reçoit l'enquête, avec le délai réglé, sans attendre de passage.

La désinscription prime toujours

Un email ou un numéro désinscrit, par exemple après un STOP ou un clic sur un lien de désinscription, ne redevient jamais consentant par l'API, même si la requête transmet un consentement. L'appel aboutit, mais le consentement n'est pas accordé : vérifiez optOuts et consents.<canal>.granted dans la réponse, et ne relancez pas la personne par ce canal. Extrait de la réponse pour un numéro désinscrit :

{
  "object": "contact",
  "id": "ct9x2b5d8f1h4j7l0n3p6r9t2v5x8z1b",
  "consents": {
    "email": { "granted": false, "collectedAt": null, "source": null, "revokedAt": null },
    "sms": {
      "granted": false,
      "collectedAt": "2026-03-02T12:10:00.000Z",
      "source": "loyalty_wheel",
      "revokedAt": "2026-09-14T08:02:51.000Z"
    }
  },
  "optOuts": { "email": false, "sms": true }
}

Passages

POST /v1/visits enregistre le passage d'un client, depuis une caisse ou un logiciel de réservation. Le client est désigné par contactId, ou par son email ou son téléphone :

curl https://api.mabellenote.com/v1/visits \
  -H "Authorization: Bearer $MBN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "establishmentId": "kh2n8p5r3t6v9b1c4d7f0g2j5k8m1n3p",
    "externalId": "ticket-2026-10-20-0187",
    "occurredAt": "2026-10-20T20:15:00.000Z",
    "phone": "+33612345678"
  }'
  • externalId est l'identifiant du passage dans votre outil, par exemple le numéro de ticket ou de réservation, de 1 à 128 caractères. Il est unique dans un établissement : le même externalId ne crée qu'un seul passage, avec ou sans Idempotency-Key. La première réponse est 201, les suivantes 200 avec le passage existant.
  • occurredAt vaut par défaut l'heure de la requête.
  • contactId exclut email et phone.
  • Un client inconnu est ajouté à la base clients sans consentement : un passage n'en donne aucun. Pour recueillir un consentement, utilisez POST /v1/contacts avec sa preuve.

Codes promo

POST /v1/promo-codes/redeem valide, depuis une caisse, un code promo envoyé par une campagne ou un scénario, selon les mêmes règles qu'une validation au comptoir dans l'application :

curl https://api.mabellenote.com/v1/promo-codes/redeem \
  -H "Authorization: Bearer $MBN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: caisse-1-ticket-2026-10-20-0187" \
  -d '{ "code": "BIS-7KQ4", "establishmentId": "kh2n8p5r3t6v9b1c4d7f0g2j5k8m1n3p", "amountCents": 4850 }'
{
  "object": "promo_code_redemption",
  "promoCodeId": "pc3j6m9p2s5v8y1b4e7h0k3n6q9t2w5z",
  "code": "BIS-7KQ4",
  "establishmentId": "kh2n8p5r3t6v9b1c4d7f0g2j5k8m1n3p",
  "contactId": "ct9x2b5d8f1h4j7l0n3p6r9t2v5x8z1b",
  "offer": "Un dessert offert",
  "campaignName": "Relance d'automne",
  "issuedAt": "2026-10-01T09:00:00.000Z",
  "expiresAt": "2026-10-31T09:00:00.000Z",
  "redeemedAt": "2026-10-20T20:15:00.000Z",
  "amountCents": 4850,
  "currency": "EUR"
}

Le code passe à l'état utilisé. Le passage du client et le montant facultatif du ticket, en centimes, sont enregistrés et comptent dans l'attribution du chiffre d'affaires de la campagne. Le code est insensible à la casse.

SituationRéponse
Code déjà utilisé409 promo_code_already_redeemed
Code expiré409 promo_code_expired
Code annulé409 conflict
Code inconnu, ou d'un autre établissement404 not_found
Trop de tentatives avec la clé429 rate_limited, avec Retry-After

Un code refusé ne peut pas être forcé par l'API : le forçage reste réservé à l'application. Affichez le motif du refus à l'équipe de caisse. Les tentatives sont limitées par clé, en plus de la limite générale, pour empêcher de deviner des codes.

Brouillons de réponse

Un brouillon déposé ou demandé par l'API apparaît dans l'application comme tout brouillon, avec son origine visible (source: "api" ou "ai"). Rien n'est publié sur Google par ces deux opérations. Un avis déjà répondu répond 409 conflict.

Déposer un brouillon rédigé

PUT /v1/reviews/{reviewId}/reply-draft dépose le texte d'une réponse rédigée dans votre outil, comme une modification faite dans l'application. Il crée le brouillon (201) ou remplace celui qui existe (200) :

curl -X PUT https://api.mabellenote.com/v1/reviews/jd7f3k2m9x8w1q4z6c5v3b2n1m0l9k8j/reply-draft \
  -H "Authorization: Bearer $MBN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "content": "Bonjour Jean, merci pour votre retour. Nous en avons parlé à l’équipe du soir." }'
Brouillon en file d'autopilote

Si le brouillon attend déjà dans la file de l'autopilote, il y reste avec votre nouveau texte : c'est ce texte que l'autopilote publiera. La modification est inscrite au journal de l'autopilote.

Demander un brouillon à l'IA

POST /v1/reviews/{reviewId}/reply-drafts, sans corps, demande à l'IA un brouillon avec le ton de l'établissement. Si l'avis a déjà un brouillon, l'IA le régénère.

curl -X POST https://api.mabellenote.com/v1/reviews/jd7f3k2m9x8w1q4z6c5v3b2n1m0l9k8j/reply-drafts \
  -H "Authorization: Bearer $MBN_API_KEY" \
  -H "Idempotency-Key: draft-jd7f3k2m9x8w1q4z6c5v3b2n1m0l9k8j-1"
  • Chaque génération consomme une unité du crédit IA mensuel du compte, exactement comme dans l'application. À crédit épuisé, l'API répond 402 ai_credits_exhausted.
  • Chaque appel compte pour 5 dans la limite de débit de la clé.
  • Avec Idempotency-Key, au plus une génération a lieu par clé d'idempotence terminée : utilisez-la pour ne pas consommer deux crédits sur une coupure réseau.
  • Un assistant IA connecté par MCP peut aussi demander ce brouillon, avec l'outil draft_reply : voir connecter Claude ou ChatGPT à vos avis.

La réponse est le brouillon (ReplyDraft), avec sensitive: true quand l'avis ou la réponse a été jugé sensible : sa publication demandera une confirmation, dans l'application comme par l'API.

Pour relire le brouillon en cours, lisez l'avis (GET /v1/reviews/{reviewId}) avec une clé qui a replies:write ou ai:generate : son champ draft porte le brouillon (ReplyDraft), ou null s'il n'y en a pas. Sans l'un de ces droits, le champ n'apparaît pas.

Mis à jour le 25 octobre 2026