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,3ou12. 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_failedouexpired. cursor- Optional. Passe
next_cursordepuis 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 àfalsepour 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 pendantonboarding. domain.modebyo(tu apportes ton propre domaine apex) ouhosted(mintbot fournit un sous-domaine*.mintbot.ai). Enhosted, 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 dessubdomain_prefix,subdomain_numberingetsubdomain_pad_widthactuels. Utilise-le pour un aperçu « voici ce que verront tes clients » sans réimplémenter les règles de numérotation. bot.modeown(bot Telegram fourni par le partenaire),borrow(utilisation du bot de mintbot pendant les tests) ouweb(panneau uniquement, pas de Telegram).template.modedefault(pas d’agent customization repo — l’agent tourne sur la base nue sans marque) oucustom(l’agent customization repo du partenaire àrepo_url). Encustom, le VPS de l’agent lui-même clone ce dépôt après le déploiement standard et exécute soninstall.sh/update.shpour 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. Pointerepo_urlvers 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
readyvauttrue. 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).nullsinon. 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
exhaustedet les nouvelles tentatives s’arrêtent. - Réponds avec un statut
2xxsous 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_limitedavec un en-têteRetry-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.