The Explainer API
Create narrated, visual explainers from your own code, then share, embed or delete them. Five endpoints, one API key, JSON in and out.
The API is in beta. To use it, request API access.

Basics
All endpoints live under one base URL and answer JSON.
https://scrimba.com/api/v1Every request sends your API key as a bearer token:
Authorization: Bearer scr_live_…A request body is application/json, or multipart/form-data when it carries files. Dates in answers are ISO 8601 strings in UTC. Ids are opaque strings. An error answer is JSON too; see Errors.
The version is in the path. Fields may be added to answers without a version change; a field is never removed or renamed within v1.
API keys
A key belongs to a Scrimba account. Everything the key makes is owned by that account, is paid from its monthly allowance, and is visible on the site under that account.
- Sign in to scrimba.com. From your profile menu open Settings, then the API keys section.
- Choose Create API key and name it, for example after the server that will hold it.
- Copy the secret from the dialog. It starts with scr_live_ and is shown once: Scrimba stores only a hash of it.
The API is in beta. An account needs API access before the key dialog opens. Until Scrimba has enabled the account, the dialog and every API call answer 403 api_not_enabled.
Keep the secret on your server, never in a browser or an app you ship. To end a key, choose Revoke from its row in the same section. A revoked key answers 401 from then on and cannot be restored; make a new one instead. An account holds at most 20 live keys, and all of them share the account's limits.
The examples below use two shell variables:
export SCRIMBA=https://scrimba.com
export SCRIMBA_KEY=scr_live_your_secret_hereQuick start
Create an explainer, wait for it to finish, then share its link. The create call answers at once; the explainer generates in the background, and its page renders live while it builds.
const base = 'https://scrimba.com/api/v1'
const headers = {Authorization: `Bearer ${process.env.SCRIMBA_KEY}`}
const created = await fetch(`${base}/explainers`, {
method: 'POST',
headers: {...headers, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID()},
body: JSON.stringify({prompt: 'Explain how the Node.js event loop schedules work', privacy: 'unlisted'})
})
if (!created.ok) throw new Error((await created.json()).error.message)
let explainer = await created.json()
while (explainer.status === 'generating') {
await new Promise(resolve => setTimeout(resolve, 5000))
explainer = await (await fetch(`${base}/explainers/${explainer.id}`, {headers})).json()
}
console.log(explainer.status, explainer.url)A short explainer is ready in under a minute. A long one can take a few minutes. Poll every 5 seconds or so.
GET /api/v1/me
The account behind the key, the key itself, the account's tier and monthly usage, and the names the create endpoint accepts.
curl -s "$SCRIMBA/api/v1/me" -H "Authorization: Bearer $SCRIMBA_KEY"{
"account": {"id": "u1a2b3c", "alias": "acme", "name": "Acme Learning"},
"key": {"id": "ak9x", "name": "server", "prefix": "scr_live_abc123", "created_at": "2026-10-09T10:12:00.000Z"},
"tier": {"name": "pro", "requests_per_minute": 120, "concurrent_generations": 3, "explainers_per_month": 100},
"explainers": {"used": 12, "remaining": 88, "resets_at": "2026-11-01T00:00:00.000Z"},
"teachers": ["eli5", "standard", "per", "tom", "lol", "professor", "tldr"],
"modes": ["landscape", "portrait", "square"],
"privacy": ["private", "unlisted", "public"]
}key.prefix is the start of the secret, enough to tell keys apart. explainers.resets_at is when the monthly count starts over.
POST /api/v1/explainers
Makes an explainer and starts its generation. Answers 201 with the explainer object as soon as the explainer exists; status is generating at that point.
Fields
| Field | Values | Default |
|---|---|---|
prompt | What to explain, as a string of up to 12,000 characters. Required unless the request has an attachment. | |
mode | landscape (16:9), portrait (9:16), square (1:1). The shape of the slides. | landscape |
privacy | private (your account only), unlisted (anyone with the link), public (listed on the site). | private |
language | An ISO 639-1 code such as en or pt-BR. The narration and the visible text are in that language, whatever language the prompt is in. A region, as in pt-BR, selects that dialect. | The language of the prompt |
teacher | eli5, standard, per, tom, lol, professor, tldr. The voice and style of the explanation. | eli5 |
watermark | true or false. false needs a Pro account. | true |
attachments | Multipart file parts, up to 5. See Attachments. |
A field you leave out, or send as null, takes its default. A field you send must name a value: language: false is refused, as privacy: false is.
JSON
curl -s "$SCRIMBA/api/v1/explainers" \
-H "Authorization: Bearer $SCRIMBA_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4821-explainer" \
-d '{
"prompt": "Explain how HTTP caching headers decide what a browser keeps",
"mode": "landscape",
"privacy": "unlisted",
"language": "en",
"teacher": "standard"
}'Multipart, with attachments
Send a multipart/form-data body. The text fields are the same. Add each file as a part named attachments. The prompt is optional when there is at least one attachment.
curl -s "$SCRIMBA/api/v1/explainers" \
-H "Authorization: Bearer $SCRIMBA_KEY" \
-F "prompt=Walk through this diagram and the code behind it" \
-F "mode=portrait" \
-F "[email protected]" \
-F "[email protected]"Idempotency
Send an Idempotency-Key header, a string of your own of 1 to 200 printable characters, one per explainer you mean to make. If a request is cut off before you read the answer, repeat it with the same key and header: the API answers the explainer it already made, with 200 instead of 201, and makes no second one. Two requests that arrive together under one header make one explainer. The header is remembered per API key; a deleted explainer frees its header.
Answer
{
"id": "guide0abc123",
"url": "https://scrimba.com/explain/guide0abc123",
"status": "generating",
"error": null,
"title": "Explain how HTTP caching headers decide what a browser keeps",
"description": null,
"mode": "landscape",
"privacy": "unlisted",
"language": null,
"created_at": "2026-10-09T10:15:02.000Z",
"finished_at": null,
"video_url": null,
"embed": "<iframe src=\"https://scrimba.com/explain/guide0abc123\" …></iframe>",
"attachments": [],
"duration_ms": 0
}Open or share url right away. The page shows the explainer being built and plays it when it is ready.
A create that is refused makes nothing and spends nothing. A create past the month's allowance answers 429 quota_exceeded, and one while the account already has its tier's generations running answers 429 too_many_running; both carry Retry-After.
GET /api/v1/explainers/:id
One explainer the account made through the API, with its current status.
curl -s "$SCRIMBA/api/v1/explainers/guide0abc123" -H "Authorization: Bearer $SCRIMBA_KEY"status moves from generating to ready or failed and then stays. When it is ready, finished_at, language and duration_ms are filled. When it is failed, error says why, the page at url says the explainer failed, and the explainer does not count against the month: its credit comes back. A generation that stops without finishing, for example on a server restart, is reported failed after ten minutes.
An explainer made on the site, or made through the API by another account, answers 404.
GET /api/v1/explainers
The explainers the account made through the API, newest first, in pages. Explainers made on the site are not in the list.
| Query parameter | Values | Default |
|---|---|---|
limit | Rows per page, 1 to 100. | 25 |
cursor | The next_cursor of the page before. | The first page |
curl -s "$SCRIMBA/api/v1/explainers?limit=25" -H "Authorization: Bearer $SCRIMBA_KEY"
# {"explainers": [{…}, {…}], "next_cursor": "guide0abc123"}
curl -s "$SCRIMBA/api/v1/explainers?limit=25&cursor=guide0abc123" -H "Authorization: Bearer $SCRIMBA_KEY"
# {"explainers": [{…}], "next_cursor": null}Each row is an explainer object without duration_ms. next_cursor is null on the last page. A cursor stays valid after its explainer is deleted; a cursor that never named one of the account's API explainers answers 400 invalid_cursor.
DELETE /api/v1/explainers/:id
Deletes one explainer the account made through the API, with its exported video if it has one. The explainer's page and its link stop working, and the explainer answers 404 from then on. Its credit is not given back.
curl -s -X DELETE "$SCRIMBA/api/v1/explainers/guide0abc123" -H "Authorization: Bearer $SCRIMBA_KEY"
# {"deleted": "guide0abc123"}The explainer object
What POST, GET and the list answer for one explainer.
| Field | Type | Meaning |
|---|---|---|
id | string | The explainer's id, used in the paths above. |
url | string | The explainer's page. Who can open it depends on privacy. |
status | string | generating, ready or failed. |
error | object or null | {"code": "generation_failed", "message": "…"} when status is failed; otherwise null. |
title | string | The explainer's title: the prompt, or a name from the attachments when there is no prompt. |
description | string or null | A short synopsis, filled when the explainer is ready. |
mode | string | landscape, portrait or square. |
privacy | string | private, unlisted or public. |
language | string or null | The narration language as a tag such as en-us. null until the explainer is ready. |
created_at | string | When the explainer was made. |
finished_at | string or null | When the generation finished; null while it runs or after it failed. |
video_url | string or null | An MP4 of the explainer. Filled only when a video export exists and the explainer is unlisted or public. |
embed | string or null | An <iframe> snippet for your own pages. Filled only when the explainer is unlisted or public: an iframe of a private explainer shows the sign-in wall to everyone but you. |
attachments | array | The files sent with the request, in the order sent: {"ref": 1, "name": "server.ts", "type": "text/plain"}. ref is the number the explainer knows the file by. |
duration_ms | number | The playback length in milliseconds; 0 until the explainer is ready. On POST and GET only, not in the list. |
Attachments
A request may carry up to 5 files, as multipart parts named attachments. The explainer reads them as context and can show pictures from them.
| Kind | Accepted as | Limit |
|---|---|---|
| Pictures | png, jpeg, gif, webp | 5 MB and 30 megapixels each |
| PDFs | application/pdf | 5 MB each |
| Text and code | Any text/* type, or a name ending in txt, md, log, env, conf, cfg, ini, js, jsx, ts, tsx, py, json, imba, html, css | 30,000 characters each |
| Jupyter notebooks | A name ending in .ipynb | Stored as markdown and measured as text: 30,000 characters |
A picture or a PDF is judged by its bytes, whatever content type the part declares. Any other file goes by the declared type, or by its bytes when the part declares none or application/octet-stream. A file of another kind answers 415 unsupported_attachment, and a file over its limit answers 413 too_large; both name the file. A refused request stores nothing.
The whole request body is at most 32 MB. A JSON body is at most 256 KB.
Limits
| Limit | Free | Pro |
|---|---|---|
| Explainers a month, per account | 5 | 100 |
| Generations running at once, per account | 1 | 3 |
| Requests a minute, all keys of the account together | 30 | 120 |
| watermark: false | No | Yes |
Pro is the account's Scrimba Pro subscription. The monthly count resets on the first of the month, UTC; GET /api/v1/me reports the exact time. Only explainers that are made count, and a failed explainer gives its credit back. A refused request spends nothing.
A refusal for a limit is a 429 with a Retry-After header in seconds. Wait for it rather than retrying at once.
Errors
Every error answers JSON in one shape, with an HTTP status that says the kind and a code that says the cause:
{"error": {"code": "invalid_prompt", "message": "The prompt is over 12000 characters"}}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_prompt, invalid_mode, invalid_privacy, invalid_language, invalid_teacher, invalid_watermark, too_many_attachments, invalid_json, invalid_multipart, invalid_idempotency_key, invalid_limit, invalid_cursor | A field, a header or the body is wrong. The message says which. |
| 401 | unauthorized | The bearer token is missing, unknown or revoked. |
| 403 | api_not_enabled | The account is not enabled for the API yet. |
| 403 | pro_required | watermark: false on an account without Pro. |
| 404 | not_found | No explainer with that id on this account, or it was deleted. |
| 413 | too_large | A JSON body is over 256 KB, a multipart body is over 32 MB, a picture or a PDF is over 5 MB or 30 megapixels, or a text file is over 30,000 characters. The message names the file. |
| 415 | unsupported_attachment, unsupported_media_type | A file of a kind an explainer does not take, or a body that is neither JSON nor multipart. |
| 429 | rate_limited | Over the account's requests a minute, or too many pictures are being uploaded site-wide. Wait for Retry-After. |
| 429 | too_many_running | The account already has its tier's generations running. Wait for Retry-After, or for one to finish. |
| 429 | quota_exceeded | The month's explainers are used up. The message says when the count resets. |
| 503 | busy | The server holds too many uploads right now. Retry in a moment. |
| 500 | internal | A fault on Scrimba's side. Retry later; if it persists, contact Scrimba with the time and the explainer id if you have one. |
Describes the Explainer API as of 14 October 2026.

