Zum Inhalt

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_mismatch zurü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, 3 oder 12 sein.
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_failed oder expired.
cursor
Optional. Übergib next_cursor der 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. Auf false setzen, 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 Status onboarding ist, lehnt die Brand Partner API Schreibvorgänge ab.
domain.mode
byo (du bringst deine eigene Apex-Domain mit) oder hosted (mintbot stellt eine *.mintbot.ai-Subdomain bereit). Bei hosted sind 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 von subdomain_prefix, subdomain_numbering und subdomain_pad_width. Nutze es für eine „So sehen es deine Kunden“-Vorschau, ohne die Nummerierungsregeln nachzubauen.
bot.mode
own (vom Partner bereitgestellter Telegram-Bot), borrow (mintbots Bot wird zum Testen ausgeliehen) oder web (nur Panel, kein Telegram).
template.mode
default (kein Agent-Customization-Repo — der Agent läuft auf der schlichten, ungebrandeten Basis) oder custom (das Agent-Customization-Repo des Partners unter repo_url). Bei custom klont der eigene VPS des Agenten dieses Repo nach dem Standard-Deploy und führt dessen install.sh / update.sh aus, um deine Persona, dein Panel-Theme und zusätzliches Tooling auf die Basis aufzusetzen — die Zentrale führt das Repo nie selbst aus. Zeige mit repo_url auf 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 ready auf true steht. 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). Andernfalls null.
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 exhausted markiert 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 /buyPOST /api/v1/orders → Stripe-Weiterleitung,
  • POST /webhooks/mintoffice mit 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_limited mit einem Retry-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.