openapi: 3.1.0 info: title: thirds.ai 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 paths: /v1/webhooks/{webhook_id}/test: post: summary: Send a signed test event description: Queue one sample render.succeeded event through the normal delivery worker. It spends no credits and creates no render. A recorded result schedules no retry, and tests cannot be replayed. Recovery can resend an unfinished attempt with the same event ID. Disabled destinations can be tested. One test per destination can wait at a time; completed tests are limited to one per minute. The destination signing secrets and egress policy apply. operationId: testWebhook security: - bearerAuth: [] - sessionCookie: [] parameters: - name: webhook_id in: path required: true description: The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". schema: type: string pattern: ^wh_[0-9a-f]{32}$ responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '401': description: A valid session or API key is required. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '403': $ref: '#/components/responses/AccountSuspended' '429': description: The request limit was reached. Retry after the time in Retry-After. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '500': description: The request failed internally. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: No destination with this ID belongs to this account. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '200': description: The test attempt finished. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/WebhookTest' '202': description: The test is still queued or in flight. Read the delivery log for its result before sending another. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/WebhookTest' /v1/webhooks/{webhook_id}/deliveries: get: summary: Read recent webhook attempts description: Return up to 20 newest attempts for this destination, in reverse time order. Attempts expire after 30 days. No request or response body is stored. operationId: listWebhookDeliveries security: - bearerAuth: [] - sessionCookie: [] parameters: - name: webhook_id in: path required: true description: The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". schema: type: string pattern: ^wh_[0-9a-f]{32}$ responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '401': description: A valid session or API key is required. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '403': $ref: '#/components/responses/AccountSuspended' '429': description: The request limit was reached. Retry after the time in Retry-After. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '500': description: The request failed internally. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: No destination with this ID belongs to this account. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '200': description: The latest attempts for this destination. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/WebhookDeliveryList' /v1/me/identities/email: post: summary: Add an email to an account without one description: Requires a signed-in browser session and its CSRF token. Sends a one-use verification link bound to that account and exact session. Open the link in the same browser while signed in, then press Complete sign in. POST /v1/auth/email/confirm proves the address and grants the current free monthly credits once. An account with an email cannot change it through this route. operationId: requestAccountEmail security: - sessionCookie: [] parameters: - name: x-csrf-token in: header required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AccountEmailRequest' responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '401': description: A live browser session is required. Expired, revoked, and suspended sessions are refused. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '403': description: The CSRF token is missing or does not match this browser session. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '429': description: The request limit was reached. Retry after the time in Retry-After. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '500': description: The request failed internally. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '202': description: The request was accepted. This does not confirm mail delivery or whether another account holds the address. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/AccountEmailAccepted' '400': description: The JSON request is invalid or has unknown fields. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '409': description: This account already has an email address. The code is email_already_set. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '422': description: The email address is invalid. The code is invalid_email. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '503': description: Email delivery is not configured. The code is auth_unavailable. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' /v1/image-assets/preview: post: summary: Preview a URL image description: Read a public HTTP or HTTPS PNG, JPEG, or WebP through the checked asset transport. Credentials, private destinations, unsafe DNS answers, and redirects to them are refused. At most five redirects and 15 seconds total. The same upload decoder checks the 10 MiB, dimension, pixel, and memory limits. Returns verified bytes without storage or a render charge. Browser sessions require CSRF. operationId: previewImageUrl security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object properties: url: type: string maxLength: 4096 required: - url additionalProperties: false responses: '200': description: The verified image bytes and dimensions after orientation. headers: x-request-id: $ref: '#/components/headers/XRequestId' x-image-width: schema: type: integer x-image-height: schema: type: integer content: image/png: schema: type: string format: binary image/jpeg: schema: type: string format: binary image/webp: schema: type: string format: binary '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/Error' '413': $ref: '#/components/responses/Error' '422': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '503': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/image-assets: post: summary: Upload an image description: Store one private, immutable PNG, JPEG, or WebP image. Send the raw bytes with the matching Content-Type. Maximum 10 MiB, 7680 by 4320 stored pixels, 32 million pixels, and 64 MiB decoded memory. Animated images are refused. Images and brand assets share an account limit of 100 files and 100 MiB. Use the returned reference in an HTML body img src, or in template data for an img src. Render creation checks ownership after template expansion and captures the exact bytes. References are not public URLs. PDF header and footer templates do not support image references. No render credit is charged for an upload. operationId: uploadImage security: - bearerAuth: [] requestBody: required: true content: image/png: schema: type: string format: binary image/jpeg: schema: type: string format: binary image/webp: schema: type: string format: binary responses: '201': description: The image is stored. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/UploadedImage' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/Error' '404': $ref: '#/components/responses/Error' '413': $ref: '#/components/responses/Error' '422': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '503': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/image-assets/{id}/content: parameters: - name: id in: path required: true schema: type: string pattern: ^asset_[a-f0-9]{32}$ get: summary: Read an owned image description: Return verified image or captured brand logo/font bytes to their owner. Missing and foreign assets return the same 404 error. The response uses private, no-store caching and nosniff. operationId: readImageContent security: - bearerAuth: [] responses: '200': description: The exact image bytes. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: font/woff2: schema: type: string format: binary image/png: schema: type: string format: binary image/jpeg: schema: type: string format: binary image/webp: schema: type: string format: binary '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/Error' '404': $ref: '#/components/responses/Error' '413': $ref: '#/components/responses/Error' '422': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '503': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/openapi.json: get: summary: Get the OpenAPI document description: Return this API contract as a JSON document. operationId: getOpenApiDocument responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '200': description: The OpenAPI document. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: type: object /v1/public-stats: get: summary: Read measured public facts description: Return the count of successful customer and playground files, the reviewed gallery template count, and the weakest uptime and current status from the public status page. The response is cached for five minutes. Customer content and account data never enter this response. operationId: getPublicStats responses: '200': description: The measured public facts. Uptime is unavailable when the status provider does not answer. headers: x-request-id: $ref: '#/components/headers/XRequestId' cache-control: schema: type: string description: The five-minute public cache policy. content: application/json: schema: $ref: '#/components/schemas/PublicStats' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/testimonials: post: summary: Offer a testimonial for private review description: Store one visitor's name, company, quote, and permission for owner review. Sending this request never publishes the quote. A same-site browser origin and a fresh CAPTCHA pass protect the form. operationId: submitTestimonial requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TestimonialRequest' responses: '201': description: The testimonial is stored for private owner review. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TestimonialSubmission' '400': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/Error' '413': $ref: '#/components/responses/Error' '415': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '503': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/pdf: post: summary: Create a PDF description: Render one PDF from raw HTML, an inline stateless template, or one owned saved-template version. An omitted saved version resolves to one exact immutable version before data validation, admission, idempotency, queueing, or billing. Template evaluation happens before storage and only evaluated HTML enters render input. Returns 200 for a terminal job or 202 while the accepted job remains queued or running. A terminal job can be succeeded, failed, or cancelled; check status before downloading. Send wait=false to skip the bounded wait. Every mode uses the same queue, renderer, retention, webhook, and fixed one-credit success price. operationId: createPdf security: - bearerAuth: [] parameters: - name: Idempotency-Key in: header required: false description: 1 to 255 printable ASCII characters. A repeated key with the same logical request replays one job; a different logical request conflicts. Stateless fingerprints include original source, sorted data, and effective render options. Saved-template fingerprints include the exact resolved template version, sorted data, and effective options. wait, request IDs, and transport choices do not affect the fingerprint. schema: type: string minLength: 1 maxLength: 255 pattern: ^[ -~]+$ requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PdfRequest' examples: raw_html: summary: Raw HTML value: html:

Quarterly report

Ready to share.

stateless_template: summary: Stateless template with one data object value: html: '

{{ report.title }}

Total: {{ report.total | currency }}

