# text2shader API v1 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"? } }. HTML: https://text2shader.com/docs. Plain text: https://text2shader.com/docs.md and https://text2shader.com/llms-full.txt. API keys: https://text2shader.com/app/api. Pricing: https://text2shader.com/pricing. Sections: 1 authenticate, 2 create a render, 3 poll until ready, 4 embed it, 5 list and delete, 6 MCP tools, 7 keys and scopes, 8 limits and errors. ## 1. authenticate - `GET /v1/me` (scope `account:read`) 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 . x-api-key: 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" ``` Response `200 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" } } ``` | status | code | when | |---|---|---| | 400 | invalid_request | two credentials, or a key sent with cookies | | 401 | unauthorized | missing, invalid, expired or revoked key | | 403 | forbidden | the key lacks account:read | ## 2. create a render - `POST /v1/renders` (scope `renders:create`) 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 1280x720 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"}' ``` Response `202 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 } ``` | status | code | when | |---|---|---| | 400 | invalid_request | invalid prompt, malformed st_ style ID, or unknown fields | | 400 | invalid_idempotency_key | Idempotency-Key isn't a UUID | | 402 | insufficient_credits | no credits; /v1/me has next_refill_at | | 403 | forbidden | the key lacks renders:create, or the app needs an eligibility confirmation | | 404 | not_found | style missing, deleted or not yours; or a deleted render replay | | 409 | idempotency_conflict | the key was used with another prompt or style ID | | 429 | rate_limited | wait Retry-After seconds, same key | | 503 | internal | paused or at capacity; retry later, same key | ## 3. poll until ready - `GET /v1/renders/{id}` (scope `renders:read`) 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" ``` Response `200 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 ] } ``` | status | code | when | |---|---|---| | 401 | unauthorized | missing, invalid, expired or revoked key | | 403 | forbidden | the key lacks renders:read | | 404 | not_found | unknown id, or not yours | ## 4. embed it - ` ``` Response `scene 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": {} } ``` ## 5. list and delete - `GET /v1/renders` (scope `renders:read`) - `DELETE /v1/renders/{id}` (scope `renders:delete`) - `GET /v1/styles` (scope `styles:read`) - `POST /v1/styles` (scope `styles:write`) - `GET /v1/styles/{id}` (scope `styles:read`) - `PATCH /v1/styles/{id}` (scope `styles:write`) - `DELETE /v1/styles/{id}` (scope `styles:write`) 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"}' ``` Response `200 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" } ``` | status | code | when | |---|---|---| | 400 | invalid_request | bad limit or cursor, invalid style body, or 50 active styles | | 401 | unauthorized | missing, invalid, expired or revoked key | | 403 | forbidden | the key lacks the scope | | 404 | not_found | render or style missing, deleted or not yours | | 503 | internal | cleanup pending; retry after Retry-After | ## 6. MCP tools - `POST /mcp` (scope `per-tool scopes`) 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":{}}}' ``` Response `200 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" } ] } } } ``` | status | code | when | |---|---|---| | 400 | invalid_request | malformed style ID on create_render | | 401 | unauthorized | missing or invalid API key | | 403 | forbidden | key lacks the tool's scope | | 404 | not_found | style is unknown, deleted or not yours | | 409 | idempotency_conflict | key reused with another prompt or style ID | ## 7. keys and scopes - `app -> api keys` (scope `browser session`) 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" ``` Response `403 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." } } ``` ## 8. limits and errors - `every endpoint` 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. Response `429 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 } } } ``` | status | code | when | |---|---|---| | 400 | invalid_request | bad body, query or credentials | | 400 | invalid_idempotency_key | Idempotency-Key isn't a UUID | | 401 | unauthorized | missing, invalid, expired or revoked key | | 402 | insufficient_credits | not enough credits to create | | 403 | forbidden | missing scope, eligibility or closed account | | 404 | not_found | the render or style isn't available to this account | | 409 | idempotency_conflict | a key reused with another prompt or style ID | | 429 | rate_limited | over a rate limit; see Retry-After | | 500/503 | internal | server trouble, pause or pending cleanup |