I-skip tungo sa nilalaman

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

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, wallet.*, agent.credit_added) is delivered to the hooks of all your active keys.
  • 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.


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
{
  "api_key": "mb_...",
  "key_prefix": "mb_xxxxxxxx",
  "created": true,
  "anonymous": true,
  "email": null,
  "wallet": {"currency": "eur", "balance_cents": 0},
  "next_steps": [
    {"method": "POST", "path": "/v1/account/email"},
    {"method": "POST", "path": "/v1/wallet/topup"},
    {"method": "POST", "path": "/v1/agents"},
    {"method": "GET",  "path": "/v1/agents/{agent_id}"}
  ]
}

The key is returned once — we store only a hash. Send it as X-API-Key on every later call. 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. Sign-in links have their own per-IP limit (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"}'

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"}

What a key does before you pay

Everything that is only a database row: list plans and models, read your wallet and spend, mint a top-up link, order an agent, read your agents, manage SSH keys and webhooks.

An order you have not paid for simply waits: it is created pending, and nothing is deployed — no server, no DNS, no TLS — until the payment lands. That is why the key can be free.

Limits, and what a refusal tells you

Limit Value
keys per IP 3 per 24 h
keys per /24 network 8 per 24 h
open unpaid crypto 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
}

A Retry-After header carries the same number of seconds. signup_blocked (429) replaces the counters with blocked_until; invalid_email is 400 and account_blocked is 409.

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={"email": "bot@example.com"}).json()["api_key"]
h = {"X-API-Key": key}

# 1. fund the wallet — open the returned URL (crypto needs no browser)
pay = httpx.post(f"{api}/wallet/topup", json={"amount_eur": 20}, headers=h).json()
print("pay here:", pay["checkout_url"])

# 2. order an agent; unpaid orders wait, paid ones deploy
order = httpx.post(f"{api}/agents",
                   json={"tier": "starter", "billing_period": "24h"}, headers=h).json()

# 3. poll until it runs, then talk to it
while True:
    agents = httpx.get(f"{api}/agents", headers=h).json()
    if any(a["status"] == "running" for a in agents):
        break
    time.sleep(30)

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: starter, pro or trial. 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 (pro, trial), 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, billed_until, paid_hours, grace_hours and runway_hours on AgentInfo. Runway = wallet balance ÷ the daily blocks of all your running hourly agents × 24 h; 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 advertisedAgentInfo.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 agentsAgentInfo

{
  "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": null,
  "paid_hours": null,
  "grace_hours": null,
  "runway_hours": null,
  "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 five billing_* / *_hours fields below it describe a legacy hourly agent and are null for every prepaid agent, period ones included. An agent that does not belong to your key is 404.

Usage by model

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

{
  "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 otherwise); an unreachable agent is 502. Works for headless agents too.

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

Returns a single-use code (valid 15 minutes) 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. 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-keys201 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. GET /webhooks lists your hooks, GET /webhooks/{id}/deliveries?limit=50 the recent attempts, DELETE /webhooks/{id} removes one.

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 running hourly agent ids (the ladder counts legacy hourly agents only)
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, agents order, credit, SSH keys, spend the key was minted by the retired anonymous endpoint and has no account — create a key on the portal
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; slow down

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.