Ana içeriğe geç

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, or 12.
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, or expired.
cursor
Optional. Pass next_cursor from 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 to false to 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 while onboarding.
domain.mode
byo (you bring your own apex domain) or hosted (mintbot provides a *.mintbot.ai subdomain). When hosted, 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 current subdomain_prefix, subdomain_numbering, and subdomain_pad_width. Use it for a "this is what your customers will see" preview without re-implementing the numbering rules.
bot.mode
own (partner-provided Telegram bot), borrow (using mintbot's bot during testing), or web (panel only, no Telegram).
template.mode
default (mintbot's reference template from mintbot-ai/agent-template) or custom (partner's forked repo at repo_url). The default template controls the agent's SOUL.md (persona), config.yaml (Hermes settings), and panel theme — fork it and point repo_url at 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 ready is true. 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). null otherwise.
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 exhausted and stops retrying.
  • Respond with a 2xx status 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 /buyPOST /api/v1/orders → Stripe redirect,
  • POST /webhooks/mintoffice with the HMAC verifier referenced above,
  • an HTTP-Basic-auth /admin event 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_limited with a Retry-After header (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.