' data: report: title: Quarterly report total: 125000 responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '200': description: The job is terminal. Check status for succeeded, failed, or cancelled; HTTP 200 does not mean the render succeeded. headers: x-request-id: $ref: '#/components/headers/XRequestId' Idempotency-Replayed: description: '"true" when this answer replays a job an earlier request with the same idempotency key already created.' schema: type: string content: application/json: schema: $ref: '#/components/schemas/PdfJob' '202': description: The job was accepted and is still queued or running. Poll GET /v1/pdf/{id} for the terminal result. headers: x-request-id: $ref: '#/components/headers/XRequestId' Idempotency-Replayed: description: '"true" when this answer replays a job an earlier request with the same idempotency key already created.' schema: type: string Location: description: The job's own status URL, /v1/pdf/{id} — where to poll for the terminal result. schema: type: string Retry-After: description: How soon polling the status URL is worthwhile, in seconds. schema: type: string content: application/json: schema: $ref: '#/components/schemas/PdfJob' '400': description: The request failed validation, saved-template data did not match its schema, bounded template evaluation failed, or the Idempotency-Key header was the wrong shape. 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' '402': description: The account does not hold the one credit required for this render. The code is insufficient_credits. The reservation settles at one credit on success and is fully released on failure. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '409': description: This idempotency key was already used for a different logical request, or the selected saved-template version changed before admission completed. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '413': description: The request is larger than this build accepts. 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' '429': description: A direct fleet-protection limit was hit. The code is rate_limited, account_concurrency_limited, or key_concurrency_limited. Each key sustains five requests per second with an idle burst of twenty-one. headers: x-request-id: $ref: '#/components/headers/XRequestId' Retry-After: description: Seconds to wait before retrying. schema: type: integer minimum: 1 maximum: 60 content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '503': description: The service is at capacity. headers: x-request-id: $ref: '#/components/headers/XRequestId' Retry-After: description: Seconds to wait before retrying. schema: type: integer minimum: 1 maximum: 60 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' get: summary: List render history description: 'List the authenticated account''s render jobs, newest first. Failed or cancelled jobs removed through DELETE do not appear. Bounded by keyset: pass the previous page''s next_cursor to continue, rather than an offset, so a page already read stays stable while new jobs are created.' operationId: listPdfHistory security: - bearerAuth: [] parameters: - name: q in: query description: Case-insensitive search of references, template names, and file types. schema: type: string maxLength: 200 pattern: ^[^\u0000-\u001f\u007f-\u009f]*$ - name: limit in: query required: false description: 1 to 100. Defaults to 20. A value outside this range is clamped rather than refused. schema: type: integer default: 20 - name: cursor in: query required: false description: An opaque value from an earlier page's next_cursor. A cursor that cannot be read is refused with 400. schema: type: string maxLength: 128 responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '200': description: One page of the account's render history. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/PdfHistoryPage' '400': description: The limit parameter was not a whole number, the cursor could not be read, or the query held an unknown parameter. 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' /v1/image: post: summary: Create a static image description: Render one PNG, JPEG, or WebP from raw HTML, an inline stateless template, or one owned saved-template version. Returns 200 for a terminal job or 202 while the accepted job remains queued or running. A terminal job can be succeeded, failed, or cancelled; check status before downloading. Send wait=false to skip the bounded wait. A successful render costs exactly one credit; a failed or cancelled render costs zero. operationId: createImage security: - bearerAuth: [] parameters: - name: Idempotency-Key in: header required: false schema: type: string minLength: 1 maxLength: 255 pattern: ^[ -~]+$ requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ImageRequest' examples: png: value: html:

Quarterly report

