Kihagyás

Developer API

The public REST API of mintbot: sign in on the developer portal, mint an API key, top up a EUR wallet with any payment method, and order agents — prepaid or by the hour, with Telegram or headless over SSH. JSON in, JSON out. Key auth. Signed webhooks.

Developer portal Live endpoint list Jump to webhooks


At a glance

Base URL https://api.mintbot.ai/v1
Auth X-API-Key: mb_… — keys are created on the portal after an e-mail sign-in
Wallet one EUR wallet per account, topped up through the checkout with card, PayPal, Bitcoin, Monero or other coins
Content type application/json
Rate limit per key, default 60 requests / minute and 5 000 / day
Webhooks HMAC-SHA256 signed, 3 delivery attempts
Errors {"detail": "<code>", "hint": "…"} with a stable detail code — see Errors
MCP the same API as tools for Claude Code, Cursor & co at https://api.mintbot.ai/mcp — see MCP server

Accounts & keys

Two ways in. On the developer portal at api.mintbot.ai you can enter your e-mail, click the one-time sign-in link (valid 30 minutes, works once), and land on a dashboard with your wallet, agents, SSH keys and API keys. Or you take a free key straight from POST /v1/signup — with an address, or with none at all — and never open a browser.

  • Create a key on the dashboard. The full key is shown exactly once — only a hash is stored. Up to 10 active keys per account; revoke any of them at any time.
  • Every key is bound to your account, so GET /v1/agents and the wallet see the same things the dashboard does. Webhooks are registered per key, but every account-level event (order.paid, agent.deployed, boost.applied, renewal.applied, wallet.*, agent.credit_added) is delivered to the hooks of all your active keys. agent.deleted goes only to the hooks of the key that deleted the agent.
  • Scopes: a key minted on the portal carries orders, agents, chat, webhooks, wallet. A request outside the key's scopes is 403.

POST /v1/keys is gone

Anonymous key minting was closed. The endpoint answers 410 Gone with a pointer to the portal. Keys created before that keep working, but they have no wallet: every wallet-bound endpoint below returns 409 no_wallet for them. Sign in on the portal and create a new key.

X-API-Key: mb_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

No key or a revoked key is 401. The public endpoints need no key: GET /v1/ (this list as JSON, with docs and portal links), GET /v1/plans, GET /v1/models, GET /v1/prices, GET /v1/agent-fleet-status ({"status": "healthy" | "degraded" | "unknown", "affected_pct"}, rounded to 5 %) and GET /v1/vps-capacity (packages whose server class is out of stock in a data centre right now, so setup may take longer).


Free signup

POST /v1/signup gives you a working API key from one unauthenticated call. No payment, no browser, no waiting for an e-mail.

curl -sS -X POST https://api.mintbot.ai/v1/signup

Response — 201 Created

{
  "api_key": "mb_...",
  "key_id": 91,
  "key_prefix": "mb_xxxxxxxxx",
  "scopes": ["orders", "agents", "chat", "webhooks", "wallet"],
  "account_id": 42,
  "email": null,
  "anonymous": true,
  "created": true,
  "wallet": {"currency": "eur", "balance_cents": 0},
  "portal_url": "https://api.mintbot.ai/",
  "docs": "https://mintbot.how/developer-api/",
  "next_steps": [
    {"method": "POST", "path": "/v1/account/email", "desc": "…"},
    {"method": "POST", "path": "/v1/wallet/topup", "desc": "…"},
    {"method": "POST", "path": "/v1/agents", "desc": "…"},
    {"method": "GET",  "path": "/v1/agents/{agent_id}", "desc": "…"}
  ],
  "auth": {"header": "X-API-Key", "value": "<api_key>"}
}

The key is returned once — we store only a hash. Send it as X-API-Key on every later call. An optional {"name": "ci"} labels the key (≤ 64 chars). The same per-IP and per-/24 limits apply as on the portal — anonymity is not a way around them.

This is an anonymous account: we store no address for it anywhere. POST /v1/account/email binds one later, once, and brings sign-in-link recovery back for the day the key is lost.

With an e-mail address

