API docs

Prompt in; a six-second animation out as a live WebGPU embed, an MP4 and a poster. It loops unless its manifest says loop: false. Base URL https://text2shader.com/v1. JSON in, JSON out. Every error is { "error": { "code", "message", "details"? } }.

Keys: /app/api. The same docs as plain text: /docs.md and /llms-full.txt.

request
GET /v1/meaccount:read

authenticate

Every call carries one secret API key. Create keys in the signed-in app with the scopes and expiry you need; the full secret is shown once and never again. Keep it on your server: never in browser code, an embed, a URL, a query string or source control.

header
Authorization: Bearer <secret>. x-api-key: <secret> also works. Send exactly one credential header and no cookies: two credentials, or a key with cookies, return 400, and an invalid key never falls back to a session.
returns
email and credits, the total you can spend now. Optional fields: free_allowance { allowance, remaining, next_refill_at }, unlimited, and eligibility when the signed-in app needs a confirmation first.
credits
The app and the API share one balance; one credit pays for one render. Free credits refill every seven days from signup and don't carry over. Purchased credits, when available, don't expire. Unlimited accounts aren't charged. Creating a key grants no credits and starts no plan.
sh
curl --fail-with-body https://text2shader.com/v1/me \
  -H "Authorization: Bearer $T2S_API_KEY"
response200 OK

credits is the balance now. What one render cost is cost_credits on its manifest.

json
{
  "email": "you@example.com",
  "credits": 17,
  "free_allowance": {
    "allowance": 20,
    "remaining": 17,
    "next_refill_at": "2026-10-03T14:12:04.000Z"
  }
}
errors
StatusCodeWhen
400invalid_requesttwo credentials, or a key sent with cookies
401unauthorizedmissing, invalid, expired or revoked key
403forbiddenthe key lacks account:read
request
POST /v1/rendersrenders:create

create a render

Turns a prompt into a short animation: a live WebGPU embed, an MP4 and a poster. It answers at once with a queued manifest and renders in the background, which can take a few minutes.

body
JSON { "prompt": string, "style_id"?: string }, prompt 1 to 500 characters after trimming. A style ID must belong to your account. No assets, scene JSON or shader source.
prompts
Abstract motion, backgrounds, loaders, simple geometric widgets and simple animated data shapes. Text, logos, supplied assets, characters, photorealism, audio, narration and multiple scenes aren't supported.
style
Send { "prompt": "a red cube", "style_id": "st_0123456789abcdef" } to use a saved style. The prompt decides what is shown and wins on conflicts; the style is visual guidance and cannot change policy or output format. Only renders:create is required. The manifest has style_id (null without a style). Later style edits or deletion do not change an admitted render; regeneration carries that frozen style.
output
Six seconds at 1280×720 and 30 fps. Most renders loop; the ready manifest states the size, duration and loop it delivered.
retries
Idempotency-Key is an optional UUID, one per deliberate create. After a lost response, send the same key, prompt and style ID: within 24 hours that returns 200 and the same render with no second charge, even after a style edit or deletion. Changing the prompt or style ID returns 409.
cost
202 proves admission only, not a finished render. A credit-funded account pays 1 credit at admission and gets it back if the render fails; Unlimited and builder accounts pay 0. cost_credits on the manifest is the actual charge.
sh
T2S_REQUEST_ID="$(uuidgen)"
curl --fail-with-body https://text2shader.com/v1/renders \
  -H "Authorization: Bearer $T2S_API_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $T2S_REQUEST_ID" \
  --data '{"prompt":"a slow aurora over a dark sea"}'
response202 Accepted

Keep the id and poll it. A replay of the same key, prompt and style ID returns 200.

