{"service":"unlimited-tts","version":"2","description":"Paid text-to-speech gateway with two independent payment flows: prepaid card credits on /tts, and USDC x402 machine payments on /x402/tts. x402 can be paid per request, or prepaid once via /x402/deposit and then spent with a per-request EIP-712 signature to remove settlement latency. All use the selected model's catalog price; Stripe top-ups and x-credit-account exist only in the card flow.","documentation":"GET /docs","openapi":{"version":"3.0.3","url":"/openapi.json","status":"canonical"},"limits":{"getTtsMaxTextLength":1500,"defaultTtsModel":"openai/tts-1","ttsModels":[{"id":"openai/tts-1","provider":"openai","maxInputCharacters":4096,"ratePerCharUsd":0.000018,"available":true,"source":"provider_model_capability"},{"id":"openai/tts-1-hd","provider":"openai","maxInputCharacters":4096,"ratePerCharUsd":0.000036,"available":true,"source":"provider_model_capability"}],"asyncMinTextLength":20000,"idempotencyKeyMaxLength":512,"rateLimit":{"limit":30,"periodSeconds":60,"scope":"GET/POST /tts, paid GET/POST /x402/tts retries, and GET /tts/jobs/:jobId/audio","key":"x-credit-account for card-credit TTS when present, client IP for direct paid x402 retries and other TTS requests, sig URL token for audio retrieval; prepaid x402 requests are excluded once their signature verifies","enforcement":"native edge rate limiter plus a persistent rolling-window counter; the initial unpaid x402 challenge is excluded so one synthesis consumes one slot","prepaidAuthFailures":"A prepaid credential that fails to verify charges one slot against the caller's IP and returns 429 RATE_LIMITED once the bucket empties. Verified prepaid requests stay unmetered; a correct client never reaches this path twice.","quoteEndpoint":"POST /x402/tts/quote is metered by IP at the edge limiter only. Quoting is optional — post the priced body straight to /x402/tts for unmetered throughput.","policyDisclosure":"The X-RateLimit-* headers follow the IETF RateLimit conventions and are published deliberately, so a client can pace itself instead of discovering the ceiling by tripping it.","headers":["Retry-After","X-RateLimit-Limit","X-RateLimit-Policy","X-RateLimit-Remaining","X-RateLimit-Reset","X-RateLimit-Scope"]}},"transport":{"cors":{"allowOrigin":"*","allowCredentials":false,"rationale":"A public machine API. Every credential is an explicit request header, no response sets a cookie, and access-control-allow-credentials is never sent, so a cross-origin caller has nothing a browser would attach on its behalf. Restrict with CORS_ORIGIN if a deployment needs it."},"responseHardening":{"strict-transport-security":"max-age=31536000; includeSubDomains","x-content-type-options":"nosniff","x-frame-options":"DENY","referrer-policy":"no-referrer","content-security-policy":"default-src 'none' on API responses; the rendered top-up pages additionally allow their one inline stylesheet and a same-origin form action"}},"payment":{"mode":"independent_card_credit_and_crypto_x402_flows","currency":"USD","pricing":{"basis":"input character count × selected model rate × shared 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 only GET/POST /tts for the prepaid card-credit flow.","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."],"x402Agents":["Use only GET/POST /x402/tts for direct machine payments.","Fetch and verify GET /.well-known/x402-service-metadata against an out-of-band pinned key before trusting a network or payee. Never bootstrap trust from PAYMENT-REQUIRED.","Every paid PaymentPayload must include the required official x402 Payment Identifier extension. Persist the identifier, request, audio, and PAYMENT-RESPONSE receipt for exact replay.","Recommended JavaScript path for a direct-text request: use @x402/fetch with an x402 v2 exact EVM client, signer, and Payment Identifier extension; the wrapper reads PAYMENT-REQUIRED, constructs the PaymentPayload including accepted, and retries automatically.","Privacy-preserving path: POST length and SHA-256 to /x402/tts/quote, obtain the unpaid challenge with {quoteToken}, then submit {quoteToken,text} only with the paid request.","HTTP-only path: read PAYMENT-REQUIRED from the 402 response, select one accepts entry, and retry with PAYMENT-SIGNATURE containing a signed x402 v2 PaymentPayload whose accepted field contains that selected entry and whose extensions contain the Payment Identifier.","Read PAYMENT-RESPONSE as the settlement receipt on success.","On a lost response, retry the exact identifier and paid request within the replay window. If PAYMENT_OUTCOME_UNKNOWN is returned, do not retry automatically; require approval for a new quote.","No Coinbase/CDP or facilitator account is required by the client; the service handles facilitator settlement.","Do not create a credit account, call /top-up, or send x-credit-account."]},"flows":{"cardCredits":{"kind":"prepaid_card_credit","endpoints":["GET /tts","POST /tts","GET/POST /top-up","GET /credits/:accountId"],"funding":"Stripe Checkout top-up","settlement":"Exact request price is debited from a prepaid credit balance before inference.","minimum":"500 cents per top-up; request debits can be sub-cent.","identity":"x-credit-account bearer token"},"cryptoX402":{"kind":"direct_per_request_machine_payment","endpoints":["GET /x402/tts","POST /x402/tts"],"protocol":"x402 v2","scheme":"exact","network":"eip155:8453","asset":"USDC (six-decimal atomic units)","facilitator":"https://facilitator.payai.network","headers":["PAYMENT-REQUIRED","PAYMENT-SIGNATURE","PAYMENT-RESPONSE"],"settlement":"One on-chain authorization is verified and settled for one successful TTS response.","minimum":"No application minimum and no top-up. The only lower bound is one atomic USDC unit (0.000001 USDC).","identity":"No API account or prepaid balance; the payment credential authorizes the request.","idempotency":{"contract":"The official required x402 Payment Identifier extension is the idempotency key. An exact replay returns the original audio and settlement receipt without verification, synthesis, or a second charge.","replayWindowSeconds":86400,"replayHeaders":["x-payment-identifier","x-idempotent-replay"],"unknownOutcome":"PAYMENT_OUTCOME_UNKNOWN is non-retryable. The client must not create or submit a replacement payment without explicit user approval."},"serviceMetadata":{"endpoint":"GET /.well-known/x402-service-metadata","signature":"ES256 over canonical stable JSON","trustRule":"Pin the first public key out of band; accept later payee/key rotations only when signed by a currently trusted, unexpired key."},"privacy":{"directChallenge":"The application does not log or retain raw unpaid-challenge text and does not forward it to the synthesis provider before payment verifies. The Worker still receives the request body.","hashQuote":"POST /x402/tts/quote accepts only length, SHA-256, model, voice, and speed. Its unpaid PAYMENT-REQUIRED challenge contains a signed quote token, not raw text."},"discovery":"x402 Bazaar discovery metadata declares JSON input schemas and audio/mpeg binary output.","refunds":"Crypto charges are final. There is no refund endpoint or refund workflow; exact idempotent response replay is the recovery mechanism.","asyncBatch":"Not available for crypto payment. Agents must not automate long-form multi-call narration until a paid async/batch endpoint is published.","paidRetry":{"sameMethodRequired":true,"sameUrlRequired":true,"methodConversionAllowed":false,"header":"PAYMENT-SIGNATURE","get":"Retry the identical GET URL as GET with PAYMENT-SIGNATURE. GET request headers are supported; do not switch to POST.","post":"Retry the identical POST URL and body as POST with PAYMENT-SIGNATURE."},"clientOptions":{"recommended":{"name":"@x402/fetch automatic payment wrapper","packages":["@x402/fetch","@x402/core","@x402/evm"],"documentation":"https://docs.cdp.coinbase.com/x402/quickstart-for-buyers","behavior":["Wrap fetch with an x402 v2 client configured with an exact EVM signer for the advertised Base network.","Register and populate the official x402 Payment Identifier extension before creating the paid request.","The wrapper parses PAYMENT-REQUIRED, selects a compatible accepts entry, constructs and signs PaymentPayload, and retries with PAYMENT-SIGNATURE.","Check the final response status and read PAYMENT-RESPONSE as the settlement receipt."]},"httpOnly":{"name":"Raw HTTP x402 v2","packageRequired":false,"steps":["Send the TTS request without PAYMENT-SIGNATURE and require a 402 response with PAYMENT-REQUIRED.","Base64-decode PAYMENT-REQUIRED as JSON and select a compatible entry from accepts.","Construct and sign the exact EVM authorization described by that entry.","Create a v2 PaymentPayload with x402Version, accepted set to the complete selected accepts entry, payload set to the scheme-specific signed authorization, and a required official Payment Identifier extension.","Base64-encode the PaymentPayload JSON and retry the identical method and URL with PAYMENT-SIGNATURE. A GET challenge is retried as GET with the header; it never changes to POST.","Require a successful audio response and read PAYMENT-RESPONSE as the settlement receipt."],"paymentSignatureEnvelope":{"x402Version":"2","accepted":"Complete selected PAYMENT-REQUIRED.accepts entry, copied without modification","payload":"Scheme-specific signed exact-EVM authorization containing signature and authorization","extensions":"Official required Payment Identifier extension containing a stable 16–128 character identifier"}}}},"cryptoX402Prepaid":{"kind":"prepaid_crypto_balance","endpoints":["POST /x402/deposit","GET /x402/accounts/:address","GET /x402/tts","POST /x402/tts"],"purpose":"Removes facilitator verification and on-chain settlement from the request path. One deposit settles once; later requests cost a local signature check and one database write.","funding":"POST /x402/deposit with the standard x402 challenge and PAYMENT-SIGNATURE retry. D1 reserves the payment identifier before settlement, and the signed payer is credited only after the exact USDC transfer reaches the configured Base confirmation depth.","identity":"The payer's public EVM address. This is not a bearer token: spending requires an EIP-712 signature from that address on every request.","headers":["x-x402-account","x-x402-nonce","x-x402-expiry","x-x402-max-amount","x-x402-signature","x-x402-balance (response)","x-x402-charged (response)"],"signature":{"algorithm":"EIP-712 typed data, secp256k1","domain":{"name":"UnlimitedTTS-Prepaid","version":"1","chainId":"8453","verifyingContract":"The advertised payTo address for this network"},"primaryType":"PrepaidRequest","fields":["account (address)","method (string)","path (string)","bodyHash (bytes32)","maxAmountMicroUsd (uint256)","nonce (string)","expiry (uint256)"],"bodyHash":"keccak256 of the exact POST body bytes, or of the canonical GET query string: parameters sorted by name then value using UTF-16 code-unit order, each percent-encoded, joined by &.","signerSupport":"EOA signers only. EIP-1271 smart-account signatures are not accepted, matching the exact scheme's EIP-3009 authorization requirement."},"settlement":"None per request. The balance was funded by a confirmed payment, so no facilitator call and no on-chain transaction occur during synthesis.","receipt":"There is no per-request PAYMENT-RESPONSE because there is no per-request settlement to report. Reconcile against x-x402-charged, the micro-USD this request retained, and x-x402-balance, the balance after it. x-x402-charged is 0 when the debit was restored.","replayProtection":"x-x402-nonce is consumed by the same write that debits the balance. A reused nonce returns 409 PREPAID_NONCE_REPLAYED and is never charged twice.","balanceDisclosure":"GET /x402/accounts/:address needs the same signature headers. A credential signed for a different address is rejected before the balance is read, so 401 means 'you do not own this address' and 404 means 'you own it and have never deposited'. Reaching either requires the private key for the address in the path, so no balance is observable for an address the caller cannot already sign for.","refunds":"Prepaid balances are spend-only. Unused balance cannot be withdrawn and no crypto refund is sent. Failed synthesis restores only the internal D1 debit.","failureHandling":"A failed request restores the exact D1 debit. Pending debits remain durable and a scheduled reconciler retries restoration after interruptions or transient database failures.","deposit":{"confirmationTimeoutMs":120000,"confirmations":2,"minMicroUsd":100000,"maxMicroUsd":100000000,"signatureTtlSeconds":120}}},"paths":[{"name":"Card — 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"]},{"name":"Crypto — direct x402 per request","endpoints":["GET /.well-known/x402-service-metadata","POST /x402/tts/quote","GET /x402/tts","POST /x402/tts"],"bestFor":"Autonomous agents with an x402-compatible USDC wallet.","requirements":["x402 v2 client with exact EVM payment support","USDC on the advertised Base network","Ability to read and send the PAYMENT-* headers","Official x402 Payment Identifier extension","Out-of-band pinned service-metadata signing key"]},{"name":"Crypto — prepaid x402 balance","endpoints":["POST /x402/deposit","GET /x402/accounts/:address","GET /x402/tts","POST /x402/tts"],"bestFor":"Latency-sensitive agents making repeated requests. One settlement up front removes the facilitator round trip and the on-chain wait from every later request.","requirements":["Everything the direct x402 path requires, for the deposit itself","A local EOA signer able to produce an EIP-712 signature per request","Acceptance that unused balance is spend-only and cannot be withdrawn"]}]},"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":"/.well-known/x402-service-metadata","summary":"Short-lived, ES256-signed x402 network, asset, payee, pricing, validity, and key-rotation metadata.","notes":["Bootstrap by pinning a current public key through an out-of-band deployment channel.","Do not trust a payee or replacement key merely because it appears in PAYMENT-REQUIRED."]},{"method":"GET","path":"/tts/models","summary":"List selectable provider-qualified models with per-model limits, pricing, voices, defaults, and availability.","response":{"success":true,"defaultModel":"openai/tts-1","total":2,"models":"array of {id, provider, maxInputCharacters, defaultVoice, speed, pricing, voices, available}"},"notes":["Pass a returned model id on synthesis and quote requests.","Models without deterministic pre-payment pricing are not advertised."]},{"method":"POST","path":"/x402/tts/quote","summary":"Create a short-lived, hash-bound price quote without sending raw text.","request":{"json":{"textLength":"integer from 1 through the selected model's maxInputCharacters","textSha256":"lowercase SHA-256 hex of the trimmed UTF-8 text","model":"optional model id; defaults to openai/tts-1","voice":"optional provider_voice id","speed":"optional float 0.5–2; defaults to 1"}},"response":{"success":{"status":200,"body":"{quoteId, quoteToken, amountMicroUsd, amountUsd, network, payTo, issuedAt, expiresAt, paymentEndpoint}"}},"notes":["Send {quoteToken} without PAYMENT-SIGNATURE to /x402/tts to receive the unpaid challenge.","Then send {quoteToken,text} with PAYMENT-SIGNATURE. Supplied voice/speed must match the quote.","The application does not log or retain unpaid raw text and does not forward it to the synthesis provider before payment verification.","A quote expires 300 seconds after it is issued (X402_QUOTE_VALIDITY_SECONDS). Read expiresAt from the response rather than assuming a fixed lifetime; after it passes, /x402/tts returns 400 QUOTE_EXPIRED.","A quote is a price quote, not a one-time ticket. Within its lifetime the same token may be spent more than once, and each spend is charged separately. Payment, not the token, is what is consumed.","The quote cannot check textSha256 against textLength, because the whole point of quoting is that the text is never sent. The binding is enforced at synthesis, where the text is present: a mismatched length or digest returns 400 QUOTE_BODY_MISMATCH and nothing is charged.","Quoting is optional. A client that already knows the price can send the ordinary {text,...} body straight to /x402/tts."]},{"method":"GET","path":"/tts/voices","summary":"List supported voices and customer pricing metadata.","query":{"model":"optional model id; defaults to openai/tts-1"},"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, so links.audio will stay null","model":"optional when body is text/plain","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","model":"openai/tts-1","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 limits and pricing are model-specific; inspect GET /tts/models before paying.","Async when text length ≥ 20000 or ?async=true.","Async jobs retain generated audio 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)","model":"optional; defaults to openai/tts-1","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"},"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 input up to the selected model's maxInputCharacters.","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":"POST","path":"/x402/tts","summary":"Synthesize speech with a direct, exact x402 USDC payment for this request. This endpoint never uses prepaid credits.","request":{"json":{"contentType":"application/json","directBody":{"text":"Hello world","model":"openai/tts-1","voice":"openai_nova","speed":1},"quoteChallengeBody":{"quoteToken":"signed quote token"},"quotePaidBody":{"quoteToken":"same signed quote token","text":"text whose trimmed UTF-8 SHA-256 matches the quote"}}},"headers":{"PAYMENT-SIGNATURE":"Base64-encoded x402 v2 PaymentPayload; accepted must contain the selected PAYMENT-REQUIRED.accepts entry and extensions must include the required official Payment Identifier"},"response":{"paymentRequired":{"status":402,"headers":{"PAYMENT-REQUIRED":"Base64-encoded x402 v2 challenge containing exact atomic USDC amount, asset, network, and payTo"}},"success":{"status":200,"contentType":"audio/mpeg","headers":{"PAYMENT-RESPONSE":"Base64-encoded x402 settlement receipt","x-payment-identifier":"stable identifier supplied by the required Payment Identifier extension","x-idempotent-replay":"false on the original response; true when returning the cached audio and receipt"}},"replayConflict":{"status":409,"codes":["PAYMENT_IDENTIFIER_CONFLICT","PAYMENT_REPLAY_PENDING","PAYMENT_OUTCOME_UNKNOWN"]},"invalidPayment":{"status":400,"code":"INVALID_PAYMENT_PAYLOAD","description":"Malformed, legacy, or schema-invalid PAYMENT-SIGNATURE; no verification, settlement, or synthesis is attempted"}},"notes":["Model selection, voice compatibility, and the selected model's input cap are validated before payment.","JSON only. text/plain belongs to the separate card-credit /tts flow.","No top-up, credit account, API key, or application minimum is required.","The exact price is character count × customer rate, represented in six-decimal USDC atomic units.","x402 v2 requires accepted inside PaymentPayload. Top-level scheme/network fields are the legacy v1 envelope and are not a substitute.","The official x402 Payment Identifier extension is required. An exact replay within the configured window returns the original audio and PAYMENT-RESPONSE without a second charge.","Crypto charges are final; no refund endpoint or workflow exists. PAYMENT_OUTCOME_UNKNOWN is non-retryable and requires user approval before creating a new quote.","Synchronous only. Long-form agents must not automate multi-call narration until a crypto-paid async/batch endpoint exists.","GET, POST, and OPTIONS are the only methods served. HEAD returns 405 with an Allow header: the response exists only once it is paid for, so a HEAD would either synthesize unpaid or bill for a body HEAD discards."]},{"method":"GET","path":"/x402/tts","summary":"Complete GET-based direct x402 flow for agents that cannot send POST. Both the challenge request and paid retry use GET.","query":{"text":"required — input text (URL-encoded)","model":"optional; defaults to openai/tts-1","voice":"optional (e.g. openai_nova)","speed":"optional float 0.5–2"},"headers":{"PAYMENT-SIGNATURE":"Base64-encoded x402 v2 PaymentPayload with the selected accepted entry and required Payment Identifier extension"},"response":{"paymentRequired":{"status":402,"headers":{"PAYMENT-REQUIRED":"x402 v2 challenge"}},"success":{"status":200,"contentType":"audio/mpeg","headers":{"PAYMENT-RESPONSE":"x402 settlement receipt"}},"invalidPayment":{"status":400,"code":"INVALID_PAYMENT_PAYLOAD"}},"notes":["Text max 1500 characters.","Retry the identical GET URL as GET with PAYMENT-SIGNATURE. GET supports request headers; never switch the paid retry to POST.","This endpoint is not challenge-only: a correctly signed second GET returns the audio response and PAYMENT-RESPONSE receipt.","Same direct x402 settlement as POST /x402/tts; no card-credit state is read or written.","An exact replay with the same official Payment Identifier returns the cached audio and original receipt without another charge."]},{"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 CARD_TOP_UP_MIN_CENTS 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 service 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","UNKNOWN_TTS_MODEL","INVALID_VOICE","INVALID_SPEED","TEXT_TOO_LONG","TTS_PROVIDER_NOT_CONFIGURED","INVALID_PAYMENT_PAYLOAD","INVALID_QUOTE","QUOTE_EXPIRED","QUOTE_BODY_MISMATCH","UNSUPPORTED_CONTENT_TYPE","PAYMENT_REQUIRED","PAYMENT_ERROR","PAYMENT_IDENTIFIER_CONFLICT","PAYMENT_REPLAY_PENDING","PAYMENT_OUTCOME_UNKNOWN","REPLAY_CACHE_UNAVAILABLE","RATE_LIMITED","NOT_FOUND","UNAUTHORIZED","JOB_NOT_READY","AUDIO_NOT_STORED","TTS_UPSTREAM_ERROR","TTS_UNAVAILABLE","INTERNAL_ERROR"],"agentGuide":{"summary":"Choose exactly one payment flow per request. Autonomous wallet agents should prefer direct x402; card clients should use prepaid credits.","steps":["GET /docs — read limits and choose either crypto x402 or card credits. Do not combine their headers or endpoints.","GET /tts/models — choose a model id; openai/tts-1 is used when omitted for backward compatibility.","GET /tts/voices?model=MODEL_ID — pick a compatible voice id (e.g. openai_nova).","x402 trust bootstrap: GET /.well-known/x402-service-metadata and verify ES256 with an out-of-band pinned key before accepting the advertised network, asset, or payee.","x402 recommended direct-text path: use @x402/fetch with a configured x402 v2 exact EVM signer and the official Payment Identifier extension; it handles the 402 challenge, PaymentPayload construction, and paid retry.","x402 private quote path: hash the trimmed UTF-8 text; POST {textLength,textSha256,model,voice,speed} to /x402/tts/quote; challenge with {quoteToken}; pay with {quoteToken,text}.","x402 HTTP-only path: read PAYMENT-REQUIRED from 402, select an accepts entry, then retry with a signed v2 PaymentPayload that copies the complete entry into accepted and supplies the required Payment Identifier extension.","x402 method invariant: preserve the original HTTP method and URL on the paid retry. Retry GET as GET with PAYMENT-SIGNATURE and POST as POST; never convert GET to POST.","x402: persist the Payment Identifier, exact paid request, audio, and PAYMENT-RESPONSE. A lost response may be replayed exactly within the advertised replay window without a second charge.","x402: PAYMENT_OUTCOME_UNKNOWN is non-retryable. Require explicit user approval before creating and paying a replacement quote.","x402 charges are final and have no refund workflow. Crypto long-form multi-call automation is disabled until a paid async/batch endpoint is published.","x402 GET fallback: use GET /x402/tts?text=... when POST is blocked.","Card credits: POST /tts (or GET /tts fallback) — expect 402 with topUpUrl if no funded 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.","Card-credit subsequent requests: send x-credit-account; no new Stripe payment is needed while the balance covers the exact micro-USD debit.","Card-credit async only: poll GET /tts/jobs/:jobId until status=completed, then GET links.audio.","Verify balance with GET /credits/:accountId."]}}