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 mo_live_… |
| Idempotency | Idempotency-Key: <uuid> sur chaque POST sauf POST /dns/records |
| 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_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
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 exige un en-tête Idempotency-Key — sauf POST /dns/records, naturellement idempotent au niveau du fournisseur, qui accepte les nouvelles tentatives sans cet en-tête. N’importe quel UUID par requête distincte convient.
- 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": "mo_month",
"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
mo_test(Test 24h),mo_month(Standard),mo_month_plus(Performance). duration_months- Durée de vie du serveur en mois calendaires. Doit être
1,3ou12.mo_testcorrespond toujours à une seule période de 24 heures, quelle que soit la valeur envoyée. credit_usd- Facultatif,
0par défaut. Une des valeurs0,5,10,20,50. Crédit de chat préfinancé (en USD) inclus avec la commande. Il s’ajoute au total de la commande et revient à mintbot — il ne fait pas partie de ta marge. language- Facultatif. Influence la locale de Stripe Checkout et la langue de l’e-mail de bienvenue.
external_id- Facultatif. 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- Facultatif. Remplacement, pour cette commande, de l’URL de webhook configurée au niveau partenaire.
product_name- Facultatif. Le libellé de ligne que le client voit sur Stripe Checkout (250 caractères max.). Par défaut, un libellé
TIER · N monthsest généré. price_usd- Facultatif. Prix forcé en USD pour l’ensemble de la commande. Il ne peut pas descendre sous ton prix plancher de gros pour ce niveau et cette durée — sinon
400 validation_error.
Réponse — 201 Created
{
"id": 42,
"tier": "mo_month",
"duration_months": 1,
"credit_usd": 10,
"amount_cents": 1200,
"currency": "usd",
"status": "pending",
"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,
"price_cents": 1200,
"partner_cut_cents": 200
}
Si tu es en mode de tarification personnalisée et que tu n’as pas fixé de prix pour ce niveau, la réponse contient aussi "pricing_fallback": true.
Exemple
curl -X POST https://mint.mintbot.ai/api/v1/orders \
-H "Authorization: Bearer $MINTBOT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"tier": "mo_month",
"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": "mo_month",
"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 $MINTBOT_API_KEY"
Lister les commandes¶
GET /orders
Liste paginée par curseur, des plus récentes aux plus anciennes.
Paramètres de requête
status- Facultatif. Filtrer par
pending,completedouexpired. Une commande payée restecompletedpendant tout son déploiement — suis la progression du déploiement via les webhooksagent.*. cursor- Facultatif. Passe
next_cursordepuis la page précédente pour continuer. limit- Facultatif. Taille de page,
50par défaut,200maximum.
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"
}
Abonnements¶
Créer un abonnement¶
POST /subscriptions
Ouvre un Stripe Checkout en mode abonnement : le client paie chaque mois un forfait mensuel et l’abonnement se renouvelle automatiquement. Comme POST /orders, il exige un Idempotency-Key et un compte partenaire live. Le prix mensuel provient de ta tarification pour ce niveau.
Requête
{
"tier": "mo_month",
"language": "en",
"success_url": "https://your.app/thanks",
"cancel_url": "https://your.app/cart",
"customer_email": "customer@example.com",
"external_id": "your-side-id"
}
customer_email, agent_id (un de tes agents existants auquel rattacher l’abonnement), external_id et product_name sont facultatifs.
Réponse — 201 Created contient id, tier, currency, amount_cents, partner_cut_cents, status, checkout_url, current_period_end, cancel_at_period_end, agent_id, language, customer_email, external_id et created_at.
Récupérer un abonnement¶
GET /subscriptions/{id}
Renvoie la même structure que POST /subscriptions.
Revenus¶
Lire les revenus¶
GET /revenue
Totaux plus les 200 derniers événements du ledger.
Paramètres de requête
include_paid- Facultatif, 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": "own",
"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": "text",
"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": {
"mo_test": { "price_cents": 100, "partner_cut_cents": 0, "updated_at": "2026-05-13 09:05:00" },
"mo_month": { "price_cents": 1200, "partner_cut_cents": 200, "updated_at": "2026-05-13 09:05:00" },
"mo_month_plus": { "price_cents": 1800, "partner_cut_cents": 300, "updated_at": "2026-05-13 09:05:00" }
},
"allowed_credit_options": [10, 20, 50]
},
"readiness": {
"ready": true,
"missing_fields": [],
"dns_blocker": null,
"status": "live"
},
"section_status": {
"domain": { "status": "ok", "mode": "own", "issue": null, "dns_blocker": null },
"bot": { "status": "ok", "mode": "own", "issue": null, "dns_blocker": null },
"template": { "status": "default", "mode": "default", "issue": null, "dns_blocker": null },
"api": { "status": "ok", "mode": "active", "issue": null, "dns_blocker": null },
"all_ready": true
}
}
Référence des champs¶
status- Cycle de vie du partenaire. Une des valeurs
onboarding,live,suspended. La Brand Partner API rejette les écritures tant que le statut n’est paslive; seule l’équipe mintbot peut te passer enlive. domain.modeown(tu apportes ton propre domaine apex) oumintbot(mintbot fournit un sous-domaine*.mintbot.ai). Enmintbot, 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),mintbot(utilisation du bot de mintbot pendant les tests) ounone(panneau uniquement, pas de Telegram).template.modedefault(pas d’agent customization repo — l’agent tourne sur la base nue sans marque) ouown(l’agent customization repo du partenaire àrepo_url). Enown, 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.
pricing.allowed_credit_options- Les montants
credit_usdque tu proposes à tes clients (choisis dans Settings → Pricing). 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- Une raison lisible quand la vérification du fournisseur DNS échoue (par ex.
partner has no zone.ee username configured).nullsinon. section_status.*- Reflète les pastilles de statut par section du tableau de bord.
statusvautok,warnoudefault(une valeur par défaut de mintbot est utilisée) ;issuenomme le champ qui bloque cette section (par ex.client_apex_domain,telegram_bot_token,dns_blocker), ounull.
Exemple
curl https://mint.mintbot.ai/api/v1/settings \
-H "Authorization: Bearer $MINTBOT_API_KEY"
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 Brand Partner
(mo_test / mo_month / mo_month_plus) 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": "mo_test",
"display_name": "Test 24h",
"description": "2 vCPU · 4 GB RAM · 40 GB SSD · 24-hour evaluation. EU.",
"featured": false,
"default_credit_usd": 5,
"durations": [
{ "months": 1, "label": "24 hours", "price_cents": 0 }
],
"subscription": { "available": false, "price_cents": null }
},
{
"tier": "mo_month",
"display_name": "Standard",
"description": "2 vCPU · 4 GB RAM · 40 GB SSD, monthly. EU.",
"featured": false,
"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 }
}
],
"credit_options": [
{ "usd": 5, "price_cents": 500 },
{ "usd": 10, "price_cents": 1000 }
]
}
| 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 ; mo_test 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. mo_test n’est jamais éligible à l’abonnement. |
credit_options[] |
Chaque montant credit_usd tarifé dans ta devise (usd = montant du crédit, price_cents = ce qu’il ajoute au total de la commande). Les montants que tu proposes sont indiqués par pricing.allowed_credit_options dans GET /settings. |
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 $MINTBOT_API_KEY" | 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.
Si ton fournisseur DNS est Manual, le proxy ne modifie rien : les écritures sont sans effet et les listes reviennent vides.
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 $MINTBOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"subdomain": "", "ip": "203.0.113.10"}'
curl https://mint.mintbot.ai/api/v1/dns/records \
-H "Authorization: Bearer $MINTBOT_API_KEY" \
-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 $MINTBOT_API_KEY"
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.
Les webhooks sont facultatifs
Les champs Webhook URL et Webhook signing secret dans Settings → API access sont entièrement facultatifs — laisse-les vides pendant l’onboarding si tu n’as pas encore de récepteur. Les livraisons ne partent qu’une fois les deux renseignés — avec une URL mais sans secret de signature, aucun événement n’est envoyé. Le tableau de bord et le reste de l’API fonctionnent très bien sans webhooks ; tu peux interroger GET /api/v1/orders/{id} pour suivre le cycle de vie. Ajoute l’URL (et génère le secret) quand tu es prêt — aucune réactivation nécessaire.
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. error_step indique l’étape qui a cassé ; error_message en donne un bref résumé. |
agent.expired |
TTL de l’agent écoulé. Le partenaire peut renouveler via POST /orders/{id}/renew. |
agent.resumed |
Un agent précédemment expiré a été renouvelé pendant sa période de grâce et est de nouveau actif. Pendant de agent.expired côté reprise. |
subscription.created · subscription.activated · subscription.updated · subscription.cancelled |
Cycle de vie d’un abonnement créé via POST /subscriptions. |
partner.activated · partner.deactivated |
L’équipe mintbot a activé ton compte ou l’a retiré de live (suspended dans le payload indique s’il a été suspendu ou ramené à l’onboarding). |
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": "mo_month",
"duration_months": 1,
"currency": "usd",
"amount_cents": 1200,
"status": "completed",
"external_id": "your-side-id",
"created_at": "2026-05-16 09:58:00",
"paid_at": "2026-05-16 10:02:14",
"expires_at": null
}
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.
Portail de référence¶
Une implémentation de référence complète et exécutable se trouve sur mintbot-ai/partner-portal-example — FastAPI + SQLite, sous Docker, livrée avec une marque d’exemple appelée ExampleAI que tu forkes puis remplaces par la tienne. La vue d’architecture — comment la vitrine, cette API, le worker de déploiement et le dépôt de personnalisation s’articulent — se trouve sur MintOffice — Technique. Le portail implémente :
- les pages d’accueil, de choix de forfait, de remerciement et d’annulation,
POST /buy→POST /api/v1/orders→ redirection vers Stripe,POST /webhooks/mintofficeavec le vérificateur HMAC présenté plus haut,- un navigateur d’événements
/adminprotégé par HTTP Basic pour vérifier les livraisons d’un coup d’œil.
Forke-le, renseigne PARTNER_BRAND, MINTOFFICE_API_KEY et MINTOFFICE_WEBHOOK_SECRET dans .env, lance docker compose up et fais pointer l’URL de webhook de ton compte partenaire vers https://<your-host>/webhooks/mintoffice. Le guide complet est dans MintOffice → Portail de référence.
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. |
partner_not_live |
HTTP 403 — une écriture (POST /orders, /orders/{id}/renew, /subscriptions) alors que ton compte n’est pas live. Le corps contient status et, pour les comptes en onboarding, missing_fields + dns_blocker. |
conflict |
HTTP 409 — une requête avec le même Idempotency-Key est encore en cours de traitement. Réessaie après Retry-After. |
unpriced_tier |
HTTP 409 — le niveau n’a aucun prix à facturer. |
internal_error |
Erreur inattendue côté serveur. Indique le request_id en contactant le support. |
payment_gateway_error |
Stripe a rejeté la création de la session Checkout. |
dns_not_configured |
POST /dns/records pour un partenaire sans apex ni identifiants DNS. À corriger dans Settings → Domain. |
dns_provider_unsupported |
La valeur dns_provider du partenaire n’est pas encore branchée sur le proxy MintOffice. |
dns_provider_error |
Le fournisseur DNS en amont a renvoyé une erreur — le message contient un extrait expurgé. |
quota_exceeded |
HTTP 429, uniquement en environnement DEV (mint.mintbot.dev) — LIVE n’a pas de plafond par partenaire. Le partenaire a utilisé tous ses emplacements de déploiement (10 par défaut, 20 après les frais de mise en place Brand Partner). Contacte le support mintbot pour relever le plafond. Renvoyé par POST /orders et POST /orders/{id}/renew. Le corps contient deploys_used + deploy_quota. |
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.