Brand Partner API¶
Eine kleine, berechenbare REST API zum Erstellen von mintbot-Bestellungen, zum Abfragen ihres Status und zum Empfangen von Lifecycle-Ereignissen. JSON rein, JSON raus. Bearer-token auth. Idempotente Schreibvorgänge. Signierte Webhooks.
Dashboard für API-Zugriff Zu Webhooks springen
Auf einen Blick¶
| Base URL | https://mint.mintbot.ai/api/v1 |
| Auth | `Authorization: Bearer *** |
| Idempotency | Idempotency-Key: <uuid> bei jedem POST |
| Content type | application/json |
| Rate limit | 120 req / 60 s pro Partner |
| Webhooks | Signiert mit HMAC-SHA256, bis zu 7 Wiederholungsversuche |
Authentifizierung¶
Jede Anfrage sendet einen Partner-API-key im Authorization header. Erstelle oder rotiere den key im Dashboard.
Authorization: Bearer mo_liv...xxxx
Content-Type: application/json
Rotation hat kein Karenzfenster
Das Rotieren des keys widerruft den vorherigen key atomar. Plane, den Wert in deiner Konfiguration zu tauschen, bevor du auf Rotate klickst.
Idempotenz¶
Jeder POST benötigt einen Idempotency-Key header. Jede UUID pro eindeutiger Anfrage funktioniert.
- Wir cachen die Antwort für 24 Stunden. Ein erneuter Versuch mit demselben key spielt die ursprüngliche Antwort mit
Idempotent-Replay: trueerneut aus. - Wenn du denselben key mit einem anderen body wiederverwendest, kommt
409 idempotency_key_mismatchzurück.
Bestellungen¶
Bestellung erstellen¶
POST /orders
Erstellt eine Bestellung 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 aufgezeichnet.
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 einer von
1,3oder12sein. credit_usd- Optional. Vorab finanziertes Chat-Guthaben, das mit der Bestellung gebündelt ist.
language- Optional. Beeinflusst die Stripe Checkout locale und die Sprache der Willkommens-E-Mail.
external_id- Optional. Wird in jedem Webhook und jeder Bestellantwort zurückgegeben — nutze es, um mintbot-Bestellungen mit Zeilen in deinem eigenen System zu verknüpfen.
success_url·cancel_url- Rückkehr-URLs für Stripe Checkout.
{ORDER_ID}wird serverseitig ersetzt. webhook_url- Optional. Bestellspezifischer Override für die Webhook-URL auf Partnerebene.
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 *** \
-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. Gibt dieselbe Struktur wie POST /orders zurück.
curl https://mint.mintbot.ai/api/v1/orders/42 \
-H "Authorization: Bearer ***
Bestellungen auflisten¶
GET /orders
Cursor-paginierte Liste, neueste zuerst.
Query parameters
status- Optional. Filtere nach
awaiting_payment,completed,deployed,deploy_failedoderexpired. cursor- Optional. Übergib
next_cursorvon 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 Infra — es wird kein neues Chat-Guthaben gebündelt. Gibt eine neue Bestell-id und eine frische Stripe Checkout URL zurück.
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 lesen¶
GET /revenue
Summen plus die neuesten 200 Ledger-Ereignisse.
Query parameters
include_paid- Optional, default
true. Setze auffalse, 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
Gibt dein Partnerprofil plus den nicht ausgezahlten Saldo zurück — praktisch, um Einnahmen in deiner eigenen Admin UI 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
Gibt deine vollständige nicht geheime Partnerkonfiguration in einem einzigen Payload zurück — gewählte Modi, Apex-Domain, DNS-Provider, Bot-Provider, Template-Repo, Subdomain-Muster (mit einem gerenderten Vorschau-Label), Preise pro Tier, API-key-Metadaten, Webhook-Metadaten und denselben Readiness- / Pro-Sektion-Status, der das MintOffice-Dashboard-Banner steuert.
Konzipiert 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 Sektionen noch Aufmerksamkeit brauchen.
Es wird niemals ein Geheimnis zurückgegeben
Alles, was der Partner als Anmeldedaten angegeben hat — Telegram-Bot-Token, Zone.ee API key, Webhook-Signaturgeheimnis, der API-key-Klartext selbst — erscheint nur als *_set boolean. Das Webhook-Geheimnis legt zusätzlich denselben 8-Zeichen-Anzeige-Präfix offen, der bereits im Dashboard angezeigt wird, sodass ein Aufrufer bestätigen kann, welches Geheimnis konfiguriert ist, ohne jemals 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. Die Brand Partner API lehnt Schreibvorgänge ab, solangeonboarding. domain.modebyo(du bringst deine eigene Apex-Domain mit) oderhosted(mintbot stellt eine*.mintbot.ai-Subdomain bereit). Beihostedsind die DNS-Provider-Felder absichtlich 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, …) angesichts des aktuellensubdomain_prefix,subdomain_numberingundsubdomain_pad_width. Nutze es für eine "so sehen es deine Kunden"-Vorschau, ohne die Nummerierungsregeln neu zu implementieren. bot.modeown(vom Partner bereitgestellter Telegram-Bot),borrow(Nutzung von mintbots Bot während des Testens) oderweb(nur Panel, kein Telegram).template.modedefault(keine agent customization repo — der Agent läuft auf der schlichten ungebrandeten Basis) odercustom(die agent customization repo des Partners unterrepo_url). Beicustomklont der eigene VPS des Agenten dieses Repo nach dem Standard-Deploy und führt seininstall.sh/update.shaus, um deine Persona, dein Panel-Theme und jegliches zusätzliche Tooling auf der Basis anzuwenden — central führt das Repo niemals selbst aus. Verweiserepo_urlauf deine GitHub-HTTPS-agent customization repo, wenn du eigenes Branding möchtest.api.webhook_secret_prefix- Erste 8 Zeichen des Webhook-Signaturgeheimnisses, nur vorhanden, wenn ein Geheimnis konfiguriert ist. Lässt einen Aufrufer unterscheiden, welches Geheimnis vorliegt, ohne den vollständigen Wert zu sehen. Das Dashboard zeigt denselben Präfix.
readiness.missing_fields- Liste der Dashboard-Feldnamen, die vor Go Live noch erforderlich sind. Leer, wenn
readytrueist. Stabil genug, um eine "was noch fehlt"-UI in deiner eigenen Admin zu treiben. readiness.dns_blocker- Auf einen kurzen Grund-String gesetzt, wenn die DNS-Verifizierung fehlschlägt (z. B.
nameservers_not_pointing,glue_record_missing). Andernfallsnull. section_status.*.issue- Pro Sektion menschenlesbarer Hinweis, wenn diese Sektion Go Live blockiert. Spiegelt die Pro-Sektion-Status-Pills des Dashboards wider.
Beispiel
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"])
Katalog¶
Die verkaufbare Paketliste für deinen Storefront — die öffentlichen Pakete
(trial / starter / pro) verknüpft mit deiner aufgelösten Preisgestaltung,
in deiner Preiswährung. Rendere deine Plan-Karten hieraus, statt Tier-Slugs und
Preise fest zu codieren: ausgemusterte Pakete verschwinden von selbst, und eine
Preis- oder Währungsänderung, die du im Dashboard vornimmst, erreicht deinen
Storefront innerhalb seines Cache-Fensters — kein Redeploy.
Dies ist der Endpoint, der einen Storefront davon abhält, die Pakete von gestern
zu bewerben. Das Referenz-Portal ruft ihn bei jedem Landing- / /buy- /
/extend-Render auf (gecacht, mit einem Sticky-Fallback).
Anzeige-Text bleibt deiner
display_name / description / featured sind die kanonischen mintbot-Paketmetadaten.
Wenn du ein Paket umbenennen oder seinen Beschreibungstext für deine Marke
umschreiben möchtest, überschreibe ihn in deinem eigenen Storefront (das
Referenz-Portal liefert genau dafür eine PLAN_OVERRIDES-Map) — der Katalog
ist die Quelle der Wahrheit dafür, welche Pakete existieren und was sie
kosten, nicht für deinen Marketing-Text.
Katalog abrufen¶
GET /api/v1/catalog
Keine Parameter. Nur Bearer-Auth — es gibt kein Aktivierungs-Gate, sodass ein Storefront den Katalog rendern kann, bevor dein Partneraccount auf live geschaltet wird.
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 unten ist darin denominiert. |
packages[] |
Ein Eintrag pro öffentlichem Paket, in Angebots-/Anzeigereihenfolge. |
tier |
Der Slug, den du an POST /orders / POST /subscriptions übergibst. |
display_name / description / featured |
Kanonischer Anzeige-Text (überschreibe ihn in deinem Storefront, wenn du möchtest). |
default_credit_usd |
Der gebündelte LLM-Guthaben-Hinweis des Pakets (informativ). |
durations[] |
Die Setup-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 bewerben 1 / 3 / 12 Monate; trial bewirbt eine einzelne 24-Stunden-Laufzeit. |
subscription |
available ist true für Pakete, die als monatliche Auto-Verlängerung über POST /subscriptions gekauft werden können; price_cents ist der monatliche Betrag. trial ist niemals abonnementfähig. |
Preise entsprechen dem, was dir berechnet wird
Jeder price_cents wird mit demselben Preis-Resolver berechnet, den der
Bestell-Endpoint verwendet, sodass ein Karten-Preis immer dem letztlichen
Stripe-Checkout-Gesamtbetrag für dieses (tier, duration_months) entspricht.
Beispiel¶
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"])
DNS-Einträge¶
Brand-Partner-Integrationen müssen oft DNS-Einträge auf der Apex-Domain des Partners provisionieren — @, www oder kundenindividuelle Subdomains auf den richtigen Server zeigen lassen. MintOffice stellt einen anbieterblinden Proxy bereit, sodass du dies tun kannst, ohne jemals den API key des Upstream-Anbieters in deiner eigenen Infrastruktur zu handhaben. Die Anmeldedaten, die du beim Onboarding gespeichert hast (heute: zone.ee; cloudflare und route53 sind hinter derselben Struktur eingeplant), bleiben verschlüsselt in MintOffice; dein Aufruf kommt mit Bearer mo_live_… herein und wir machen den Upstream-Aufruf in deinem Namen.
Der Geltungsbereich ist die eigene konfigurierte Apex des Partners (domain.client_apex_domain aus GET /settings). Du kannst keinen Eintrag in einer Zone berühren, die dir nicht gehört.
Heute unterstützt der Proxy die Operationen, die die Deploy-Pipeline selbst verwendet — A-Einträge (upsert / list / delete by id). CNAME- / TXT- / MX-Unterstützung wird derselben Struktur folgen, wenn die zugrunde liegende Provider-Abstraktion sie ergänzt.
Idempotency-Key hier nicht erforderlich
Anders als bei den Endpoints zur Bestellerstellung sind DNS-Upserts auf der Provider-Ebene natürlich idempotent (erneutes Ausführen mit demselben (subdomain, ip) ist ein No-op und veraltete Duplikate werden bei jedem Aufruf entfernt). Du musst den Idempotency-Key header nicht senden — Wiederholungsversuche konvergieren immer auf einen einzigen A-Eintrag pro FQDN.
Einen A-Eintrag upserten¶
POST /dns/records
Lässt <subdomain>.<your-apex> idempotent auf eine IPv4-Adresse zeigen. Übergib "" (oder "@") für die Apex selbst.
Anfrage
{
"subdomain": "www",
"ip": "203.0.113.10"
}
subdomain akzeptiert:
""oder"@"— die Apex selbst (example.com).- Ein einzelnes DNS-Label —
"www","api"— für<label>.<apex>. - Punktierte Sub-Labels —
"api.eu"— für verschachtelte Subdomains (api.eu.example.com).
ip muss eine punktierte IPv4-Adresse sein. Jedes Label ist [a-z0-9_-], 1–63 Zeichen, kein führendes/abschließendes -.
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-Anbieters auf den FQDN nach einem Duplikat-Sweep wider — eine erfolgreiche Antwort mit genau einem Eintrag ist also das Happy-Path-Signal, dass nichts Veraltetes übrig bleibt.
Einträge auflisten¶
GET /dns/records
Gibt jeden A-Eintrag auf der konfigurierten Apex zurück. Füge ?name=<label-or-fqdn> hinzu, um auf einen FQDN zu filtern — der Wert kann ein bloßes Label ("www"), eine punktierte Subdomain ("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" }
]
}
Einen Eintrag löschen¶
DELETE /dns/records/{record_id}
record_id ist der Anbieter-Identifikator, der von GET /dns/records zurückgegeben wird (zone.ee vergibt Integer-ids, an der API-Grenze in Strings umgewandelt). Das Löschen eines Eintrags, der nicht mehr existiert, wird als Erfolg behandelt — der Endpoint ist clientseitig idempotent.
Antwort — 200
{ "deleted": true, "id": "9182734" }
Fehler¶
| HTTP | error.code |
Ursache |
|---|---|---|
| 401 | unauthenticated / invalid_api_key |
Fehlender / falscher Bearer-Token. |
| 422 | validation_error |
Fehlerhafte Subdomain oder Nicht-IPv4-Ziel. |
| 422 | dns_not_configured |
Partner hat keine client_apex_domain oder die Anmeldedaten des konfigurierten Anbieters fehlen. Behebe es in Settings → Domain. |
| 422 | dns_provider_unsupported |
Der DNS-Provider des Partners ist noch nicht in den MintOffice-Proxy eingebunden (z. B. cloudflare-Platzhalter). Wechsle vorerst zu zone_ee. |
| 502 | dns_provider_error |
Upstream-Anbieter gab einen 4xx/5xx zurück oder der Transport ist fehlgeschlagen. Die error message trägt den Upstream-Status und einen kurzen, geheimnis-redigierten Auszug des Upstream-Body. Sicher zum Wiederholen. |
Beispiel¶
# Point apex + www at your server's IP.
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"}'
# Verify.
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¶
Wenn etwas mit einer Bestellung passiert, senden wir ein signiertes JSON-Ereignis per POST an deine konfigurierte Webhook URL.
Ereignistypen¶
| Event | Wann es ausgelöst wird |
|---|---|
order.created |
Stripe Checkout session wurde erstellt, Zahlung ausstehend. |
order.paid |
Stripe hat die Zahlung bestätigt. Umsatzereignis wurde aufgezeichnet. |
order.cancelled |
Bestellung ist abgelaufen oder wurde ausdrücklich storniert. |
agent.provisioning_started |
Deploy pipeline für diese Bestellung wurde gestartet. |
agent.ready |
Deploy erfolgreich. Payload enthält panel_url und expires_at. |
agent.failed |
Deploy pipeline ist fehlgeschlagen. Prüfe das Feld error für den defekten Schritt. |
agent.expired |
Agent TTL ist abgelaufen. Der Partner kann über POST /orders/{id}/renew verlängern. |
Zustellung & Wiederholungsversuche¶
- 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
2xxstatus, um zu bestätigen.
Request headers¶
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 verifizieren¶
Die Signatur ist HMAC-SHA256(secret, "{timestamp}.{raw_body}"). Verifiziere immer den rohen request body — wenn du dein geparstes JSON erneut serialisierst, bricht der Vergleich.
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 deinen Dedupe-key — Wiederholungsversuche verwenden dieselbe id wieder, sodass ein zeilenweises INSERT … ON CONFLICT DO NOTHING auf dieser Spalte deinen Handler sicher hält.
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 spiegelt den X-Request-Id response header wider — füge ihn Support-Tickets hinzu.
{
"error": {
"code": "invalid_api_key",
"message": "API key is unknown or revoked.",
"request_id": "f0c2d6c4-…"
}
}
| Code | Bedeutung |
|---|---|
unauthenticated |
Fehlender oder fehlerhafter Authorization header. |
invalid_api_key |
API key ist unbekannt oder wurde herausrotiert. |
rate_limited |
Rate limit pro Partner oder pro IP überschritten — siehe Retry-After. |
missing_idempotency_key |
POST request ohne Idempotency-Key. |
idempotency_key_mismatch |
Derselbe key wurde mit einem anderen request body wiederverwendet. |
validation_error |
Request body hat die Schema-Validierung nicht bestanden. |
not_found |
Ressource existiert nicht oder gehört nicht zu deinem Partner. |
payment_gateway_error |
Stripe hat die Erstellung der Checkout session abgelehnt. |
Rate limits¶
- 120 requests / 60 s rolling window, pro Partner. Burst und steady-state teilen sich denselben bucket.
- 60 requests / 60 s separater bucket pro IP für nicht authentifizierte und fehlgeschlagene Auth-Anfragen — verhindert, dass ein Bearer brute-force die Partnerquote aufbraucht.
- Überschüssige Anfragen geben
429 rate_limitedmit einemRetry-Afterheader zurück (Sekunden zum Zurückziehen).
Brauchst du Hilfe?¶
Diese Dokumentation ist für Partner geschrieben, die die API wirklich nutzen. Wenn etwas fehlt, unklar oder veraltet ist, erwähne es gegenüber deinem mintbot-Agenten — er leitet das Feedback weiter und wir aktualisieren die Seite.