Post an address and you get no key from this call. Instead the portal's one-time sign-in link is e-mailed to it and the answer is 202 sign_in_link_sent — the same answer whether the address is new or already has an account, so nobody can use this call to find out which addresses do. Open the link: a new address becomes an account, a known one signs in, and you create API keys on the portal. The body is {"detail": "sign_in_link_sent", "email", "message", "portal_url", "docs"}. Asking for a link spends a signup slot like a key does, and links have their own limit too (3 per IP, 8 per /24 network, per 24 h — 429 sign_in_link_rate_limited); if the mail cannot be sent right now the answer is 503 sign_in_link_failed, and the portal login works all the same.

curl -sS -X POST https://api.mintbot.ai/v1/signup \
  -H 'content-type: application/json' \
  -d '{"email": "you@example.com"}'

The key is the account

An anonymous account has no recovery path. Lose the key and the wallet balance and the running agents go with it: there is nothing we can check ownership against, and support cannot help. That is the trade for not handing over an address.

Want the safety net back later? Bind an address once — from the dashboard, or:

curl -sS -X POST https://api.mintbot.ai/v1/account/email \
  -H "X-API-Key: $KEY" -H 'content-type: application/json' \
  -d '{"email": "you@example.com"}'

It answers 200 {"account_id": 42, "email": "you@example.com", "anonymous": false, "bound": true}. From that moment the ordinary sign-in link on the portal reaches this account. Binding is once only and never moves an address off the account that holds it: 409 already_bound if this account already has one, 409 email_taken if another account does, 400 invalid_email if it is not an address. Re-sending the address the account already holds is a no-op 200, so a retry is safe.

GET /v1/account tells you where you stand:

{"account_id": 42, "email": null, "anonymous": true, "created_at": "2026-09-16 09:12:03"}

Both /v1/account endpoints use scope wallet and answer 409 no_wallet for a key that has no account (see Errors).

What a key does before you pay

Everything that costs nothing: list plans and models, read your wallet and spend, mint a top-up link, read your agents, manage SSH keys and webhooks.

Ordering an agent needs money in the wallet. POST /v1/agents with a balance that does not cover the order answers 402 insufficient_funds and writes nothing — no order, no server, no DNS, no TLS. That is why the key can be free.

Limits, and what a refusal tells you

Limit Value
keys (and e-mailed sign-in links) per IP 3 per 24 h
keys (and e-mailed sign-in links) per /24 network 8 per 24 h
open unpaid crypto top-up payments, before your first funding 2 at a time

Refusals are machine-readable and always say when you may continue:

{
  "detail": "signup_rate_limited",
  "reason": "This IP address has already taken 3 API keys in the last 24 hours (limit 3). Existing keys keep working.",
  "scope": "ip", "limit": 3, "window_hours": 24, "used": 3,
  "retry_after_seconds": 51840,
  "retry_after": "2026-09-17T14:20:00Z",
  "blocked_until": null
}

scope is ip or subnet24, and the body also carries a docs link. A Retry-After header carries the same number of seconds. signup_blocked (429, scope ip_blocked or subnet_blocked) is a network that kept taking keys it never funded: it replaces the counters with blocked_until. invalid_email is 400.

The crypto cap is concurrent, not a lifetime quota: an unpaid crypto order expires by itself and frees the slot, and card or PayPal are never capped. It exists because starting an on-chain payment reserves a real address from a finite pool.

A bot, start to finish

import httpx, time

api = "https://api.mintbot.ai/v1"
key = httpx.post(f"{api}/signup").json()["api_key"]   # no body: anonymous account
h = {"X-API-Key": key}

# 1. fund the wallet — a human opens the returned URL and pays
pay = httpx.post(f"{api}/wallet/topup", json={"amount_eur": 20}, headers=h).json()
print("pay here:", pay["checkout_url"])
while httpx.get(f"{api}/wallet", headers=h).json()["balance_cents"] < 100:
    time.sleep(30)

# 2. order an agent — charged from the wallet at once (402 if it cannot cover it)
order = httpx.post(f"{api}/agents", headers=h,
                   json={"tier": "starter", "billing_period": "24h"}).json()

