Brand Partner API¶
A small, predictable REST API for creating mintbot orders, polling their status, and receiving lifecycle events. JSON in, JSON out. Bearer-token auth. Idempotent writes. Signed webhooks.
API access dashboard Jump to webhooks
At a glance¶
| Base URL | https://mint.mintbot.ai/api/v1 |
| Auth | Authorization: Bearer mo_live_… |
| Idempotency | Idempotency-Key: <uuid> on every POST except POST /dns/records |
| Content type | application/json |
| Rate limit | 120 req / 60 s per partner |
| Webhooks | Signed with HMAC-SHA256, retried up to 7 times |
Authentication¶
Every request sends a partner API key in the Authorization header. Generate or rotate the key in the dashboard.
Authorization: Bearer mo_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Rotation has no grace window
Rotating the key revokes the previous one atomically. Plan to swap the value in your config before clicking Rotate.
Idempotency¶
Every POST requires an Idempotency-Key header — except POST /dns/records, which is naturally idempotent at the provider layer and accepts retries without one. Any UUID per distinct request works.
- We cache the response for 24 hours. A retry with the same key replays the original response with
Idempotent-Replay: true. - Reusing the same key with a different body returns
409 idempotency_key_mismatch.
Orders¶
Create order¶
POST /orders
Creates an order and returns a Stripe Checkout URL. Once payment confirms, mintbot provisions the agent and the partner-cut revenue event is recorded automatically.
Request
{
"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- One of
trial,s1,s2,s4. duration_months- Calendar months of server lifetime. Must be one of
1,3, or12. credit_usd- Optional. Pre-funded chat credit bundled with the order.
language- Optional. Affects Stripe Checkout locale and welcome-email language.
external_id- Optional. Echoed back on every webhook and order response — use it to thread mintbot orders to rows in your own system.
success_url·cancel_url- Stripe Checkout return URLs.
{ORDER_ID}is substituted server-side. webhook_url- Optional. Per-order override of the partner-level webhook URL.
Response — 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
}
Example
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"]
Get order¶
GET /orders/{id}
Fetches a single order by its mintbot id. Returns the same shape as POST /orders.
curl https://mint.mintbot.ai/api/v1/orders/42 \
-H "Authorization: Bearer $MINTBOT_API_KEY"
List orders¶
GET /orders
Cursor-paginated list, newest first.
Query parameters
status- Optional. Filter by
awaiting_payment,completed,deployed,deploy_failed, orexpired. cursor- Optional. Pass
next_cursorfrom the previous page to continue.
Response
{
"items": [ /* OrderResponse … */ ],
"next_cursor": "37"
}
next_cursor is null when there are no more pages.
Renew order¶
POST /orders/{id}/renew
Extends an existing agent for another duration_months. Infra-only — no fresh chat credit is bundled. Returns a new order id and a fresh Stripe Checkout URL.
Request
{
"duration_months": 1,
"external_id": "your-side-id",
"success_url": "https://your.app/thanks?id={ORDER_ID}",
"cancel_url": "https://your.app/account"
}
Revenue¶
Read revenue¶
GET /revenue
Totals plus the latest 200 ledger events.
Query parameters
include_paid- Optional, default
true. Set tofalseto see only events that haven't been paid out yet.
Response
{
"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
}
]
}
Partner profile¶
Get profile¶
GET /partner
Echoes your partner profile plus the unpaid balance — handy for surfacing earnings inside your own admin UI without storing them yourself.
Response
{
"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"
}
Settings¶
Get settings¶
GET /settings
Returns your full non-secret partner configuration in a single payload — chosen modes, apex domain, DNS provider, bot provider, template repo, subdomain pattern (with a rendered preview label), per-tier pricing, API key metadata, webhook metadata, and the same readiness / per-section status that drives the MintOffice dashboard banner.
Designed for integration agents that need to discover how the partner is set up without scraping the dashboard or interrogating the operator. A typical caller is a coding agent installing your white-label setup on a fresh server — it can read this endpoint, branch on readiness.status, and tell the operator exactly which sections still need attention.
No secret is ever returned
Anything the partner supplied as a credential — Telegram bot token, Zone.ee API key, webhook signing secret, the API key plaintext itself — surfaces only as a *_set boolean. The webhook secret additionally exposes the same 8-char display prefix that's already shown in the dashboard, so a caller can confirm which secret is configured without ever seeing the value.
Response
{
"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
}
}
Field reference¶
status- Partner lifecycle. One of
onboarding,live,paused. The Brand Partner API rejects writes whileonboarding. domain.modebyo(you bring your own apex domain) orhosted(mintbot provides a*.mintbot.aisubdomain). Whenhosted, the DNS-provider fields are intentionally empty regardless of what the partner previously entered.domain.subdomain_label_preview- The rendered label the first customer agent would receive (
agent1,1,001, …) given the currentsubdomain_prefix,subdomain_numbering, andsubdomain_pad_width. Use it for a "this is what your customers will see" preview without re-implementing the numbering rules. bot.modeown(partner-provided Telegram bot),borrow(using mintbot's bot during testing), orweb(panel only, no Telegram).template.modedefault(mintbot's reference template from mintbot-ai/agent-template) orcustom(partner's forked repo atrepo_url). The default template controls the agent's SOUL.md (persona), config.yaml (Hermes settings), and panel theme — fork it and pointrepo_urlat your fork when you want custom branding.api.webhook_secret_prefix- First 8 characters of the webhook signing secret, only present when a secret is configured. Lets a caller distinguish which secret is in place without seeing the full value. The dashboard shows the same prefix.
readiness.missing_fields- List of dashboard field names still required before Go Live. Empty when
readyistrue. Stable enough to drive a "what's still missing" UI in your own admin. readiness.dns_blocker- Set to a short reason string when DNS verification fails (e.g.
nameservers_not_pointing,glue_record_missing).nullotherwise. section_status.*.issue- Per-section human-readable hint when that section blocks Go Live. Mirrors the dashboard's per-section status pills.
Example
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"])
DNS records¶
Brand-partner integrations often need to provision DNS records on the partner's apex domain — pointing @, www, or per-customer subdomains at the right server. MintOffice exposes a vendor-blind proxy so you can do this without ever handling the upstream provider's API key in your own infrastructure. The credentials you saved during onboarding (today: zone.ee; cloudflare and route53 are roadmapped behind the same shape) stay encrypted in MintOffice; your call comes in with Bearer mo_live_… and we make the upstream call on your behalf.
Scope is the partner's own configured apex (domain.client_apex_domain from GET /settings). You can not touch a record on a zone you don't own.
Today the proxy supports the operations the deploy pipeline itself uses — A-records (upsert / list / delete by id). CNAME / TXT / MX support will follow the same shape when the underlying provider abstraction grows them.
Idempotency-Key not required here
Unlike order-creation endpoints, DNS upserts are naturally idempotent at the provider layer (re-running with the same (subdomain, ip) is a no-op and stale duplicates are swept on every call). You do not need to send the Idempotency-Key header — retries always converge on a single A-record per FQDN.
Upsert an A-record¶
POST /dns/records
Idempotently point <subdomain>.<your-apex> at an IPv4 address. Pass "" (or "@") for the apex itself.
Request
{
"subdomain": "www",
"ip": "203.0.113.10"
}
subdomain accepts:
""or"@"— the apex itself (example.com).- A single DNS label —
"www","api"— for<label>.<apex>. - Dotted sub-labels —
"api.eu"— for nested subdomains (api.eu.example.com).
ip must be a dotted IPv4 address. Each label is [a-z0-9_-], 1–63 chars, no leading/trailing -.
Response — 200
{
"apex": "example.com",
"provider": "zone_ee",
"fqdn": "www.example.com",
"records": [
{ "id": "9182734", "name": "www.example.com", "destination": "203.0.113.10" }
]
}
The post-write records list always reflects the upstream provider's current view of the FQDN after a duplicate sweep — so a successful response with exactly one record is the happy-path signal that nothing stale remains.
List records¶
GET /dns/records
Returns every A-record on the configured apex. Add ?name=<label-or-fqdn> to filter to one FQDN — the value may be a bare label ("www"), a dotted subdomain ("api.eu"), or the full FQDN ("www.example.com").
Response — 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" }
]
}
Delete a record¶
DELETE /dns/records/{record_id}
record_id is the vendor identifier returned by GET /dns/records (zone.ee issues integer ids, stringified at the API boundary). Deleting a record that no longer exists is treated as a success — the endpoint is idempotent on the client side.
Response — 200
{ "deleted": true, "id": "9182734" }
Errors¶
| HTTP | error.code |
Cause |
|---|---|---|
| 401 | unauthenticated / invalid_api_key |
Missing / bad Bearer token. |
| 422 | validation_error |
Bad subdomain or non-IPv4 destination. |
| 422 | dns_not_configured |
Partner has no client_apex_domain or the configured provider's credentials are missing. Fix in Settings → Domain. |
| 422 | dns_provider_unsupported |
The partner's DNS provider is not yet wired into the MintOffice proxy (e.g. cloudflare placeholder). Switch to zone_ee for now. |
| 502 | dns_provider_error |
Upstream provider returned a 4xx/5xx, or the transport failed. The error message carries the upstream status and a short, secret-redacted excerpt of the upstream body. Safe to retry. |
Example¶
# 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¶
When something happens to an order, we POST a signed JSON event to your configured webhook URL.
Webhooks are optional
The Webhook URL and Webhook signing secret in Settings → API access are entirely optional — leave them blank during onboarding if you don't have a receiver yet. The dashboard and the rest of the API work fine without webhooks; you can poll GET /api/v1/orders/{id} to track lifecycle instead. Add the URL (and generate the secret) whenever you're ready, no re-activation needed.
Event types¶
| Event | When it fires |
|---|---|
order.created |
Stripe Checkout session minted, awaiting payment. |
order.paid |
Stripe confirmed payment. Revenue event recorded. |
order.cancelled |
Order timed out or was explicitly cancelled. |
agent.provisioning_started |
Deploy pipeline started for this order. |
agent.ready |
Deploy succeeded. Payload contains panel_url and expires_at. |
agent.failed |
Deploy pipeline errored. Check the error field for the step that broke. |
agent.expired |
Agent TTL elapsed. The partner may renew via POST /orders/{id}/renew. |
Delivery & retries¶
- Up to 7 attempts on an exponential schedule:
0s, 30s, 2m, 10m, 1h, 6h, 24h. - After the last attempt, the delivery is marked
exhaustedand stops retrying. - Respond with a
2xxstatus within 10 seconds to acknowledge.
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
Sample 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"
}
Verifying the signature¶
The signature is HMAC-SHA256(secret, "{timestamp}.{raw_body}"). Always verify the raw request body — re-serialising your parsed JSON will break the comparison.
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"),
);
}
Idempotent receivers
Use X-Mintbot-Event-Id as your dedup key — retries reuse the same id, so a row-level INSERT … ON CONFLICT DO NOTHING on that column keeps your handler safe.
Reference portal¶
A runnable end-to-end reference lives at mintbot-ai/partner-portal-example — FastAPI + SQLite, Dockerised, ships with an example brand called ExampleAI. A live brand fork is at mintbot-ai/partner-portal-agent99 (the actual storefront running on agent99.cc). The architectural picture — how the storefront, this API, the deploy worker, and the agent template fit together — is on MintOffice — Technical. The portal implements:
- the landing / plan-picker / thank-you / cancel pages,
POST /buy→POST /api/v1/orders→ Stripe redirect,POST /webhooks/mintofficewith the HMAC verifier referenced above,- an HTTP-Basic-auth
/adminevent browser so you can eyeball deliveries.
Fork it, set PARTNER_BRAND, MINTOFFICE_API_KEY, and MINTOFFICE_WEBHOOK_SECRET in .env, run docker compose up, and point your partner row's webhook URL at https://<your-host>/webhooks/mintoffice. See MintOffice → Reference portal for the full walkthrough.
Errors¶
Every error response carries a stable code you can branch on programmatically. The message field is a human-readable hint and may change between releases. The request_id echoes the X-Request-Id response header — include it in support tickets.
{
"error": {
"code": "invalid_api_key",
"message": "API key is unknown or revoked.",
"request_id": "f0c2d6c4-…"
}
}
| Code | Meaning |
|---|---|
unauthenticated |
Missing or malformed Authorization header. |
invalid_api_key |
API key is unknown or has been rotated out. |
rate_limited |
Per-partner or per-IP rate limit exceeded — see Retry-After. |
missing_idempotency_key |
POST request without Idempotency-Key. |
idempotency_key_mismatch |
Same key reused with a different request body. |
validation_error |
Request body failed schema validation. |
not_found |
Resource does not exist or does not belong to your partner. |
payment_gateway_error |
Stripe rejected the Checkout session creation. |
dns_not_configured |
POST /dns/records against a partner with no apex / no DNS credentials. Fix in Settings → Domain. |
dns_provider_unsupported |
The partner's dns_provider value is not wired into the MintOffice proxy yet. |
dns_provider_error |
Upstream DNS provider returned an error — message carries a redacted excerpt. |
quota_exceeded |
Partner has consumed every deploy slot (defaults to 10). Contact mintbot support to raise the cap. Returned by POST /orders and POST /orders/{id}/renew. The body carries deploys_used + deploy_quota. |
Rate limits¶
- 120 requests / 60 s rolling window, per partner. Burst and steady-state share the same bucket.
- 60 requests / 60 s separate per-IP bucket for unauthenticated and failed-auth requests — keeps a bearer brute-force from chewing through the partner quota.
- Excess requests return
429 rate_limitedwith aRetry-Afterheader (seconds to back off).
Need help?¶
These docs are written for the partners who actually use the API. If something is missing, confusing, or stale, mention it to your mintbot agent — they'll forward the feedback and we'll update the page.