Skip to the content
Back to Nymbot

Knowledge base Developers

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:

It is paid from the same 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

UseBase URL
OpenAI SDKs and OpenAI-compatible toolshttps://nymbot.ai/api/v1
Anthropic SDKs and Claude Codehttps://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. 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.

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.

Authenticating a request

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

HeaderSent 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.

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.

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 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:

HeaderMeaning
X-Nymbot-Cost-SatsWhat this request cost, in sats.
X-Nymbot-Balance-SatsWhat is left on the balance it was paid from, in sats.
X-Request-IdAn 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 in dollars and sats, and on the price sheet. 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
  }
}
StatusWhen
400 invalid_request_errorThe 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_errorThe key is missing, unknown, revoked or expired, or a signed request is invalid or reused.
402 insufficient_quotaThe balance cannot cover the request. Code insufficient_balance, with balance, required_sats and balance_sats.
403 permission_errorThe 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_errorAn unknown path (unknown_endpoint), a model that does not exist (model_not_found), or an unknown key, invoice or video.
405The path exists but not with that method (method_not_allowed). The Allow header lists the methods it takes.
413The body or file is too large (payload_too_large, file_too_large), or a recording is too long (audio_too_long). See limits.
415 invalid_request_errorThe body is not sent as application/json (or, for the upload endpoints, multipart/form-data): unsupported_media_type.
422A voice and language that do not go together in text to speech (voice_language_mismatch, unsupported_language).
429 rate_limit_errorToo 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_errorSomething went wrong on Nymbot's side (internal_error).
502 api_errorThe provider did not return an answer (upstream_error), or a Lightning invoice could not be made (invoice_unavailable).
503 api_errorThe 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

LimitValue
Requests per key120 a minute. Above that, 429 with Retry-After.
Requests without a key120 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 authentication30 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 keys60 an hour per nym and 120 an hour per address.
Top-up invoices60 an hour per nym and 120 an hour per address.
NWC wallet connections10 an hour per nym and 30 an hour per address. The wallet relay has to use wss:// on the standard port.
Refund tokens60 requests a minute per token.
AddressesAn IPv6 address counts as its whole /64 in every per-address limit, and an IPv4-mapped IPv6 address as its IPv4 address.
JSON request body4 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 request20. Each is an https:// or http:// link to a public host, or a data:image/… URL.
Images per generation request1 to 4 (n)
Output tokensCapped at the model's own maximum. A larger max_tokens is lowered to it, not refused.
Speech input800 characters for the standard voice, 2,000 for Aura 2.
Transcription30 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.
Embeddings100 inputs per request.
Video jobsKept for 24 hours after they are submitted; a render is given up after an hour. At most 10 rendering at once per nym.
Query historyKept for 90 days.
Active keys per nym25. 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. 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 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 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 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 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, 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. 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

EndpointWhat it doesAuth
GET /api/v1/modelsLists models, with pricesNone
POST /api/v1/chat/completionsChat CompletionsKey
POST /api/v1/responsesResponses APIKey
POST /api/v1/messagesAnthropic MessagesKey
POST /api/v1/messages/count_tokensEstimates input tokensKey
POST /api/v1/images/generationsGenerates imagesKey or Lightning
POST /api/v1/images/editsEdits an imageKey or Lightning
POST /api/v1/videos, GET /api/v1/videos, GET /api/v1/videos/{id}Starts, lists and checks videosKey, or Lightning to start one
POST /api/v1/audio/speechText to speechKey or Lightning
GET /api/v1/audio/models, GET /api/v1/audio/voicesAudio models and voicesNone
POST /api/v1/audio/transcriptions, POST /api/v1/audio/translationsSpeech to text, and to EnglishKey or Lightning
POST /api/v1/embeddingsEmbeddingsKey or Lightning
GET /api/v1/credits/balance (or POST)Both balancesKey
GET /api/v1/topup/payment-methodsWays to payNone
POST /api/v1/topup/create/btc-lightningA Lightning invoiceKey
GET /api/v1/topup/status/{invoice_id}Checks and credits itKey
GET /api/v1/queries/historyWhat each request costKey or nym
GET /api/v1/accountAccount summaryNym
/api/v1/keysMakes, changes and revokes keysNym
/api/v1/nwc-auto-topupAutomatic top-upsNym
GET /api/v1/l402/refunds, POST /api/v1/l402/refunds/redeemChecks or redeems a refund tokenRefund token; nym to redeem

“Nym” means a request signed by your Nostr key, described in signing account requests. For tools that already speak these formats, see tools and SDKs, and for coding agents such as Claude Code, Codex and Cline, see coding tools.