# 3. poll until it runs, then talk to it
while True:
    agents = httpx.get(f"{api}/agents", headers=h).json()["agents"]
    running = [a for a in agents if a["status"] == "running"]
    if running:
        break
    time.sleep(30)
r = httpx.post(f"{api}/agents/{running[0]['id']}/chat", headers=h,
               json={"message": "Hello!"}, timeout=130)
print(r.json()["reply"])

Wallet

The wallet is denominated in EUR and moves in whole cents. Every change is journaled in a ledger you can read back; the balance never goes negative — a purchase the wallet cannot cover is refused with 402 insufficient_funds and nothing is written.

Read the wallet

GET /wallet · scope wallet

{
  "currency": "eur",
  "balance_cents": 1470,
  "balance_eur": 14.7,
  "ledger": [
    { "id": 12, "kind": "hourly_charge", "amount_cents": -100, "balance_after_cents": 1470,
      "order_id": null, "agent_id": 8123, "note": "prepaid 24h to 2026-09-11T11:05:00+00:00",
      "created_at": "2026-09-10 11:05:00" },
    { "id": 11, "kind": "topup", "amount_cents": 2000, "balance_after_cents": 2000,
      "order_id": 5310, "agent_id": null, "note": "card", "created_at": "2026-09-09 08:41:12" }
  ]
}

ledger holds the newest 50 rows. kind is one of topup, agent_order, hourly_charge (one renewal of a period — the name is historical), llm_credit, refund, admin_adjust; amount_cents is signed (credits positive, debits negative) and balance_after_cents is the running balance.

Top up

POST /wallet/topup · scope wallet

{ "amount_eur": 20 }

amount_eur is 5–500, whole cents. The response is 201 with a checkout URL on the reception site where you (or your user) pick the payment method — card, PayPal, Bitcoin, Monero or other coins through CCPayment — exactly like buying an agent on mintbot.ai:

{
  "order_id": null,
  "checkout_url": "https://reception.mintbot.ai/wallet/<token>",
  "amount_cents": 2000,
  "currency": "eur",
  "expires_at": "2026-09-10 12:41:12"
}

The URL is single-use and valid for one hour; the order is created there once a method is chosen. When the payment lands the ledger gets a topup row, the wallet.topup webhook fires, and a receipt is e-mailed to the account.

curl -X POST https://api.mintbot.ai/v1/wallet/topup \
  -H "X-API-Key: $MINTBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount_eur": 20}'
import os, requests

API = "https://api.mintbot.ai/v1"
H = {"X-API-Key": os.environ["MINTBOT_API_KEY"]}

r = requests.post(f"{API}/wallet/topup", headers=H, json={"amount_eur": 20}, timeout=10)
r.raise_for_status()
print("pay here:", r.json()["checkout_url"])

Where the money went

GET /wallet/spend?days=30 · scope wallet

The last days (1–365, default 30) of the ledger rolled up, plus a per-day list of the debits for a bar chart:

{
  "currency": "eur",
  "days": 30,
  "summary": {
    "days": 30, "topups_cents": 2000, "infra_cents": 960, "llm_credit_cents": 500,
    "refunds_cents": 0, "adjust_cents": 0, "net_cents": 540
  },
  "daily": [
    { "date": "2026-09-09", "infra_cents": 930, "llm_credit_cents": 0 },
    { "date": "2026-09-10", "infra_cents": 30,  "llm_credit_cents": 500 }
  ]
}

infra_cents is agent orders plus period renewals; an agent order counts the whole order, including any LLM credit bought together with the agent. llm_credit_cents is credit moved onto agents afterwards with POST /agents/{id}/credit. net_cents is the signed sum of every row in the window.


Agents

Order an agent

POST /agents · scope agents

Buys an agent from the wallet and hands it to the deploy pipeline in one request.