json
{
  "id": "r_8f3k2a0b1c2d3e4f",
  "version": 1,
  "status": "queued",
  "prompt": "a slow aurora over a dark sea",
  "style_id": null,
  "created_at": "2026-09-26T14:12:04.000Z",
  "cost_credits": 1
}
errors
StatusCodeWhen
400invalid_requestinvalid prompt, malformed st_ style ID, or unknown fields
400invalid_idempotency_keyIdempotency-Key isn't a UUID
402insufficient_creditsno credits; /v1/me has next_refill_at
403forbiddenthe key lacks renders:create, or the app needs an eligibility confirmation
404not_foundstyle missing, deleted or not yours; or a deleted render replay
409idempotency_conflictthe key was used with another prompt or style ID
429rate_limitedwait Retry-After seconds, same key
503internalpaused or at capacity; retry later, same key
request
GET /v1/renders/{id}renders:read

poll until ready

Returns the render's manifest. Read it about every two seconds while status is queued or rendering, and stop at ready or failed: both are final and complete. A failed HTTP request is not a failed render; keep polling the same id.

progress
A pending manifest may carry progress { completed, activity }. completed is an ordered prefix of plan_accepted, candidate_checked, review_completed and media_rendered. activity is planning, creating, checking, reviewing, rendering, publishing, repairing, retrying or unknown. Neither is a percentage or an estimate of time left.
ready
embed, mp4, poster, size, duration_s and loop are all present, and nothing is still being written. style_id is the selected style or null; the style text stays private.
failed
error.code is out_of_scope or render_failed, and error.message is safe to show as is. Credit settlement is complete, and there are no media URLs.
sh
curl --fail-with-body https://text2shader.com/v1/renders/r_8f3k2a0b1c2d3e4f \
  -H "Authorization: Bearer $T2S_API_KEY"
response200 OK

The last manifest you read is the one to keep.

json
{
  "id": "r_8f3k2a0b1c2d3e4f",
  "version": 1,
  "status": "ready",
  "prompt": "a slow aurora over a dark sea",
  "style_id": null,
  "created_at": "2026-09-26T14:12:04.000Z",
  "cost_credits": 1,
  "embed": "https://cdn.text2shader.com/v1/r_8f3k2a0b1c2d3e4f.js",
  "mp4": "https://cdn.text2shader.com/v1/r_8f3k2a0b1c2d3e4f/1.mp4",
  "poster": "https://cdn.text2shader.com/v1/r_8f3k2a0b1c2d3e4f/1.png",
  "duration_s": 6,
  "loop": true,
  "size": [
    1280,
    720
  ]
}
errors
StatusCodeWhen
401unauthorizedmissing, invalid, expired or revoked key
403forbiddenthe key lacks renders:read
404not_foundunknown id, or not yours
request
<script type="module">no key

embed it

Paste the ready manifest's embed URL as a module script where the graphic should appear. The URL is public and needs no key; use it exactly as returned. Copy embed in the app gives the same tag.

plays
Live WebGPU where the browser has it, drawn at the render's size and scaled to the page width. Without WebGPU it plays the MP4. Both repeat when the manifest's loop is true and stop on the last frame when it's false. With reduced motion it shows the poster.
static
PDFs, email and slides can't run scripts: use the mp4 or poster URL from the same manifest.
scene
An embed holds a version 1 JSON scene descriptor and portable WGSL; the runtime owns all JavaScript. Time is the frame number divided by the frame rate, and named parameters have explicit numeric types. Descriptor settings aren't API controls.
deletion
Deleting the render removes its embed, video and poster. Copies already loaded or downloaded may remain.
html
<script type="module" src="https://cdn.text2shader.com/v1/r_8f3k2a0b1c2d3e4f.js"></script>
responsescene descriptor

What the embed runs, with portable WGSL. Read sizes from the ready manifest.

json
{
  "format": 1,
  "duration_s": 6,
  "fps": 30,
  "loop": true,
  "size": [
    1280,
    720
  ],
  "seed": 0,
  "params": {}
}
request
GET /v1/rendersrenders:read
DELETE /v1/renders/{id}renders:delete
GET /v1/stylesstyles:read
POST /v1/stylesstyles:write
GET /v1/styles/{id}styles:read
PATCH /v1/styles/{id}styles:write
DELETE /v1/styles/{id}styles:write

