{"service":"unlimited-tts-worker","version":"1","description":"Paid text-to-speech gateway. Synthesizes speech via OpenAI tts-1. Every POST /tts is priced by input character count and requires payment (or a prepaid credit balance) before synthesis runs.","documentation":"GET /docs","limits":{"maxTextLength":4096,"getTtsMaxTextLength":1500,"asyncMinTextLength":20000,"idempotencyKeyMaxLength":512,"rateLimit":{"limit":30,"periodSeconds":60,"scope":"GET /tts, POST /tts, and GET /tts/jobs/:jobId/audio","key":"x-credit-account for TTS when present, CF-Connecting-IP for other TTS requests, sig URL token for audio retrieval","enforcement":"Cloudflare native rate limit binding plus hard D1 rolling-window counter","headers":["Retry-After","X-RateLimit-Limit","X-RateLimit-Policy","X-RateLimit-Remaining","X-RateLimit-Reset","X-RateLimit-Scope"]}},"payment":{"mode":"credit_account","currency":"USD","pricing":{"basis":"input character count × customer rate (OpenAI tts-1 rate × markup)","markupMultiplier":1.2,"microUsdPerCent":10000,"cardTopUpMinimumCents":500},"creditAccounts":{"enabled":true,"header":"x-credit-account","responseHeaders":["x-credit-account","x-credit-balance"],"balanceUnit":"micro-USD (1 cent = 10,000 micro-USD); x-credit-balance is the remaining balance in micro-USD"},"clientRequirements":{"cardOrBrowserAgents":["Use an existing prepaid x-credit-account balance when available.","Open the topUpUrl from a 402 response or GET /top-up to fund credits with Stripe Checkout.","Stripe Checkout collects card details; this API never receives raw card data."],"stripeMpp":["Disabled until the seller Stripe account has a US legal entity and SPT eligibility.","Do not send Authorization: Payment credentials for normal card payments; use prepaid credit accounts instead."]},"paths":[{"name":"Prepaid credit account","endpoints":["GET /top-up","POST /top-up","GET /top-up/complete","GET /top-up/status","GET /tts","POST /tts","GET /credits/:accountId"],"bestFor":"Browser-based agents, card agents, or constrained clients after an account has been funded.","requirements":["Stripe Checkout to fund or top up an x-credit-account","Ability to send x-credit-account header on TTS requests"]}]},"endpoints":[{"method":"GET","path":"/","summary":"Service health and endpoint index."},{"method":"GET","path":"/docs","summary":"This document — machine-readable API reference for agents and clients."},{"method":"GET","path":"/tts/voices","summary":"List supported voices and customer pricing metadata.","response":{"success":true,"total":"number","pricing":"object","voices":[{"id":"openai_nova","provider":"openai","voice":"nova"}]},"notes":["Voice ids use the form provider_slug, e.g. openai_nova — bare upstream slugs like nova are rejected."]},{"method":"POST","path":"/tts","summary":"Synthesize speech. Sync (200 + audio/mpeg) or async (202 + jobId).","query":{"async":"true — force async queue even for short text","storeAudio":"false — async only; do not retain generated audio in R2, so links.audio will stay null","voice":"optional when body is text/plain (e.g. openai_nova)","speed":"optional float 0.5–2 when body is text/plain"},"request":{"json":{"contentType":"application/json","body":{"text":"Hello world","voice":"openai_nova","speed":1,"storeAudio":true}},"plain":{"contentType":"text/plain; charset=utf-8","body":"Hello world"}},"headers":{"idempotency-key":"optional; max 512 chars; async only — replays return the same job","x-credit-account":"optional UUID; debits exact micro-USD price from prepaid balance"},"response":{"sync":{"status":200,"contentType":"audio/mpeg"},"async":{"status":202,"body":{"jobId":"uuid","status":"queued","links":{"status":"GET /tts/jobs/:jobId","audio":"GET /tts/jobs/:jobId/audio?sig=..."},"result":"null until completed; completed no-store jobs return result.stored=false and no audioUrl"}},"paymentRequired":{"status":402,"body":{"type":"https://paymentauth.org/problems/payment-required","amount":"minor currency unit string","currency":"usd","topUpUrl":"https://api.example.com/top-up?amount=...","creditAccount":"object describing the x-credit-account bearer-token contract"}}},"notes":["Text max 4096 characters.","Async when text length ≥ 20000 or ?async=true.","Async jobs store generated audio in R2 by default. Send storeAudio=false in JSON, or ?storeAudio=false for GET/text/plain callers, to opt out. Completed no-store jobs cannot be downloaded later.","Unpaid requests return 402. Card top-up minimum is 500 cents; surplus becomes credit balance.","All responses include x-trace-id.","Failed sync/async requests refund credit-settled payments back to the account.","Preferred for all clients that can send POST requests."]},{"method":"GET","path":"/tts","summary":"Synthesize speech via query string — fallback when POST /tts is blocked by the client environment.","query":{"text":"required — input text (URL-encoded)","voice":"optional (e.g. openai_nova)","speed":"optional float 0.5–2","async":"true — force async queue","storeAudio":"false — async only; do not retain generated audio in R2"},"headers":{"idempotency-key":"optional; async only","x-credit-account":"optional UUID"},"response":{"sync":{"status":200,"contentType":"audio/mpeg"},"async":{"status":202,"body":{"jobId":"uuid","status":"queued"}},"paymentRequired":{"status":402,"body":{"amount":"minor currency unit string","currency":"usd","topUpUrl":"https://api.example.com/top-up?amount=..."}}},"notes":["Text max 1500 characters (GET_TTS_MAX_TEXT_LENGTH) — text lives in the URL query string and is subject to proxy/browser URL length limits.","Use POST /tts for text up to 4096 characters.","Only use GET /tts when POST is unavailable in your environment (e.g. egress-blocked cloud agents).","Same payment, credit-account, async, and response behaviour as POST /tts.","Useful for browser agents because payment challenge details are present in the JSON body as well as WWW-Authenticate.","Example: GET /tts?text=Hello%20world&voice=openai_nova"]},{"method":"GET","path":"/tts/jobs/:jobId","summary":"Poll async job status.","response":{"jobId":"uuid","status":"queued | processing | completed | failed","traceId":"uuid","links":{"status":"url","audio":"url or null until completed"},"result":"null until completed; completed jobs include stored, audioUrl, mimeType, and sizeBytes","error":"null or { code, message }"}},{"method":"GET","path":"/tts/jobs/:jobId/audio","summary":"Download completed async audio (audio/mpeg).","query":{"sig":"required when JOB_SIGNING_SECRET is configured"},"notes":["Returns 409 JOB_NOT_READY while processing.","Returns 410 AUDIO_NOT_STORED when the job completed with storeAudio=false.","Returns 401 without valid sig when signing is enabled."]},{"method":"GET","path":"/top-up","summary":"Accessible top-up launcher page for funding a prepaid credit account.","query":{"amount":"optional amount in cents; values below MPP_FIAT_MIN_AMOUNT_MINOR are raised to the minimum","account":"optional existing credit account id to top up","challenge":"optional 402 challenge id carried through Stripe metadata"},"response":{"status":200,"contentType":"text/html"},"notes":["The Worker renders only the amount/account launcher; Stripe Checkout renders and collects the payment details.","Leaving account blank creates a new credit account after successful checkout.","Stripe Checkout may auto-select locale/country from browser IP; verify the billing country matches the card before submitting payment."]},{"method":"POST","path":"/top-up","summary":"Create a Stripe Checkout Session and redirect to Stripe-hosted payment.","request":{"form":{"amount":"amount in cents","accountId":"optional existing credit account id","challengeId":"optional 402 challenge id"}},"response":{"status":303,"headers":{"Location":"Stripe Checkout URL"}}},{"method":"GET","path":"/top-up/complete","summary":"Stripe Checkout return URL; verifies the session and credits the account once.","query":{"session_id":"Stripe Checkout Session id"},"response":{"status":200,"contentType":"text/html","body":{"accountId":"credit account bearer id","balance":"rendered in USD when available"}}},{"method":"GET","path":"/top-up/status","summary":"Poll credit-account funding status after Checkout redirects or browser timeouts.","query":{"account":"required credit account id"},"response":{"accountId":"uuid","status":"active | not_found","balanceMicroUsd":"integer","balanceUsd":"string (6 decimal places)","currency":"USD"},"notes":["Use this when Stripe Checkout submission outcome is unclear.","Returns 503 CONFIG_ERROR with migration recovery details when credit tables are missing."]},{"method":"GET","path":"/credits/:accountId","summary":"Check prepaid credit account balance.","response":{"accountId":"uuid","balanceMicroUsd":"integer","balanceUsd":"string (6 decimal places)","currency":"USD"},"notes":["Account id is a bearer token — anyone holding it can spend the balance."]}],"errorCodes":["CONFIG_ERROR","INVALID_JSON","INVALID_REQUEST","UNSUPPORTED_CONTENT_TYPE","PAYMENT_REQUIRED","PAYMENT_ERROR","RATE_LIMITED","NOT_FOUND","UNAUTHORIZED","JOB_NOT_READY","AUDIO_NOT_STORED","TTS_UPSTREAM_ERROR","TTS_UNAVAILABLE","INTERNAL_ERROR"],"agentGuide":{"summary":"Recommended flow for automated testing against staging/production.","steps":["GET /docs — read limits, payment mode, and endpoint shapes.","GET /tts/voices — pick a voice id (e.g. openai_nova).","If POST is blocked, use GET /tts?text=... (max getTtsMaxTextLength chars) instead of POST /tts.","POST /tts?async=true with text/plain body — expect 402 with topUpUrl if no x-credit-account is supplied.","Open the 402 topUpUrl or GET /top-up, complete Stripe Checkout, then save the rendered x-credit-account value.","On Stripe Checkout, verify the country selector matches the card billing country; proxy geolocation can choose the wrong default.","If Checkout redirects time out, poll GET /top-up/status?account=ACCOUNT_ID or GET /credits/:accountId.","After a successful top-up, save x-credit-account and x-credit-balance from the response.","Subsequent requests: send x-credit-account header; no payment needed while balance covers the micro-USD price.","Async: poll GET /tts/jobs/:jobId until status=completed, then GET links.audio.","Verify balance with GET /credits/:accountId."]}}