openapi: 3.0.3
info:
  title: 'Snapsbrew API'
  description: 'Generate images, PDFs and screenshots from reusable templates. The Snapsbrew API turns design templates into a render pipeline you can call from any stack.'
  version: 1.0.0
  contact:
    email: support@snapsbrew.com
servers:
  -
    url: 'https://snapsbrew.com/api'
    description: Production
tags:
  -
    name: Authentication
    description: 'Authenticated endpoints use a Sanctum personal access token. Send it as a bearer token in the Authorization header. Tokens must carry the `api_requests` ability. Authorization: Bearer <your-token> Accept: application/json'
  -
    name: Usage
    description: 'Read the credit balance for the current billing period. Purely informational — it does not consume credits.'
  -
    name: Templates
    description: 'Templates are reusable designs with addressable layers. List and read them, or manage them programmatically. All template endpoints are scoped to your company.'
  -
    name: Media
    description: 'Upload and list images stored in your account media library. Use these as image-layer sources in snaps.'
  -
    name: Screenshots
    description: 'Render a web page to an image. Creating screenshots requires an active subscription or trial.'
  -
    name: Snaps
    description: 'A snap renders a template (or every template in a collection) into a finished image or PDF, with your content substituted into each layer. Render synchronously for an instant result, or asynchronously with a webhook for bulk work. Creating snaps requires an active subscription or trial.'
  -
    name: Tags
    description: 'Tags organise templates. Create them, search them, and attach them to templates. All tags are scoped to your company.'
  -
    name: 'Embedded Forms'
    description: 'Embedded forms render snaps from a signed URL — no API token. The URL must be a Laravel signed URL; an invalid signature returns 404. The form owner is charged for the render.'
