Brand Partner API¶
Eine kleine, berechenbare REST-API zum Anlegen von mintbot-Bestellungen, zum Abfragen ihres Status und zum Empfangen von Lifecycle-Ereignissen. JSON rein, JSON raus. Bearer-Token-Authentifizierung. Idempotente Schreibvorgänge. Signierte Webhooks.
Dashboard für API-Zugriff Zu den Webhooks springen
Auf einen Blick¶
| Base-URL | https://mint.mintbot.ai/api/v1 |
| Auth | Authorization: Bearer mo_live_… |
| Idempotenz | Idempotency-Key: <uuid> bei jedem POST außer POST /dns/records |
| Content-Type | application/json |
| Rate-Limit | 120 Anfragen / 60 s pro Partner |
| Webhooks | Signiert mit HMAC-SHA256, bis zu 7 Zustellversuche |
Authentifizierung¶
Jede Anfrage sendet einen Partner-API-Key im Authorization-Header. Erzeuge oder rotiere den Key im Dashboard.
Authorization: Bearer mo_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Rotation ohne Übergangsfrist
Beim Rotieren wird der bisherige Key atomar widerrufen. Plane, den Wert in deiner Konfiguration zu tauschen, bevor du auf Rotate klickst.
Idempotenz¶
Jeder POST braucht einen Idempotency-Key-Header — außer POST /dns/records, das auf Provider-Ebene von Natur aus idempotent ist und Wiederholungen auch ohne Header akzeptiert. Jede UUID pro eigenständiger Anfrage funktioniert.
- Wir cachen die Antwort 24 Stunden lang. Eine Wiederholung mit demselben Key liefert die ursprüngliche Antwort erneut, mit
Idempotent-Replay: true. - Wird derselbe Key mit einem anderen Body wiederverwendet, kommt
409 idempotency_key_mismatchzurück.
Bestellungen¶
Bestellung anlegen¶
POST /orders
Legt eine Bestellung an und gibt eine Stripe-Checkout-URL zurück. Sobald die Zahlung bestätigt ist, provisioniert mintbot den Agenten, und das Umsatzereignis für den Partneranteil wird automatisch verbucht.
Anfrage
{
"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- Einer von
trial,s1,s2,s4. duration_months- Kalendermonate Serverlaufzeit. Muss
1,3oder12sein. credit_usd- Optional. Vorab aufgeladenes Chat-Guthaben, das mit der Bestellung gebündelt wird.
language- Optional. Bestimmt die Sprache des Stripe-Checkouts und der Willkommens-E-Mail.
external_id- Optional. Wird in jedem Webhook und jeder Bestellantwort zurückgegeben — damit verknüpfst du mintbot-Bestellungen mit Datensätzen in deinem eigenen System.
success_url·cancel_url- Rücksprung-URLs für den Stripe-Checkout.
{ORDER_ID}wird serverseitig ersetzt. webhook_url- Optional. Überschreibt für diese Bestellung die auf Partnerebene hinterlegte Webhook-URL.
Antwort — 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
}
Beispiel
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": "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"]
Bestellung abrufen¶
GET /orders/{id}
Ruft eine einzelne Bestellung anhand ihrer mintbot-ID ab. Liefert dieselbe Struktur wie POST /orders.
curl https://mint.mintbot.ai/api/v1/orders/42 \\
-H "Authorization: Bearer $MINTBOT_API_KEY"
Bestellungen auflisten¶
GET /orders
Cursor-paginierte Liste, neueste zuerst.
Query-Parameter
status- Optional. Filtert nach
awaiting_payment,completed,deployed,deploy_failedoderexpired. cursor- Optional. Übergib
next_cursorder vorherigen Seite, um fortzufahren.
Antwort
{
"items": [ /* OrderResponse … */ ],
"next_cursor": "37"
}
next_cursor ist null, wenn es keine weiteren Seiten gibt.
Bestellung verlängern¶
POST /orders/{id}/renew
Verlängert einen bestehenden Agenten um weitere duration_months. Nur Infrastruktur — es wird kein neues Chat-Guthaben gebündelt. Liefert eine neue Bestell-ID und eine frische Stripe-Checkout-URL.
Anfrage
{
"duration_months": 1,
"external_id": "your-side-id",
"success_url": "https://your.app/thanks?id={ORDER_ID}",
"cancel_url": "https://your.app/account"
}
Umsatz¶
Umsatz abrufen¶
GET /revenue
Summen plus die letzten 200 Ledger-Ereignisse.
Query-Parameter
include_paid- Optional, Standard
true. Auffalsesetzen, um nur Ereignisse zu sehen, die noch nicht ausgezahlt wurden.
Antwort
{
"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
}
]
}
Partnerprofil¶
Profil abrufen¶
GET /partner
Liefert dein Partnerprofil plus den nicht ausgezahlten Saldo — praktisch, um Einnahmen in deiner eigenen Admin-Oberfläche anzuzeigen, ohne sie selbst zu speichern.
Antwort
{
"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"
}
Einstellungen¶
Einstellungen abrufen¶
GET /settings
Liefert deine vollständige nicht geheime Partnerkonfiguration in einer einzigen Antwort — gewählte Modi, Apex-Domain, DNS-Provider, Bot-Provider, Customization-Repo, Subdomain-Muster (mit gerendertem Vorschau-Label), Preise pro Tier, API-Key-Metadaten, Webhook-Metadaten sowie denselben Readiness- und Abschnittsstatus, der das Banner im MintOffice-Dashboard steuert.
Gedacht für Integrations-Agenten, die herausfinden müssen, wie der Partner eingerichtet ist, ohne das Dashboard zu scrapen oder den Betreiber zu befragen. Ein typischer Aufrufer ist ein Coding-Agent, der dein White-Label-Setup auf einem frischen Server installiert — er kann diesen Endpoint lesen, anhand von readiness.status verzweigen und dem Betreiber genau sagen, welche Abschnitte noch Aufmerksamkeit brauchen.
Es wird nie ein Geheimnis zurückgegeben
Alles, was der Partner als Zugangsdaten hinterlegt hat — Telegram-Bot-Token, Zone.ee-API-Key, Webhook-Signaturgeheimnis, der API-Key selbst im Klartext —, erscheint nur als *_set-Boolean. Das Webhook-Geheimnis gibt zusätzlich dasselbe 8-stellige Anzeigepräfix preis, das schon im Dashboard zu sehen ist, sodass ein Aufrufer bestätigen kann, welches Geheimnis konfiguriert ist, ohne je den Wert zu sehen.
Antwort
{
"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
}
}
Feldreferenz¶
status- Partner-Lifecycle. Einer von
onboarding,live,paused. Solange der Statusonboardingist, lehnt die Brand Partner API Schreibvorgänge ab. domain.modebyo(du bringst deine eigene Apex-Domain mit) oderhosted(mintbot stellt eine*.mintbot.ai-Subdomain bereit). Beihostedsind die DNS-Provider-Felder bewusst leer — unabhängig davon, was der Partner zuvor eingegeben hat.domain.subdomain_label_preview- Das gerenderte Label, das der erste Kundenagent erhalten würde (
agent1,1,001, …), berechnet aus den aktuellen Werten vonsubdomain_prefix,subdomain_numberingundsubdomain_pad_width. Nutze es für eine „So sehen es deine Kunden“-Vorschau, ohne die Nummerierungsregeln nachzubauen. bot.modeown(vom Partner bereitgestellter Telegram-Bot),borrow(mintbots Bot wird zum Testen ausgeliehen) oderweb(nur Panel, kein Telegram).template.modedefault(kein Agent-Customization-Repo — der Agent läuft auf der schlichten, ungebrandeten Basis) odercustom(das Agent-Customization-Repo des Partners unterrepo_url). Beicustomklont der eigene VPS des Agenten dieses Repo nach dem Standard-Deploy und führt desseninstall.sh/update.shaus, um deine Persona, dein Panel-Theme und zusätzliches Tooling auf die Basis aufzusetzen — die Zentrale führt das Repo nie selbst aus. Zeige mitrepo_urlauf dein Agent-Customization-Repo (GitHub, HTTPS), wenn du eigenes Branding willst.api.webhook_secret_prefix- Die ersten 8 Zeichen des Webhook-Signaturgeheimnisses, nur vorhanden, wenn ein Geheimnis konfiguriert ist. Damit kann ein Aufrufer unterscheiden, welches Geheimnis hinterlegt ist, ohne den vollständigen Wert zu sehen. Das Dashboard zeigt dasselbe Präfix.
readiness.missing_fields- Liste der Dashboard-Feldnamen, die vor dem Go-Live noch fehlen. Leer, wenn
readyauftruesteht. Stabil genug, um eine „Was fehlt noch?“-Ansicht in deiner eigenen Admin-Oberfläche zu speisen. readiness.dns_blocker- Enthält einen kurzen Grund-String, wenn die DNS-Prüfung fehlschlägt (z. B.
nameservers_not_pointing,glue_record_missing). Andernfallsnull. section_status.*.issue- Menschenlesbarer Hinweis pro Abschnitt, wenn dieser Abschnitt das Go-Live blockiert. Entspricht den Status-Pills der einzelnen Abschnitte im Dashboard.
Beispiel
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"])
Katalog¶
Die Liste der verkaufbaren Pakete für deinen Storefront — die öffentlichen
Pakete (trial / starter / pro) verknüpft mit deinen aufgelösten Preisen
in deiner Preiswährung. Baue deine Plan-Karten aus dieser Liste, statt Tier-Slugs
und Preise fest zu verdrahten: Ausgemusterte Pakete verschwinden von selbst, und
eine Preis- oder Währungsänderung im Dashboard erreicht deinen Storefront
innerhalb seines Cache-Fensters — ohne Redeploy.
Dieser Endpoint verhindert, dass ein Storefront die Pakete von gestern bewirbt.
Das Referenz-Portal ruft ihn bei jedem Rendern von Landing-Page, /buy und
/extend auf (gecacht, mit einem beständigen Fallback).
Die Anzeigetexte bleiben deine Sache
display_name / description / featured sind die kanonischen
Paket-Metadaten von mintbot. Wenn du ein Paket umbenennen oder seinen
Beschreibungstext an deine Marke anpassen willst, überschreibe ihn in deinem
eigenen Storefront (das Referenz-Portal bringt genau dafür eine
PLAN_OVERRIDES-Map mit) — der Katalog ist die maßgebliche Quelle dafür,
welche Pakete es gibt und was sie kosten, nicht für deine Marketingtexte.
Katalog abrufen¶
GET /api/v1/catalog
Keine Parameter. Nur Bearer-Auth — es gibt keine Aktivierungssperre, ein Storefront kann den Katalog also rendern, bevor dein Partneraccount auf live geschaltet ist.
Antwort
{
"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 }
}
]
}
| Feld | Bedeutung |
|---|---|
currency |
Deine Preiswährung. Jeder price_cents-Wert unten ist darin angegeben. |
packages[] |
Ein Eintrag pro öffentlichem Paket, in Angebots-/Anzeigereihenfolge. |
tier |
Der Slug, den du an POST /orders / POST /subscriptions übergibst. |
display_name / description / featured |
Kanonische Anzeigetexte (kannst du in deinem Storefront überschreiben). |
default_credit_usd |
Hinweis auf das im Paket gebündelte LLM-Guthaben (informativ). |
durations[] |
Die Laufzeiten, für die dieses Paket gekauft werden kann. months ist der Wert, den du als duration_months sendest; price_cents ist, was die Bestellung berechnet. Monatspakete bieten 1 / 3 / 12 Monate an; trial bietet eine einzelne 24-Stunden-Laufzeit. |
subscription |
available ist true bei Paketen, die über POST /subscriptions als monatlich automatisch verlängerndes Abo gekauft werden können; price_cents ist der Monatsbetrag. trial ist nie abonnierbar. |
Die Preise entsprechen dem, was berechnet wird
Jeder price_cents-Wert wird mit demselben Preis-Resolver berechnet, den
auch der Bestell-Endpoint nutzt. Ein Kartenpreis entspricht also immer dem
späteren Gesamtbetrag im Stripe-Checkout für dieses (tier, duration_months).
Beispiel¶
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"])
DNS-Einträge¶
Brand-Partner-Integrationen müssen oft DNS-Einträge auf der Apex-Domain des Partners anlegen — @, www oder Subdomains pro Kunde auf den richtigen Server zeigen lassen. MintOffice stellt dafür einen anbieterneutralen Proxy bereit, sodass du das erledigen kannst, ohne je den API-Key des Upstream-Providers in deiner eigenen Infrastruktur zu handhaben. Die Zugangsdaten, die du beim Onboarding gespeichert hast (heute: zone.ee; cloudflare und route53 sind mit derselben Struktur geplant), bleiben verschlüsselt in MintOffice; dein Aufruf kommt mit Bearer mo_live_… herein, und wir führen den Upstream-Aufruf in deinem Namen aus.
Der Geltungsbereich ist die konfigurierte Apex-Domain des Partners (domain.client_apex_domain aus GET /settings). Einträge in einer Zone, die dir nicht gehört, kannst du nicht anfassen.
Derzeit unterstützt der Proxy die Operationen, die auch die Deploy-Pipeline selbst nutzt — A-Einträge (Upsert / Auflisten / Löschen per ID). CNAME- / TXT- / MX-Unterstützung folgt nach demselben Muster, sobald die zugrunde liegende Provider-Abstraktion sie bekommt.
Idempotency-Key hier nicht nötig
Anders als die Endpoints zum Anlegen von Bestellungen sind DNS-Upserts auf Provider-Ebene von Natur aus idempotent (ein erneuter Aufruf mit demselben (subdomain, ip) ist ein No-op, und veraltete Duplikate werden bei jedem Aufruf entfernt). Du musst den Idempotency-Key-Header nicht senden — Wiederholungen laufen immer auf genau einen A-Eintrag pro FQDN hinaus.
A-Eintrag anlegen oder aktualisieren¶
POST /dns/records
Lässt <subdomain>.<your-apex> idempotent auf eine IPv4-Adresse zeigen. Übergib "" (oder "@") für die Apex-Domain selbst.
Anfrage
{
"subdomain": "www",
"ip": "203.0.113.10"
}
subdomain akzeptiert:
""oder"@"— die Apex-Domain selbst (example.com).- Ein einzelnes DNS-Label —
"www","api"— für<label>.<apex>. - Labels mit Punkt —
"api.eu"— für verschachtelte Subdomains (api.eu.example.com).
ip muss eine IPv4-Adresse in Punktschreibweise sein. Jedes Label besteht aus [a-z0-9_-], ist 1–63 Zeichen lang und beginnt oder endet nicht mit -.
Antwort — 200
{
"apex": "example.com",
"provider": "zone_ee",
"fqdn": "www.example.com",
"records": [
{ "id": "9182734", "name": "www.example.com", "destination": "203.0.113.10" }
]
}
Die records-Liste nach dem Schreiben spiegelt immer die aktuelle Sicht des Upstream-Providers auf den FQDN nach einem Duplikat-Sweep wider — eine erfolgreiche Antwort mit genau einem Eintrag ist also das Signal, dass nichts Veraltetes übrig ist.
Einträge auflisten¶
GET /dns/records
Liefert jeden A-Eintrag auf der konfigurierten Apex-Domain. Hänge ?name=<label-or-fqdn> an, um auf einen FQDN zu filtern — der Wert kann ein bloßes Label ("www"), eine Subdomain mit Punkt ("api.eu") oder der vollständige FQDN ("www.example.com") sein.
Antwort — 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" }
]
}
Eintrag löschen¶
DELETE /dns/records/{record_id}
record_id ist die Provider-Kennung, die GET /dns/records zurückgibt (zone.ee vergibt Integer-IDs, die an der API-Grenze in Strings umgewandelt werden). Das Löschen eines Eintrags, der nicht mehr existiert, gilt als Erfolg — der Endpoint ist clientseitig idempotent.
Antwort — 200
{ "deleted": true, "id": "9182734" }
Fehler¶
| HTTP | error.code |
Ursache |
|---|---|---|
| 401 | unauthenticated / invalid_api_key |
Fehlendes / ungültiges Bearer-Token. |
| 422 | validation_error |
Ungültige Subdomain oder Ziel ist keine IPv4-Adresse. |
| 422 | dns_not_configured |
Der Partner hat keine client_apex_domain, oder die Zugangsdaten des konfigurierten Providers fehlen. Behebst du unter Settings → Domain. |
| 422 | dns_provider_unsupported |
Der DNS-Provider des Partners ist noch nicht an den MintOffice-Proxy angebunden (z. B. der cloudflare-Platzhalter). Wechsle vorerst zu zone_ee. |
| 502 | dns_provider_error |
Der Upstream-Provider hat einen 4xx/5xx zurückgegeben, oder die Verbindung ist fehlgeschlagen. Die message des Fehlers enthält den Upstream-Status und einen kurzen, um Geheimnisse bereinigten Auszug aus dem Upstream-Body. Wiederholung ist unbedenklich. |
Beispiel¶
# Point apex + www at your server's IP.
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"}'
# Verify.
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¶
Wenn mit einer Bestellung etwas passiert, senden wir per POST ein signiertes JSON-Ereignis an deine konfigurierte Webhook-URL.
Webhooks sind optional
Webhook URL und Webhook signing secret unter Settings → API access sind komplett optional — lass sie beim Onboarding leer, wenn du noch keinen Empfänger hast. Das Dashboard und der Rest der API funktionieren auch ohne Webhooks; du kannst stattdessen GET /api/v1/orders/{id} abfragen, um den Lifecycle zu verfolgen. Trage die URL ein (und erzeuge das Geheimnis), sobald du so weit bist — eine erneute Aktivierung ist nicht nötig.
Ereignistypen¶
| Ereignis | Wann es ausgelöst wird |
|---|---|
order.created |
Stripe-Checkout-Session angelegt, Zahlung ausstehend. |
order.paid |
Stripe hat die Zahlung bestätigt. Umsatzereignis verbucht. |
order.cancelled |
Bestellung ist abgelaufen oder wurde ausdrücklich storniert. |
agent.provisioning_started |
Deploy-Pipeline für diese Bestellung gestartet. |
agent.ready |
Deploy erfolgreich. Payload enthält panel_url und expires_at. |
agent.failed |
Deploy-Pipeline fehlgeschlagen. Im Feld error steht der fehlgeschlagene Schritt. |
agent.expired |
Die Laufzeit (TTL) des Agenten ist abgelaufen. Der Partner kann über POST /orders/{id}/renew verlängern. |
agent.resumed |
Ein zuvor abgelaufener Agent wurde innerhalb seiner Karenzfrist verlängert und läuft wieder. Gegenstück zu agent.expired. |
Zustellung & Wiederholungen¶
- Bis zu 7 Versuche nach exponentiellem Zeitplan:
0s, 30s, 2m, 10m, 1h, 6h, 24h. - Nach dem letzten Versuch wird die Zustellung als
exhaustedmarkiert und nicht weiter wiederholt. - Antworte innerhalb von 10 Sekunden mit einem
2xx-Status, um den Empfang zu bestätigen.
Request-Header¶
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
Beispiel-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"
}
Signatur prüfen¶
Die Signatur ist HMAC-SHA256(secret, "{timestamp}.{raw_body}"). Prüfe immer den rohen Request-Body — wenn du dein geparstes JSON neu serialisierst, schlägt der Vergleich fehl.
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"),
);
}
Idempotente Empfänger
Nutze X-Mintbot-Event-Id als Dedup-Schlüssel — Wiederholungen verwenden dieselbe ID, sodass ein zeilenweises INSERT … ON CONFLICT DO NOTHING auf dieser Spalte deinen Handler absichert.
Referenz-Portal¶
Eine lauffähige End-to-End-Referenz liegt unter mintbot-ai/partner-portal-example — FastAPI + SQLite, dockerisiert, mit einer Beispielmarke namens ExampleAI, die du forkst und austauschst. Das Architekturbild — wie Storefront, diese API, der Deploy-Worker und das Customization-Repo zusammenspielen — findest du unter MintOffice — Technisch. Das Portal implementiert:
- die Seiten für Landing, Planauswahl, Danke und Abbruch,
POST /buy→POST /api/v1/orders→ Stripe-Weiterleitung,POST /webhooks/mintofficemit dem oben gezeigten HMAC-Prüfer,- einen
/admin-Ereignisbrowser hinter HTTP-Basic-Auth, mit dem du Zustellungen im Blick behältst.
Forke es, setze PARTNER_BRAND, MINTOFFICE_API_KEY und MINTOFFICE_WEBHOOK_SECRET in .env, starte docker compose up und zeige mit der Webhook-URL deines Partnereintrags auf https://<your-host>/webhooks/mintoffice. Die vollständige Anleitung steht unter MintOffice → Referenz-Portal.
Fehler¶
Jede Fehlerantwort enthält einen stabilen code, nach dem du programmatisch verzweigen kannst. Das Feld message ist ein menschenlesbarer Hinweis und kann sich zwischen Releases ändern. request_id entspricht dem Response-Header X-Request-Id — gib ihn in Support-Tickets an.
{
"error": {
"code": "invalid_api_key",
"message": "API key is unknown or revoked.",
"request_id": "f0c2d6c4-…"
}
}
| Code | Bedeutung |
|---|---|
unauthenticated |
Authorization-Header fehlt oder ist fehlerhaft. |
invalid_api_key |
API-Key ist unbekannt oder wurde per Rotation ersetzt. |
rate_limited |
Rate-Limit pro Partner oder pro IP überschritten — siehe Retry-After. |
missing_idempotency_key |
POST-Anfrage ohne Idempotency-Key. |
idempotency_key_mismatch |
Derselbe Key wurde mit einem anderen Request-Body wiederverwendet. |
validation_error |
Der Request-Body hat die Schema-Validierung nicht bestanden. |
not_found |
Die Ressource existiert nicht oder gehört nicht zu deinem Partner. |
payment_gateway_error |
Stripe hat das Anlegen der Checkout-Session abgelehnt. |
dns_not_configured |
POST /dns/records bei einem Partner ohne Apex-Domain / ohne DNS-Zugangsdaten. Behebst du unter Settings → Domain. |
dns_provider_unsupported |
Der dns_provider-Wert des Partners ist noch nicht an den MintOffice-Proxy angebunden. |
dns_provider_error |
Der Upstream-DNS-Provider hat einen Fehler zurückgegeben — die Nachricht enthält einen bereinigten Auszug. |
quota_exceeded |
Der Partner hat alle Deploy-Slots verbraucht (Standard: 10). Wende dich an den mintbot-Support, um das Limit zu erhöhen. Wird von POST /orders und POST /orders/{id}/renew zurückgegeben. Der Body enthält deploys_used + deploy_quota. |
Rate-Limits¶
- 120 Anfragen / 60 s im gleitenden Fenster, pro Partner. Bursts und Dauerlast teilen sich denselben Bucket.
- 60 Anfragen / 60 s in einem separaten Bucket pro IP für nicht authentifizierte und fehlgeschlagene Auth-Anfragen — damit ein Bearer-Brute-Force nicht das Partnerkontingent aufbraucht.
- Überzählige Anfragen erhalten
429 rate_limitedmit einemRetry-After-Header (Wartezeit in Sekunden).
Brauchst du Hilfe?¶
Diese Dokumentation ist für Partner geschrieben, die die API tatsächlich nutzen. Wenn etwas fehlt, unklar oder veraltet ist, sag es deinem mintbot-Agenten — er leitet das Feedback weiter, und wir aktualisieren die Seite.