Aller au contenu

Brand Partner API

Une petite API REST prévisible pour créer des commandes mintbot, consulter leur statut et recevoir des événements de cycle de vie. JSON en entrée, JSON en sortie. Authentification par bearer token. Écritures idempotentes. Webhooks signés.

Dashboard d’accès API Aller aux webhooks


En bref

Base URL https://mint.mintbot.ai/api/v1
Auth `Authorization: Bearer ***
Idempotency Idempotency-Key: <uuid> sur chaque POST
Content type application/json
Rate limit 120 req / 60 s par partenaire
Webhooks Signés avec HMAC-SHA256, réessayés jusqu’à 7 fois

Authentification

Chaque requête envoie une clé API partenaire dans l’en-tête Authorization. Génère ou fais tourner la clé dans le dashboard.

Authorization: Bearer mo_liv...xxxx
Content-Type: application/json

La rotation n’a pas de fenêtre de grâce

Faire tourner la clé révoque atomiquement la précédente. Prévois de remplacer la valeur dans ta config avant de cliquer sur Rotate.

Idempotence

Chaque POST requiert un en-tête Idempotency-Key. N’importe quel UUID convient pour chaque requête distincte.

  • Nous mettons la réponse en cache pendant 24 heures. Une nouvelle tentative avec la même clé rejoue la réponse d’origine avec Idempotent-Replay: true.
  • Réutiliser la même clé avec un corps différent renvoie 409 idempotency_key_mismatch.

Commandes

Créer une commande

POST /orders

Crée une commande et renvoie une URL Stripe Checkout. Une fois le paiement confirmé, mintbot provisionne l’agent et l’événement de revenu correspondant à la part partenaire est enregistré automatiquement.

Requête

{
  "tier": "s1",
  "duration_months": 1,
  "credit_usd": 10,
  "language": "en",
  "external_id": "your-side-id",
  "success_url": "https://your.app/thanks?id={ORDER_ID}",
  "cancel_url":  "https://your.app/cart",
  "webhook_url": "https://your.app/mintbot-webhook"
}
tier
Une des valeurs trial, s1, s2, s4.
duration_months
Durée de vie du serveur en mois calendaires. Doit être 1, 3 ou 12.
credit_usd
Optional. Crédit de chat préfinancé inclus avec la commande.
language
Optional. Influence la locale de Stripe Checkout et la langue de l’e-mail de bienvenue.
external_id
Optional. Renvoyé dans chaque webhook et chaque réponse de commande — utilise-le pour relier les commandes mintbot aux lignes de ton propre système.
success_url · cancel_url
URL de retour Stripe Checkout. {ORDER_ID} est remplacé côté serveur.
webhook_url
Optional. Remplacement, pour cette commande, de l’URL de webhook configurée au niveau partenaire.

Réponse — 201 Created

{
  "id": 42,
  "tier": "s1",
  "duration_months": 1,
  "credit_usd": 10,
  "amount_cents": 1200,
  "currency": "usd",
  "status": "awaiting_payment",
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_test_…",
  "panel_url": null,
  "expires_at": null,
  "language": "en",
  "external_id": "your-side-id",
  "created_at": "2026-05-16 09:58:00",
  "paid_at": null
}

Exemple

curl -X POST https://mint.mintbot.ai/api/v1/orders \
  -H "Authorization: Bearer *** \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "tier": "s1",
    "duration_months": 1,
    "credit_usd": 10,
    "success_url": "https://your.app/thanks?id={ORDER_ID}",
    "cancel_url":  "https://your.app/cart"
  }'
import os, uuid, requests

r = requests.post(
    "https://mint.mintbot.ai/api/v1/orders",
    headers={
        "Authorization": f"Bearer {os.environ['MINTBOT_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "tier": "s1",
        "duration_months": 1,
        "credit_usd": 10,
        "success_url": "https://your.app/thanks?id={ORDER_ID}",
        "cancel_url":  "https://your.app/cart",
    },
    timeout=10,
)
r.raise_for_status()
order = r.json()
redirect_to = order["checkout_url"]

Récupérer une commande

GET /orders/{id}

Récupère une commande unique à partir de son id mintbot. Renvoie la même forme que POST /orders.

curl https://mint.mintbot.ai/api/v1/orders/42 \
  -H "Authorization: Bearer ***

Lister les commandes

GET /orders

Liste paginée par curseur, des plus récentes aux plus anciennes.

Paramètres de requête

status
Optional. Filtrer par awaiting_payment, completed, deployed, deploy_failed ou expired.
cursor
Optional. Passe next_cursor depuis la page précédente pour continuer.

Réponse

{
  "items": [ /* OrderResponse … */ ],
  "next_cursor": "37"
}

next_cursor vaut null quand il n’y a plus de pages.


Renouveler une commande

POST /orders/{id}/renew

Prolonge un agent existant de duration_months supplémentaires. Infra uniquement — aucun nouveau crédit de chat n’est inclus. Renvoie un nouvel id de commande et une nouvelle URL Stripe Checkout.

Requête

{
  "duration_months": 1,
  "external_id": "your-side-id",
  "success_url": "https://your.app/thanks?id={ORDER_ID}",
  "cancel_url":  "https://your.app/account"
}

Revenus

Lire les revenus

GET /revenue

Totaux plus les 200 derniers événements du ledger.

Paramètres de requête

include_paid
Optional, default true. Mets à false pour voir uniquement les événements qui n’ont pas encore été payés.

Réponse

{
  "currency": "usd",
  "gross_cents": 12000,
  "partner_cut_cents": 2000,
  "mintbot_cut_cents": 10000,
  "unpaid_cents": 800,
  "events": [
    {
      "id": 7,
      "order_id": 42,
      "kind": "order_paid",
      "gross_cents": 1200,
      "partner_cut_cents": 200,
      "mintbot_cut_cents": 1000,
      "currency": "usd",
      "created_at": "2026-05-16 09:58:00",
      "payout_id": null,
      "payout_at": null
    }
  ]
}

Profil partenaire

Récupérer le profil

GET /partner

Renvoie ton profil partenaire ainsi que le solde impayé — pratique pour afficher les gains dans ta propre interface d’administration sans les stocker toi-même.

Réponse

{
  "id": 12,
  "email": "you@kliendifirma.com",
  "pricing_currency": "usd",
  "balance_unpaid_cents": 800,
  "webhook_url": "https://your.app/mintbot-webhook",
  "api_key_prefix": "mo_live_a12b"
}

Paramètres

Récupérer les paramètres

GET /settings

Renvoie l’intégralité de ta configuration partenaire non secrète dans un seul payload — modes choisis, domaine apex, fournisseur DNS, fournisseur de bot, dépôt template, motif de sous-domaine (avec un libellé d’aperçu rendu), tarification par palier, métadonnées de la clé API, métadonnées de webhook, et le même état de préparation / statut par section qui pilote la bannière du tableau de bord MintOffice.

Conçu pour les agents d’intégration qui ont besoin de découvrir comment le partenaire est configuré sans scraper le tableau de bord ni interroger l’opérateur. Un appelant typique est un agent de codage qui installe ta configuration en marque blanche sur un serveur vierge — il peut lire cet endpoint, brancher sur readiness.status, et indiquer à l’opérateur exactement quelles sections nécessitent encore de l’attention.

Aucun secret n’est jamais renvoyé

Tout ce que le partenaire a fourni comme identifiant — token de bot Telegram, clé API Zone.ee, secret de signature de webhook, le texte en clair de la clé API lui-même — n’apparaît que sous forme de booléen *_set. Le secret de webhook expose en plus le même préfixe d’affichage de 8 caractères déjà montré dans le tableau de bord, pour qu’un appelant puisse confirmer quel secret est configuré sans jamais en voir la valeur.

Réponse

{
  "id": 12,
  "email": "you@kliendifirma.com",
  "status": "live",
  "created_at": "2026-05-12 14:22:01",
  "onboarded_at": "2026-05-12 14:48:33",
  "activated_at": "2026-05-13 09:10:05",
  "last_login_at": "2026-05-20 00:51:18",
  "preferred_language": "en",
  "pricing_currency": "usd",
  "balance_unpaid_cents": 800,

  "domain": {
    "mode": "byo",
    "client_apex_domain": "agent99.cc",
    "dns_provider": "zone.ee",
    "zone_ee_username": "you@kliendifirma.com",
    "zone_ee_api_key_set": true,
    "subdomain_prefix": "agent",
    "subdomain_numbering": "natural",
    "subdomain_pad_width": 3,
    "subdomain_label_preview": "agent1"
  },

  "bot": {
    "mode": "own",
    "provider": "telegram",
    "telegram_bot_username": "agent99cc_bot",
    "telegram_bot_token_set": true
  },

  "template": {
    "mode": "default",
    "repo_url": null
  },

  "api": {
    "key_prefix": "mo_live_a12b",
    "key_created_at": "2026-05-13 09:11:42",
    "webhook_url": "https://your.app/mintbot-webhook",
    "webhook_secret_set": true,
    "webhook_secret_prefix": "ab12cd34"
  },

  "pricing": {
    "mode": "default",
    "currency": "usd",
    "tiers": {
      "trial": { "price_cents": 0,    "partner_cut_cents": 0,   "updated_at": null },
      "s1":    { "price_cents": 1200, "partner_cut_cents": 200, "updated_at": "2026-05-13 09:05:00" },
      "s2":    { "price_cents": 2400, "partner_cut_cents": 400, "updated_at": "2026-05-13 09:05:00" },
      "s4":    { "price_cents": 4800, "partner_cut_cents": 800, "updated_at": "2026-05-13 09:05:00" }
    }
  },

  "readiness": {
    "ready": true,
    "missing_fields": [],
    "dns_blocker": null,
    "status": "live"
  },

  "section_status": {
    "domain":   { "status": "ready", "mode": "byo",     "issue": null, "dns_blocker": null },
    "bot":      { "status": "ready", "mode": "own",     "issue": null, "dns_blocker": null },
    "template": { "status": "ready", "mode": "default", "issue": null, "dns_blocker": null },
    "api":      { "status": "ready", "mode": "live",    "issue": null, "dns_blocker": null },
    "all_ready": true
  }
}

Référence des champs

status
Cycle de vie du partenaire. Une des valeurs onboarding, live, paused. La Brand Partner API rejette les écritures pendant onboarding.
domain.mode
byo (tu apportes ton propre domaine apex) ou hosted (mintbot fournit un sous-domaine *.mintbot.ai). En hosted, les champs du fournisseur DNS sont intentionnellement vides quoi que le partenaire ait saisi auparavant.
domain.subdomain_label_preview
Le libellé rendu que le premier agent client recevrait (agent1, 1, 001, …) compte tenu des subdomain_prefix, subdomain_numbering et subdomain_pad_width actuels. Utilise-le pour un aperçu « voici ce que verront tes clients » sans réimplémenter les règles de numérotation.
bot.mode
own (bot Telegram fourni par le partenaire), borrow (utilisation du bot de mintbot pendant les tests) ou web (panneau uniquement, pas de Telegram).
template.mode
default (pas d’agent customization repo — l’agent tourne sur la base nue sans marque) ou custom (l’agent customization repo du partenaire à repo_url). En custom, le VPS de l’agent lui-même clone ce dépôt après le déploiement standard et exécute son install.sh / update.sh pour appliquer ta persona, le thème du panneau et tout outillage supplémentaire par-dessus la base — central n’exécute jamais le dépôt lui-même. Pointe repo_url vers ton agent customization repo GitHub HTTPS quand tu veux une marque personnalisée.
api.webhook_secret_prefix
Les 8 premiers caractères du secret de signature de webhook, présents uniquement quand un secret est configuré. Permet à un appelant de distinguer quel secret est en place sans voir la valeur complète. Le tableau de bord affiche le même préfixe.
readiness.missing_fields
Liste des noms de champs du tableau de bord encore requis avant la mise en ligne (Go Live). Vide quand ready vaut true. Suffisamment stable pour piloter une UI « ce qui manque encore » dans ta propre administration.
readiness.dns_blocker
Défini à une courte chaîne de raison quand la vérification DNS échoue (par ex. nameservers_not_pointing, glue_record_missing). null sinon.
section_status.*.issue
Indication lisible par un humain, par section, quand cette section bloque la mise en ligne. Reflète les pastilles de statut par section du tableau de bord.

Exemple

curl https://mint.mintbot.ai/api/v1/settings \
  -H "Authorization: Bearer ***
import os, requests

r = requests.get(
    "https://mint.mintbot.ai/api/v1/settings",
    headers={"Authorization": f"Bearer {os.environ['MINTBOT_API_KEY']}"},
    timeout=10,
)
r.raise_for_status()
settings = r.json()

if not settings["readiness"]["ready"]:
    print("Still missing:", settings["readiness"]["missing_fields"])

Catalogue

La liste des forfaits vendables pour ta vitrine — les forfaits publics (trial / starter / pro) joints à ta tarification résolue, dans ta devise de tarification. Génère tes cartes de forfaits à partir de ceci au lieu de coder en dur les slugs de palier et les prix : les forfaits retirés disparaissent d’eux-mêmes, et un changement de prix ou de devise que tu fais dans le tableau de bord atteint ta vitrine dans sa fenêtre de cache — sans redéploiement.

C’est l’endpoint qui empêche une vitrine d’annoncer les forfaits d’hier. Le portail de référence l’appelle à chaque rendu de landing / /buy / /extend (mis en cache, avec un fallback persistant).

Les textes d’affichage restent les tiens

display_name / description / featured sont les métadonnées canoniques des forfaits mintbot. Si tu veux renommer un forfait ou réécrire son descriptif pour ta marque, remplace-le dans ta propre vitrine (le portail de référence fournit une map PLAN_OVERRIDES précisément pour ça) — le catalogue est la source de vérité pour quels forfaits existent et ce qu’ils coûtent, pas pour tes textes marketing.

Récupérer le catalogue

GET /api/v1/catalog

Aucun paramètre. Auth bearer uniquement — il n’y a aucune barrière d’activation, donc une vitrine peut afficher le catalogue avant que ton compte partenaire ne soit passé en live.

Réponse

{
  "currency": "usd",
  "packages": [
    {
      "tier": "trial",
      "display_name": "Trial",
      "description": "One day to kick the tires.",
      "featured": false,
      "default_credit_usd": 5,
      "durations": [
        { "months": 1, "label": "24 hours", "price_cents": 0 }
      ],
      "subscription": { "available": false, "price_cents": null }
    },
    {
      "tier": "starter",
      "display_name": "Starter",
      "description": "A month of assistant time.",
      "featured": true,
      "default_credit_usd": 10,
      "durations": [
        { "months": 1,  "label": "1 month",   "price_cents": 1500 },
        { "months": 3,  "label": "3 months",  "price_cents": 4500 },
        { "months": 12, "label": "12 months", "price_cents": 18000 }
      ],
      "subscription": { "available": true, "price_cents": 1500 }
    }
  ]
}
Champ Signification
currency Ta devise de tarification. Chaque price_cents ci-dessous y est libellé.
packages[] Une entrée par forfait public, dans l’ordre d’offre/d’affichage.
tier Le slug que tu passes à POST /orders / POST /subscriptions.
display_name / description / featured Textes d’affichage canoniques (remplace-les dans ta vitrine si tu veux).
default_credit_usd Indication du crédit LLM inclus avec le forfait (informatif).
durations[] Les durées de configuration pour lesquelles ce forfait peut être acheté. months est la valeur que tu envoies comme duration_months ; price_cents est ce que la commande facturera. Les forfaits mensuels affichent 1 / 3 / 12 mois ; trial affiche une durée unique de 24 heures.
subscription available vaut true pour les forfaits qui peuvent être achetés en renouvellement automatique mensuel via POST /subscriptions ; price_cents est le montant mensuel. trial n’est jamais éligible à l’abonnement.

Les prix correspondent à ce qui te sera facturé

Chaque price_cents est calculé avec le même résolveur de tarification que celui utilisé par l’endpoint de commande, donc le prix d’une carte est toujours égal au total final de Stripe Checkout pour ce (tier, duration_months).

Exemple

curl -s https://mint.mintbot.ai/api/v1/catalog \
  -H "Authorization: Bearer *** | jq
import os, requests

r = requests.get(
    "https://mint.mintbot.ai/api/v1/catalog",
    headers={"Authorization": f"Bearer {os.environ['MINTBOT_API_KEY']}"},
    timeout=10,
)
r.raise_for_status()
catalog = r.json()

for pkg in catalog["packages"]:
    month1 = next(d for d in pkg["durations"] if d["months"] == 1)
    print(pkg["display_name"], month1["price_cents"], catalog["currency"])

Enregistrements DNS

Les intégrations brand-partner ont souvent besoin de provisionner des enregistrements DNS sur le domaine apex du partenaire — pointer @, www, ou des sous-domaines par client vers le bon serveur. MintOffice expose un proxy aveugle au fournisseur pour que tu puisses le faire sans jamais manipuler la clé API du fournisseur en amont dans ta propre infrastructure. Les identifiants que tu as enregistrés pendant l’onboarding (aujourd’hui : zone.ee ; cloudflare et route53 sont sur la feuille de route derrière la même forme) restent chiffrés dans MintOffice ; ton appel arrive avec Bearer mo_live_… et nous faisons l’appel en amont en ton nom.

La portée est l’apex configuré par le partenaire lui-même (domain.client_apex_domain depuis GET /settings). Tu ne peux pas toucher un enregistrement sur une zone que tu ne possèdes pas.

Aujourd’hui, le proxy prend en charge les opérations qu’utilise le pipeline de déploiement lui-même — enregistrements A (upsert / list / delete par id). La prise en charge de CNAME / TXT / MX suivra la même forme quand l’abstraction du fournisseur sous-jacent les intégrera.

Idempotency-Key non requis ici

Contrairement aux endpoints de création de commande, les upserts DNS sont naturellement idempotents au niveau du fournisseur (réexécuter avec le même (subdomain, ip) est un no-op et les doublons obsolètes sont balayés à chaque appel). Tu n’as pas besoin d’envoyer l’en-tête Idempotency-Key — les nouvelles tentatives convergent toujours vers un seul enregistrement A par FQDN.

Insérer ou mettre à jour un enregistrement A

POST /dns/records

Pointe de façon idempotente <subdomain>.<your-apex> vers une adresse IPv4. Passe "" (ou "@") pour l’apex lui-même.

Requête

{
  "subdomain": "www",
  "ip": "203.0.113.10"
}

subdomain accepte :

  • "" ou "@" — l’apex lui-même (example.com).
  • Un label DNS unique — "www", "api" — pour <label>.<apex>.
  • Des sous-labels pointés — "api.eu" — pour des sous-domaines imbriqués (api.eu.example.com).

ip doit être une adresse IPv4 en notation pointée. Chaque label est [a-z0-9_-], 1–63 caractères, sans - en début ou fin.

Réponse — 200

{
  "apex": "example.com",
  "provider": "zone_ee",
  "fqdn": "www.example.com",
  "records": [
    { "id": "9182734", "name": "www.example.com", "destination": "203.0.113.10" }
  ]
}

La liste records post-écriture reflète toujours la vue actuelle du fournisseur en amont pour le FQDN après un balayage des doublons — donc une réponse réussie avec exactement un enregistrement est le signal de chemin heureux indiquant qu’il ne reste rien d’obsolète.

Lister les enregistrements

GET /dns/records

Renvoie chaque enregistrement A sur l’apex configuré. Ajoute ?name=<label-or-fqdn> pour filtrer sur un seul FQDN — la valeur peut être un label nu ("www"), un sous-domaine pointé ("api.eu") ou le FQDN complet ("www.example.com").

Réponse — 200

{
  "apex": "example.com",
  "provider": "zone_ee",
  "items": [
    { "id": "9182734", "name": "example.com",     "destination": "203.0.113.10" },
    { "id": "9182735", "name": "www.example.com", "destination": "203.0.113.10" }
  ]
}

Supprimer un enregistrement

DELETE /dns/records/{record_id}

record_id est l’identifiant fournisseur renvoyé par GET /dns/records (zone.ee émet des id entiers, convertis en chaîne à la frontière de l’API). Supprimer un enregistrement qui n’existe plus est traité comme un succès — l’endpoint est idempotent côté client.

Réponse — 200

{ "deleted": true, "id": "9182734" }

Erreurs

HTTP error.code Cause
401 unauthenticated / invalid_api_key Token Bearer manquant / incorrect.
422 validation_error Sous-domaine incorrect ou destination non-IPv4.
422 dns_not_configured Le partenaire n’a pas de client_apex_domain ou les identifiants du fournisseur configuré sont manquants. Corrige dans Settings → Domain.
422 dns_provider_unsupported Le fournisseur DNS du partenaire n’est pas encore raccordé au proxy MintOffice (par ex. placeholder cloudflare). Bascule vers zone_ee pour l’instant.
502 dns_provider_error Le fournisseur en amont a renvoyé un 4xx/5xx, ou le transport a échoué. Le message d’erreur porte le statut en amont et un court extrait, expurgé de tout secret, du corps en amont. Réessayable sans risque.

Exemple

# Pointe l’apex + www vers l’IP de ton serveur.
curl https://mint.mintbot.ai/api/v1/dns/records \
  -H "Authorization: Bearer *** \
  -H "Content-Type: application/json" \
  -d '{"subdomain": "",    "ip": "203.0.113.10"}'

curl https://mint.mintbot.ai/api/v1/dns/records \
  -H "Authorization: Bearer *** \
  -H "Content-Type: application/json" \
  -d '{"subdomain": "www", "ip": "203.0.113.10"}'

# Vérifie.
curl "https://mint.mintbot.ai/api/v1/dns/records" \
  -H "Authorization: Bearer ***
import os, requests

URL = "https://mint.mintbot.ai/api/v1/dns/records"
H = {"Authorization": f"Bearer {os.environ['MINTBOT_API_KEY']}"}

for sub in ("", "www"):
    r = requests.post(URL, headers=H, json={"subdomain": sub, "ip": "203.0.113.10"})
    r.raise_for_status()
    print(sub or "@", r.json()["records"])

Webhooks

Quand quelque chose arrive à une commande, nous envoyons par POST un événement JSON signé à ton URL de webhook configurée.

Types d’événements

Événement Quand il se déclenche
order.created Session Stripe Checkout créée, en attente de paiement.
order.paid Paiement confirmé par Stripe. Événement de revenu enregistré.
order.cancelled Commande expirée ou explicitement annulée.
agent.provisioning_started Pipeline de déploiement démarré pour cette commande.
agent.ready Déploiement réussi. Le payload contient panel_url et expires_at.
agent.failed Erreur dans le pipeline de déploiement. Consulte le champ error pour l’étape qui a cassé.
agent.expired TTL de l’agent écoulé. Le partenaire peut renouveler via POST /orders/{id}/renew.

Livraison et nouvelles tentatives

  • Jusqu’à 7 tentatives selon un calendrier exponentiel : 0s, 30s, 2m, 10m, 1h, 6h, 24h.
  • Après la dernière tentative, la livraison est marquée exhausted et les nouvelles tentatives s’arrêtent.
  • Réponds avec un statut 2xx sous 10 secondes pour acquitter.

En-têtes de requête

Content-Type: application/json
User-Agent: mintbot-webhook/1.0
X-Mintbot-Signature: t=<unix_ts>,v1=<hex_hmac_sha256>
X-Mintbot-Event-Id: evt_42_order.paid_1747371234567
X-Mintbot-Event-Type: order.paid

Exemple de payload — order.paid

{
  "id": 42,
  "tier": "s1",
  "duration_months": 1,
  "credit_usd": 10,
  "amount_cents": 1200,
  "currency": "usd",
  "status": "completed",
  "external_id": "your-side-id",
  "paid_at": "2026-05-16 10:02:14"
}

Vérifier la signature

La signature est HMAC-SHA256(secret, "{timestamp}.{raw_body}"). Vérifie toujours le corps de requête brut — resérialiser le JSON déjà parsé cassera la comparaison.

import hmac, hashlib, time

def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
    try:
        ts_part, v1_part = header.split(",", 1)
        ts  = int(ts_part.split("=", 1)[1])
        sig = v1_part.split("=", 1)[1]
    except Exception:
        return False
    if abs(int(time.time()) - ts) > tolerance:
        return False
    expected = hmac.new(
        secret.encode(),
        f"{ts}.".encode() + body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, sig)
const crypto = require("crypto");

function verify(secret, rawBody, header, toleranceSec = 300) {
  const [tsPart, v1Part] = header.split(",");
  const ts  = Number(tsPart.split("=")[1]);
  const sig = v1Part.split("=")[1];
  if (Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.`)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(sig, "hex"),
  );
}