paths:
  /v1/auth:
    get:
      tags:
        - Authentication
      operationId: auth_show
      summary: 'Verify the current token'
      description: |-
        Returns the account the token belongs to. Use it to confirm a token is valid and to read the signed-in identity.

        Gates: logApiRequest.
      security:
        -
          bearerToken: {  }
      responses:
        200:
          description: 'Token is valid.'
          content:
            application/json:
              example:
                data:
                  email: dev@example.com
                  name: 'Ada Lovelace'
        401:
          description: 'Missing or invalid token.'
          content:
            application/json:
              example:
                message: Unauthenticated.
        403:
          description: 'Token lacks the api_requests ability.'
          content:
            application/json:
              example:
                message: 'Invalid ability provided.'
  /v1/usage:
    get:
      tags:
        - Usage
      operationId: usage_show
      summary: 'Get current usage'
      description: |-
        Returns purchased, used and remaining requests for the active billing period.

        Gates: logApiRequest.

        Note: The response carries an `X-Request-Uuid` header you can quote in support tickets.
      security:
        -
          bearerToken: {  }
      responses:
        200:
          description: 'Usage for the active period.'
          content:
            application/json:
              example:
                period_start_date: '2026-05-01T00:00:00Z'
                period_end_date: '2026-05-31T23:59:59Z'
                period_purchased_requests: 1000
                period_used_requests: 240
                period_available_requests: 760
        401:
          description: 'Missing or invalid token.'
          content:
            application/json:
              example:
                message: Unauthenticated.
  /v1/templates:
    get:
      tags:
        - Templates
      operationId: templates_index
      summary: 'List templates'
      description: |-
        Returns a cursor-paginated list of your templates.

        Gates: logApiRequest.
      security:
        -
          bearerToken: {  }
      parameters:
        -
          name: cursor
          in: query
          required: false
          description: 'Cursor token from a previous response''s meta.next_cursor.'
          schema:
            type: string
      responses:
        200:
          description: 'A page of templates.'
          content:
            application/json:
              example:
                data:
                  -
                    uuid: 141e8454-8aa1-420a-bd0b-18a85ba949a2
                    title: 'Template 1'
                    width: 1200
                    height: 630
                    layers:
                      - { layer: review_title, type: text, available_modifications: { text: 'The worst Time-Travel movie ever made' } }
                    created_at: '2026-05-01T12:00:00Z'
                meta:
                  previous_cursor: null
                  next_cursor: eyJpZCI6MTB9
    post:
      tags:
        - Templates
      operationId: templates_store
      summary: 'Create a template'
      description: |-
        Creates a template and generates its preview image. Requires the create-template policy.

        Gates: policy:create,Template.

        Note: This endpoint does not validate input. Send well-formed values — bad data is persisted as-is.
      security:
        -
          bearerToken: {  }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  description: 'Template name. Rules: no server validation.'
                width:
                  type: integer
                  description: 'Canvas width in pixels. Rules: no server validation.'
                height:
                  type: integer
                  description: 'Canvas height in pixels. Rules: no server validation.'
                layers:
                  type: array
                  items:
                    type: string
                  description: 'Layer definitions; stored JSON-encoded. Rules: no server validation.'
      responses:
        200:
          description: 'Template created.'
          content:
            application/json:
              example:
                data:
                  uuid: ...
                  title: 'New template'
                message: 'Template created successfully'
  '/v1/templates/{templateUuid}':
    get:
      tags:
        - Templates
      operationId: templates_show
      summary: 'Get a template'
      description: |-
        Returns a single template and its parsed layers.

        Gates: logApiRequest.
      security:
        -
          bearerToken: {  }
      parameters:
        -
          name: templateUuid
          in: path
          required: true
          description: 'UUID of the template.'
          schema:
            type: string
      responses:
        200:
          description: 'The template.'
          content:
            application/json:
              example:
                uuid: 141e8454-8aa1-420a-bd0b-18a85ba949a2
                title: 'Template 1'
                width: 1200
                height: 630
                layers:
                  -
                    layer: review_poster
                    type: image
                    available_modifications:
                      image_url: 'https://snapsbrew.com/crazy-doc-brown-jailed'
                  -
                    layer: review_title
                    type: text
                    available_modifications:
                      text: 'The worst Time-Travel movie ever made'
                created_at: '2026-05-01T12:00:00Z'
        404:
          description: 'Template not found in your company.'
          content:
            application/json:
              example:
                message: 'Not found.'
  '/v1/templates/{templateId}':
    patch:
      tags:
        - Templates
      operationId: templates_update
      summary: 'Update a template'
      description: 'Updates a template and regenerates its preview image. The path segment is the numeric template ID, not the UUID.'
      security:
        -
          bearerToken: {  }
      parameters:
        -
          name: templateId
          in: path
          required: true
          description: 'Numeric template ID, scoped to your company.'
          schema:
            type: integer
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  description: 'Template name. Rules: no server validation.'
                width:
                  type: integer
                  description: 'Canvas width in pixels. Rules: no server validation.'
                height:
                  type: integer
                  description: 'Canvas height in pixels. Rules: no server validation.'
                layers:
                  type: array
                  items:
                    type: string
                  description: 'Layer definitions; stored JSON-encoded. Rules: no server validation.'
      responses:
        200:
          description: 'Template updated.'
          content:
            application/json:
              example:
                message: 'template updated sucessfully'
        404:
          description: 'Template not found in your company.'
          content:
            application/json:
              example:
                message: 'Not found.'
    delete:
      tags:
        - Templates
      operationId: templates_destroy
      summary: 'Delete a template'
      description: |-
        Deletes a template. The path segment is the numeric template ID. Requires the delete-template policy.

        Gates: policy:delete,Template.
      security:
        -
          bearerToken: {  }
      parameters:
        -
          name: templateId
          in: path
          required: true
          description: 'Numeric template ID, scoped to your company.'
          schema:
            type: integer
      responses:
        200:
          description: 'Template deleted.'
          content:
            application/json:
              example:
                message: 'template deleted sucessfully'
        404:
          description: 'Template not found in your company.'
          content:
            application/json:
              example:
                message: 'Not found.'
  /v1/media:
    get:
      tags:
        - Media
      operationId: media_index
      summary: 'List media'
      description: 'Returns your uploaded images, 12 per page, newest first.'
      security:
        -
          bearerToken: {  }
      parameters:
        -
          name: page
          in: query
          required: false
          description: 'Page number for the standard paginator.'
          schema:
            type: integer
      responses:
        200:
          description: 'A page of media.'
          content:
            application/json:
              example:
                data:
                  -
                    id: 12
                    file_name: logo.png
                    src: 'https://snapsbrew.com/storage/12/conversions/logo-image.jpg'
                links:
                  first: ...
                  last: ...
                  prev: null
                  next: null
                meta:
                  current_page: 1
                  per_page: 12
                  total: 1
    post:
      tags:
        - Media
      operationId: media_store
      summary: 'Upload media'
      description: 'Uploads an image into your media library. Send as multipart/form-data.'
      security:
        -
          bearerToken: {  }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: 'The image to upload. Rules: required, file, image.'
              required:
                - file
      responses:
        200:
          description: 'Media uploaded.'
          content:
            application/json:
              example:
                id: 12
                file_name: logo.png
                src: 'https://snapsbrew.com/storage/12/conversions/logo-image.jpg'
        422:
          description: 'File missing or not an image.'
          content:
            application/json:
              example:
                message: 'The file must be an image.'
  /v1/screenshots:
    get:
      tags:
        - Screenshots
      operationId: screenshots_index
      summary: 'List screenshots'
      description: |-
        Returns a cursor-paginated list of your screenshots.

        Gates: logApiRequest.
      security:
        -
          bearerToken: {  }
      parameters:
        -
          name: cursor
          in: query
          required: false
          description: 'Cursor token from meta.next_cursor.'
          schema:
            type: string
      responses:
        200:
          description: 'A page of screenshots.'
          content:
            application/json:
              example:
                data:
                  -
                    uuid: a1b2c3
                    url: 'https://example.com'
                    image_url: 'https://snapsbrew.com/storage/screenshots/a1b2c3.png'
                    width: 1330
                    height: 780
                    created_at: '2026-05-01T12:00:00Z'
                meta:
                  previous_cursor: null
                  next_cursor: null
    post:
      tags:
        - Screenshots
      operationId: screenshots_store
      summary: 'Create a screenshot'
      description: |-
        Captures a web page. Defaults to a 1330x780 viewport when width/height are omitted.

        Gates: logApiRequest, hasActiveSubscription.

        Credits: Requires available credits (canCreateSnap); returns 417 when exhausted.
      security:
        -
          bearerToken: {  }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  description: 'Page to capture. Validated as a URL when present. Rules: url.'
                width:
                  type: integer
                  description: 'Viewport width. Defaults to 1330. Rules: integer.'
                height:
                  type: integer
                  description: 'Viewport height. Defaults to 780. Rules: integer.'
      responses:
        200:
          description: 'Screenshot created.'
          content:
            application/json:
              example:
                data:
                  id: a1b2c3
                  url: 'https://example.com'
                  image_url: 'https://snapsbrew.com/storage/screenshots/a1b2c3.png'
                  width: 1330
                  height: 780
                  created_at: '2026-05-01T12:00:00Z'
        401:
          description: 'No active subscription or trial.'
          content:
            application/json:
              example:
                message: Unauthenticated.
        417:
          description: 'Monthly credit quota exhausted.'
          content:
            application/json:
              example:
                message: 'You have reached your snap quota.'
  '/v1/screenshots/{screenshotUuid}':
    get:
      tags:
        - Screenshots
      operationId: screenshots_show
      summary: 'Get a screenshot'
      description: |-
        Returns a single screenshot.

        Gates: logApiRequest.
      security:
        -
          bearerToken: {  }
      parameters:
        -
          name: screenshotUuid
          in: path
          required: true
          description: 'UUID of the screenshot.'
          schema:
            type: string
      responses:
        200:
          description: 'The screenshot.'
          content:
            application/json:
              example:
                data:
                  id: a1b2c3
                  url: 'https://example.com'
                  image_url: 'https://snapsbrew.com/storage/screenshots/a1b2c3.png'
                  width: 1330
                  height: 780
                  created_at: '2026-05-01T12:00:00Z'
        404:
          description: 'Screenshot not found in your company.'
          content:
            application/json:
              example:
                message: 'Not found.'
  /v1/snaps:
    post:
      tags:
        - Snaps
      operationId: snaps_store
      summary: 'Create a snap'
      description: |-
        Renders a snap from a single template (`template_id`) or every template in a collection (`collection_id`). Provide exactly one of the two.

        Synchronous (default): the response carries the finished `media_url` and returns 201.

        Asynchronous (`async: true`): the snap is queued, the response returns 202 immediately, and the result is delivered to `webhook_url` on completion.

        Gates: logApiRequest, hasActiveSubscription.

        Credits: Synchronous: 1 credit per rendered template. Asynchronous: 0 credits at accept time (HTTP 202); the credit is charged when the queued snap reaches completed.

        Note: Every response carries an `X-Request-Uuid` header. Quote it in support tickets.

        Note: A collection request returns an array of snap objects, one per template.
      security:
        -
          bearerToken: {  }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                template_id:
                  type: string
                  description: 'UUID of a single template to render. Rules: required_without:collection_id, prohibits:collection_id, exists templates.uuid scoped to company.'
                collection_id:
                  type: string
                  description: 'UUID of a collection; one snap is rendered per template. Rules: required_without:template_id, prohibits:template_id, exists template_collections.uuid scoped to company.'
                type:
                  type: string
                  description: 'Output format. Defaults to image. Rules: nullable, in:image,pdf.'
                async:
                  type: boolean
                  description: 'Queue the render and return 202. Rules: sometimes, boolean.'
                webhook_url:
                  type: string
                  description: 'Callback URL invoked when an async snap completes. Rules: url, required_if:async,true.'
                variants:
                  type: array
                  items:
                    type: string
                  description: 'Per-layer content overrides applied to the render. Rules: no server validation.'
      responses:
        201:
          description: 'Synchronous snap rendered.'
          content:
            application/json:
              example:
                id: 9f1c2d3e
                media_url: 'https://snapsbrew.com/storage/snaps/9f1c2d3e.png'
                type: image
                status: completed
                is_async: false
                webhook_url: null
                completed_at: '2026-05-01T12:00:03Z'
                last_error: null
                variants: {  }
                template_id: 141e8454-8aa1-420a-bd0b-18a85ba949a2
                template_title: 'Launch card'
                created_at: '2026-05-01T12:00:00Z'
        202:
          description: 'Asynchronous snap queued.'
          content:
            application/json:
              example:
                id: 9f1c2d3e
                media_url: null
                type: image
                status: pending
                is_async: true
                webhook_url: 'https://example.com/hooks/snap'
                completed_at: null
                last_error: null
                variants: {  }
                template_id: 141e8454-8aa1-420a-bd0b-18a85ba949a2
                template_title: 'Launch card'
                created_at: '2026-05-01T12:00:00Z'
        404:
          description: 'Template or collection not found, or collection empty.'
          content:
            application/json:
              example:
                message: 'Not found.'
        417:
          description: 'Monthly credit quota exhausted.'
          content:
            application/json:
              example:
                message: 'You have reached your snap quota.'
        422:
          description: 'Validation failed.'
          content:
            application/json:
              example:
                message: 'The template id field is required when collection id is not present.'
  /v1/tags:
    get:
      tags:
        - Tags
      operationId: tags_index
      summary: 'List tags'
      description: |-
        Returns up to 15 tags, ordered by name, optionally filtered by a search term.

        Gates: logApiRequest.
      security:
        -
          bearerToken: {  }
      parameters:
        -
          name: search
          in: query
          required: false
          description: 'Case-insensitive substring match on tag name.'
          schema:
            type: string
      responses:
        200:
          description: 'Matching tags.'
          content:
            application/json:
              example:
                data:
                  -
                    id: 1
                    name: Launch
                    slug: launch
    post:
      tags:
        - Tags
      operationId: tags_store
      summary: 'Create a tag'
      description: |-
        Creates a tag. The slug is generated from the name; collisions get a numeric suffix.

        Gates: logApiRequest.
      security:
        -
          bearerToken: {  }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: 'Display name of the tag. Rules: required, string, max:255.'
              required:
                - name
      responses:
        201:
          description: 'Tag created.'
          content:
            application/json:
              example:
                data:
                  id: 1
                  name: Launch
                  slug: launch
        422:
          description: 'Validation failed.'
          content:
            application/json:
              example:
                message: 'The name field is required.'
  '/v1/templates/{templateUuid}/tags':
    post:
      tags:
        - Tags
      operationId: tags_template_store
      summary: 'Sync template tags'
      description: |-
        Replaces all tags on a template with the supplied set. Send an empty array to clear every tag.

        Gates: logApiRequest.
      security:
        -
          bearerToken: {  }
      parameters:
        -
          name: templateUuid
          in: path
          required: true
          description: 'UUID of the template.'
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                tag_ids:
                  type: array
                  items:
                    type: string
                  description: 'Tag IDs to attach. Replaces the existing set. Rules: array.'
      responses:
        200:
          description: 'Tags synced.'
          content:
            application/json:
              example:
                message: 'Tags updated successfully.'
                tags:
                  -
                    id: 1
                    name: Launch
                    slug: launch
        404:
          description: 'Template not found in your company.'
          content:
            application/json:
              example:
                message: 'Not found.'
        422:
          description: 'A tag ID is invalid or not yours.'
          content:
            application/json:
              example:
                message: 'The selected tag_ids.0 is invalid.'
  '/v1/embedded-forms/{formUuid}':
    post:
      tags:
        - 'Embedded Forms'
      operationId: embedded_forms_store
      summary: 'Submit an embedded form'
      description: |-
        Renders a snap from a configured form. Template forms return one snap; collection forms return an array of snaps.

        Layer keys: template forms use the plain layer key (`headline`); collection forms use `{templateUuid}__{layerKey}`. Only layers the form was configured with are accepted; the rest are dropped silently.

        Gates: embeddedFormMiddleware, valid signature required.

        Credits: The form owner is charged: 1 credit for a template form, or 1 per template for a collection form.

        Note: Collection forms return a bare array of snap objects, not a `data`-wrapped object.

        Note: This route has no rate limiting.
      parameters:
        -
          name: formUuid
          in: path
          required: true
          description: 'UUID of the form.'
          schema:
            type: string
        -
          name: signature
          in: query
          required: true
          description: 'Laravel URL signature. The whole URL must be signed.'
          schema:
            type: string
      requestBody:
        required: false
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                layers:
                  type: object
                  description: 'Layer overrides. Value keys by layer type: image -> image_url (file), text -> text, empty -> background_color, qr_code -> qr_content. Rules: keys matched against the form configuration.'
      responses:
        201:
          description: 'Snap rendered (template form).'
          content:
            application/json:
              example:
                data:
                  id: 9f1c2d3e
                  media_url: 'https://snapsbrew.com/storage/snaps/9f1c2d3e.png'
                  type: image
                  status: completed
                  is_async: false
                  webhook_url: null
                  completed_at: '2026-05-01T12:00:03Z'
                  last_error: null
                  variants: {  }
                  template_id: '141e8454'
                  template_title: 'Launch card'
                  created_at: '2026-05-01T12:00:00Z'
        404:
          description: 'Form not found or invalid signature.'
          content:
            application/json:
              example:
                message: 'Not found.'
        422:
          description: 'Collection form has no templates configured.'
          content:
            application/json:
              example:
                message: 'The form does not include any templates.'