{
  "tier": "starter",
  "billing_period": "24h",
  "channels": ["telegram", "web"],
  "ssh_key_id": 3,
  "vps_location": "hel1",
  "language": "en",
  "credit_eur": 5,
  "customer_ref": "your-side-id"
}
tier
A public EUR package: trial, starter, pro or business. GET /v1/plans lists them. billing_period is sold on starter only.
billing_period
Buys the agent one period at a time: 4h (€0.50), 24h (€1), month (€9) — see the period model below. starter only; any other tier, an unknown period or an empty string is 400 invalid_period. This is the product the developer portal sells, and the period renews itself from the wallet for as long as the balance covers it. Omit the field for a one-off whole package order (trial, pro, business), which buys the package's own lifetime and never renews.
billing
Optional. prepaid, the default and the only accepted value. hourly is retired and answers 400 invalid_billing — buy time with billing_period.
channels
Optional. Subset of ["telegram", "web"], default both. Any list without telegram — [] or ["web"] — orders a headless agent.
ssh_key_id
One of your SSH keys. Required for a headless agent; optional for a Telegram agent, where the key is installed best-effort. A missing key on a headless order, or an id that is not one of your keys on any order, is 400 ssh_key_required.
vps_location
Optional. Data centre for the server: hel1 (Helsinki), fsn1 (Falkenstein) or nbg1 (Nürnberg) — the same choice the portal's New agent card offers. Omit it and the fleet default (Helsinki) is used. An unknown slug is 400 invalid_location, never a server quietly built somewhere else. Echoed back as vps_location; null when the package's provider takes no location.
language
Optional. UI language code of the agent (en, et, de, …).
credit_eur
Optional. LLM credit bought together with the agent (0–500, whole cents). 0 means bring your own keys — you can add credit later.
customer_ref
Optional. Free-text reference (≤ 128 chars) stored on the order.

Response — 201 Created

{
  "order_id": 5311,
  "agent_id": null,
  "tier": "starter",
  "billing": "prepaid",
  "billing_period": "24h",
  "period_cents": 100,
  "auto_renew": true,
  "renew_ahead_hours": 6,
  "charged_cents": 600,
  "infra_cents": 100,
  "credit_cents": 500,
  "balance_after_cents": 1400,
  "currency": "eur",
  "channels": ["telegram", "web"],
  "headless": false,
  "ssh_key_fingerprint": "SHA256:…",
  "vps_location": "hel1",
  "poll_url": "https://api.mintbot.ai/v1/orders/5311"
}

The agent id is not known yet — the deploy worker creates it within seconds and the VPS is ready a few minutes later. Poll poll_url (GET /orders/{order_id} carries the agent stub once it exists) or GET /agents, or subscribe to the agent.deployed webhook. Every portal-ordered agent also triggers an e-mail hand-off to the account address.

billing_period, period_cents, auto_renew and renew_ahead_hours describe the standing charge: on a period order they echo the period, its price in cents, true, and how many hours before the end the renewal is charged. On a whole-package order (no billing_period) they are null / false — nothing renews itself from your wallet.

Errors: 402 insufficient_funds (with required_cents, balance_cents, topup_url), 400 invalid_tier | invalid_billing | invalid_period | invalid_location | invalid_language | invalid_credit | invalid_channels | ssh_key_required, 409 no_wallet.

curl -X POST https://api.mintbot.ai/v1/agents \
  -H "X-API-Key: $MINTBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tier": "starter", "billing_period": "24h", "credit_eur": 5}'
import os, time, requests

API = "https://api.mintbot.ai/v1"
H = {"X-API-Key": os.environ["MINTBOT_API_KEY"]}

r = requests.post(f"{API}/agents", headers=H, timeout=10,
                  json={"tier": "starter", "billing_period": "24h", "credit_eur": 5})
if r.status_code == 402:
    raise SystemExit(f"top up first: {r.json()['topup_url']}")
r.raise_for_status()
order = r.json()

# Wait for the deploy worker to create and provision the agent.
while True:
    status = requests.get(order["poll_url"], headers=H, timeout=10).json()
    agent = status.get("agent")
    if agent and agent.get("status") == "running":
        break
    time.sleep(15)
print("agent", agent["id"], "is up; chat at", agent["chat_url"])

