# API overview

The models, generators and balance you use in the app, from your own code. The API speaks the OpenAI and Anthropic formats, so most tools and SDKs work by changing a base URL and a key.

## What the API is

An HTTP API on `nymbot.ai` that answers the same requests an OpenAI or Anthropic client already sends. You get:

- [Chat Completions](https://nymbot.ai/docs/api-chat/#chat-completions), the [Responses API](https://nymbot.ai/docs/api-chat/#responses) and [Anthropic Messages](https://nymbot.ai/docs/api-chat/#messages), with streaming, tools, pictures, reasoning and web search, for every model in the [catalog](https://nymbot.ai/docs/models/#catalog) and for Nymbot's own auto-routing.
- [Images](https://nymbot.ai/docs/api-media/#images), [video](https://nymbot.ai/docs/api-media/#video), [speech](https://nymbot.ai/docs/api-media/#speech), [transcription](https://nymbot.ai/docs/api-media/#transcription) and [embeddings](https://nymbot.ai/docs/api-media/#embeddings).
- Your [balance](https://nymbot.ai/docs/api-account/#balance), [Lightning top-ups](https://nymbot.ai/docs/api-account/#topup), [automatic top-ups from your wallet](https://nymbot.ai/docs/api-account/#nwc) and a [history](https://nymbot.ai/docs/api-account/#history) of what each request cost.

It is paid from the same two [balances](https://nymbot.ai/docs/credits/#two-balances) as the app, at the same prices. There is no subscription and no free allowance on the API: every request is paid for from credits you bought.

What the API does not do is add anything of Nymbot's own. Your messages go to the model as you sent them: no Nymbot system prompt, no memory, no date or language hints. What comes back is the model's answer and a note of what it cost.

## Base URLs

| Use | Base URL |
| --- | --- |
| OpenAI SDKs and OpenAI-compatible tools | `https://nymbot.ai/api/v1` |
| Anthropic SDKs and [Claude Code](https://nymbot.ai/docs/api-tools/#claude-code) | `https://nymbot.ai/api` (the SDK adds `/v1/messages` itself) |

Every endpoint lives under `/api/v1/`. An unknown path returns `404` and a known path called with the wrong method returns `405`, both as JSON.

The API answers cross-origin requests from any site, so a browser page can call it. Anything you ship to a browser can be read by whoever opens it, though, so only do that with a key that has a small [cap](#keys). The endpoints signed with your nym (keys, the account summary, NWC auto-top-up and refund redemption) are the exception: in a browser they answer only Nymbot's own sites. See [signing account requests](https://nymbot.ai/docs/api-account/#nip98).

Send every JSON body with `Content-Type: application/json`. Any other type is refused with `415`, so a plain HTML form or a `text/plain` request from another site cannot reach the API. With cURL, pass `-H "Content-Type: application/json"` along with `-d`.

## API keys

Keys are made in the app. Open **API** in the sidebar of the web app, or in the menu on Android and iOS, and tap **Create key**. Give it a name and, if you like, a cap and an expiry date.

The key is shown once. Copy it somewhere safe before you close the sheet: Nymbot keeps only a fingerprint of it, so it cannot show it to you again. A lost key cannot be recovered; revoke it and make another.

A key looks like `sk-nymbot-` followed by 43 letters, digits, dashes and underscores. The app lists each key by its name and a short hint such as `sk-nymbot-Qm7x…c2Lw`.

- **A key belongs to your nym.** It spends your balance, and only your nym can make, change or revoke it. Anyone holding the key can spend with it, so treat it like a password.
- **Caps are in sats.** A key can have a spending cap, and the cap can reset every day, every week (Monday) or every month (the first), at 00:00 UTC. It counts both balances, a standard credit as 10 sats and a Pro credit as 100, so it means the same whichever balance a request spends. Before a request runs, the most it could cost (rounded up to whole credits) is set against what is left of the cap. If it does not fit, the request is refused with `403` `key_limit_reached`, even if the answer would have come in under the cap; the error says how much is left and when the cap resets. Lowering `max_tokens` lowers that worst case. A request is charged what it actually cost and that counts against the cap, so if the provider reports more tokens than were set aside, the last request that fit can take the key a little past its cap; the next one is then refused.
- **A spent cap only stops spending.** A key at its cap can still check the balance, read its history, list models, count tokens, top up and check on a video it already started.
- **Expiry is optional.** After the date you set, the key stops working.
- **Revoking is immediate and final.** A revoked key fails its next request. It stays in the list, marked revoked, so its spending history still makes sense.
- You can have up to 25 active keys, each with its own name. Changing a key's reset period starts a new period at zero.

The same sheet shows each key's spending this period and in total, when it was last used, both of your balances, and your recent API requests. The endpoints behind it are documented under [managing keys](https://nymbot.ai/docs/api-account/#key-endpoints).

## Authenticating a request

Send the key in any one of these headers. They are equivalent, so use whichever your client sends by default:

| Header | Sent by |
| --- | --- |
| `Authorization: Bearer sk-nymbot-…` | OpenAI SDKs, most tools, Claude Code with `ANTHROPIC_AUTH_TOKEN` |
| `x-api-key: sk-nymbot-…` | Anthropic SDKs |
| `api-key: sk-nymbot-…` | Azure-style clients |

A missing, unknown, revoked or expired key returns `401`, with the code `missing_api_key`, `invalid_api_key`, `revoked_api_key` or `expired_api_key`. Listing models, audio models and voices, and payment methods needs no key.

Pictures, video, speech, transcription and embeddings can also be paid for one request at a time over Lightning with no key at all: send the request without one and pay the invoice in the `402` answer. See [paying per request without a key](https://nymbot.ai/docs/api-account/#l402).

Key management, the account summary and automatic top-ups are the exception: they take a signature from your nym instead of a key, so a leaked key cannot make more keys. See [signing account requests](https://nymbot.ai/docs/api-account/#nip98).

## Your first request

Put the key in an environment variable, then ask a model something. The examples throughout these pages read it from `NYMBOT_API_KEY`.

cURL

```
export NYMBOT_API_KEY="sk-nymbot-..."

curl https://nymbot.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $NYMBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "nymbot/auto",
    "messages": [{"role": "user", "content": "What is a Lightning invoice?"}]
  }'
```

Python

```
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://nymbot.ai/api/v1",
    api_key=os.environ["NYMBOT_API_KEY"],
)

reply = client.chat.completions.create(
    model="nymbot/auto",
    messages=[{"role": "user", "content": "What is a Lightning invoice?"}],
)
print(reply.choices[0].message.content)
```

JavaScript

```
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://nymbot.ai/api/v1",
  apiKey: process.env.NYMBOT_API_KEY,
});

const reply = await client.chat.completions.create({
  model: "nymbot/auto",
  messages: [{ role: "user", content: "What is a Lightning invoice?" }],
});
console.log(reply.choices[0].message.content);
```

`nymbot/auto` is Nymbot's own routing, paid from the standard balance. Put a catalog model's id there instead, such as `anthropic/claude-sonnet-5`, to use that model from the Pro balance. [Listing models](https://nymbot.ai/docs/api-chat/#models) gives every id.

## What a request costs

The API bills exactly the way the app does.

- **Which balance.** `nymbot/auto` spends the **standard** balance (10 sats a credit). Every other chat model spends the **Pro** balance (100 sats a credit). The standard image generator and the standard voice spend standard credits; every other generator spends Pro. Embeddings spend standard credits. Transcription spends standard credits when the standard balance can cover it, and Pro credits otherwise. The models list says which balance each model spends.
- **How much.** A chat request is metered on the tokens the model actually read and wrote, at the provider's published rates. That price has a 5% fee added and is then multiplied by 1.5, so you pay 1.575 times the provider's list price. It is converted to sats at the live Bitcoin price and charged in thousandths of a credit. Pro pictures, video and speech are priced per generation, per second or per character, and transcription per second of audio, with the same fee and margin. The standard picture is a flat 5 standard credits and the standard voice a flat 3.
- **The minimum.** Every metered request that runs costs at least 0.05 credit: half a sat on the standard balance, 5 sats on Pro. Fractions of a credit are carried over, not rounded up each time.
- **Hold, then settle.** Before a request runs, the most it could cost is held from your balance, based on what you sent and the most tokens it may write. Text outside plain ASCII is sized from its UTF-8 bytes, and ASCII digits and punctuation count as a token each, so text in any script, code and numbers are held for in full. The hold is in whole credits, at least one, so any request needs at least 10 sats free on the standard balance or 100 sats on Pro to start. Only the actual cost is charged; the rest is released when it finishes. A long request keeps its hold for as long as it runs; if the held credits stop being available anyway, a stream stops with a `402` `insufficient_balance` error event, and what was generated is charged. If the actual cost is more than the balance can pay, the whole balance is taken, the rest is owed (`owed_sats` in the `nymbot` object, and a negative balance). What is owed is paid first out of the next credits that reach that balance. Until it is paid, nothing on that balance can be spent: not by the API, replies in the app, a transfer or a gift.
- **Not enough credit.** If the balance cannot cover the hold, the request is refused with `402` before anything runs. The error says which balance is short, how many sats the request needs and how many are free. A smaller `max_tokens` means a smaller hold.
- **Failures.** A request that fails costs nothing, unless the provider billed for the work it did before failing, or a web search or the `nymbot/auto` task check had already run; then that is what you pay, at least 0.05 credit. A stream you cut off is charged for the tokens the provider reports: Nymbot keeps reading the provider's stream for up to 25 seconds after you leave to get that count. If it does not arrive, the charge is estimated from what you sent and what was written, and for a model that reasons it includes the whole output allowance of the hold. A request whose connection drops still finishes and is charged.
- **Web search** costs $0.008 a search, converted to sats, whenever a search ran, whether the model then answers or fails. The pages it reads are also sent to the model as input, so they add their tokens.

Every response says what it cost. JSON responses carry a `nymbot` object with the balance it was paid from, the charge in credits and sats, and what is left:

The cost object

```
"nymbot": {
  "balance": "pro",
  "charged_credits": 0.162,
  "charged_sats": 16.2,
  "balance_credits": 412.425,
  "balance_sats": 41242.5
}
```

Paid responses also carry these headers, which is where to look for the cost of a response that is not JSON, such as speech:

| Header | Meaning |
| --- | --- |
| `X-Nymbot-Cost-Sats` | What this request cost, in sats. |
| `X-Nymbot-Balance-Sats` | What is left on the balance it was paid from, in sats. |
| `X-Request-Id` | An id for the request, on every response. Quote it if you contact support. |

Rates per million tokens, already including the fee and margin, are in the [models list](https://nymbot.ai/docs/api-chat/#models) in dollars and sats, and on [the price sheet](https://nymbot.ai/docs/credits/#every-model). If the Bitcoin price cannot be read, paid requests return `503` `price_unavailable` with `Retry-After: 60` rather than guessing.

## Errors

Every error has the same shape, the one OpenAI clients already understand. `code` is a stable name you can match on; `message` is for people and may change.

Error body

```
{
  "error": {
    "message": "This request needs up to 200 sats (2 credits) on your Pro balance, and 40 sats are free. Catalog models spend the Pro balance. Top up in the Nymbot app or with POST /api/v1/topup/create/btc-lightning.",
    "type": "insufficient_quota",
    "code": "insufficient_balance",
    "param": null,
    "balance": "pro",
    "required_sats": 200,
    "balance_sats": 40
  }
}
```

| Status | When |
| --- | --- |
| `400` `invalid_request_error` | The body is not valid JSON (`invalid_json`), a required field is missing (`missing_required_parameter`), a value is wrong, or the request asks for something the model or endpoint does not do, such as tools on `nymbot/auto` (`unsupported_tool`). `param` names the field. Also `upstream_rejected` when the provider refused the request. |
| `401` `authentication_error` | The key is missing, unknown, revoked or expired, or a signed request is invalid or reused. |
| `402` `insufficient_quota` | The balance cannot cover the request. Code `insufficient_balance`, with `balance`, `required_sats` and `balance_sats`. |
| `403` `permission_error` | The request does not fit the key's cap (`key_limit_reached`, with `limit_sats`, `used_sats` and `reset_at`), or the account may not use the service (`account_denied`). |
| `404` `not_found_error` | An unknown path (`unknown_endpoint`), a model that does not exist (`model_not_found`), or an unknown key, invoice or video. |
| `405` | The path exists but not with that method (`method_not_allowed`). The `Allow` header lists the methods it takes. |
| `413` | The body or file is too large (`payload_too_large`, `file_too_large`), or a recording is too long (`audio_too_long`). See [limits](#limits). |
| `415` `invalid_request_error` | The body is not sent as `application/json` (or, for the upload endpoints, `multipart/form-data`): `unsupported_media_type`. |
| `422` | A voice and language that do not go together in text to speech (`voice_language_mismatch`, `unsupported_language`). |
| `429` `rate_limit_error` | Too many requests on this key, from this address, or with credentials that failed to verify (`rate_limit_exceeded`), or the provider is rate-limiting (`upstream_rate_limited`). Wait for the seconds in `Retry-After`. |
| `500` `api_error` | Something went wrong on Nymbot's side (`internal_error`). |
| `502` `api_error` | The provider did not return an answer (`upstream_error`), or a Lightning invoice could not be made (`invoice_unavailable`). |
| `503` `api_error` | The provider is overloaded (`upstream_overloaded`), the Bitcoin price cannot be read (`price_unavailable`), or a part of the service is down (`service_unavailable`, `media_hosting_unavailable`). `Retry-After` says when to try again, where it is known. |

Two exceptions:

- `/api/v1/messages` answers in Anthropic's error format, since that is what Anthropic clients parse: `{"type": "error", "error": {"type": "authentication_error", "message": "…"}}`. The type follows the status: `invalid_request_error`, `authentication_error`, `billing_error` (402), `permission_error`, `not_found_error`, `request_too_large`, `rate_limit_error`, `api_error` or `overloaded_error` (503).
- A streaming request that fails before its first byte gets an ordinary JSON error with the status above, not an event stream. A failure after the stream has started is sent as the stream's last event, in that endpoint's own format.

Error messages never contain another service's internal details. A provider's own error is reworded before it reaches you.

## Limits

| Limit | Value |
| --- | --- |
| Requests per key | 120 a minute. Above that, `429` with `Retry-After`. |
| Requests without a key | 120 a minute per address, for the model, audio and payment method listings, refund token checks, and paid endpoints called without a key or payment. Above that, `429` with `Retry-After`. |
| Failed authentication | 30 a minute per address for keys, signatures, payment credentials and refund tokens that do not verify. Above that, every request from the address that carries a credential gets `429` with `Retry-After` until the minute is over. Credentials are checked before the body is read. |
| New keys | 60 an hour per nym and 120 an hour per address. |
| Top-up invoices | 60 an hour per nym and 120 an hour per address. |
| NWC wallet connections | 10 an hour per nym and 30 an hour per address. The wallet relay has to use `wss://` on the standard port. |
| Refund tokens | 60 requests a minute per token. |
| Addresses | An IPv6 address counts as its whole /64 in every per-address limit, and an IPv4-mapped IPv6 address as its IPv4 address. |
| JSON request body | 4 MB; 64 KB for requests signed with your nym. |
| Multipart request body (uploads) | 32 MB. A picture to edit can be up to 20 MB, an audio file up to 25 MB. At most 64 parts, each with at most 8 KB of part headers, and a boundary of 1 to 70 characters; otherwise `400` `invalid_multipart`. |
| Pictures in one chat request | 20. Each is an `https://` or `http://` link to a public host, or a `data:image/…` URL. |
| Images per generation request | 1 to 4 (`n`) |
| Output tokens | Capped at the model's own maximum. A larger `max_tokens` is lowered to it, not refused. |
| Speech input | 800 characters for the standard voice, 2,000 for Aura 2. |
| Transcription | 30 minutes of audio, 25 MB. A longer recording is refused with `413` and not charged, even when its length is only known once Whisper has heard it. |
| Embeddings | 100 inputs per request. |
| Video jobs | Kept for 24 hours after they are submitted; a render is given up after an hour. At most 10 rendering at once per nym. |
| Query history | Kept for 90 days. |
| Active keys per nym | 25. Only the newest 50 revoked keys are kept. |

A link to a picture must point to a public host: an address on a private or local network, or on Nymbot's own sites, is refused. Provider pacing that applies in the app applies here too, so a burst of requests to one provider can be slowed down rather than failed.

## What the API can see

The API is not private in the way the apps are, and it is worth being exact about how.

- **It is not end-to-end encrypted.** In the apps, a message is sealed on your device to keys only Nymbot holds and travels as a [gift wrap](https://nymbot.ai/docs/encryption/#gift-wraps). An API request is ordinary HTTPS: it is encrypted on the way to Nymbot, and Nymbot's server reads it in the clear to handle it.
- **Prompts and replies are not stored.** They pass through to the model and the answer passes back. What is kept is the bill: for each request the time, model, kind, token counts, cost, balance, key and whether it succeeded, for 90 days, which is what [query history](https://nymbot.ai/docs/api-account/#history) shows you. A usage record of the same request (time, kind, model, token counts, cost, duration and whether it used web search or succeeded) is also kept for 90 days, alongside the app's own.
- **Everything else held for a nym is small and listed here.** [API keys](https://nymbot.ai/docs/api-account/#key-endpoints) are stored as a hash, never the key, with their name, a short hint, spending cap, reset period, expiry and when they were made and last used; only the newest 50 revoked keys are kept. An [automatic top-up](https://nymbot.ai/docs/api-account/#nwc) wallet connection is stored encrypted, with its threshold, amount and the result of the last top-up. Video jobs are kept for 24 hours. Requests paid per call leave only a Lightning payment hash for 7 days and a hashed refund token for 30 days, linked to no nym. Charges a balance could not cover are kept as owed until a top-up pays them.
- **Wiping the app deletes it.** A [device wipe](https://nymbot.ai/docs/identity/#panic) revokes and deletes every API key, and deletes the query history, usage records, wallet connection and video jobs. The balance and any charges still owed remain on the nym.
- **The model's provider sees your request**, as it does from the app. Catalog models run at their makers; standard routes and embeddings run on Cloudflare.
- **Generated media is public.** Pictures and video delivered as links are uploaded to public Blossom file hosts, where a file's address is its hash. Anyone with the link can open it, and Nymbot cannot take it down again. Ask for `b64_json` and a generated picture comes back in the response and is never uploaded.
- **So are the pictures you give a generator.** A picture you upload to [edit](https://nymbot.ai/docs/api-media/#image-edits), or send as a `data:` URL in a generator's `image_url`, is uploaded to a public Blossom host first so the generator can fetch it, and the same applies to it. A picture sent by `https://` link is passed on as that link. Pictures in a chat request go to the model's provider, not to Blossom.
- **A key is linked to your nym.** Everything a key spends comes off your nym's balance, so API use is not [anonymous](https://nymbot.ai/docs/anonymous/). If you want API use kept apart from your everyday nym, make the keys from a separate nym with its own balance.

If you need the protection of the apps, use the apps. The API is for when you need the models in your own tools.

## Every endpoint

| Endpoint | What it does | Auth |
| --- | --- | --- |
| `GET /api/v1/models` | [Lists models](https://nymbot.ai/docs/api-chat/#models), with prices | None |
| `POST /api/v1/chat/completions` | [Chat Completions](https://nymbot.ai/docs/api-chat/#chat-completions) | Key |
| `POST /api/v1/responses` | [Responses API](https://nymbot.ai/docs/api-chat/#responses) | Key |
| `POST /api/v1/messages` | [Anthropic Messages](https://nymbot.ai/docs/api-chat/#messages) | Key |
| `POST /api/v1/messages/count_tokens` | [Estimates input tokens](https://nymbot.ai/docs/api-chat/#count-tokens) | Key |
| `POST /api/v1/images/generations` | [Generates images](https://nymbot.ai/docs/api-media/#images) | Key or [Lightning](https://nymbot.ai/docs/api-account/#l402) |
| `POST /api/v1/images/edits` | [Edits an image](https://nymbot.ai/docs/api-media/#image-edits) | Key or [Lightning](https://nymbot.ai/docs/api-account/#l402) |
| `POST /api/v1/videos`, `GET /api/v1/videos`, `GET /api/v1/videos/{id}` | [Starts, lists and checks videos](https://nymbot.ai/docs/api-media/#video) | Key, or [Lightning](https://nymbot.ai/docs/api-account/#l402) to start one |
| `POST /api/v1/audio/speech` | [Text to speech](https://nymbot.ai/docs/api-media/#speech) | Key or [Lightning](https://nymbot.ai/docs/api-account/#l402) |
| `GET /api/v1/audio/models`, `GET /api/v1/audio/voices` | [Audio models and voices](https://nymbot.ai/docs/api-media/#voices) | None |
| `POST /api/v1/audio/transcriptions`, `POST /api/v1/audio/translations` | [Speech to text, and to English](https://nymbot.ai/docs/api-media/#transcription) | Key or [Lightning](https://nymbot.ai/docs/api-account/#l402) |
| `POST /api/v1/embeddings` | [Embeddings](https://nymbot.ai/docs/api-media/#embeddings) | Key or [Lightning](https://nymbot.ai/docs/api-account/#l402) |
| `GET /api/v1/credits/balance` (or `POST`) | [Both balances](https://nymbot.ai/docs/api-account/#balance) | Key |
| `GET /api/v1/topup/payment-methods` | [Ways to pay](https://nymbot.ai/docs/api-account/#payment-methods) | None |
| `POST /api/v1/topup/create/btc-lightning` | [A Lightning invoice](https://nymbot.ai/docs/api-account/#topup) | Key |
| `GET /api/v1/topup/status/{invoice_id}` | [Checks and credits it](https://nymbot.ai/docs/api-account/#topup-status) | Key |
| `GET /api/v1/queries/history` | [What each request cost](https://nymbot.ai/docs/api-account/#history) | Key or nym |
| `GET /api/v1/account` | [Account summary](https://nymbot.ai/docs/api-account/#account) | Nym |
| `/api/v1/keys` | [Makes, changes and revokes keys](https://nymbot.ai/docs/api-account/#key-endpoints) | Nym |
| `/api/v1/nwc-auto-topup` | [Automatic top-ups](https://nymbot.ai/docs/api-account/#nwc) | Nym |
| `GET /api/v1/l402/refunds`, `POST /api/v1/l402/refunds/redeem` | [Checks or redeems a refund token](https://nymbot.ai/docs/api-account/#l402) | Refund token; nym to redeem |

“Nym” means a request signed by your Nostr key, described in [signing account requests](https://nymbot.ai/docs/api-account/#nip98). For tools that already speak these formats, see [tools and SDKs](https://nymbot.ai/docs/api-integrations/), and for coding agents such as Claude Code, Codex and Cline, see [coding tools](https://nymbot.ai/docs/api-tools/).