list and delete

List and delete renders, or save visual styles for later renders. Render lists are newest first, each a full manifest. Deleting a render removes its hosted files.

query
limit 1 to 50, default 20. Pass next_cursor unchanged as cursor for the next page; it's absent on the last page.
delete
204 means cleanup finished. 503 with Retry-After means it's still running: repeat the same DELETE. A 404 from GET alone doesn't prove cleanup or a refund.
styles
POST { "name": string, "prompt": string } returns 201. Name is 1 to 64 characters and prompt 1 to 500 after trimming. Up to 50 active styles per account; the next POST returns 400 invalid_request. GET /v1/styles returns { styles: [...] }, newest first; GET one returns the style.
style shape
{ "id": string, "name": string, "prompt": string, "created_at": string, "updated_at": string }. PATCH with name, prompt or both returns 200 and the updated style; DELETE returns 204 and frees a slot. Timestamps are UTC.
sh
curl --fail-with-body "https://text2shader.com/v1/renders?limit=20" \
  -H "Authorization: Bearer $T2S_API_KEY"

curl --fail-with-body -X DELETE https://text2shader.com/v1/renders/r_8f3k2a0b1c2d3e4f \
  -H "Authorization: Bearer $T2S_API_KEY"

curl --fail-with-body https://text2shader.com/v1/styles \
  -H "Authorization: Bearer $T2S_API_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"name":"Mono","prompt":"brutalist monochrome, hard shadows"}'
response200 OK · 204 No Content

A failed render lists with its error and cost_credits 0.

json
{
  "renders": [
    {
      "id": "r_0123456789abcdef",
      "version": 1,
      "status": "failed",
      "prompt": "a company logo spinning",
      "created_at": "2026-09-26T13:40:00.000Z",
      "cost_credits": 0,
      "error": {
        "code": "out_of_scope",
        "message": "That request is outside the kinds of scenes this app can create. Your credit is back."
      }
    }
  ],
  "next_cursor": "OPAQUE_CURSOR"
}
errors
StatusCodeWhen
400invalid_requestbad limit or cursor, invalid style body, or 50 active styles
401unauthorizedmissing, invalid, expired or revoked key
403forbiddenthe key lacks the scope
404not_foundrender or style missing, deleted or not yours
503internalcleanup pending; retry after Retry-After
request
POST /mcpper-tool scopes

MCP tools

Streamable HTTP on this Worker, using one scoped API key per request. Cookies and JSON-RPC batches are refused. MCP clients may use the current protocol or ordinary legacy stateless calls; OAuth is not available yet.

create_render
Requires renders:create. Send prompt and optional owned style_id; an explicit render prompt overrides the style's visual guidance. The queued manifest is followed with wait_for_render. Reuse the same UUID idempotency_key, prompt and style_id (including omission) for uncertain retries within 24 hours.
list_styles
Requires styles:read and returns your saved styles with ID, name and guidance prompt (plus timestamps). A key with only renders:create may use an owned style_id but cannot list styles. Style creation, editing and deletion are REST-only.
other tools
get_render, wait_for_render and list_renders require renders:read; delete_render requires renders:delete; get_account requires account:read.
errors
Tool errors set isError and include the REST error code and message in structuredContent. Unknown or deleted styles are not_found; malformed style IDs are invalid_request. Retry-After seconds appear as error.retry_after_s.
sh
curl --fail-with-body https://text2shader.com/mcp \
  -H "Authorization: Bearer $T2S_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_styles","arguments":{}}}'
response200 OK