The period model

Agent time is sold as three prepaid periods, and these are the prices — the same ones the developer portal quotes.

Period Price Renewed
4h €0.50 1 h before it ends
24h €1.00 6 h before it ends
month (a calendar month) €9.00 72 h before it ends

The two short periods are retail prices, deliberately not the month divided by its days: a four-hour agent still costs a whole provisioning run and at least a day of infrastructure, because the provider bills the box, not the four hours you keep it. month buys exactly the starter package and therefore carries that package's price.

  • The first period is charged with the order. Each renewal is charged renew_ahead_hours before the paid time runs out, for as long as the balance covers it. expires_at is always the end of the paid time.
  • Every renewal is one ledger row, kind: "hourly_charge" (the name is historical), noted prepaid <period> to <end UTC>.
  • When the wallet cannot cover the next period the agent is paused (server off, data kept) for a grace window proportional to the time already paid, then deleted. A top-up during the pause resumes it on the next billing tick, and that renewal starts from the moment of the resume — you are never billed for the time the box was off.
  • A period agent is deliberately left out of the "your agent expires soon" notices: it renews itself.

The grace window is the margin already earned on the agent, spent on keeping the box:

margin_ratio = (package price − VPS cost) / VPS cost      # starter: 0.32
grace_hours  = clamp(paid_hours × margin_ratio, 1, 72)
Paid lifetime paid_hours Grace
1 day 24 8 h
3 days 72 23 h
10 days 240 72 h (cap)
30 days 720 72 h

A whole-package agent — one ordered without billing_period — is untouched by all of this: it keeps the package's fixed lifetime and grace period, and never charges the wallet again.

Hourly billing is retired

billing: "hourly" was orderable between 2026-09-10 and 2026-09-16 and now answers 400 invalid_billing: it charged €0.30 for the same 24 hours the price list above sells for €1, so what you paid depended on which surface you ordered from. Buy time with billing_period.