components:
  securitySchemes:
    bearerToken:
      type: http
      scheme: bearer
      description: 'Sanctum personal access token with the api_requests ability.'
  schemas:
    Template:
      type: object
      description: 'A reusable design with addressable layers.'
      properties:
        uuid:
          type: string
          description: 'Stable public identifier.'
        title:
          type: string
          description: 'Human-readable name.'
        width:
          type: integer
          description: 'Canvas width in pixels.'
        height:
          type: integer
          description: 'Canvas height in pixels.'
        layers:
          type: string
          description: 'Addressable layers and their modifications.'
        created_at:
          type: string
          description: 'ISO-8601 creation timestamp.'
    Layer:
      type: object
      description: 'A single addressable element inside a template.'
      properties:
        layer:
          type: string
          description: 'Layer key used when overriding content.'
        type:
          type: string
          description: 'One of image, text, empty, qr_code.'
        available_modifications:
          type: object
          description: 'Override keys this layer accepts (image_url, text, background_color, qr_content).'
    Screenshot:
      type: object
      description: 'A rendered capture of a web page.'
      properties:
        id:
          type: string
          description: 'Screenshot UUID.'
        url:
          type: string
          description: 'Captured page URL.'
        image_url:
          type: string
          description: 'URL of the rendered image.'
        width:
          type: integer
          description: 'Viewport width in pixels.'
        height:
          type: integer
          description: 'Viewport height in pixels.'
        created_at:
          type: string
          description: 'ISO-8601 creation timestamp.'
    Snap:
      type: object
      description: 'A render job produced from a template or collection.'
      properties:
        id:
          type: string
          description: 'Snap UUID.'
        media_url:
          type: string
          description: 'URL of the finished asset, null until completed.'
        type:
          type: string
          description: 'image or pdf.'
        status:
          type: string
          description: 'pending, processing, completed, failed or canceled.'
        is_async:
          type: boolean
          description: 'Whether the snap was queued.'
        webhook_url:
          type: string
          description: 'Callback URL for async completion.'
        completed_at:
          type: string
          description: 'When rendering finished.'
        last_error:
          type: string
          description: 'Failure reason, when status is failed.'
        variants:
          type: array
          description: 'Per-layer content overrides applied.'
        template_id:
          type: string
          description: 'Source template UUID.'
        template_title:
          type: string
          description: 'Source template name.'
        created_at:
          type: string
          description: 'ISO-8601 creation timestamp.'
