openapi: 3.1.0 info: title: Puppetry Developer API Beta Audio Uploads Text to Speech API description: Live Puppetry Developer API beta for Puppetry Voice API access, hosted audio uploads, usage, and credit-backed text/audio-to-video jobs. version: '2026-05-11' contact: name: Puppetry Support url: https://www.puppetry.com email: support@puppetry.com termsOfService: https://www.puppetry.com/tos license: name: Proprietary servers: - url: https://www.puppetry.com description: Production security: - developerApiKey: [] tags: - name: Text to Speech paths: /api/v1/tts/puppetry: post: operationId: createPuppetrySpeech x-openai-isConsequential: true summary: Generate speech with a Puppetry voice description: Creates a hosted WAV audio file from text using a public Puppetry voice. Requires the tts:create scope. tags: - Text to Speech security: - developerApiKey: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TtsRequest' examples: speech: summary: Generate speech value: voice_id: puppetry-af_bella text: Hello from Puppetry. speed: 1 responses: '200': description: Generated audio headers: X-RateLimit-Limit: $ref: '#/components/headers/RateLimitLimit' X-RateLimit-Remaining: $ref: '#/components/headers/RateLimitRemaining' X-RateLimit-Reset: $ref: '#/components/headers/RateLimitReset' content: application/json: schema: $ref: '#/components/schemas/TtsResponse' '400': $ref: '#/components/responses/InvalidRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': description: Unknown Puppetry voice content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: voiceNotFound: value: error: voice_not_found message: Unknown Puppetry voice '429': $ref: '#/components/responses/RateLimited' '502': $ref: '#/components/responses/UpstreamFailure' '503': $ref: '#/components/responses/TemporarilyUnavailable' components: responses: Forbidden: description: The API key does not include the required endpoint scope. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: forbidden: value: error: forbidden message: This API key does not have the required scope RateLimited: description: Rate limit or monthly beta quota exceeded headers: X-RateLimit-Limit: $ref: '#/components/headers/RateLimitLimit' X-RateLimit-Remaining: $ref: '#/components/headers/RateLimitRemaining' X-RateLimit-Reset: $ref: '#/components/headers/RateLimitReset' Retry-After: $ref: '#/components/headers/RetryAfter' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: rateLimited: value: error: rate_limited message: 'Developer API beta limit exceeded: 10 requests per minute.' retryable: true retry_after_seconds: 30 retryAfter: 30 retryAfterSeconds: 30 next_poll_at: '2026-06-22T08:50:30.000Z' nextPollAt: '2026-06-22T08:50:30.000Z' details: retryable: true retry_after_seconds: 30 retryAfter: 30 retryAfterSeconds: 30 next_poll_at: '2026-06-22T08:50:30.000Z' nextPollAt: '2026-06-22T08:50:30.000Z' voiceCharacterLimit: value: error: voice_character_limit_exceeded message: Monthly Puppetry Voice character limit exceeded details: retryable: true retry_after_seconds: 86400 retryAfter: 86400 retryAfterSeconds: 86400 concurrentVideoJobLimit: value: error: concurrent_job_limit_exceeded message: Developer API concurrent job limit exceeded details: retryable: true concurrent_jobs: 2 concurrentJobs: 2 active_jobs: 2 activeJobs: 2 available_slots: 0 availableSlots: 0 active_job_expires_in_seconds: 512 activeJobExpiresInSeconds: 512 retry_after_seconds: 512 retryAfter: 512 retryAfterSeconds: 512 Unauthorized: description: Missing, malformed, invalid, or revoked API key content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: unauthorized: value: error: unauthorized message: 'Missing or invalid Authorization header. Use: Bearer pk_live_...' TemporarilyUnavailable: description: A beta quota, usage lookup, or job counter is temporarily unavailable. headers: Retry-After: $ref: '#/components/headers/RetryAfter' X-Puppetry-Next-Poll-At: $ref: '#/components/headers/NextPollAt' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: apiAuthUnavailable: value: error: api_auth_unavailable message: Developer API authentication is temporarily unavailable retryable: true retry_after_seconds: 30 retryAfter: 30 retryAfterSeconds: 30 next_poll_at: '2026-06-22T08:50:30.000Z' nextPollAt: '2026-06-22T08:50:30.000Z' details: retryable: true retry_after_seconds: 30 retryAfter: 30 retryAfterSeconds: 30 next_poll_at: '2026-06-22T08:50:30.000Z' nextPollAt: '2026-06-22T08:50:30.000Z' InvalidRequest: description: Invalid request body or unsupported input content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: hostedAudioUploadUnavailable: value: error: hosted_audio_upload_unavailable message: Hosted audio upload ownership is temporarily unavailable details: retry_after_seconds: 217 retryAfter: 217 retryAfterSeconds: 217 hostedAudioUploadNotFound: value: error: invalid_request message: audio_url must reference a hosted API audio upload owned by this API key details: retryable: false retry_blocked_reason: audio_url must reference a hosted API audio upload owned by this API key retryBlockedReason: audio_url must reference a hosted API audio upload owned by this API key retry_block_code: HOSTED_AUDIO_UPLOAD_NOT_FOUND retryBlockCode: HOSTED_AUDIO_UPLOAD_NOT_FOUND hostedAudioUploadPending: value: error: invalid_request message: audio_url hosted upload has not finished uploading; upload the file before creating a video details: retryable: false retry_blocked_reason: audio_url hosted upload has not finished uploading; upload the file before creating a video retryBlockedReason: audio_url hosted upload has not finished uploading; upload the file before creating a video retry_block_code: HOSTED_AUDIO_UPLOAD_PENDING retryBlockCode: HOSTED_AUDIO_UPLOAD_PENDING invalidImage: value: error: invalid_image message: image_url must be a supported image details: retryable: false retry_blocked_reason: image_url must be a supported image retryBlockedReason: image_url must be a supported image retry_block_code: INVALID_VIDEO_IMAGE retryBlockCode: INVALID_VIDEO_IMAGE audioTooLong: value: error: audio_too_long message: Audio is too long (529s). Maximum supported duration is 300 seconds (5 minutes). Please use a shorter script or split into multiple videos. details: duration_seconds: 529.42 max_duration_seconds: 300 retryable: false retry_blocked_reason: Audio is too long (529s). Maximum supported duration is 300 seconds (5 minutes). Please use a shorter script or split into multiple videos. retryBlockedReason: Audio is too long (529s). Maximum supported duration is 300 seconds (5 minutes). Please use a shorter script or split into multiple videos. retry_block_code: VIDEO_CREATE_AUDIO_TOO_LONG retryBlockCode: VIDEO_CREATE_AUDIO_TOO_LONG UpstreamFailure: description: Puppetry could not create or inspect the requested asset. headers: Retry-After: $ref: '#/components/headers/RetryAfter' X-Puppetry-Next-Poll-At: $ref: '#/components/headers/NextPollAt' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: videoQueueFailed: value: error: video_queue_failed message: Animation service did not return a video job id details: retryable: true retry_after_seconds: 30 retryAfter: 30 retryAfterSeconds: 30 next_poll_at: '2026-06-22T08:50:30.000Z' nextPollAt: '2026-06-22T08:50:30.000Z' creditDebitUnavailable: value: error: credit_debit_unavailable message: Video credit debit is temporarily unavailable details: retryable: true retry_after_seconds: 30 retryAfter: 30 retryAfterSeconds: 30 next_poll_at: '2026-06-22T08:50:30.000Z' nextPollAt: '2026-06-22T08:50:30.000Z' assetProbeUnavailable: value: error: asset_probe_unavailable message: audio_url is temporarily unavailable for inspection details: retryable: true retry_after_seconds: 30 retryAfter: 30 retryAfterSeconds: 30 jobStatusUnavailable: value: error: job_status_unavailable message: Video job status is temporarily unavailable retryable: true retry_after_seconds: 10 retryAfter: 10 retryAfterSeconds: 10 next_poll_at: '2026-06-22T08:50:10.000Z' nextPollAt: '2026-06-22T08:50:10.000Z' credits: charged: 0 remaining: null creation_credits: charged: 1 remaining_after_debit: 299 remainingAfterDebit: 299 creationCredits: charged: 1 remaining_after_debit: 299 remainingAfterDebit: 299 details: retryable: true retry_after_seconds: 10 retryAfter: 10 retryAfterSeconds: 10 next_poll_at: '2026-06-22T08:50:10.000Z' nextPollAt: '2026-06-22T08:50:10.000Z' object: video_job id: api_video_abc123 job_id: api_video_abc123 jobId: api_video_abc123 task_id: api_video_abc123 taskId: api_video_abc123 status_url: /api/v1/videos/api_video_abc123 statusUrl: /api/v1/videos/api_video_abc123 credits: charged: 0 remaining: null creation_credits: charged: 1 remaining_after_debit: 299 remainingAfterDebit: 299 creationCredits: charged: 1 remaining_after_debit: 299 remainingAfterDebit: 299 created_at: '2026-06-22T08:50:00.000Z' createdAt: '2026-06-22T08:50:00.000Z' content_location: /api/v1/videos/api_video_abc123 contentLocation: /api/v1/videos/api_video_abc123 jobResultUnavailable: value: error: job_result_unavailable message: Video job result is temporarily unavailable retryable: true retry_after_seconds: 10 retryAfter: 10 retryAfterSeconds: 10 next_poll_at: '2026-06-22T08:50:10.000Z' nextPollAt: '2026-06-22T08:50:10.000Z' credits: charged: 0 remaining: null creation_credits: charged: 1 remaining_after_debit: 299 remainingAfterDebit: 299 creationCredits: charged: 1 remaining_after_debit: 299 remainingAfterDebit: 299 details: retryable: true retry_after_seconds: 10 retryAfter: 10 retryAfterSeconds: 10 next_poll_at: '2026-06-22T08:50:10.000Z' nextPollAt: '2026-06-22T08:50:10.000Z' object: video_job id: api_video_abc123 job_id: api_video_abc123 jobId: api_video_abc123 task_id: api_video_abc123 taskId: api_video_abc123 status_url: /api/v1/videos/api_video_abc123 statusUrl: /api/v1/videos/api_video_abc123 credits: charged: 0 remaining: null creation_credits: charged: 1 remaining_after_debit: 299 remainingAfterDebit: 299 creationCredits: charged: 1 remaining_after_debit: 299 remainingAfterDebit: 299 created_at: '2026-06-22T08:50:00.000Z' createdAt: '2026-06-22T08:50:00.000Z' content_location: /api/v1/videos/api_video_abc123 contentLocation: /api/v1/videos/api_video_abc123 jobFinalizationUnavailable: value: error: job_finalization_unavailable message: Video job finalization is temporarily unavailable retryable: true retry_after_seconds: 10 retryAfter: 10 retryAfterSeconds: 10 next_poll_at: '2026-06-22T08:50:10.000Z' nextPollAt: '2026-06-22T08:50:10.000Z' credits: charged: 0 remaining: null creation_credits: charged: 1 remaining_after_debit: 299 remainingAfterDebit: 299 creationCredits: charged: 1 remaining_after_debit: 299 remainingAfterDebit: 299 details: retryable: true retry_after_seconds: 10 retryAfter: 10 retryAfterSeconds: 10 next_poll_at: '2026-06-22T08:50:10.000Z' nextPollAt: '2026-06-22T08:50:10.000Z' object: video_job id: api_video_abc123 job_id: api_video_abc123 jobId: api_video_abc123 task_id: api_video_abc123 taskId: api_video_abc123 status_url: /api/v1/videos/api_video_abc123 statusUrl: /api/v1/videos/api_video_abc123 credits: charged: 0 remaining: null creation_credits: charged: 1 remaining_after_debit: 299 remainingAfterDebit: 299 creationCredits: charged: 1 remaining_after_debit: 299 remainingAfterDebit: 299 created_at: '2026-06-22T08:50:00.000Z' createdAt: '2026-06-22T08:50:00.000Z' content_location: /api/v1/videos/api_video_abc123 contentLocation: /api/v1/videos/api_video_abc123 headers: NextPollAt: description: Absolute UTC retry or poll time. Mirrors next_poll_at/nextPollAt in the JSON body. schema: type: string format: date-time example: '2026-06-22T08:50:30.000Z' RetryAfter: description: Seconds to wait before retrying a limited/transient request or polling a non-terminal video job again. schema: type: integer example: 30 RateLimitRemaining: description: Requests remaining in the current one-minute window. schema: type: integer example: 9 RateLimitReset: description: Unix timestamp, in seconds, when the current rate-limit window resets. schema: type: integer example: 1778520000 RateLimitLimit: description: Requests allowed in the current one-minute window. schema: type: integer example: 10 schemas: ErrorResponse: type: object required: - error - message properties: error: type: string examples: - invalid_request - unauthorized - rate_limited message: type: string object: type: string const: video_job description: Present on retryable transient video-status errors when the affected API video job is known. id: type: string description: Present on retryable transient video-status errors. API video job ID to keep polling. credits: type: object description: Present on retryable transient video-status errors for accepted API video jobs. Status polls are never billed, so charged is 0. properties: charged: type: integer minimum: 0 example: 0 remaining: type: - integer - 'null' minimum: 0 description: Current balance if available; null when the status poll did not fetch credit state. additionalProperties: false creation_credits: $ref: '#/components/schemas/VideoJobCreationCredits' description: Original create-time video credit debit evidence for the affected API video job when available. creationCredits: $ref: '#/components/schemas/VideoJobCreationCredits' description: SDK-style alias for creation_credits. retryable: type: boolean description: Present on retryable transient errors and non-retryable terminal video-create/status errors so generated clients can identify retry/backoff behavior without parsing details. retry_blocked_reason: type: string description: Present on non-retryable terminal video-create/status errors. Redacted reason explaining why clients should not retry the same request. retryBlockedReason: type: string description: SDK-style alias for retry_blocked_reason. retry_block_code: type: string description: Present on non-retryable terminal video-create/status errors. Stable retry-block code suitable for generated clients. retryBlockCode: type: string description: SDK-style alias for retry_block_code. retry_after_seconds: type: integer minimum: 0 description: Present on retryable transient errors. Mirrors Retry-After and details.retry_after_seconds. retryAfter: type: integer minimum: 0 description: SDK-style alias for retry_after_seconds. retryAfterSeconds: type: integer minimum: 0 description: CamelCase alias for retry_after_seconds. next_poll_at: type: string format: date-time description: Present on retryable transient errors. Absolute UTC time when clients should retry the request or poll again. nextPollAt: type: string format: date-time description: SDK-style alias for next_poll_at. job_id: type: string description: Present on retryable transient video-status errors so clients can correlate the retry to the API video job. jobId: type: string description: SDK-style alias for job_id. task_id: type: string description: Task-style alias for job_id on retryable transient video-status errors. taskId: type: string description: CamelCase task-style alias for job_id. source: type: string enum: - text - audio description: Video creation source for the affected API video job. request_source: type: string enum: - text - audio description: Alias for source. requestSource: type: string enum: - text - audio description: SDK-style alias for source. status_url: type: string description: Present on retryable transient video-status errors so clients know which job resource to poll next. statusUrl: type: string description: SDK-style alias for status_url. created_at: type: string format: date-time description: Creation time for the affected API video job. createdAt: type: string format: date-time description: SDK-style alias for created_at. expires_at: type: string format: date-time description: When the affected API video job record expires from status lookup. expiresAt: type: string format: date-time description: SDK-style alias for expires_at. details: description: Endpoint-specific diagnostic fields. type: object additionalProperties: true content_location: type: string description: Body alias for the status poll URL, mirroring the Content-Location response header target for clients that cannot reliably read headers. contentLocation: type: string description: SDK-style alias for content_location. additionalProperties: true VideoJobCreationCredits: type: object required: - charged - remaining_after_debit properties: charged: type: integer minimum: 0 example: 1 description: Video credits charged when the Developer API video job was created. Historical jobs may omit creation_credits when this evidence was not recorded. remaining_after_debit: type: integer nullable: true minimum: 0 example: 299 description: Workspace credit balance immediately after the original create debit, or null when unavailable. remainingAfterDebit: type: integer nullable: true minimum: 0 example: 299 description: SDK-style alias for remaining_after_debit. additionalProperties: false TtsResponse: type: object required: - object - id - voice_id - audio_url - format - usage properties: object: type: string const: audio id: type: string description: Generated audio asset ID. voice_id: type: string example: puppetry-af_bella audio_url: type: string format: uri description: Hosted URL for the generated WAV audio. format: type: string const: wav usage: type: object required: - voice_characters properties: voice_characters: type: object required: - used - limit - remaining properties: used: type: integer example: 20 limit: type: integer example: 100000 remaining: type: integer example: 99980 additionalProperties: false additionalProperties: false additionalProperties: false TtsRequest: type: object required: - voice_id - text properties: voice_id: type: string description: Public voice ID from GET /api/v1/voices/puppetry. example: puppetry-af_bella text: type: string minLength: 1 maxLength: 5000 description: Text to synthesize. example: Hello from Puppetry. speed: type: number minimum: 0.5 maximum: 2 default: 1 description: Speech speed multiplier. additionalProperties: false securitySchemes: developerApiKey: type: http scheme: bearer bearerFormat: Puppetry API key description: 'Use the Authorization header with a live Puppetry API key: Bearer pk_live_...'