A tool result has text and structuredContent; legacy clients may receive it as an SSE message.

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"styles\":[{\"id\":\"st_0123456789abcdef\",\"name\":\"Mono\",\"prompt\":\"black shadows\",\"created_at\":\"2026-09-26T14:12:04.000Z\",\"updated_at\":\"2026-09-26T14:12:04.000Z\"}]}"
      }
    ],
    "structuredContent": {
      "styles": [
        {
          "id": "st_0123456789abcdef",
          "name": "Mono",
          "prompt": "black shadows",
          "created_at": "2026-09-26T14:12:04.000Z",
          "updated_at": "2026-09-26T14:12:04.000Z"
        }
      ]
    }
  }
}
errors
StatusCodeWhen
400invalid_requestmalformed style ID on create_render
401unauthorizedmissing or invalid API key
403forbiddenkey lacks the tool's scope
404not_foundstyle is unknown, deleted or not yours
409idempotency_conflictkey reused with another prompt or style ID
request
app → api keysbrowser session

keys and scopes

Keys are created, listed and revoked in the signed-in app, never through the API. Scopes are independent; grant only what the caller needs. Creating and polling renders needs renders:create and renders:read, which the key form starts with. New keys do not preselect style scopes, and existing keys gain none.

renders:read
GET /v1/renders, GET /v1/renders/{id}
renders:create
POST /v1/renders
renders:delete
DELETE /v1/renders/{id}
account:read
GET /v1/me
styles:read
GET /v1/styles, GET /v1/styles/{id}
styles:write
POST /v1/styles, PATCH and DELETE /v1/styles/{id}
app only
Managing keys, reading request logs, confirming eligibility, recording plan interest and payments need the signed-in app.
limits
Names 1 to 64 characters. Expiry within a year, or none. 20 active keys and 100 retained key records per account; inactive keys are removed 30 days after they expire or are revoked.
rotate
Create a replacement, deploy it, confirm a successful call, then revoke the old key. Revocation stops later requests; ones already authorized can finish. A lost key can't be shown again: revoke it and make another.
sh
curl --fail-with-body https://text2shader.com/v1/me \
  -H "x-api-key: $T2S_API_KEY"
response403 Forbidden

A key without the scope is refused, and the refusal counts toward rate limits.

json
{
  "error": {
    "code": "forbidden",
    "message": "This API key does not have permission for this operation."
  }
}
request
every endpoint

limits and errors

Every error uses one envelope. Branch on error.code; error.message is safe to show. A render that fails arrives as a 200 manifest with status failed, not as an HTTP error.

rate
60 requests per key and 120 across an account's keys, per UTC minute. Scope refusals count; invalid credentials don't. A 429 carries Retry-After and error.details.retry_after_s. Fixed windows allow a burst at a minute boundary.
retry
Honor Retry-After on 429 and 503; a service pause may have no estimate. Retry a create with the same Idempotency-Key, and don't start a new render because polling failed.
request id
Every /v1 response has an X-Request-Id. The app's request logs keep the newest 1,000 requests within 30 days, at most 150 a minute: method, route, status, duration and key, never prompts, bodies or secrets. They're best-effort, not an audit trail, and a logged 202 is admission, not a finished render.
response429 Too Many Requests

Wait the given seconds, then send the same request again.

http
HTTP/1.1 429 Too Many Requests
Retry-After: 23

{
  "error": {
    "code": "rate_limited",
    "message": "API request limit reached. Try again soon.",
    "details": {
      "retry_after_s": 23
    }
  }
}
errors
StatusCodeWhen
400invalid_requestbad body, query or credentials
400invalid_idempotency_keyIdempotency-Key isn't a UUID
401unauthorizedmissing, invalid, expired or revoked key
402insufficient_creditsnot enough credits to create
403forbiddenmissing scope, eligibility or closed account
404not_foundthe render or style isn't available to this account
409idempotency_conflicta key reused with another prompt or style ID
429rate_limitedover a rate limit; see Retry-After
500/503internalserver trouble, pause or pending cleanup