image: format: png width: 1280 height: 720 transparent: false responses: '200': description: The job is terminal. Check status for succeeded, failed, or cancelled; HTTP 200 does not mean the render succeeded. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ImageJob' '202': description: The image job is durable and remains queued or running. headers: x-request-id: $ref: '#/components/headers/XRequestId' Location: schema: type: string Retry-After: schema: type: string content: application/json: schema: $ref: '#/components/schemas/ImageJob' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '402': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '409': $ref: '#/components/responses/Error' '413': $ref: '#/components/responses/Error' '415': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '503': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' get: summary: List image history description: 'List the authenticated account''s image jobs, newest first. Failed or cancelled jobs removed through DELETE do not appear. Bounded by keyset: pass the previous page''s next_cursor to continue, rather than an offset, so a page already read stays stable while new jobs are created.' operationId: listImageHistory security: - bearerAuth: [] parameters: - name: q in: query description: Case-insensitive search of references, template names, and file types. schema: type: string maxLength: 200 pattern: ^[^\u0000-\u001f\u007f-\u009f]*$ - name: limit in: query required: false description: 1 to 100. Defaults to 20. A value outside this range is clamped rather than refused. schema: type: integer default: 20 - name: cursor in: query required: false description: An opaque value from an earlier page's next_cursor. A cursor that cannot be read is refused with 400. schema: type: string maxLength: 128 responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '200': description: One page of the account's image history. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ImageHistoryPage' '400': description: The limit parameter was not a whole number, the cursor could not be read, or the query held an unknown parameter. 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' /v1/image/{id}: parameters: - name: id in: path required: true schema: type: string pattern: ^image_[0-9a-f]{32}$ get: summary: Get a static image's status operationId: getImage security: - bearerAuth: [] responses: '200': description: The image job's current status. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ImageJob' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' delete: summary: Delete an image file or an unsuccessful render description: Remove a successful job's file, or remove a failed or cancelled job from history. Repeated requests are safe. The job stays readable by ID, and billing, abuse limits, and idempotency keys stay unchanged. Queued and running jobs return 409. operationId: deleteImage security: - bearerAuth: [] responses: '200': description: The image job after file or history removal. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ImageJob' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '409': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/image-packs: post: summary: Create an image pack description: 'Render one saved template version at every requested size: original for the version''s own canvas, a saved size id, a preset slug, or a WxH custom size adapted from the version''s original. Creates one normal image job per size. A size that cannot be adapted fails immediately with resize_source_unsupported; the other sizes still run. The ledger reserves one credit per job, settles it on success, and releases it on failure.' operationId: createImagePack security: - bearerAuth: [] parameters: - name: Idempotency-Key in: header required: true schema: type: string minLength: 1 maxLength: 255 pattern: ^[ -~]+$ requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ImagePackCreate' examples: socialSet: value: template_id: tpl_2f6a2f6a2f6a2f6a2f6a2f6a2f6a2f6a version: 3 data: headline: Autumn sale format: png sizes: - square-post - story - 1200x628 responses: '201': description: The pack was created, or an earlier request with the same idempotency key and body is replayed. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ImagePack' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '409': $ref: '#/components/responses/Error' '413': $ref: '#/components/responses/Error' '415': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '503': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/image-packs/{id}: parameters: - name: id in: path required: true schema: type: string pattern: ^pack_[0-9a-f]{32}$ get: summary: Get an image pack's status description: Returns the pack with each member's own state and, once any member has succeeded, a signed archive_url for the ZIP of every succeeded file. operationId: getImagePack security: - bearerAuth: [] responses: '200': description: The pack's current status. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ImagePack' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/image-packs/{id}/retry: post: summary: Retry an image pack's failed members description: Creates one new job for every member whose last attempt failed or that never reached a job. A queued, running, or already succeeded member is left untouched. operationId: retryImagePack security: - bearerAuth: [] parameters: - name: id in: path required: true schema: type: string pattern: ^pack_[0-9a-f]{32}$ responses: '200': description: The pack after retry, with a new job for each member that was retried. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ImagePackRetry' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/carousels: post: summary: Create a carousel description: Render selected pages of one saved template version as images, in the requested order. Creates one normal image job per page. The ledger reserves one credit per job, settles it on success, and releases it on failure. A successful PDF of the same pages still costs one credit through POST /v1/pdf. operationId: createCarousel security: - bearerAuth: [] parameters: - name: Idempotency-Key in: header required: true schema: type: string minLength: 1 maxLength: 255 pattern: ^[ -~]+$ requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CarouselCreate' examples: launchSlides: value: template_id: tpl_2f6a2f6a2f6a2f6a2f6a2f6a2f6a2f6a version: 1 data: brand: Northline Studio format: png pages: - cover - message - offer - proof - action responses: '201': description: The carousel was created, or an earlier request with the same idempotency key and body is replayed. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/Carousel' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '409': $ref: '#/components/responses/Error' '413': $ref: '#/components/responses/Error' '415': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '503': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/carousels/{id}: parameters: - name: id in: path required: true schema: type: string pattern: ^carousel_[0-9a-f]{32}$ get: summary: Get a carousel's status description: Returns the carousel with each member's own state and, once any member has succeeded, a signed archive_url for the ZIP of every succeeded file. operationId: getCarousel security: - bearerAuth: [] responses: '200': description: The carousel's current status. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/Carousel' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/carousels/{id}/retry: post: summary: Retry a carousel's failed members description: Creates one new job for every member whose last attempt failed or that never reached a job. A queued, running, or already succeeded member is left untouched. operationId: retryCarousel security: - bearerAuth: [] parameters: - name: id in: path required: true schema: type: string pattern: ^carousel_[0-9a-f]{32}$ responses: '200': description: The carousel after retry, with a new job for each member that was retried. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/CarouselRetry' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/batches: post: summary: Create a render batch description: 'Render one saved template version once per data row as PDF, PNG, JPEG, or WebP. The account plan sets how many rows one run accepts: 10 on Free and a pack-only account, 50 on Starter, 100 on Growth, and 200 on Scale. A run over that limit answers 400 batch_row_limit with error.plan_limit. Validates every row against the version''s schema before any job exists. Creates one normal render job per row as account and key concurrency allow. Remaining rows stay pending until a later retry or the background drain. The ledger reserves one credit per admitted job, settles it on success, and releases it on failure or cancellation. quoted_credits is the row count, not a discount.' operationId: createBatch security: - bearerAuth: [] parameters: - name: Idempotency-Key in: header required: true schema: type: string minLength: 1 maxLength: 255 pattern: ^[ -~]+$ requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BatchCreate' examples: certificates: value: template_id: tpl_2f6a2f6a2f6a2f6a2f6a2f6a2f6a2f6a version: 1 format: pdf rows: - issuer: Fieldnote Learning learner: Alex Morgan course: Clear plans date: 4 September 2026 - issuer: Fieldnote Learning learner: Theo Lee course: Clear plans date: 4 September 2026 responses: '201': description: The batch was created, or an earlier request with the same idempotency key and body is replayed. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/Batch' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '409': $ref: '#/components/responses/Error' '413': $ref: '#/components/responses/Error' '415': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '503': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/batches/{id}: parameters: - name: id in: path required: true schema: type: string pattern: ^batch_[0-9a-f]{32}$ get: summary: Get a render batch's status description: Returns the batch with each row's own state and, once any row has succeeded, a signed archive_url for the ZIP of every succeeded file. GET never starts billed work. operationId: getBatch security: - bearerAuth: [] responses: '200': description: The batch's current status. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/Batch' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/batches/{id}/retry: post: summary: Retry a batch's failed, cancelled, or pending rows description: Creates one new job for every row whose last attempt failed or was cancelled, and admits still-pending rows as concurrency allows. A queued, running, or already succeeded row is left untouched. Retry never bills a row that already succeeded. operationId: retryBatch security: - bearerAuth: [] parameters: - name: id in: path required: true schema: type: string pattern: ^batch_[0-9a-f]{32}$ responses: '200': description: The batch after retry, with a new job for each row that was retried. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/BatchRetry' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/batches/{id}/cancel: post: summary: Cancel a batch's pending and queued rows description: Stops rows that have no job yet and cancels queued jobs. Running jobs finish. Succeeded files stay downloadable. Cancelled and failed rows cost nothing. operationId: cancelBatch security: - bearerAuth: [] parameters: - name: id in: path required: true schema: type: string pattern: ^batch_[0-9a-f]{32}$ responses: '200': description: The batch after cancel. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/Batch' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/template-builds: post: summary: Start a template build description: 'Start one durable Build a template conversation from words, supported HTML, one source image, or your own working template source. The fixed successful price is 50 credits. A build from source makes no model call and costs 0 credits. A failed build releases the full reservation. Free monthly credits cannot pay for AI work: an account whose trial, pack, and subscription credits cannot cover the price answers 402 ai_needs_paid_credits and holds nothing. An AI message often takes a minute or two.' operationId: createTemplateBuild security: - bearerAuth: [] parameters: - name: Idempotency-Key in: header required: true schema: type: string minLength: 1 maxLength: 255 pattern: ^[ -~]+$ requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TemplateBuildCreate' responses: '200': description: An earlier matching request was replayed. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateBuild' '202': description: The durable build was accepted. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateBuild' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '402': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '409': $ref: '#/components/responses/Error' '413': $ref: '#/components/responses/Error' '503': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/template-builds/{build_id}: parameters: - name: build_id in: path required: true schema: $ref: '#/components/schemas/TemplateBuildId' get: summary: Get a template build operationId: getTemplateBuild security: - bearerAuth: [] responses: '200': description: The current draft and latest message facts. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateBuild' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/template-builds/{build_id}/messages: parameters: - name: build_id in: path required: true schema: $ref: '#/components/schemas/TemplateBuildId' get: summary: List template chat messages operationId: listTemplateMessages security: - bearerAuth: [] responses: '200': description: The user-visible messages in chat order. One chat holds at most 100 messages. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: type: array maxItems: 100 items: $ref: '#/components/schemas/TemplateMessage' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' post: summary: Edit the working template description: 'Run one later AI edit. A text or HTML edit costs 25 credits. A new source image costs 50 credits. A source message replaces the draft with your own working template, makes no model call, and costs 0 credits. A failed edit keeps the previous valid draft and releases the reservation. Free monthly credits cannot pay for AI work: an account whose trial, pack, and subscription credits cannot cover the price answers 402 ai_needs_paid_credits and holds nothing. An AI message often takes a minute or two.' operationId: editTemplateBuild security: - bearerAuth: [] parameters: - name: Idempotency-Key in: header required: true schema: type: string minLength: 1 maxLength: 255 pattern: ^[ -~]+$ requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TemplateMessageCreate' responses: '200': description: An earlier matching edit was replayed. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateBuild' '202': description: The durable edit was accepted. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateBuild' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '402': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '409': $ref: '#/components/responses/Error' '413': $ref: '#/components/responses/Error' '503': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/template-builds/{build_id}/draft: parameters: - name: build_id in: path required: true schema: $ref: '#/components/schemas/TemplateBuildId' get: summary: Get the working template description: Read the current draft as editable source, sample data, schema, and output. The build has no draft until its first message succeeds. operationId: getTemplateBuildDraft security: - bearerAuth: [] responses: '200': description: The current working template. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateBuildDraft' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/template-builds/{build_id}/messages/{message_id}/draft: parameters: - name: build_id in: path required: true schema: $ref: '#/components/schemas/TemplateBuildId' - name: message_id in: path required: true schema: $ref: '#/components/schemas/TemplateMessageId' get: summary: Get a completed message draft description: Read the retained final source, sample data, schema, and output from a succeeded message in an owned build. The response excludes the brand snapshot. Reading it changes no state and costs no credits. Pending, failed, and cancelled messages have no final draft. operationId: getTemplateMessageDraft security: - bearerAuth: [] responses: '200': description: The final draft. Cache-Control is private, no-store. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateBuildDraft' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/template-builds/{build_id}/messages/{message_id}: parameters: - name: build_id in: path required: true schema: $ref: '#/components/schemas/TemplateBuildId' - name: message_id in: path required: true schema: $ref: '#/components/schemas/TemplateMessageId' delete: summary: Cancel a pending template message operationId: cancelTemplateMessage security: - bearerAuth: [] responses: '200': description: The message was cancelled and its reservation was released. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplateMessage' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '409': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/template-builds/{build_id}/publish: parameters: - name: build_id in: path required: true schema: $ref: '#/components/schemas/TemplateBuildId' post: summary: Publish the current working template description: Explicitly publish the current valid draft as one normal immutable saved-template version. Repeating publication of the same draft returns the same version. operationId: publishTemplateBuild security: - bearerAuth: [] responses: '200': description: This draft was already published. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplatePublication' '201': description: The immutable template version was published. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/TemplatePublication' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '404': $ref: '#/components/responses/Error' '409': $ref: '#/components/responses/Error' '500': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /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' 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' /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' /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' /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' 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' 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' /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' 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' /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' /v1/brand-kits: post: summary: Create a brand kit description: 'Create one mutable account-owned brand kit. Logos and fonts are uploaded through the kit''s asset route after creation. The account plan sets how many kits the account can keep: 1 on Free, a pack-only account, and Starter, 5 on Growth, and 20 on Scale. Every kit of the account counts, archived kits too. A kit over that limit answers 409 brand_kit_limit with error.plan_limit. An account above its limit after a downgrade keeps every kit and can still change and archive them.' operationId: createBrandKit security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BrandKitWrite' responses: '201': description: The brand kit was created. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/BrandKitDetail' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' '403': $ref: '#/components/responses/AccountSuspended' '409': $ref: '#/components/responses/Error' '413': $ref: '#/components/responses/Error' '415': $ref: '#/components/responses/Error' '422': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' get: summary: List brand kits description: List the account's active and archived brand kits newest first. Asset metadata is omitted from the page; read one kit for its current assets. operationId: listBrandKits security: - bearerAuth: [] parameters: - name: status in: query schema: type: string enum: - active - archived - all default: all - name: limit in: query schema: type: integer default: 20 - name: cursor in: query schema: type: string maxLength: 128 responses: '200': description: One page of brand-kit summaries. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/BrandKitPage' '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' /v1/brand-kits/{brand_kit_id}: parameters: - name: brand_kit_id in: path required: true schema: $ref: '#/components/schemas/BrandKitId' get: summary: Get a brand kit description: Return one owned kit and its current logo and font metadata. Replaced assets remain private durable content but are omitted. operationId: getBrandKit security: - bearerAuth: [] responses: '200': description: The brand kit and its current assets. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/BrandKitDetail' '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' patch: summary: Update a brand kit description: Replace any supplied mutable brand values. Omitted fields stay unchanged; a null tone_guidance clears it. Archived kits cannot change. operationId: updateBrandKit security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BrandKitPatch' responses: '200': description: The updated kit and its current assets. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/BrandKitDetail' '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' '415': $ref: '#/components/responses/Error' '422': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' delete: summary: Archive a brand kit description: Archive one owned kit. The operation is idempotent. The kit and every current or replaced asset stay durable for already captured references. operationId: archiveBrandKit security: - bearerAuth: [] responses: '204': description: The brand kit is archived. headers: x-request-id: $ref: '#/components/headers/XRequestId' '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' /v1/brand-kits/{brand_kit_id}/assets: parameters: - name: brand_kit_id in: path required: true schema: $ref: '#/components/schemas/BrandKitId' post: summary: Add a brand asset description: Add one logo or WOFF2 font to an active owned kit. The declared Content-Type and the complete bytes must agree. No customer bytes or storage reference appears in the response. operationId: createBrandAsset security: - bearerAuth: [] requestBody: required: true content: image/png: schema: type: string format: binary image/jpeg: schema: type: string format: binary image/webp: schema: type: string format: binary font/woff2: schema: type: string format: binary responses: '201': description: The immutable asset was accepted and selected by the kit. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/BrandAsset' '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' '415': $ref: '#/components/responses/Error' '422': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '503': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/brand-kits/{brand_kit_id}/assets/{asset_id}: parameters: - name: brand_kit_id in: path required: true schema: $ref: '#/components/schemas/BrandKitId' - name: asset_id in: path required: true schema: $ref: '#/components/schemas/BrandAssetId' get: summary: Read a brand asset description: Read the validated logo or font bytes from an owned kit. Captured assets remain readable after replacement or archive. Responses are private and must not be cached. operationId: readBrandAsset security: - bearerAuth: [] responses: '200': description: The validated image or WOFF2 font bytes. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: image/png: schema: type: string format: binary image/jpeg: schema: type: string format: binary image/webp: schema: type: string format: binary font/woff2: schema: type: string format: binary '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' put: summary: Replace a brand asset description: Create a new immutable asset of the same kind and make it current. The old asset and bytes stay unchanged for any template or queued operation that already captured its ID. operationId: replaceBrandAsset security: - bearerAuth: [] requestBody: required: true content: image/png: schema: type: string format: binary image/jpeg: schema: type: string format: binary image/webp: schema: type: string format: binary font/woff2: schema: type: string format: binary responses: '201': description: The new immutable current asset. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/BrandAsset' '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' '415': $ref: '#/components/responses/Error' '422': $ref: '#/components/responses/Error' '429': $ref: '#/components/responses/Error' '503': $ref: '#/components/responses/Error' '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' /v1/pdf/{id}: get: summary: Get a PDF's status description: Return one render job's current status, in the same representation POST /v1/pdf answers with. Mints a fresh signed download link on every call while the PDF is still available. operationId: getPdf security: - bearerAuth: [] parameters: - name: id in: path required: true description: The job's public identifier, such as "pdf_1f8b3c7d5e2a49061f8b3c7d5e2a4906". schema: type: string pattern: ^pdf_[0-9a-f]{32}$ responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '200': description: The job's current status. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/PdfJob' '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 job with this identifier belongs to the authenticated account. This is also the answer for an identifier that does not exist at all, or that is not well formed, 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' delete: summary: Delete a PDF file or an unsuccessful render description: Remove a successful job's PDF from storage, or remove a failed or cancelled job from history. The job stays readable by ID, and billing, abuse limits, and idempotency keys stay unchanged. The file is removed before its database marker; a file-removal failure returns 500 and leaves the record live for a safe retry. Repeated requests are safe. At or after the fixed retention cutoff, the removal reason remains expired. operationId: deletePdf security: - bearerAuth: [] parameters: - name: id in: path required: true description: The job's public identifier, such as "pdf_1f8b3c7d5e2a49061f8b3c7d5e2a4906". schema: type: string pattern: ^pdf_[0-9a-f]{32}$ responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '200': description: The job's current status after file or history removal. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/PdfJob' '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 job with this identifier belongs to the authenticated account. This is also the answer for an identifier that does not exist at all, or that is not well formed, 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' '409': description: The job is queued or running, so it cannot be deleted. 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' /v1/downloads/{token}: get: summary: Download a finished PDF or image description: 'Return the finished PDF, PNG, JPEG, or WebP file a signed download link names. The link carries its own authority, so no other credential is read. The file is opened before 200 is returned: deletion that unlinks first gives 404, while a download that opens first finishes and every later download fails.' operationId: getDownload parameters: - name: token in: path required: true description: The signed download link. schema: type: string responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '200': description: The finished PDF or image, bounded by the 25 MiB artifact limit. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/pdf: schema: type: string format: binary image/png: schema: type: string format: binary image/jpeg: schema: type: string format: binary image/webp: schema: type: string format: binary '403': description: The link is genuine but its time is over. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: The link is not valid. This is also the answer for a link whose job or file does not exist, so a link can never be used to ask what this product holds. 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' /v1/image-packs/archive/{token}: get: summary: Download an image pack's ZIP archive description: Stream a ZIP of one pack's succeeded members, in requested order with numbered filenames, plus manifest.json recording that order and any failed sizes. The link carries its own authority, so no other credential is read. operationId: getImagePackArchive parameters: - name: token in: path required: true description: The signed archive link. schema: type: string responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '200': description: The ZIP archive of succeeded members plus manifest.json. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/zip: schema: type: string format: binary '403': description: The link is genuine but its time is over. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: The link is not valid, or the pack has no succeeded member to archive. 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' /v1/carousels/archive/{token}: get: summary: Download a carousel's ZIP archive description: Stream a ZIP of one carousel's succeeded members, in requested order with numbered filenames such as 01-cover.png, plus manifest.json recording that order and any failed pages. The link carries its own authority, so no other credential is read. operationId: getCarouselArchive parameters: - name: token in: path required: true description: The signed archive link. schema: type: string responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '200': description: The ZIP archive of succeeded members plus manifest.json. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/zip: schema: type: string format: binary '403': description: The link is genuine but its time is over. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: The link is not valid, or the carousel has no succeeded member to archive. 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' /v1/batches/archive/{token}: get: summary: Download a batch's ZIP archive description: Stream a ZIP of one batch's succeeded rows, in requested order with numbered filenames such as 001.pdf, plus manifest.json recording that order and any failed or cancelled rows. The link carries its own authority, so no other credential is read. The manifest never includes customer data. operationId: getBatchArchive parameters: - name: token in: path required: true description: The signed archive link. schema: type: string responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '200': description: The ZIP archive of succeeded rows plus manifest.json. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/zip: schema: type: string format: binary '403': description: The link is genuine but its time is over. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '404': description: The link is not valid, or the batch has no succeeded row to archive. 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' /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' 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' /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' 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' /v1/webhooks: post: summary: Register a webhook destination description: 'Register one HTTPS destination for terminal render events. The URL must pass the same egress policy the platform applies to every outbound request — public HTTPS on port 443, no credentials, and a DNS answer set of public addresses only — and delivery rechecks all of it on every connection, so a name that later resolves privately is refused then too. The signing secret is returned in this response and never again. Every delivery carries a Thirds-Signature header of the form "t=,v1=": each v1 value is HMAC-SHA256 over the exact bytes "." with one signing secret, so verify against the raw body before parsing it, and refuse a timestamp too far from your own clock to bound replays. During a rotation overlap the header carries two v1 values — the current secret''s first, then the previous secret''s — and a receiver accepts the delivery when any one value matches.' operationId: createWebhook security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/NewWebhook' responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '403': $ref: '#/components/responses/AccountSuspended' '201': description: The destination was registered and is enabled. This is the only response, with rotation's, that ever carries the signing secret. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/WebhookWithSecret' '400': description: The URL failed the egress policy — not HTTPS on port 443, malformed, carrying credentials, or resolving to a private, loopback, link-local, metadata, or reserved address — or the events list was empty, had duplicates, had more than three entries, or named an unknown event. The details entry carries the stable policy code that refused it. 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 webhook destinations. Delete one before registering 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' get: summary: List webhook destinations description: List every webhook destination of the authenticated account, most recently created first. The signing secret never appears here. operationId: listWebhooks security: - bearerAuth: [] responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '403': $ref: '#/components/responses/AccountSuspended' '200': description: The account's destinations. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/WebhookList' '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' /v1/webhooks/{webhook_id}: get: summary: Read a webhook destination description: Read one destination of the authenticated account, without its signing secret. operationId: getWebhook security: - bearerAuth: [] parameters: - name: webhook_id in: path required: true description: The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". schema: type: string pattern: ^wh_[0-9a-f]{32}$ responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '403': $ref: '#/components/responses/AccountSuspended' '200': description: The destination. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/Webhook' '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 destination 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' delete: summary: Delete a webhook destination description: Delete one destination of the authenticated account. Deliveries stop at once, and events not yet delivered to it are dropped. operationId: deleteWebhook security: - bearerAuth: [] parameters: - name: webhook_id in: path required: true description: The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". schema: type: string pattern: ^wh_[0-9a-f]{32}$ responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '403': $ref: '#/components/responses/AccountSuspended' '204': description: The destination is deleted. There is no body. headers: x-request-id: $ref: '#/components/headers/XRequestId' '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 destination 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' /v1/webhooks/{webhook_id}/rotate: post: summary: Rotate a webhook destination's signing secret description: 'Mint a new signing secret for one destination. The new secret signs immediately and is returned in this response and never again. The previous secret keeps signing alongside it for 24 hours — every delivery in the overlap carries both signatures — so switch the receiver to the new secret inside that window. Rotating again during an overlap replaces the previous secret at once: only the last two secrets ever sign.' operationId: rotateWebhookSecret security: - bearerAuth: [] parameters: - name: webhook_id in: path required: true description: The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". schema: type: string pattern: ^wh_[0-9a-f]{32}$ responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '403': $ref: '#/components/responses/AccountSuspended' '200': description: The destination with its new secret. This is the only response, with creation's, that ever carries the signing secret. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/WebhookWithSecret' '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 destination 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' /v1/webhooks/{webhook_id}/disable: post: summary: Disable a webhook destination description: Stop deliveries to one destination without deleting it. Disabling a destination that is already disabled returns the same answer again rather than an error, so a retried request is never rejected. operationId: disableWebhook security: - bearerAuth: [] parameters: - name: webhook_id in: path required: true description: The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". schema: type: string pattern: ^wh_[0-9a-f]{32}$ responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '403': $ref: '#/components/responses/AccountSuspended' '200': description: The destination, now disabled by owner request. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/Webhook' '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 destination 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' /v1/webhooks/{webhook_id}/enable: post: summary: Enable a webhook destination description: Resume deliveries to one destination, whether the owner disabled it or repeated delivery failures did, and reset its consecutive failure count. Enabling a destination that is already enabled returns the same answer again rather than an error, so a retried request is never rejected. operationId: enableWebhook security: - bearerAuth: [] parameters: - name: webhook_id in: path required: true description: The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". schema: type: string pattern: ^wh_[0-9a-f]{32}$ responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '403': $ref: '#/components/responses/AccountSuspended' '200': description: The destination, enabled again. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/Webhook' '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 destination 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' /v1/webhooks/{webhook_id}/replay: post: summary: Replay a webhook destination's failed events description: Return every event of one destination that ran out of delivery attempts to the queue with a fresh attempt budget. An event gets eight attempts per creation or replay, spread over roughly one day of growing backoff; after that it is failed and waits here. A replayed delivery carries the same event id and the same body as before, with a current signature and timestamp, so a receiver that stores event ids deduplicates it like any other retry. Replaying a destination with no failed events answers a count of zero rather than an error, so a retried request is never rejected. operationId: replayWebhook security: - bearerAuth: [] parameters: - name: webhook_id in: path required: true description: The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". schema: type: string pattern: ^wh_[0-9a-f]{32}$ responses: '405': $ref: '#/components/responses/MethodNotAllowed' '431': $ref: '#/components/responses/RequestHeadersTooLarge' '403': $ref: '#/components/responses/AccountSuspended' '200': description: How many failed events returned to the queue. headers: x-request-id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/WebhookReplay' '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 destination 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' 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' components: 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 schemas: AccountEmailRequest: type: object properties: email: type: string next: type: string description: A safe local path after proof; defaults to /account. required: - email additionalProperties: false AccountEmailAccepted: type: object additionalProperties: false WebhookTest: type: object properties: status_code: type: - integer - 'null' minimum: 200 maximum: 599 delivered: type: boolean required: - status_code - delivered additionalProperties: false WebhookDelivery: type: object properties: id: type: string pattern: ^wa_[0-9a-f]{32}$ event: type: string enum: - render.succeeded - render.failed - render.cancelled created_at: type: string format: date-time status_code: type: - integer - 'null' minimum: 200 maximum: 599 description: Null when no HTTP status is received. is_test: type: boolean description: True for a sample sent by the test action. retry_count: type: integer minimum: 0 maximum: 7 required: - id - event - created_at - status_code - retry_count - is_test additionalProperties: false WebhookDeliveryList: type: object properties: data: type: array maxItems: 20 items: $ref: '#/components/schemas/WebhookDelivery' required: - data additionalProperties: false PublicStats: type: object description: Measured facts shown on the public sales site. properties: renders_served: type: integer minimum: 0 gallery_templates: type: integer minimum: 0 uptime_percent: type: - number - 'null' minimum: 0 maximum: 100 uptime_days: type: integer minimum: 0 status: type: string enum: - operational - degraded - downtime - maintenance - unavailable required: - renders_served - gallery_templates - uptime_percent - uptime_days - status additionalProperties: false TestimonialRequest: type: object description: One private testimonial offer and the fresh browser check that protects it. properties: name: type: string minLength: 2 maxLength: 80 company: type: string minLength: 2 maxLength: 120 quote: type: string minLength: 20 maxLength: 1000 permission: type: boolean const: true description: The visitor permits contact about the quote. This does not publish it. captcha_token: type: string minLength: 1 maxLength: 2048 required: - name - company - quote - permission - captcha_token additionalProperties: false TestimonialSubmission: type: object description: The private submission identifier returned after storage. properties: id: type: string format: uuid required: - id additionalProperties: false UploadedImage: type: object additionalProperties: false required: - id - reference - media_type - byte_size - width - height properties: id: type: string pattern: ^asset_[a-f0-9]{32}$ reference: type: string pattern: ^asset://asset_[a-f0-9]{32}$ description: Private template reference. This is not a fetchable URL. media_type: type: string enum: - image/png - image/jpeg - image/webp byte_size: type: integer minimum: 1 maximum: 10485760 width: type: integer minimum: 1 maximum: 7680 description: Display width after image orientation. height: type: integer minimum: 1 maximum: 7680 description: Display height after image orientation. 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 BrandKitId: type: string pattern: ^kit_[0-9a-f]{32}$ BrandAssetId: type: string pattern: ^asset_[0-9a-f]{32}$ BrandKitWrite: type: object properties: name: type: string minLength: 1 maxLength: 100 colours: type: array maxItems: 16 uniqueItems: true items: type: string pattern: ^#[0-9A-Fa-f]{6}$ default: [] tone_guidance: type: - string - 'null' minLength: 1 maxLength: 500 required: - name additionalProperties: false BrandKitPatch: type: object properties: archived_at: type: 'null' description: Restore the item. Omit this field to keep its archive state. name: type: string minLength: 1 maxLength: 100 colours: type: array maxItems: 16 uniqueItems: true items: type: string pattern: ^#[0-9A-Fa-f]{6}$ tone_guidance: type: - string - 'null' minLength: 1 maxLength: 500 minProperties: 1 additionalProperties: false BrandKitSummary: type: object properties: id: $ref: '#/components/schemas/BrandKitId' name: type: string minLength: 1 maxLength: 100 colours: type: array maxItems: 16 uniqueItems: true items: type: string pattern: ^#[0-9a-f]{6}$ tone_guidance: type: - string - 'null' minLength: 1 maxLength: 500 created_at: type: string format: date-time updated_at: type: string format: date-time archived_at: type: - string - 'null' format: date-time assets: type: array maxItems: 16 items: $ref: '#/components/schemas/BrandAsset' required: - id - name - colours - tone_guidance - created_at - updated_at - archived_at - assets additionalProperties: false BrandAsset: type: object properties: id: $ref: '#/components/schemas/BrandAssetId' kind: type: string enum: - logo - font media_type: type: string enum: - image/png - image/jpeg - image/webp - font/woff2 byte_size: type: integer minimum: 1 maximum: 10485760 created_at: type: string format: date-time required: - id - kind - media_type - byte_size - created_at additionalProperties: false BrandKitDetail: type: object properties: archive_history: type: array items: $ref: '#/components/schemas/ArchivePeriod' id: $ref: '#/components/schemas/BrandKitId' name: type: string minLength: 1 maxLength: 100 colours: type: array maxItems: 16 uniqueItems: true items: type: string pattern: ^#[0-9a-f]{6}$ tone_guidance: type: - string - 'null' minLength: 1 maxLength: 500 created_at: type: string format: date-time updated_at: type: string format: date-time archived_at: type: - string - 'null' format: date-time assets: type: array maxItems: 16 items: $ref: '#/components/schemas/BrandAsset' required: - archive_history - id - name - colours - tone_guidance - created_at - updated_at - archived_at - assets additionalProperties: false BrandKitPage: type: object properties: items: type: array maxItems: 100 items: $ref: '#/components/schemas/BrandKitSummary' next_cursor: type: - string - 'null' maxLength: 128 required: - items - next_cursor additionalProperties: false TemplateBuildId: type: string pattern: ^build_[0-9a-f]{32}$ TemplateMessageId: type: string pattern: ^msg_[0-9a-f]{32}$ TemplateBuildInput: oneOf: - type: object properties: type: const: prompt prompt: type: string minLength: 1 maxLength: 65536 required: - type - prompt additionalProperties: false - type: object properties: type: const: html html: type: string minLength: 1 maxLength: 65536 required: - type - html additionalProperties: false - type: object description: Rebuild a still PNG, JPEG, or WebP as an editable branded template. Inspired by the source, not a pixel copy. At most 10 MiB, 7680 by 4320, and 32 million pixels. An invalid file fails before the model. properties: type: const: image base64: type: string minLength: 1 maxLength: 13981016 contentEncoding: base64 required: - type - base64 additionalProperties: false - type: object description: Your own working template, sent as data. It must pass the saved-template checks and evaluate with sample_data. It becomes the draft without a model call and costs 0 credits. properties: type: const: source source: type: string minLength: 1 maxLength: 1048576 sample_data: type: object schema: oneOf: - type: object - type: boolean required: - type - source additionalProperties: false TemplateOutputIntent: oneOf: - type: object description: Let the model choose PDF or image from the message. An image result from a source image uses the source dimensions. properties: type: const: auto required: - type additionalProperties: false - type: object properties: type: const: pdf required: - type additionalProperties: false - type: object description: Request an image. Set both width and height to override the source dimensions. Without a source image, omitted dimensions use 1200 by 628. properties: type: const: image format: type: string enum: - png - jpeg - webp default: png width: type: integer minimum: 320 maximum: 7680 height: type: integer minimum: 200 maximum: 4320 required: - type additionalProperties: false TemplateOutput: oneOf: - type: object properties: type: const: pdf required: - type additionalProperties: false - type: object properties: type: const: image format: type: string enum: - png - jpeg - webp width: type: integer minimum: 320 maximum: 7680 height: type: integer minimum: 200 maximum: 4320 required: - type - format - width - height additionalProperties: false TemplateBuildCreate: type: object properties: brand_kit_id: $ref: '#/components/schemas/BrandKitId' input: $ref: '#/components/schemas/TemplateBuildInput' input_mode: type: string enum: - blank - html description: How new manual work starts. Only source input accepts this field. Omit it when the source comes from a saved template or an existing chat. output: $ref: '#/components/schemas/TemplateOutputIntent' required: - brand_kit_id - input if: required: - input_mode then: properties: input: properties: type: const: source additionalProperties: false TemplateMessageCreate: type: object properties: input: $ref: '#/components/schemas/TemplateBuildInput' output: $ref: '#/components/schemas/TemplateOutputIntent' required: - input additionalProperties: false TemplateMessage: type: object properties: id: $ref: '#/components/schemas/TemplateMessageId' state: type: string enum: - queued - running - previewing - succeeded - failed - cancelled phase: type: string enum: - generate - candidate - review - final input_type: type: string enum: - prompt - html - image - source credits: type: integer enum: - 0 - 25 - 50 preview_id: type: - string - 'null' pattern: ^(pdf|image)_[0-9a-f]{32}$ output: oneOf: - $ref: '#/components/schemas/TemplateOutput' - type: 'null' review_count: type: integer minimum: 0 maximum: 5 max_reviews: type: integer minimum: 0 maximum: 5 input_text: type: string maxLength: 65536 repair_code: type: string pattern: ^[a-z][a-z0-9_]{0,63}$ description: The last safe generation or render code that blocked this message. Present while a repair is in progress and after a failed message. It never holds customer content. failure_category: type: - string - 'null' enum: - provider - validation - renderer - unsupported_input - internal - null failure_code: type: - string - 'null' pattern: ^[a-z][a-z0-9_]{0,63}$ required: - id - state - phase - input_type - credits - preview_id - output - review_count - max_reviews - failure_category - failure_code additionalProperties: false TemplateBuild: type: object properties: id: $ref: '#/components/schemas/TemplateBuildId' brand_kit_id: $ref: '#/components/schemas/BrandKitId' current_draft: type: boolean current_message_id: oneOf: - $ref: '#/components/schemas/TemplateMessageId' - type: 'null' published_template_id: oneOf: - $ref: '#/components/schemas/TemplateId' - type: 'null' message: $ref: '#/components/schemas/TemplateMessage' required: - id - brand_kit_id - current_draft - current_message_id - published_template_id - message additionalProperties: false TemplateBuildDraft: type: object properties: source: type: string maxLength: 1048576 sample_data: type: object schema: {} output: $ref: '#/components/schemas/TemplateOutput' required: - source - sample_data - schema - output additionalProperties: false TemplatePublication: type: object properties: template_id: $ref: '#/components/schemas/TemplateId' version: type: integer minimum: 1 required: - template_id - version additionalProperties: false TemplateId: type: string pattern: ^tpl_[0-9a-f]{32}$ 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 TemplateTags: type: array maxItems: 20 uniqueItems: true items: type: string minLength: 1 maxLength: 40 description: Trimmed tags. Each tag is nonblank. Case is preserved. 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. 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 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 TemplatePreview: type: object properties: html: type: string maxLength: 5242880 required: - html 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 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 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 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 TemplateVersionSummary: type: object properties: version: type: integer minimum: 1 created_at: type: string format: date-time required: - version - created_at 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 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 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 SizeVariantList: type: array maxItems: 20 items: $ref: '#/components/schemas/SizeVariant' 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 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 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 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 ImageRequestCommon: type: object properties: filename: type: string minLength: 1 maxLength: 120 pattern: ^(?!.* $)(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9 ._-]*$ description: Download name. Surrounding spaces are refused. The server uses the real output extension. Omit it for a safe template-name and UTC creation-date name, or html-render and date for HTML. reference: type: string minLength: 1 maxLength: 200 pattern: ^[^\u0000-\u001f\u007f-\u009f]+$ description: Your reference, preserved in the job, history, and webhook. Omitted when not supplied. 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. image: type: object properties: format: type: string enum: - png - jpeg - webp width: type: integer minimum: 320 maximum: 7680 default: 1280 height: type: integer minimum: 200 maximum: 4320 default: 720 quality: type: integer minimum: 1 maximum: 100 transparent: type: boolean default: false required: - format allOf: - if: properties: format: const: png then: not: required: - quality else: properties: quality: default: 80 - if: properties: format: const: jpeg then: properties: transparent: const: false additionalProperties: false javascript: type: object properties: mode: type: string enum: - disabled - enabled additionalProperties: false wait: type: boolean default: true required: - image RawImageRequest: allOf: - $ref: '#/components/schemas/ImageRequestCommon' - type: object properties: html: type: string minLength: 1 maxLength: 5242880 required: - html unevaluatedProperties: false StatelessTemplateImageRequest: allOf: - $ref: '#/components/schemas/ImageRequestCommon' - type: object properties: html: type: string minLength: 1 maxLength: 1048576 data: type: object required: - html - data unevaluatedProperties: false SavedTemplateImageRequest: allOf: - $ref: '#/components/schemas/ImageRequestCommon' - type: object properties: template_id: $ref: '#/components/schemas/TemplateId' version: type: integer minimum: 1 data: type: object size_id: type: - string - 'null' pattern: ^[a-z0-9][a-z0-9-]{0,39}$ description: Selects one saved size of the template instead of the original. The image width and height must match the size exactly. required: - template_id - data unevaluatedProperties: false ImageRequest: oneOf: - $ref: '#/components/schemas/RawImageRequest' - $ref: '#/components/schemas/StatelessTemplateImageRequest' - $ref: '#/components/schemas/SavedTemplateImageRequest' ImageJob: type: object properties: reference: type: string minLength: 1 maxLength: 200 pattern: ^[^\u0000-\u001f\u007f-\u009f]+$ description: Your reference, preserved in the job, history, and webhook. Omitted when not supplied. id: type: string pattern: ^image_[0-9a-f]{32}$ status: type: string enum: - queued - running - succeeded - failed - cancelled created_at: type: string format: date-time finished_at: type: - string - 'null' format: date-time template: type: - object - 'null' description: The exact saved template version this job rendered. Null for raw HTML or an inline stateless template. properties: id: $ref: '#/components/schemas/TemplateId' version: type: integer minimum: 1 required: - id - version additionalProperties: false credit: type: string enum: - reserved - settled - released - none description: 'What happened to the credit this job holds: "reserved" while it is queued or running, "settled" once it succeeded, "released" once it failed or was cancelled, and "none" when the job was never billed.' artifact: type: - object - 'null' properties: media_type: type: string enum: - image/png - image/jpeg - image/webp byte_size: type: integer minimum: 0 sha256: type: string pattern: ^[0-9a-f]{64}$ expires_at: type: string format: date-time description: When retention ends and the file stops being served. removed_reason: type: - string - 'null' enum: - expired - deleted - null required: - media_type - byte_size - sha256 - expires_at - removed_reason additionalProperties: false error: type: - object - 'null' properties: category: type: string enum: - invalid_input - unsafe_asset - resource_limit - timeout - renderer_failure - internal_failure code: type: string required: - category - code additionalProperties: false download: type: - object - 'null' properties: url: type: string expires_at: type: string format: date-time required: - url - expires_at additionalProperties: false required: - id - status - created_at - finished_at - template - credit - artifact - error - download additionalProperties: false ImagePackCreate: type: object properties: template_id: $ref: '#/components/schemas/TemplateId' version: type: integer minimum: 1 description: Omitted to use the template's latest version at creation time. data: type: object description: Template data. Each size's own saved data overrides apply over this data. Omitted to use an empty object. format: type: string enum: - png - jpeg - webp sizes: type: array items: type: string minItems: 1 maxItems: 20 description: '1 to 20 different entries: original for the version''s own canvas, a saved size id, a preset slug, or a WxH custom size.' required: - template_id - format - sizes additionalProperties: false ImagePackMember: type: object properties: position: type: integer minimum: 0 size_id: type: string name: type: string width: type: integer minimum: 1 height: type: integer minimum: 1 state: type: string enum: - queued - running - succeeded - failed error: type: - object - 'null' properties: code: type: string required: - code additionalProperties: false download_url: type: - string - 'null' required: - position - size_id - name - width - height - state - error - download_url additionalProperties: false ImagePack: type: object properties: id: type: string pattern: ^pack_[0-9a-f]{32}$ template_id: $ref: '#/components/schemas/TemplateId' version: type: integer minimum: 1 format: type: string enum: - png - jpeg - webp created_at: type: string format: date-time state: type: string enum: - queued - running - complete - partial - failed members: type: array items: $ref: '#/components/schemas/ImagePackMember' archive_url: type: - string - 'null' description: A signed ZIP download link, present once any member has succeeded. required: - id - template_id - version - format - created_at - state - members - archive_url additionalProperties: false ImagePackRetry: $ref: '#/components/schemas/ImagePack' CarouselCreate: type: object properties: template_id: $ref: '#/components/schemas/TemplateId' version: type: integer minimum: 1 description: Omitted to use the template's latest version at creation time. data: type: object description: Template data applied to every selected page. Omitted to use an empty object. format: type: string enum: - png - jpeg - webp pages: type: array items: type: string minLength: 1 maxLength: 64 pattern: ^[A-Za-z][A-Za-z0-9_-]{0,63}$ minItems: 1 maxItems: 100 description: 1 to 100 different canvas data-thirds-page ids in output order. required: - template_id - format - pages additionalProperties: false CarouselMember: type: object properties: position: type: integer minimum: 0 page_id: type: string name: type: string width: type: integer minimum: 1 height: type: integer minimum: 1 state: type: string enum: - queued - running - succeeded - failed error: type: - object - 'null' properties: code: type: string required: - code additionalProperties: false download_url: type: - string - 'null' required: - position - page_id - name - width - height - state - error - download_url additionalProperties: false Carousel: type: object properties: id: type: string pattern: ^carousel_[0-9a-f]{32}$ template_id: $ref: '#/components/schemas/TemplateId' version: type: integer minimum: 1 format: type: string enum: - png - jpeg - webp created_at: type: string format: date-time state: type: string enum: - queued - running - complete - partial - failed members: type: array items: $ref: '#/components/schemas/CarouselMember' archive_url: type: - string - 'null' description: A signed ZIP download link, present once any member has succeeded. required: - id - template_id - version - format - created_at - state - members - archive_url additionalProperties: false CarouselRetry: $ref: '#/components/schemas/Carousel' BatchCreate: type: object properties: template_id: $ref: '#/components/schemas/TemplateId' version: type: integer minimum: 1 description: Omitted to use the template's latest version at creation time. The batch pins that exact version. format: type: string enum: - pdf - png - jpeg - webp rows: type: array items: type: object minItems: 1 maxItems: 200 description: 1 to 200 JSON objects. Each object is one render. 200 is the system cap; the account plan can allow fewer. The CSV parser lives in the studio; this API accepts rows only. required: - template_id - format - rows additionalProperties: false BatchRow: type: object properties: position: type: integer minimum: 0 state: type: string enum: - queued - running - succeeded - failed - cancelled error: type: - object - 'null' properties: code: type: string required: - code additionalProperties: false download_url: type: - string - 'null' required: - position - state - error - download_url additionalProperties: false Batch: type: object properties: id: type: string pattern: ^batch_[0-9a-f]{32}$ template_id: $ref: '#/components/schemas/TemplateId' version: type: integer minimum: 1 format: type: string enum: - pdf - png - jpeg - webp created_at: type: string format: date-time state: type: string enum: - queued - running - complete - partial - failed - cancelled quoted_credits: type: integer minimum: 1 maximum: 200 description: The row count. Each successful file settles one credit. Failed and cancelled rows cost nothing. There is no bulk discount. rows: type: array items: $ref: '#/components/schemas/BatchRow' archive_url: type: - string - 'null' description: A signed ZIP download link, present once any row has succeeded. required: - id - template_id - version - format - created_at - state - quoted_credits - rows - archive_url additionalProperties: false BatchRetry: $ref: '#/components/schemas/Batch' PdfRequestCommon: type: object description: Render options shared by raw HTML, stateless-template, and saved-template requests. Every field is optional; an absent field takes the renderer's own default. properties: filename: type: string minLength: 1 maxLength: 120 pattern: ^(?!.* $)(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9 ._-]*$ description: Download name. Surrounding spaces are refused. The server uses the real output extension. Omit it for a safe template-name and UTC creation-date name, or html-render and date for HTML. reference: type: string minLength: 1 maxLength: 200 pattern: ^[^\u0000-\u001f\u007f-\u009f]+$ description: Your reference, preserved in the job, history, and webhook. Omitted when not supplied. 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. pdf: type: object description: PDF layout options. The page defaults to A4 when neither "format" nor both "width" and "height" are given. properties: format: type: string enum: - A0 - A1 - A2 - A3 - A4 - A5 - A6 - Letter - Legal - Ledger - Tabloid description: A named page size, such as "A4" or "Letter". Mutually exclusive with "width"/"height". width: type: string pattern: ^(?:0\.(?=[0-9]{1,4}(?:px|in|cm|mm)$)(?=[0-9]*[1-9])[0-9]{1,4}|[1-9][0-9]*(?:\.[0-9]{1,4})?)(?:px|in|cm|mm)$ description: An explicit page width with its unit, such as "8.5in". Requires "height". height: type: string pattern: ^(?:0\.(?=[0-9]{1,4}(?:px|in|cm|mm)$)(?=[0-9]*[1-9])[0-9]{1,4}|[1-9][0-9]*(?:\.[0-9]{1,4})?)(?:px|in|cm|mm)$ description: An explicit page height with its unit. Requires "width". margins: type: object description: Each side defaults to "0mm". properties: top: type: string pattern: ^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,4})?(?:px|in|cm|mm)$ right: type: string pattern: ^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,4})?(?:px|in|cm|mm)$ bottom: type: string pattern: ^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,4})?(?:px|in|cm|mm)$ left: type: string pattern: ^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,4})?(?:px|in|cm|mm)$ additionalProperties: false landscape: type: boolean description: Defaults to false. print_background: type: boolean description: Defaults to true. scale: type: number minimum: 0.1 maximum: 2.0 description: Defaults to 1.0. display_header_footer: type: boolean description: Defaults to false. header_template: type: string maxLength: 100000 description: Defaults to empty. footer_template: type: string maxLength: 100000 description: Defaults to empty. title: type: string maxLength: 256 author: type: string maxLength: 256 subject: type: string maxLength: 256 password: type: string minLength: 1 maxLength: 127 description: PDF open password, at most 127 UTF-8 bytes. AES-256 encryption. oneOf: - required: - format not: anyOf: - required: - width - required: - height - required: - width - height not: required: - format - not: anyOf: - required: - format - required: - width - required: - height additionalProperties: false viewport: type: object properties: width: type: integer minimum: 320 maximum: 7680 description: Defaults to 1280. height: type: integer minimum: 200 maximum: 4320 description: Defaults to 720. device_scale_factor: type: number minimum: 1.0 maximum: 3.0 description: Defaults to 1.0. additionalProperties: false javascript: type: object properties: mode: type: string enum: - disabled - enabled description: Defaults to "disabled". additionalProperties: false wait: type: boolean description: Defaults to true. False answers as soon as the job is durable instead of holding the bounded wait; poll GET /v1/pdf/{id} for the terminal result. A delivery preference only — it is not part of the idempotency fingerprint and changes nothing about the render or its cost. pages: type: array items: type: string minLength: 1 maxLength: 64 pattern: ^[A-Za-z][A-Za-z0-9_-]{0,63}$ minItems: 1 maxItems: 100 description: Canvas data-thirds-page ids in output order. Omitted keeps every page in document order. Duplicate or unknown ids are refused. RawPdfRequest: description: A raw HTML render. The HTML enters the existing render workflow unchanged. allOf: - $ref: '#/components/schemas/PdfRequestCommon' - type: object properties: html: type: string minLength: 1 maxLength: 5242880 description: The raw HTML document to render. Up to 5,242,880 bytes. required: - html unevaluatedProperties: false StatelessTemplatePdfRequest: description: A bounded stateless template evaluation followed by the existing PDF render workflow. The request saves no reusable template. Data must be exactly one JSON object. Only evaluated HTML and the effective render options enter short-lived render input storage. allOf: - $ref: '#/components/schemas/PdfRequestCommon' - type: object properties: html: type: string minLength: 1 maxLength: 1048576 description: The inline Jinja-style template source. The evaluator accepts at most 1,048,576 bytes. data: type: object description: The JSON-compatible data object available to the template. Arrays, scalars, and null are not accepted as the top-level value. Its canonical recursively key-sorted JSON representation may contain at most 1,048,576 bytes; JSON Schema cannot express that serialized-byte bound. required: - html - data unevaluatedProperties: false SavedTemplatePdfRequest: description: Render one exact owned saved-template version. Omit version to resolve the latest version once before validation and admission. Data must be exactly one JSON object. allOf: - $ref: '#/components/schemas/PdfRequestCommon' - type: object properties: template_id: $ref: '#/components/schemas/TemplateId' version: type: integer minimum: 1 description: An exact immutable version. Omit to select latest once. data: type: object description: The JSON-compatible data object validated against the selected version's optional schema before evaluation or admission. required: - template_id - data unevaluatedProperties: false PdfRequest: description: 'Exactly one supported PDF input mode: raw HTML, an inline stateless template, or one saved-template version. Image request shapes are not part of this endpoint.' oneOf: - $ref: '#/components/schemas/RawPdfRequest' - $ref: '#/components/schemas/StatelessTemplatePdfRequest' - $ref: '#/components/schemas/SavedTemplatePdfRequest' PdfJob: type: object description: The state of one render job. This is the same shape whether the job just finished (200) or is still in progress (202). properties: reference: type: string minLength: 1 maxLength: 200 pattern: ^[^\u0000-\u001f\u007f-\u009f]+$ description: Your reference, preserved in the job, history, and webhook. Omitted when not supplied. id: type: string pattern: ^pdf_[0-9a-f]{32}$ description: 'The job''s public identifier: "pdf_" followed by 32 lowercase hexadecimal characters.' status: type: string enum: - queued - running - succeeded - failed - cancelled created_at: type: string format: date-time finished_at: type: - string - 'null' format: date-time template: type: - object - 'null' description: The exact saved template version this job rendered. Null for raw HTML or an inline stateless template. properties: id: $ref: '#/components/schemas/TemplateId' version: type: integer minimum: 1 required: - id - version additionalProperties: false credit: type: string enum: - reserved - settled - released - none description: 'What happened to the credit this job holds: "reserved" while it is queued or running, "settled" once it succeeded, "released" once it failed or was cancelled, and "none" when the job was never billed.' artifact: type: - object - 'null' description: Present only for a succeeded job. properties: media_type: const: application/pdf byte_size: type: integer minimum: 0 sha256: type: string pattern: ^[0-9a-f]{64}$ description: 64 lowercase hexadecimal characters. expires_at: type: string format: date-time description: When retention ends and the file stops being served. removed_reason: type: - string - 'null' enum: - expired - deleted - null description: Null while the file is still available. "expired" once its retention window or an internal reconciliation has passed; "deleted" once the account removed it through DELETE /v1/pdf/{id}. required: - media_type - byte_size - sha256 - expires_at - removed_reason additionalProperties: false error: type: - object - 'null' description: Present only for a terminal failure. Never carries a message, only a fixed code. properties: category: type: string enum: - invalid_input - unsafe_asset - resource_limit - timeout - renderer_failure - internal_failure code: type: string required: - category - code additionalProperties: false download: type: - object - 'null' description: Present only while a succeeded job's PDF is still available. A fresh signed link every time this job is read. properties: url: type: string expires_at: type: string format: date-time required: - url - expires_at additionalProperties: false required: - id - status - created_at - finished_at - template - credit - artifact - error - download additionalProperties: false PdfHistoryItem: type: object description: One job in a bounded account history — the subset of PdfJob a history page lists. Never carries a download link, since a history page can name many jobs at once. properties: reference: type: string minLength: 1 maxLength: 200 pattern: ^[^\u0000-\u001f\u007f-\u009f]+$ description: Your reference, preserved in the job, history, and webhook. Omitted when not supplied. id: type: string pattern: ^pdf_[0-9a-f]{32}$ status: type: string enum: - queued - running - succeeded - failed - cancelled created_at: type: string format: date-time finished_at: type: - string - 'null' format: date-time template: type: - object - 'null' description: The exact saved template version this job rendered. Null for raw HTML or an inline stateless template. properties: id: $ref: '#/components/schemas/TemplateId' version: type: integer minimum: 1 required: - id - version additionalProperties: false credit: type: string enum: - reserved - settled - released - none description: 'What happened to the credit this job holds: "reserved" while it is queued or running, "settled" once it succeeded, "released" once it failed or was cancelled, and "none" when the job was never billed.' artifact: type: - object - 'null' properties: media_type: const: application/pdf byte_size: type: integer minimum: 0 sha256: type: string pattern: ^[0-9a-f]{64}$ expires_at: type: string format: date-time description: When retention ends and the file stops being served. removed_reason: type: - string - 'null' enum: - expired - deleted - null required: - media_type - byte_size - sha256 - expires_at - removed_reason additionalProperties: false error: type: - object - 'null' properties: category: type: string enum: - invalid_input - unsafe_asset - resource_limit - timeout - renderer_failure - internal_failure code: type: string required: - category - code additionalProperties: false required: - id - status - created_at - finished_at - template - credit - artifact - error additionalProperties: false PdfHistoryPage: type: object description: One bounded page of an account's render history, newest first. properties: items: type: array maxItems: 100 items: $ref: '#/components/schemas/PdfHistoryItem' next_cursor: type: - string - 'null' maxLength: 128 description: Pass as the cursor query parameter to read the next page. Null on the last page. required: - items - next_cursor additionalProperties: false ImageHistoryItem: type: object description: One job in a bounded account history — the subset of ImageJob a history page lists. Never carries a download link, since a history page can name many jobs at once. properties: reference: type: string minLength: 1 maxLength: 200 pattern: ^[^\u0000-\u001f\u007f-\u009f]+$ description: Your reference, preserved in the job, history, and webhook. Omitted when not supplied. id: type: string pattern: ^image_[0-9a-f]{32}$ status: type: string enum: - queued - running - succeeded - failed - cancelled created_at: type: string format: date-time finished_at: type: - string - 'null' format: date-time template: type: - object - 'null' description: The exact saved template version this job rendered. Null for raw HTML or an inline stateless template. properties: id: $ref: '#/components/schemas/TemplateId' version: type: integer minimum: 1 required: - id - version additionalProperties: false credit: type: string enum: - reserved - settled - released - none description: 'What happened to the credit this job holds: "reserved" while it is queued or running, "settled" once it succeeded, "released" once it failed or was cancelled, and "none" when the job was never billed.' artifact: type: - object - 'null' properties: media_type: type: string enum: - image/png - image/jpeg - image/webp byte_size: type: integer minimum: 0 sha256: type: string pattern: ^[0-9a-f]{64}$ expires_at: type: string format: date-time description: When retention ends and the file stops being served. removed_reason: type: - string - 'null' enum: - expired - deleted - null required: - media_type - byte_size - sha256 - expires_at - removed_reason additionalProperties: false error: type: - object - 'null' properties: category: type: string enum: - invalid_input - unsafe_asset - resource_limit - timeout - renderer_failure - internal_failure code: type: string required: - category - code additionalProperties: false required: - id - status - created_at - finished_at - template - credit - artifact - error additionalProperties: false ImageHistoryPage: type: object description: One bounded page of an account's image history, newest first. properties: items: type: array maxItems: 100 items: $ref: '#/components/schemas/ImageHistoryItem' next_cursor: type: - string - 'null' maxLength: 128 description: Pass as the cursor query parameter to read the next page. Null on the last page. required: - items - next_cursor 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 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 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 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 NewWebhook: type: object description: What creating a webhook destination needs. properties: url: type: string format: uri maxLength: 2048 description: The HTTPS URL deliveries are sent to. It must use port 443, carry no credentials, and resolve to public addresses only; private, loopback, link-local, metadata, and reserved destinations are refused, at registration and again on every delivery connection. events: type: array minItems: 1 maxItems: 3 uniqueItems: true items: type: string enum: - render.cancelled - render.failed - render.succeeded description: The terminal render events this destination receives. A body without this field selects every event. required: - url additionalProperties: false WebhookFields: type: object properties: id: type: string pattern: ^wh_[0-9a-f]{32}$ description: 'The destination''s public identifier: "wh_" followed by 32 lowercase hexadecimal characters.' url: type: string format: uri maxLength: 2048 events: type: array minItems: 1 maxItems: 3 uniqueItems: true items: type: string enum: - render.cancelled - render.failed - render.succeeded description: The selected events, always sorted and distinct. status: type: string enum: - enabled - disabled disabled_reason: type: - string - 'null' enum: - owner_request - delivery_failure - null description: 'Why a disabled destination is disabled: the owner asked, or delivery failed repeatedly. Null while the destination is enabled.' display_prefix: type: string description: The first characters of the signing secret, enough to tell destinations apart in a list. last_delivery_at: type: - string - 'null' format: date-time description: Time of the most recent recorded attempt, including tests. failure_count: type: integer minimum: 0 description: Consecutive normal delivery failures. A success or enable resets it. Test events do not change it. created_at: type: string format: date-time previous_secret_expires_at: type: - string - 'null' format: date-time description: 'Set while a rotation overlap is running: until this instant every delivery also carries a signature made with the previous secret.' required: - id - url - events - status - disabled_reason - display_prefix - created_at - last_delivery_at - failure_count - previous_secret_expires_at Webhook: description: One webhook destination, without its signing secret. allOf: - $ref: '#/components/schemas/WebhookFields' unevaluatedProperties: false WebhookWithSecret: description: One webhook destination exactly as creation and rotation answer it. This is the only shape that ever carries the signing secret. allOf: - $ref: '#/components/schemas/WebhookFields' - type: object properties: secret: type: string pattern: ^thirds_whsec_[0-9a-f]{64}$ description: 'The full signing secret: "thirds_whsec_" followed by 64 lowercase hexadecimal characters. It is shown here once and never again; store it and verify every delivery''s Thirds-Signature header with it.' required: - secret unevaluatedProperties: false WebhookList: type: object description: Every webhook destination the account holds. The cap is ten, so the list is never paged. properties: data: type: array maxItems: 10 items: $ref: '#/components/schemas/Webhook' required: - data additionalProperties: false WebhookReplay: type: object description: What one replay request did. properties: replayed: type: integer minimum: 0 description: How many failed events returned to the queue with a fresh attempt budget. required: - replayed 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 responses: 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' 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' NotFound: description: The requested backend route does not exist. 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' 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_...".'