# Images, audio and embeddings

The generators from the app, and the building blocks for search: pictures, video, speech, transcription and embeddings, each billed per use from your balance.

## Generating images

Makes one to four pictures from a prompt and returns them when they are done. It is the same set of generators as `?image` in the app.

`POST https://nymbot.ai/api/v1/images/generations` — needs an API key, or [a Lightning payment per request](https://nymbot.ai/docs/api-account/#l402).

Ids come from `GET /api/v1/models?type=image`. The standard generator, `@cf/black-forest-labs/flux-1-schnell`, costs a flat 5 standard credits (50 sats) a picture. Every other generator spends Pro credits at its own price, with the usual fee and margin, as in [the app](https://nymbot.ai/docs/media/#generators). `n` pictures cost `n` times as much. A field a generator does not take is dropped rather than refused; the standard generator uses the prompt only.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | Yes | A generator id, such as `nano-banana`. |
| `prompt` | string | Yes | What to draw. Only the first 2,000 characters are used. |
| `n` | integer | No | 1 to 4. Default 1. Also accepted as `num_images`. |
| `size` | string | No | An aspect ratio such as `"16:9"`, or an OpenAI size such as `"1792x1024"`, which becomes the nearest of 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 21:9, 5:4 and 4:5. `"auto"` leaves it to the generator. |
| `aspect_ratio` | string | No | The same as `size`, and wins if both are sent. |
| `quality` | string | No | Generator-specific, such as `"low"`, `"medium"` or `"high"`. |
| `negative_prompt` | string | No | What to leave out. |
| `output_format` | string | No | `"png"`, `"jpeg"` or `"webp"`. |
| `response_format` | string | No | `"url"` (the default) or `"b64_json"`. |
| `image_url` | string | No | A picture to start from: an `https://` link of at most 4,096 characters with no user name or password in it, or a `data:image/…;base64` URL (PNG, JPEG, WebP or GIF; not SVG). Only for generators with `edit`; required by those with `requires_image_url`. |

`resolution` is not used: each generator makes its own size. With `url`, each picture is uploaded to a public Blossom host and you get its link (a few generators only hand back a link of their own, which is passed on). Anyone with the link can open it and it cannot be taken down again; see [what the API can see](https://nymbot.ai/docs/api/#privacy). With `b64_json` the picture comes back in the response and is never uploaded. A picture sent as a `data:` URL in `image_url` is checked the same way as an [edit](#image-edits) and uploaded to Blossom first so the generator can fetch it; it is deleted again if the request fails unbilled.

If some of the `n` pictures fail, you get the ones that worked, are charged only for those (plus any the provider billed despite failing), and `nymbot.failed` says how many are missing. If all fail, the request returns `502`.

Response

```
{
  "created": 1790726400,
  "model": "nano-banana",
  "cost": 0.2126,
  "data": [
    {
      "url": "https://blossom.example/5c0e4b7d3a91f2e8...c4.png",
      "content_type": "image/png"
    }
  ],
  "nymbot": { "balance": "pro", "charged_credits": 1.817, "charged_sats": 181.7, "balance_credits": 410.608, "balance_sats": 41060.8 }
}
```

| Status | When |
| --- | --- |
| `400` | No prompt; `n` outside 1 to 4 (`invalid_value`); a bad size (`invalid_size`) or picture (`invalid_image`, `invalid_image_url`); a model that exists but is not an image generator (`invalid_model`); a picture for a generator that cannot edit (`edit_unsupported`); no picture for one that needs it (`image_required`). |
| `402` | The balance cannot cover every picture asked for. |
| `404` | No model by that name (`model_not_found`). |
| `502` | The generator failed (`upstream_error`). Nothing is charged unless the provider billed for the attempt; then the error has `charged_sats`. |
| `503` | Pictures cannot be hosted right now (`media_hosting_unavailable`). `b64_json` still works. |

cURL

```
curl https://nymbot.ai/api/v1/images/generations \
  -H "Authorization: Bearer $NYMBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "nano-banana",
    "prompt": "A lighthouse at dusk, oil painting",
    "size": "16:9"
  }'
```

Python

```
import os
import requests

res = requests.post(
    "https://nymbot.ai/api/v1/images/generations",
    headers={"Authorization": "Bearer " + os.environ["NYMBOT_API_KEY"]},
    json={"model": "nano-banana", "prompt": "A lighthouse at dusk, oil painting", "size": "16:9"},
)
res.raise_for_status()
print(res.json()["data"][0]["url"])
```

JavaScript

```
const res = await fetch("https://nymbot.ai/api/v1/images/generations", {
  method: "POST",
  headers: {
    "Authorization": "Bearer " + process.env.NYMBOT_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ model: "nano-banana", prompt: "A lighthouse at dusk, oil painting", size: "16:9" }),
});
const body = await res.json();
console.log(body.data[0].url);
```

## Editing images

Changes a picture you upload, following a prompt. This is OpenAI's edit endpoint, so the OpenAI SDKs' `images.edit` works as it is. Only Pro generators with `edit` in the model list accept it.

`POST https://nymbot.ai/api/v1/images/edits` — needs an API key, or [a Lightning payment per request](https://nymbot.ai/docs/api-account/#l402). The body is `multipart/form-data`, up to 32 MB.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `image` | file | Yes | The picture to edit: PNG, JPEG, WebP or GIF, up to 20 MB. Also accepted as `image[]`; the first one is used. |
| `prompt` | string | Yes | What to change. |
| `model` | string | Yes | A generator with `edit`. |
| `n` | integer | No | 1 to 4. |
| `size` `aspect_ratio` `quality` `negative_prompt` `output_format` | string | No | As for generation. |
| `response_format` | string | No | `"b64_json"` (the default here) or `"url"`. |

The picture you upload is checked to be a well-formed PNG, JPEG, WebP or GIF, sent on as its real type, and put on a public Blossom host, because the generators fetch their input by link. Treat it as public. If the edit fails and nothing is charged, the hosted copy is deleted again. The response has the same shape as for generation. Pricing is the generator's price, which for some generators includes a charge for the input picture.

Response

```
{
  "created": 1790726460,
  "model": "nano-banana",
  "cost": 0.2126,
  "data": [{ "b64_json": "iVBORw0KGgoAAAANSUhEUgAA..." }],
  "nymbot": { "balance": "pro", "charged_credits": 1.817, "charged_sats": 181.7, "balance_credits": 408.791, "balance_sats": 40879.1 }
}
```

| Status | When |
| --- | --- |
| `400` | No image or prompt, a file that is not a picture (`invalid_image`), or a generator that cannot edit (`edit_unsupported`). |
| `413` | The picture is over 20 MB (`file_too_large`), or the whole request over 32 MB. |
| `503` | The picture cannot be hosted right now (`media_hosting_unavailable`). |

cURL

```
curl https://nymbot.ai/api/v1/images/edits \
  -H "Authorization: Bearer $NYMBOT_API_KEY" \
  -F model=nano-banana \
  -F image=@photo.png \
  -F prompt="Make it a snowy evening"
```

Python

```
import base64, os
from openai import OpenAI

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

with open("photo.png", "rb") as f:
    result = client.images.edit(model="nano-banana", image=f, prompt="Make it a snowy evening")

with open("edited.png", "wb") as out:
    out.write(base64.b64decode(result.data[0].b64_json))
```

JavaScript

```
import fs from "node:fs";
import { writeFile } from "node:fs/promises";
import OpenAI from "openai";

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

const result = await client.images.edit({
  model: "nano-banana",
  image: fs.createReadStream("photo.png"),
  prompt: "Make it a snowy evening",
});
await writeFile("edited.png", Buffer.from(result.data[0].b64_json, "base64"));
```

## Video

A video takes minutes, so it runs in the background. You submit it, get an id straight away, and ask about the id until it is done. Every video generator spends the Pro balance.

`POST https://nymbot.ai/api/v1/videos` — needs an API key, or [a Lightning payment per request](https://nymbot.ai/docs/api-account/#l402). Returns `202`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | Yes | A video generator id from `GET /api/v1/models?type=video`, such as `veo`. |
| `prompt` | string | Yes | What should happen. |
| `duration` | number or string | No | Length in seconds, more than 0 and up to 60. Used only by generators that let you choose the length; the others make their usual length, which is what `seconds` in the response and the price follow. |
| `aspect_ratio` | string | No | Such as `"16:9"` or `"9:16"`; a size such as `"1280x720"` becomes the nearest ratio. |
| `resolution` `quality` | string | No | One of the generator's `resolutions` in the model list, such as `"720p"` or `"1080p"`. It sets the price. Anything else is refused with `unsupported_resolution`. |
| `image_url` | string | No | A picture to start from: an `https://` link or a `data:image/…` URL, which is uploaded to a public Blossom host first. Required by generators with `requires_image_url`. |

The price is fixed when you submit, from the generator, resolution and length, so it is charged from your Pro balance as soon as the provider accepts the job, and shown as `cost`, `estimated_cost` and `nymbot`. If the provider refuses the job, nothing is charged. If the render later fails, or the finished clip cannot be stored and the provider did not bill for it, the charge is refunded once, automatically. A render that is still going after an hour is given up, and keeps its charge, because the provider bills a render once it accepts it. A refund returns the credits but does not lower the key's spend count.

Response (202)

```
{
  "id": "vid_3e9a51c07b2f4d86e0a1c4f2",
  "object": "video",
  "model": "veo",
  "status": "in_progress",
  "created": 1790726400,
  "seconds": 8,
  "resolution": "1080p",
  "estimated_cost": 3.18,
  "expires_at": 1790812800,
  "cost": 3.18,
  "nymbot": { "balance": "pro", "charged_credits": 31.8, "charged_sats": 3180, "balance_credits": 378.808, "balance_sats": 37880.8 }
}
```

`status` starts as `in_progress`, or `completed` with the video already in `data` when the generator returned it straight away.

### Checking on a video

`GET https://nymbot.ai/api/v1/videos/{id}` — needs a key belonging to the nym that submitted it. A key that has reached its cap can still check.

It answers `202` while the video is being made and `200` once it is finished or has failed. Ask every 5 to 10 seconds. Each check asks the provider once and answers at once; it never waits for the video. A job is kept for 24 hours after it was submitted. `GET /api/v1/videos` lists the key's last 50 jobs as `{"object": "list", "data": […]}`.

Response (200)

```
{
  "id": "vid_3e9a51c07b2f4d86e0a1c4f2",
  "object": "video",
  "model": "veo",
  "status": "completed",
  "created": 1790726400,
  "seconds": 8,
  "resolution": "1080p",
  "estimated_cost": 3.18,
  "expires_at": 1790812800,
  "completed_at": 1790726590,
  "data": { "url": "https://blossom.example/9b41d0c2e7a3f658...1e.mp4", "content_type": "video/mp4" },
  "cost": 3.18,
  "nymbot": { "balance": "pro", "charged_credits": 31.8, "charged_sats": 3180 }
}
```

`status` is `in_progress`, `completed` or `failed`. A failed job carries `error` (`{"code": "generation_failed", "message": …}`); if it was refunded, `cost` is 0 and `nymbot` adds `refunded_credits` and `refunded_sats`. The finished video is on a public Blossom host, like generated pictures.

| Status | When |
| --- | --- |
| `400` | No prompt; a duration outside 0 to 60 (`invalid_value`); a resolution the generator does not offer (`unsupported_resolution`); no picture for a generator that needs one (`image_required`); a model that is not a video generator (`invalid_model`). |
| `402` | The Pro balance cannot cover the video. |
| `404` | No such generator (`model_not_found`), or no job by that id for your nym, or it is more than 24 hours old (`video_not_found`). |
| `429` | Your nym already has 10 videos rendering (`video_jobs_limit`). Check on them and submit more once one finishes. |
| `502` | The generator refused the job. Nothing is charged unless the provider billed for it. |
| `503` | Videos cannot be delivered or stored right now (`media_hosting_unavailable`, `service_unavailable`). |

cURL

```
curl https://nymbot.ai/api/v1/videos \
  -H "Authorization: Bearer $NYMBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "veo", "prompt": "Waves rolling onto a beach at sunrise", "duration": 8, "aspect_ratio": "16:9"}'

curl https://nymbot.ai/api/v1/videos/vid_3e9a51c07b2f4d86 \
  -H "Authorization: Bearer $NYMBOT_API_KEY"
```

Python

```
import os, time
import requests

headers = {"Authorization": "Bearer " + os.environ["NYMBOT_API_KEY"]}

job = requests.post(
    "https://nymbot.ai/api/v1/videos",
    headers=headers,
    json={"model": "veo", "prompt": "Waves rolling onto a beach at sunrise", "duration": 8, "aspect_ratio": "16:9"},
).json()

while True:
    time.sleep(8)
    status = requests.get("https://nymbot.ai/api/v1/videos/" + job["id"], headers=headers).json()
    if status["status"] in ("completed", "failed"):
        break

print(status["data"]["url"] if status["status"] == "completed" else status["error"])
```

JavaScript

```
const headers = {
  "Authorization": "Bearer " + process.env.NYMBOT_API_KEY,
  "Content-Type": "application/json",
};

const job = await (await fetch("https://nymbot.ai/api/v1/videos", {
  method: "POST",
  headers,
  body: JSON.stringify({ model: "veo", prompt: "Waves rolling onto a beach at sunrise", duration: 8, aspect_ratio: "16:9" }),
})).json();

let status;
do {
  await new Promise((r) => setTimeout(r, 8000));
  status = await (await fetch("https://nymbot.ai/api/v1/videos/" + job.id, { headers })).json();
} while (status.status !== "completed" && status.status !== "failed");

console.log(status.status === "completed" ? status.data.url : status.error);
```

## Text to speech

Reads text aloud and returns the audio file itself, not a link. It is OpenAI's speech endpoint, so `audio.speech.create` in the OpenAI SDKs works.

`POST https://nymbot.ai/api/v1/audio/speech` — needs an API key, or [a Lightning payment per request](https://nymbot.ai/docs/api-account/#l402).

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | No | A speech model from [the audio models list](#voices). Default: the standard voice, `@cf/myshell-ai/melotts`. `aura-2` is Deepgram Aura 2. |
| `input` | string | Yes | The text to read: up to 800 characters for the standard voice, 2,000 for Aura 2. |
| `voice` | string | No | A voice id from [the voices list](#voices): an Aura speaker such as `luna` (or `aura-2-luna-en`), or a MeloTTS language (`en`, `es`, `fr`, `zh`). OpenAI voice names such as `alloy` are accepted and give the model's default voice. |
| `language` | string | No | A language code such as `es`. Without a voice, it picks one for that language. A voice that speaks another language returns `422` `voice_language_mismatch`, and a language the model has no voice for `422` `unsupported_language`. |
| `response_format` | string | No | The standard voice returns `mp3` only. Aura 2 returns `mp3` (the default), `opus`, `aac`, `flac`, `wav` or `pcm`. |
| `speed` | number | No | Accepted and ignored. |

The standard voice costs a flat 3 standard credits (30 sats) a request, as in the app. Aura 2 is Pro, priced by the characters you send. The response body is the audio, with a matching `Content-Type`; the cost is in the `X-Nymbot-Cost-Sats` header. If the speech model fails, nothing is charged.

Response

```
HTTP/1.1 200 OK
Content-Type: audio/mpeg
X-Nymbot-Cost-Sats: 4.2
X-Nymbot-Balance-Sats: 41055.6

(audio bytes)
```

| Status | When |
| --- | --- |
| `400` | No input; input over the model's limit (`input_too_long`); a format the model cannot produce (`unsupported_response_format`); an unknown voice (`invalid_voice`); a model that is not a speech model (`invalid_model`). |
| `422` | The voice does not speak the language given, or the model has no voice for it. |
| `502` | The speech model failed. Nothing is charged. |

cURL

```
curl https://nymbot.ai/api/v1/audio/speech \
  -H "Authorization: Bearer $NYMBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "aura-2", "voice": "luna", "input": "Your invoice has been paid."}' \
  --output speech.mp3
```

Python

```
import os
from openai import OpenAI

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

audio = client.audio.speech.create(
    model="aura-2",
    voice="luna",
    input="Your invoice has been paid.",
)
audio.write_to_file("speech.mp3")
```

JavaScript

```
import { writeFile } from "node:fs/promises";
import OpenAI from "openai";

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

const audio = await client.audio.speech.create({
  model: "aura-2",
  voice: "luna",
  input: "Your invoice has been paid.",
});
await writeFile("speech.mp3", Buffer.from(await audio.arrayBuffer()));
```

## Audio models and voices

Two public lists for picking a speech model and voice, or a transcription model. There are 41 Aura 2 English speakers and four MeloTTS languages.

`GET https://nymbot.ai/api/v1/audio/models` — no key needed.

`GET https://nymbot.ai/api/v1/audio/voices?language=en` — no key needed.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `language` | string (query) | No | Only voices for this language. |
| `model` | string (query) | No | Only voices for this model. |

Response (voices)

```
{
  "object": "list",
  "data": [
    { "id": "luna", "name": "Luna", "provider": "deepgram", "model_id": "aura-2", "models": ["aura-2"], "language": "en", "description": "Aura 2 English voice", "gender": "female", "preview_url": null },
    { "id": "en", "name": "MeloTTS English", "provider": "myshell", "model_id": "@cf/myshell-ai/melotts", "models": ["@cf/myshell-ai/melotts", "melotts"], "language": "en", "description": "English (MeloTTS)", "gender": null, "preview_url": null }
  ]
}
```

The models list answers `{"object": "list", "data": {"tts": […], "stt": […]}}`. Each speech model gives its balance, price, `max_input_chars`, `response_formats`, `languages` and `default_voice`; the transcription model gives its aliases, `max_file_bytes`, `max_seconds`, formats and price per minute.

cURL

```
curl https://nymbot.ai/api/v1/audio/models
curl "https://nymbot.ai/api/v1/audio/voices?language=en"
```

Python

```
import requests

print(requests.get("https://nymbot.ai/api/v1/audio/models").json())
for voice in requests.get("https://nymbot.ai/api/v1/audio/voices", params={"language": "en"}).json()["data"]:
    print(voice["id"], voice["model_id"])
```

JavaScript

```
console.log(await (await fetch("https://nymbot.ai/api/v1/audio/models")).json());
const voices = await (await fetch("https://nymbot.ai/api/v1/audio/voices?language=en")).json();
for (const v of voices.data) console.log(v.id, v.model_id);
```

## Speech to text

Turns a recording into text with Whisper. This is OpenAI's transcription endpoint, so `audio.transcriptions.create` works.

`POST https://nymbot.ai/api/v1/audio/transcriptions` — needs an API key, or [a Lightning payment per request](https://nymbot.ai/docs/api-account/#l402). The body is `multipart/form-data`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `file` | file | Yes | The recording, from 256 bytes to 25 MB and up to 30 minutes: WAV, WebM, MP4 or M4A, Ogg or Opus, MP3 and the other formats Whisper reads. MP3 and other formats whose length cannot be read up front are limited to 1.8 MB. |
| `model` | string | No | Default `whisper`, which is Whisper Large v3 Turbo. `whisper-1`, `whisper-large-v3-turbo` and `@cf/openai/whisper-large-v3-turbo` mean the same. |
| `language` | string | No | The spoken language, such as `en`. Detected if left out. |
| `prompt` | string | No | Words or spellings to expect, such as names. Only the first 1,000 characters are used. |
| `response_format` | string | No | `json` (the default), `text`, `srt`, `vtt` or `verbose_json`. |

For WAV, WebM, MP4 and Ogg files the length is read from the file before anything runs, so a recording over 30 minutes is refused up front (with 3 seconds' grace). A WAV file's length comes from its sample rate, channels and sample size. The length of other formats, such as MP3, cannot be read up front, so they are limited by size: at the lowest bitrate Nymbot assumes, 8 kbps, 30 minutes is 1.8 MB, and a larger file in such a format is refused with `413` `audio_too_long` before anything runs. Send longer recordings as WAV, WebM, MP4 or Ogg. If Whisper then reports more than 30 minutes anyway, the request is refused with `413` `audio_too_long`, and since Whisper did run, 30 minutes are charged.

You pay for the length of the audio, rounded up to the second: the longest of the length read from the file, the length Whisper reports, and the least the file's size allows, at Whisper's per-minute price with the usual fee and margin, and at least 0.05 credit a request. It is paid from the standard balance when that can cover the hold, and from Pro otherwise; a `402` gives what each balance would need. In the app, transcribing a voice message may be free; on the API it is always charged. If Whisper fails, nothing is charged.

Response

```
{
  "text": "Your invoice has been paid.",
  "usage": { "type": "duration", "seconds": 3 },
  "nymbot": { "balance": "standard", "charged_credits": 0.05, "charged_sats": 0.5, "balance_credits": 120.35, "balance_sats": 1203.5 }
}
```

`text` returns plain text, `srt` and `vtt` return subtitles, and `verbose_json` adds the language, duration and timed segments. For the three that are not JSON, the cost is in the headers.

| Status | When |
| --- | --- |
| `400` | No file, a file with no usable audio (`invalid_audio`), an unknown format, or a model that is not a transcription model (`invalid_model`). |
| `402` | Neither balance can cover it. |
| `413` | The file is over 25 MB (`file_too_large`) or the recording over 30 minutes, or, in a format whose length cannot be read up front, over 1.8 MB (`audio_too_long`). |
| `502` | Whisper could not process the audio. Nothing is charged. |

cURL

```
curl https://nymbot.ai/api/v1/audio/transcriptions \
  -H "Authorization: Bearer $NYMBOT_API_KEY" \
  -F file=@memo.m4a \
  -F model=whisper \
  -F response_format=json
```

Python

```
import os
from openai import OpenAI

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

with open("memo.m4a", "rb") as f:
    transcript = client.audio.transcriptions.create(model="whisper", file=f)
print(transcript.text)
```

JavaScript

```
import fs from "node:fs";
import OpenAI from "openai";

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

const transcript = await client.audio.transcriptions.create({
  model: "whisper",
  file: fs.createReadStream("memo.m4a"),
});
console.log(transcript.text);
```

### Translating speech to English

`POST https://nymbot.ai/api/v1/audio/translations` — needs an API key, or [a Lightning payment per request](https://nymbot.ai/docs/api-account/#l402). The body is `multipart/form-data`.

The same as transcription, with the same fields apart from `language`, limits and price, but the text comes back in English whatever language was spoken. The OpenAI SDKs call it with `audio.translations.create`.

cURL

```
curl https://nymbot.ai/api/v1/audio/translations \
  -H "Authorization: Bearer $NYMBOT_API_KEY" \
  -F file=@nota.m4a \
  -F model=whisper
```

Python

```
import os
from openai import OpenAI

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

with open("nota.m4a", "rb") as f:
    translation = client.audio.translations.create(model="whisper", file=f)
print(translation.text)
```

JavaScript

```
import fs from "node:fs";
import OpenAI from "openai";

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

const translation = await client.audio.translations.create({
  model: "whisper",
  file: fs.createReadStream("nota.m4a"),
});
console.log(translation.text);
```

## Embeddings

Turns text into vectors for search, clustering and deduplication. The models run on Cloudflare and spend the standard balance.

`POST https://nymbot.ai/api/v1/embeddings` — needs an API key, or [a Lightning payment per request](https://nymbot.ai/docs/api-account/#l402).

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | Yes | An embedding model from `GET /api/v1/models?type=embedding`. |
| `input` | string or array of strings | Yes | The text to embed, up to 100 non-empty strings. Token arrays are refused (`token_input_unsupported`). Each input has to fit the model's `context_length`, counted as about four bytes a token. |
| `encoding_format` | string | No | `float` (the default) or `base64` (little-endian 32-bit floats). |
| `dimensions` | integer | No | The model's own size, or for a model with `supported_dimensions` one of those; the vector is then shortened and normalized. Anything else is refused. |

| Model | Dimensions | Context |
| --- | --- | --- |
| `@cf/baai/bge-m3` | 1024, many languages | 8,192 tokens |
| `@cf/qwen/qwen3-embedding-0.6b` | 1024, many languages | 8,192 tokens |
| `@cf/baai/bge-large-en-v1.5` | 1024, English | 512 tokens |
| `@cf/baai/bge-base-en-v1.5` | 768, English | 512 tokens |
| `@cf/baai/bge-small-en-v1.5` | 384, English | 512 tokens |

The models list is the source of truth for which are available and what they cost; Google's EmbeddingGemma, with shorter vectors on request, appears there too when the catalog has a price for it. Embeddings are metered per input token, and the 0.05-credit minimum applies to each request, not to each input, so batching many texts into one request is much cheaper than sending them one at a time.

Response

```
{
  "object": "list",
  "data": [
    { "object": "embedding", "index": 0, "embedding": [0.0213, -0.0487, 0.0112] }
  ],
  "model": "@cf/baai/bge-m3",
  "usage": { "prompt_tokens": 9, "total_tokens": 9 },
  "nymbot": { "balance": "standard", "charged_credits": 0.05, "charged_sats": 0.5, "balance_credits": 1204.35, "balance_sats": 12043.5 }
}
```

| Status | When |
| --- | --- |
| `400` | No input or an empty one; token arrays (`token_input_unsupported`); more than 100 inputs (`too_many_inputs`); an input longer than the model takes (`input_too_long`); a size the model cannot give (`unsupported_dimensions`). |
| `402` | The standard balance is too low. Embeddings never use the Pro balance. |
| `404` | No embedding model by that name. |
| `502` | The model failed. Nothing is charged. |

cURL

```
curl https://nymbot.ai/api/v1/embeddings \
  -H "Authorization: Bearer $NYMBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "@cf/baai/bge-m3", "input": ["Lightning invoices expire.", "Invoices on Lightning have a time limit."]}'
```

Python

```
import os
from openai import OpenAI

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

result = client.embeddings.create(
    model="@cf/baai/bge-m3",
    input=["Lightning invoices expire.", "Invoices on Lightning have a time limit."],
)
print(len(result.data), len(result.data[0].embedding))
```

JavaScript

```
import OpenAI from "openai";

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

const result = await client.embeddings.create({
  model: "@cf/baai/bge-m3",
  input: ["Lightning invoices expire.", "Invoices on Lightning have a time limit."],
});
console.log(result.data.length, result.data[0].embedding.length);
```