Agents bought while it was on sale keep billing exactly as before — one 24 h block at a time (€0.30 for starter / pro, the monthly price over the package's days), charged about 6 h before the paid block ends. They read "billing_mode": "hourly" and carry daily_block_cents on AgentInfo besides the billing fields every period agent has.

Runway is the time until the first of your running wallet-billed agents (period and hourly alike) stops: the renewal schedule replayed against the balance. At 24 h, 6 h and 1 h of runway the account gets an e-mail and the wallet.low webhook, once per step.

Headless agents

An agent ordered without the telegram channel is headless:

  • no Telegram binding exists and none can be requested — POST /agents/{id}/link-telegram answers 409 headless_agent;
  • the web panel is provisioned (updates and the API chat depend on it) but never advertised — AgentInfo.bot_username is null and no panel URL appears anywhere;
  • your SSH public key (ssh_key_id) is installed for root during provisioning; AgentInfo.ssh is root@<vps_ip> once the VPS is up;
  • the hand-off e-mail carries the SSH line and the fingerprint of the installed key.

You reach it with ssh root@<vps_ip> (the agent's own panel daemon manages further keys from there) or with POST /agents/{id}/chat — the API chat works for every agent.

List and inspect

GET /agents · scope agents → {"agents": [AgentInfo…], "count": n}

GET /agents/{agent_id} · scope agents → AgentInfo

{
  "id": 8123,
  "status": "running",
  "tier": "starter",
  "vps_ip": "203.0.113.10",
  "expires_at": "2026-09-11T11:05:00+00:00",
  "created_at": "2026-09-10 11:02:31",
  "credit_usd": 5.0,
  "chat_url": "https://api.mintbot.ai/v1/agents/8123/chat",
  "bot_username": "mintbot_ai_bot",
  "billing_mode": "prepaid",
  "daily_block_cents": null,
  "billed_until": "2026-09-11T11:05:00+00:00",
  "paid_hours": 24.0,
  "grace_hours": 8,
  "runway_hours": 360.0,
  "headless": false,
  "ssh": null
}

credit_usd is the agent's LLM credit balance in the agent's own wallet currency — EUR for every agent ordered here; the field name is historical. status is one of deploying, running, suspended (paused in the grace window), expired (deleted after the grace window), deploy_failed, failed, deleted. expires_at is the end of the paid time — on a period agent it moves forward with every renewal. The billing fields below it are filled for every agent that renews from the wallet: billed_until (end of the paid time), paid_hours (cumulative paid lifetime), grace_hours (the pause it would get if the wallet ran dry now) and runway_hours (account-wide: how long the wallet keeps every renewing agent going; null when it funds a year of renewals). daily_block_cents is set only on a legacy hourly agent. A whole-package agent has null in all five. An agent that does not belong to your key is 404.

Usage by model

GET /agents/{agent_id}/usage?days=30 · scope agents · days 1–365

{
  "agent_id": 8123,
  "days": 30,
  "by_model": [
    { "model": "anthropic/claude-opus-5", "label": "Claude Opus 5", "requests": 42,
      "tokens_in": 120000, "tokens_out": 18000, "cost_usd": 2.13, "last_used_at": "2026-09-10 10:58:02" }
  ]
}

cost_usd is the upstream provider cost as metered by the proxy. The agent's EUR credit is charged that cost converted to EUR plus 1 %.

Add LLM credit from the wallet

POST /agents/{agent_id}/credit · scope agents

{ "amount_eur": 5 }

Moves amount_eur (0.01–500, whole cents) from your wallet onto the agent's LLM credit and fires agent.credit_added. The agent must be running or suspended, and it must keep its credit in EUR (every agent ordered through this API does; amounts are never converted).

Response — 200

{
  "agent_id": 8123,
  "credited_cents": 500,
  "currency": "eur",
  "agent_credit_after": 10.0,
  "balance_after_cents": 970
}

Errors: 402 insufficient_funds, 400 invalid_amount | agent_not_creditable | currency_mismatch, 404 agent_not_found, 409 no_wallet. The two ledgers move in order — wallet first, then the agent; if the agent side fails the wallet debit is refunded in the same request (you will see an llm_credit and a refund row).

curl -X POST https://api.mintbot.ai/v1/agents/8123/credit \
  -H "X-API-Key: $MINTBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount_eur": 5}'
import os, requests

API = "https://api.mintbot.ai/v1"
H = {"X-API-Key": os.environ["MINTBOT_API_KEY"]}

r = requests.post(f"{API}/agents/8123/credit", headers=H, json={"amount_eur": 5}, timeout=10)
r.raise_for_status()
print("agent credit now", r.json()["agent_credit_after"], "EUR")

Chat

POST /agents/{agent_id}/chat · scope chat

{ "message": "Summarise yesterday's inbox." }

Relays one user message to the agent and returns {"reply", "model", "credit_after_usd"} (credit_after_usd again in the agent's wallet currency). Messages are 1–8 000 characters. The agent must be running (409 "Agent is <status>" otherwise); the relay waits up to 120 s, and an unreachable or timed-out agent is 502; 503 means the agent is not fully provisioned yet. Works for headless agents too.

POST /agents/{agent_id}/link-telegram · scope agents

Returns {"code", "expires_in_minutes": 15, "instructions"}: a single-use code that the Telegram user sends to the mintbot bot as /link <code> to bind their account to the agent. 409 headless_agent for a headless agent.

Delete

DELETE /agents/{agent_id}?confirm=true · scope agents

Destroys the agent and its VPS permanently and answers {"ok": true, "agent_id", "vps_error", "purged"} (vps_error is null unless the provider refused the destroy). confirm=true is mandatory (400 without it). Send an Idempotency-Key header to make a retried delete return the first response instead of erroring. Fires agent.deleted.


SSH keys

Public keys that can be installed for root on your agents. Validation matches the agent panel: exactly one <type> <base64> [comment] line (ed25519, RSA, ECDSA or an OpenSSH security key), no options prefix; the fingerprint is the OpenSSH SHA256:… form that ssh-keygen -lf prints. Up to 10 keys per account, one row per fingerprint. All three endpoints use scope agents and need a portal key (409 no_wallet otherwise).

GET /ssh-keys

{
  "keys": [
    { "id": 3, "fingerprint": "SHA256:…", "type": "ssh-ed25519", "label": "laptop",
      "comment": "you@laptop", "created_at": "2026-09-10 09:12:44" }
  ],
  "count": 1,
  "max_keys": 10
}

POST /ssh-keys → 201 with the new row.

{ "public_key": "ssh-ed25519 AAAA… you@laptop", "label": "laptop" }

Errors: 400 invalid_key | too_many_keys, 409 duplicate_key.

DELETE /ssh-keys/{id} → 204; 404 ssh_key_not_found for an unknown or foreign id. Deleting a key never changes an order already placed — the order carries a snapshot of the key line — and never removes it from servers it is already installed on.

curl -X POST https://api.mintbot.ai/v1/ssh-keys \
  -H "X-API-Key: $MINTBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"public_key\": \"$(cat ~/.ssh/id_ed25519.pub)\", \"label\": \"laptop\"}"

Webhooks

Register an HTTPS URL and mintbot POSTs a signed JSON event to it whenever something happens to your orders, agents or wallet.

Manage

POST /webhooks · scope webhooks

{ "url": "https://your.app/mintbot-webhook", "events": ["agent.deployed", "wallet.low"] }

The response (200) carries the webhook id, url, events, status, created_at and the signing secret — shown once, store it. url must be https:// to a public host (≤ 2 048 chars); events is 1–20 names from the table below. Up to 10 webhooks per key. A bad URL, an unknown event or the limit is 400 with a message. GET /webhooks → {"webhooks": […], "count"}, GET /webhooks/{id}/deliveries?limit=50 (1–200) → {"webhook_id", "deliveries": […]}, DELETE /webhooks/{id} → {"ok": true, "deleted": id}.

Event types

Event When it fires data
order.paid an order was paid — also for wallet-paid agent orders {order_id, type, tier, amount_usd, currency, customer_ref} — for a wallet-paid order currency is "wallet" and amount_usd the USD equivalent of the EUR total
agent.deployed the deploy pipeline finished; the agent is running {agent_id, order_id, tier, vps_ip, expires_at, chat_url}
agent.deleted the agent and its VPS were destroyed {agent_id, vps_machine_id, vps_error, purged}
boost.applied a crypto credit top-up (POST /orders/boost) landed {order_id, agent_id, amount_usd}
renewal.applied a crypto renewal (POST /orders/renewal) landed {order_id, agent_id, tier, days_added, new_expiry}
wallet.topup a wallet top-up was credited {order_id, amount_cents, currency, balance_after_cents}
wallet.low the account's runway crossed 24 h / 6 h / 1 h (once per step; resets above 24 h) {balance_cents, currency, runway_hours, stage, agents} — stage is "24h", "6h" or "1h", agents the ids of the running agents that renew from the wallet (period and legacy hourly)
agent.credit_added LLM credit was moved from the wallet onto an agent {agent_id, amount_cents, currency, agent_credit_after}
order.created, order.expired, agent.deploying accepted by POST /webhooks for forward compatibility; no emitter today —

Delivery

Each delivery is a POST with these headers:

Content-Type: application/json
User-Agent: mintbot-webhook/1.0
X-Mintbot-Event: wallet.low
X-Mintbot-Webhook-Id: 17
X-Mintbot-Signature: sha256=<hex HMAC-SHA256 of the raw body>

and this body:

{
  "event": "wallet.low",
  "delivered_at": "2026-09-10T11:05:00.412318+00:00",
  "data": { "balance_cents": 60, "currency": "eur", "runway_hours": 24.0, "stage": "24h", "agents": [8123] }
}
  • Respond with a 2xx within 10 seconds.
  • 3 attempts — immediately, after 5 s, after 30 s. A 4xx answer is treated as permanent and not retried; 5xx and network errors are.
  • After 20 failed deliveries in a row the webhook is switched to disabled; register a new one when your receiver is back.
  • Deliveries are queued durably before they are sent, so a restart on our side never drops one.

Verifying the signature

X-Mintbot-Signature is sha256= + HMAC-SHA256(secret, raw_body) in hex. Always verify the raw request body — re-serialising your parsed JSON will break the comparison.

import hmac, hashlib

def verify(secret: str, body: bytes, header: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")
const crypto = require("crypto");

function verify(secret, rawBody, header) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return expected.length === (header || "").length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}

Errors

Authentication, scope and validation failures use FastAPI's shape — {"detail": "…"} with a message (401, 403, 422). Business errors carry a stable code in detail and a human hint in hint:

{ "detail": "insufficient_funds", "required_cents": 900, "balance_cents": 500,
  "currency": "eur", "topup_url": "https://api.mintbot.ai/", "hint": "top up with POST /v1/wallet/topup or on the developer portal" }
HTTP detail Where Meaning
401 (message) everywhere missing, unknown or revoked X-API-Key
403 (message) everywhere the key lacks the endpoint's scope
409 no_wallet wallet, account, agents order, credit, SSH keys, spend the key has no developer account (for example one from the retired POST /v1/keys) — create a key on the portal or with POST /v1/signup
400 invalid_email POST /v1/signup, POST /account/email not an e-mail address
409 already_bound / email_taken POST /account/email this account already has an address / the address belongs to another account
409 (message) POST /agents/{id}/chat Agent is <status> — chat needs a running agent
502 (message) POST /agents/{id}/chat the agent did not answer within 120 s
402 insufficient_funds POST /agents, POST /agents/{id}/credit the wallet cannot cover it; required_cents, balance_cents, topup_url tell how much
400 invalid_amount POST /wallet/topup, POST /agents/{id}/credit not whole cents or outside the range (5–500 for a top-up, 0.01–500 for credit)
422 (validation error) every POST with a body malformed JSON, a missing field, or a money field that is not a number (true, a non-numeric string) — nothing is charged. NaN / Infinity / absurd magnitudes are the 400 invalid_* of the endpoint
400 invalid_tier / invalid_billing / invalid_location / invalid_language / invalid_credit / invalid_channels POST /agents a field is outside the allowed values — invalid_billing also answers the retired billing: "hourly", invalid_location a data centre we do not offer
400 invalid_period POST /agents billing_period is not 4h / 24h / month, is empty, or the tier is not starter
400 ssh_key_required POST /agents headless order without an SSH key, or an ssh_key_id that is not one of your keys (any order)
400 agent_not_creditable POST /agents/{id}/credit the agent is not running / suspended
400 currency_mismatch POST /agents/{id}/credit the agent keeps its credit in another currency than the EUR wallet (never converted)
404 agent_not_found POST /agents/{id}/credit unknown id or not owned by this key
404 (message) agent routes unknown id or not owned by this key
404 ssh_key_not_found DELETE /ssh-keys/{id} unknown or foreign key id
400 invalid_key / too_many_keys POST /ssh-keys not a valid single-line public key / 10 keys already
409 duplicate_key POST /ssh-keys that fingerprint is already on the account
409 headless_agent POST /agents/{id}/link-telegram the agent has no Telegram channel
410 (message) POST /keys anonymous key minting is closed — use the portal
429 (message) everywhere per-key rate limit (Rate limit: 60 requests/minute exceeded / Daily quota: 5000 requests/day exceeded); slow down
429 signup_rate_limited / signup_blocked / sign_in_link_rate_limited POST /v1/signup see Limits

Legacy crypto orders

POST /orders, POST /orders/boost, POST /orders/renewal, GET /orders/{id} and POST /orders/{id}/check (scope orders) are the older pay-per-order flow: each call returns a Bitcoin or Monero payment address for one order. They keep working for every key, but for a wallet account POST /agents and POST /agents/{id}/credit are simpler — one balance, every payment method, no per-order polling. GET /orders/{id} is still the poll URL an agent order returns — for a wallet-paid order it carries no payment address, currency is EUR, payment_uri is empty and time_left is blank (there is nothing to pay); watch status and the agent stub.


Need help?

If something here is missing, confusing or stale, tell your mintbot agent — it forwards the feedback and we update the page.