Récepteurs idempotents

Utilise X-Mintbot-Event-Id comme clé de déduplication — les nouvelles tentatives réutilisent le même id, donc un INSERT … ON CONFLICT DO NOTHING au niveau ligne sur cette colonne garde ton handler sûr.


Erreurs

Chaque réponse d’erreur porte un code stable sur lequel tu peux brancher ton code. Le champ message est une indication lisible par un humain et peut changer entre les versions. request_id reprend l’en-tête de réponse X-Request-Id — inclus-le dans les tickets de support.

{
  "error": {
    "code": "invalid_api_key",
    "message": "API key is unknown or revoked.",
    "request_id": "f0c2d6c4-…"
  }
}
Code Signification
unauthenticated En-tête Authorization manquant ou mal formé.
invalid_api_key La clé API est inconnue ou a été remplacée par rotation.
rate_limited Limite de débit par partenaire ou par IP dépassée — voir Retry-After.
missing_idempotency_key Requête POST sans Idempotency-Key.
idempotency_key_mismatch Même clé réutilisée avec un corps de requête différent.
validation_error Le corps de la requête a échoué à la validation du schéma.
not_found La ressource n’existe pas ou n’appartient pas à ton partenaire.
payment_gateway_error Stripe a rejeté la création de la session Checkout.

Limites de débit

  • 120 requêtes / fenêtre glissante de 60 s, par partenaire. Les bursts et le débit stable partagent le même bucket.
  • 60 requêtes / 60 s dans un bucket séparé par IP pour les requêtes non authentifiées et les échecs d’authentification — cela empêche une attaque brute-force bearer de consommer le quota partenaire.
  • Les requêtes en trop renvoient 429 rate_limited avec un en-tête Retry-After (secondes d’attente).

Besoin d’aide ?

Cette documentation est écrite pour les partenaires qui utilisent réellement l’API. Si quelque chose manque, prête à confusion ou n’est plus à jour, signale-le à ton agent mintbot — il transmettra le retour et nous mettrons la page à jour.