openapi: 3.2.0 info: title: thirds.ai Keys API version: 1.0.0 description: Turn one design into content at scale. Create branded images and PDFs for your campaigns and clients. Automate each new version through our API or your AI tools. Render saved templates with new data, or send HTML directly. Every error uses one envelope, every response carries an x-request-id header, and every JSON request body rejects fields it does not expect. servers: - url: https://thirds.ai tags: - name: Keys paths: /v1/keys: post: summary: Create an API key description: Create one API key for the authenticated account. The full secret is returned in this response and never again; only its verifier is kept. operationId: createApiKey security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/NewApiKey' responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '201': description: The key was created. This is the only response that ever carries the secret. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ApiKeyWithSecret' '400': description: The name was empty, longer than 100 characters, or held a character that is not printable ASCII. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '413': description: The request body is larger than 16 KiB. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '415': description: The request did not carry a JSON content type. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '401': description: The request did not carry a valid, active API key. headers: x-request-id: $ref: '#/components/headers/XRequestId' WWW-Authenticate: description: Always "Bearer" on this response. schema: type: string content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '409': description: The account already holds ten active keys. Revoke one before creating another. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '500': description: An internal error occurred. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' tags: - Keys get: summary: List API keys description: List one bounded page of the authenticated account's keys, most recently created first. A revoked key stays in this history; its secret never appears here. operationId: listApiKeys security: - bearerAuth: [] parameters: - name: limit in: query required: false description: The page size. Defaults to 20 and must be from 1 through 100. schema: type: integer minimum: 1 maximum: 100 default: 20 - name: cursor in: query required: false description: An opaque value from an earlier page's next_cursor. schema: type: string maxLength: 128 responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '200': description: One page of the account's key history. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ApiKeyList' '400': description: The page size or cursor is not valid. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '401': description: The request did not carry a valid, active API key. headers: x-request-id: $ref: '#/components/headers/XRequestId' WWW-Authenticate: description: Always "Bearer" on this response. schema: type: string content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '500': description: An internal error occurred. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' tags: - Keys /v1/keys/{key_id}: delete: summary: Revoke an API key description: Revoke one key of the authenticated account. Revoking a key that is already revoked returns the same answer again rather than an error, so a retried request is never rejected. operationId: revokeApiKey security: - bearerAuth: [] parameters: - name: key_id in: path required: true description: The key's public identifier, such as "key_1f8b3c7d5e2a49061f8b3c7d5e2a4906". schema: type: string pattern: ^key_[0-9a-f]{32}$ responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '200': description: The key, now revoked. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ApiKey' '401': description: The request did not carry a valid, active API key. headers: x-request-id: $ref: '#/components/headers/XRequestId' WWW-Authenticate: description: Always "Bearer" on this response. schema: type: string content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: No key with this identifier belongs to the authenticated account. This is also the answer for an identifier that does not exist at all, so a request can never learn which one is true. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '500': description: An internal error occurred. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' tags: - Keys patch: summary: Set or clear an API key's monthly overage cap description: 'Change how many overage credits this key may take on per calendar month (UTC). A positive number sets the cap; an explicit null, or a body without the field, clears it. The cap bounds only overage: work the prepaid balance fully covers is never refused by it.' operationId: updateApiKey security: - bearerAuth: [] parameters: - name: key_id in: path required: true description: The key's public identifier, such as "key_1f8b3c7d5e2a49061f8b3c7d5e2a4906". schema: type: string pattern: ^key_[0-9a-f]{32}$ requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ApiKeyUpdate' responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '200': description: The key with its new cap. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ApiKey' '400': description: The cap was zero, negative, not a whole number, or the body held a field this operation does not know. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '413': description: The request body is larger than 16 KiB. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '415': description: The request did not carry a JSON content type. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '401': description: The request did not carry a valid, active API key. headers: x-request-id: $ref: '#/components/headers/XRequestId' WWW-Authenticate: description: Always "Bearer" on this response. schema: type: string content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: No key with this identifier belongs to the authenticated account. This is also the answer for an identifier that does not exist at all, so a request can never learn which one is true. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '500': description: An internal error occurred. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' tags: - Keys components: responses: MethodNotAllowed: description: The method is not allowed on this route. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' RequestHeadersTooLarge: description: The request has more than 64 headers or more than 32 KiB of header names and values. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' schemas: ErrorEnvelope: type: object description: The one error shape every backend response uses. properties: error: type: object properties: code: type: string enum: - account_suspended - account_concurrency_limited - abuse_limited - ai_failure_limit_reached - ai_needs_paid_credits - already_subscribed - auth_unavailable - batch_row_limit - billing_unavailable - brand_asset_account_limit_reached - brand_asset_invalid - brand_asset_kind_invalid - brand_asset_kind_mismatch - brand_asset_limit_reached - brand_asset_too_large - brand_asset_type_unsupported - brand_data_conflict - brand_font_glyphs_exceeded - brand_font_tables_invalid - brand_image_animated - brand_image_dimensions_invalid - brand_image_pixels_exceeded - brand_kit_colours_invalid - brand_kit_conflict - brand_kit_limit - brand_kit_name_invalid - brand_kit_not_found - brand_kit_patch_empty - brand_kit_tone_invalid - brand_kit_unavailable - captcha_rejected - checkout_superseded - cross_origin_rejected - csrf_rejected - download_expired - email_already_set - gallery_template_not_found - generated_template_invalid - idempotency_conflict - image_asset_header_unsupported - image_asset_invalid - image_asset_limit - image_asset_not_found - image_asset_reference_invalid - image_asset_too_large - image_asset_type_unsupported - image_asset_unavailable - image_url_invalid - image_url_unavailable - insufficient_credits - internal_error - invalid_cursor - invalid_email - invalid_event - invalid_link - invalid_profile - invalid_request - invalid_upload - job_not_finished - key_concurrency_limited - key_limit_reached - method_not_allowed - no_billing_customer - not_found - operation_conflict - operation_limit_exceeded - operation_pending - overage_limit_reached - overage_unavailable - overloaded - playground_busy - playground_request_invalid - playground_selection_invalid - playground_session_limited - playground_unavailable - policy_version_stale - provider_unavailable - rate_limited - render_probe_busy - render_probe_failed - render_probe_not_configured - render_probe_required - render_probe_timeout - request_headers_too_large - request_too_large - resize_timeout - signed_out - spend_cap_reached - template_build_not_found - template_data_collection_limit - template_data_depth_limit - template_data_invalid - template_data_limit - template_depth_limit - template_draft_not_found - template_evaluation_error - template_invalid_filter_input - template_missing_data - template_output_limit - template_not_found - template_schema_complexity - template_schema_draft_unsupported - template_schema_invalid - template_schema_too_large - template_size_canvas_mismatch - template_size_data_overrides_too_large - template_size_duplicate_id - template_size_invalid_dimensions - template_size_invalid_id - template_size_invalid_name - template_sizes_too_large - template_sizes_too_many - template_source_limit - template_syntax_error - template_timeout - template_version_changed - template_work_limit - testimonial_busy - testimonial_invalid - testimonial_rate_limited - testimonial_unavailable - unauthorized - unsupported_media_type - webhook_limit_reached description: A fixed, machine-readable error code. message: type: string description: A fixed, human-readable message. request_id: type: string format: uuid description: The identifier this answer also carries in its x-request-id header. details: type: array description: Present on a validation failure. Schema failures return at most 16 entries with bounded data paths and fixed reasons. Values from the request are never repeated. items: $ref: '#/components/schemas/FieldDetail' retry: $ref: '#/components/schemas/RetryInfo' plan_limit: $ref: '#/components/schemas/PlanLimitInfo' required: - code - message - request_id additionalProperties: false required: - error additionalProperties: false NewApiKey: type: object description: What a new key is named. properties: name: type: string minLength: 1 maxLength: 100 pattern: ^(?=.*[^ ])[ -~]+$ description: 1 to 100 printable ASCII characters. required: - name additionalProperties: false ApiKey: type: object description: One key, without its secret. properties: id: type: string pattern: ^key_[0-9a-f]{32}$ description: 'The key''s public identifier: "key_" followed by 32 lowercase hexadecimal characters.' name: type: string display_prefix: type: string description: The first characters of the secret, enough to tell keys apart in a list. status: type: string enum: - active - revoked created_at: type: string format: date-time revoked_at: type: - string - 'null' format: date-time last_used_at: type: - string - 'null' format: date-time monthly_overage_cap: type: - integer - 'null' minimum: 1 description: The most overage credits this key may take on per calendar month (UTC). Null means the key sets no cap of its own. required: - id - name - display_prefix - status - created_at - revoked_at - last_used_at - monthly_overage_cap additionalProperties: false ApiKeyList: type: object description: One bounded page of the account's key history. properties: data: type: array maxItems: 100 items: $ref: '#/components/schemas/ApiKey' next_cursor: type: - string - 'null' maxLength: 128 required: - data - next_cursor additionalProperties: false RetryInfo: type: object description: How long the caller must wait before it retries. properties: retry_after_seconds: type: integer minimum: 1 maximum: 60 required: - retry_after_seconds additionalProperties: false ApiKeyWithSecret: type: object description: One key exactly as creation answers it. This is the only shape that ever carries the secret. properties: id: type: string pattern: ^key_[0-9a-f]{32}$ description: 'The key''s public identifier: "key_" followed by 32 lowercase hexadecimal characters.' name: type: string secret: type: string pattern: ^thirds_sk_v1_[0-9a-f]{64}$ description: 'The full secret: "thirds_sk_v1_" followed by 64 lowercase hexadecimal characters. It is shown here once and never again.' display_prefix: type: string description: The first characters of the secret, enough to tell keys apart in a list. status: type: string enum: - active created_at: type: string format: date-time required: - id - name - secret - display_prefix - status - created_at additionalProperties: false FieldDetail: type: object description: One request field that failed, its fixed safe reason, and an optional bounded source location. Details never carry template source, customer values, rendered output, or raw evaluator prose. properties: field: type: string maxLength: 260 description: The path to the field, such as "pdf.scale". Schema errors use data followed by a JSON Pointer, such as data/items/0/count. The pointer is cut at 256 UTF-8 bytes. Empty for a problem with the whole document. reason: type: string enum: - malformed JSON - missing field - unknown field - wrong type - invalid value - A required value is missing. - Use the expected value type. - Declare this variable before using it. - Choose an allowed value. - Use the required format. - Add a value. - Use a shorter value. - Use a number within the allowed range. - Check this value against its data rule. description: A fixed, safe reason. It never repeats the value the caller sent. line: type: integer minimum: 1 maximum: 1000000 description: The one-based template source line when the evaluator provides one within the published bound. column: type: integer minimum: 1 maximum: 1000000 description: The one-based template source column when the evaluator provides one within the published bound. required: - field - reason additionalProperties: false ApiKeyUpdate: type: object description: What an update may change about a key. properties: name: type: string minLength: 1 maxLength: 100 pattern: ^(?=.*[!-~])[ -~]+$ description: New key name. The key secret and prefix stay the same. monthly_overage_cap: type: - integer - 'null' minimum: 1 description: The most overage credits this key may take on per calendar month (UTC), or null to clear the cap. Omit it to keep the current cap. additionalProperties: false PlanLimitInfo: type: object description: 'Present when the account''s plan causes the refusal: brand_kit_limit and batch_row_limit. It names the limit and the plan that sets it, so a caller can act without a second request. The message text never names the number.' properties: plan: type: string enum: - scale - growth - starter - pack - free description: The account's plan at the time of the refusal. limit: type: integer minimum: 1 description: 'What that plan allows: brand kits for brand_kit_limit, rows in one run for batch_row_limit.' required: - plan - limit additionalProperties: false headers: XRequestId: description: The UUID that identifies this request and matches error.request_id on an error response. required: true schema: type: string format: uuid securitySchemes: sessionCookie: type: apiKey in: cookie name: __Host-thirds_session description: A browser session. Browser writes also require the matching x-csrf-token header from GET /v1/me. bearerAuth: type: http scheme: bearer description: 'An API key''s secret, sent as "Authorization: Bearer thirds_sk_v1_...".' x-unmatched-v1-responses: description: A request below /v1 that matches no operation receives the shared safe envelope. OpenAPI has no standard path item for an unmatched route, so this extension records the fallback contract without claiming that a catch-all operation exists. '404': $ref: '#/components/responses/NotFound' '431': $ref: '#/components/responses/RequestHeadersTooLarge'