openapi: 3.2.0 info: title: thirds.ai Templates 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: Templates paths: /v1/templates: post: summary: Create a saved template operationId: createTemplate security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TemplateWriteRequest' responses: '201': description: The created template, including version 1 source and schema. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateDetail' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '413': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' tags: - Templates get: summary: List saved templates description: List the account's active and archived templates newest first. Source and schema are omitted. Pass next_cursor to continue with a stable keyset page. operationId: listTemplates security: - bearerAuth: [] parameters: - name: status in: query schema: type: string enum: - active - archived - all default: all - name: q in: query description: Search names on the server. schema: type: string maxLength: 120 pattern: ^[^\u0000-\u001f\u007f-\u009f]*$ - name: tag in: query description: Match one exact tag on the server. schema: type: string minLength: 1 maxLength: 40 pattern: ^[^\u0000-\u001f\u007f-\u009f]+$ - name: limit in: query schema: type: integer default: 20 - name: cursor in: query schema: type: string maxLength: 128 responses: '200': description: One page of saved-template summaries. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplatePage' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' tags: - Templates /v1/templates/preview: post: summary: Preview template data as HTML description: Expand source and a JSON data object through the bounded template engine. Optional schema validation uses the saved-template rules. This does not save a template, create a render, or spend credits. The response is untrusted customer HTML for an isolated preview. Browser sessions need the current CSRF token. operationId: previewTemplate security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TemplatePreviewRequest' responses: '200': description: 'The expanded HTML. This response uses Cache-Control: private, no-store.' headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplatePreview' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '405': $ref: '#/components/responses/MethodNotAllowed' '413': $ref: '#/components/responses/Error' '415': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '503': $ref: '#/components/responses/Error' tags: - Templates /v1/templates/adapt: post: summary: Adapt one HTML source to many canvas sizes description: Deterministically adapt one canonical HTML source to up to 20 target canvas sizes. This is free, needs a session or key, and uses the preview rate limit. It does not save a template, create a render, or spend credits. The response gives the adapted source per size, or a safe error code for that size. operationId: adaptTemplate security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TemplateAdaptRequest' responses: '200': description: One entry per requested size, each with its adapted source or a safe error code. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateAdaptResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '405': $ref: '#/components/responses/MethodNotAllowed' '413': $ref: '#/components/responses/Error' '415': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '504': $ref: '#/components/responses/Error' tags: - Templates /v1/templates/{template_id}: parameters: - name: template_id in: path required: true schema: $ref: '#/components/schemas/TemplateId' get: summary: Get a saved template operationId: getTemplate security: - bearerAuth: [] responses: '200': description: The template and its latest immutable version. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateDetail' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' tags: - Templates patch: summary: Rename a saved template description: Set or clear the name of one owned template. The name is trimmed and can have 1 to 120 characters. A null or blank name clears it. operationId: renameTemplate security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TemplateRename' responses: '200': description: The template with its new name. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateSummary' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '413': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' tags: - Templates delete: summary: Archive a saved template description: Archive one owned template. The operation is idempotent. Archived templates and versions remain readable, but they cannot receive another version or create another render. operationId: archiveTemplate security: - bearerAuth: [] responses: '200': description: The template is archived. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateSummary' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' tags: - Templates /v1/templates/{template_id}/versions: parameters: - name: template_id in: path required: true schema: $ref: '#/components/schemas/TemplateId' post: summary: Create a template version description: Edit a template by publishing one complete new immutable source and optional schema. Existing versions never change. Archived templates cannot receive another version. operationId: createTemplateVersion security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TemplateVersionWriteRequest' responses: '201': description: The new immutable version, including source and schema. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateVersionDetail' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '413': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' tags: - Templates get: summary: List template versions description: List immutable versions newest first. Source and schema are omitted. Archived template versions remain readable. operationId: listTemplateVersions security: - bearerAuth: [] parameters: - name: limit in: query schema: type: integer default: 20 - name: cursor in: query schema: type: string maxLength: 32 responses: '200': description: One page of immutable version summaries. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateVersionPage' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' tags: - Templates /v1/templates/{template_id}/versions/{version}: parameters: - name: template_id in: path required: true schema: $ref: '#/components/schemas/TemplateId' - name: version in: path required: true schema: type: integer minimum: 1 get: summary: Get a template version operationId: getTemplateVersion security: - bearerAuth: [] responses: '200': description: The exact immutable template version. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateVersionDetail' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' tags: - Templates components: schemas: TemplateWriteRequest: type: object properties: tags: $ref: '#/components/schemas/TemplateTags' name: $ref: '#/components/schemas/TemplateName' input_mode: type: string enum: - blank - html default: html description: How this new template starts. It records a blank-page start or imported HTML. New versions do not accept this field. source: type: string maxLength: 1048576 schema: oneOf: - type: object - type: boolean - type: 'null' sizes: $ref: '#/components/schemas/SizeVariantList' required: - source additionalProperties: false TemplateVersionWriteRequest: type: object properties: source: type: string maxLength: 1048576 schema: oneOf: - type: object - type: boolean - type: 'null' sizes: $ref: '#/components/schemas/SizeVariantList' required: - source additionalProperties: false TemplateRename: type: object description: Omitted fields keep their current values. Set name to null to clear it, tags to an empty array to clear them, or archived_at to null to restore the template. properties: archived_at: type: 'null' description: Restore the item. Omit this field to keep its archive state. tags: $ref: '#/components/schemas/TemplateTags' name: $ref: '#/components/schemas/TemplateName' additionalProperties: false TemplateVersionDetail: type: object properties: version: type: integer minimum: 1 created_at: type: string format: date-time source: type: string maxLength: 1048576 schema: {} sizes: $ref: '#/components/schemas/SizeVariantList' required: - version - created_at - source - schema - sizes additionalProperties: false SizeVariant: type: object description: One saved canvas size for a template version. `docs/SMART-RESIZE.md` owns the rules. properties: id: type: string pattern: ^[a-z0-9][a-z0-9-]{0,39}$ description: Unique within the version. The id original is reserved for the version's own canvas. name: type: string minLength: 1 maxLength: 120 width: type: integer minimum: 320 maximum: 7680 height: type: integer minimum: 200 maximum: 4320 status: type: string enum: - auto - edited source: type: string maxLength: 1048576 description: The source canvas must declare exactly width by height. data_overrides: type: object description: Merged over the version's rendered data for this size only. required: - id - name - width - height - status - source additionalProperties: false TemplateTags: type: array maxItems: 20 uniqueItems: true items: type: string minLength: 1 maxLength: 40 description: Trimmed tags. Each tag is nonblank. Case is preserved. TemplateSummary: type: object properties: tags: $ref: '#/components/schemas/TemplateTags' id: $ref: '#/components/schemas/TemplateId' name: $ref: '#/components/schemas/TemplateName' created_at: type: string format: date-time archived_at: type: - string - 'null' format: date-time latest_version: type: integer minimum: 1 required: - id - name - tags - created_at - archived_at - latest_version 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 TemplatePreview: type: object properties: html: type: string maxLength: 5242880 required: - html additionalProperties: false TemplatePreviewRequest: type: object properties: brand_kit_id: type: - string - 'null' pattern: ^kit_[a-f0-9]{32}$ description: Select an owned active brand kit. This uses template evaluation and its 1 MiB source and data limits even when data is omitted. Overrides the source thirds-brand-kit meta default. Supplies reserved brand.name, brand.palette, brand.colours.primary/secondary/accent, brand.logo, brand.logos, and brand.fonts entries with family and src. data.brand is refused when a kit is selected. Only present palette roles and logos are supplied. Assets are captured before enqueue. Schemas validate customer data before brand is added. source: type: string maxLength: 1048576 data: type: object description: Template values, bounded to 1,048,576 encoded JSON bytes with the template depth and collection limits. schema: oneOf: - type: object - type: boolean - type: 'null' required: - source - data additionalProperties: false TemplateAdaptResponse: type: object properties: sizes: type: array items: type: object properties: width: type: integer height: type: integer source: type: string maxLength: 1048576 error: type: object properties: code: type: string enum: - resize_source_unsupported - resize_no_layout required: - code additionalProperties: false required: - width - height additionalProperties: false required: - sizes additionalProperties: false TemplateId: type: string pattern: ^tpl_[0-9a-f]{32}$ TemplateVersionPage: type: object properties: items: type: array maxItems: 100 items: $ref: '#/components/schemas/TemplateVersionSummary' next_cursor: type: - string - 'null' maxLength: 32 required: - items - next_cursor additionalProperties: false TemplateName: type: - string - 'null' maxLength: 120 description: The customer's own name for a template. It is trimmed and can have 1 to 120 characters. Null or blank means no name. ArchivePeriod: type: object properties: archived_at: type: string format: date-time restored_at: type: - string - 'null' format: date-time required: - archived_at - restored_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 TemplateDetail: type: object properties: archive_history: type: array items: $ref: '#/components/schemas/ArchivePeriod' tags: $ref: '#/components/schemas/TemplateTags' id: $ref: '#/components/schemas/TemplateId' name: $ref: '#/components/schemas/TemplateName' created_at: type: string format: date-time archived_at: type: - string - 'null' format: date-time latest_version: type: integer minimum: 1 source: type: string maxLength: 1048576 schema: {} sizes: $ref: '#/components/schemas/SizeVariantList' required: - archive_history - id - name - tags - created_at - archived_at - latest_version - source - schema - sizes additionalProperties: false TemplateAdaptRequest: type: object description: Adapt one HTML source to many target canvas sizes with the deterministic layout algorithm. This is free and does not save anything. properties: source: type: string maxLength: 1048576 sizes: type: array maxItems: 20 items: type: object properties: width: type: integer minimum: 320 maximum: 7680 height: type: integer minimum: 200 maximum: 4320 required: - width - height additionalProperties: false required: - source - sizes additionalProperties: false 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 TemplateVersionSummary: type: object properties: version: type: integer minimum: 1 created_at: type: string format: date-time required: - version - created_at additionalProperties: false TemplatePage: type: object properties: tag_options: type: array maxItems: 100 uniqueItems: true items: type: string minLength: 1 maxLength: 40 description: The first 100 distinct account tags, sorted. Use the tag filter for any other tag. items: type: array maxItems: 100 items: $ref: '#/components/schemas/TemplateSummary' next_cursor: type: - string - 'null' maxLength: 128 required: - items - next_cursor - tag_options additionalProperties: false SizeVariantList: type: array maxItems: 20 items: $ref: '#/components/schemas/SizeVariant' 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 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' AccountSuspended: description: The authenticated account is suspended. The code is account_suspended. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' Error: description: The one error envelope every backend response uses. 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' 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'