openapi: 3.0.3
info:
  title: Jawwws Bytes Developer API
  version: 0.4.0
  description: |
    Public Developer API for Jawwws Bytes workspace integrations.

    The current public API supports creating, listing, reading, updating and
    deleting short links, resolving Activity Groups, assigning links to Activity
    Groups, managing workspace tags, and attaching tags to links.
    Only documented /api/v1 endpoints are supported for external integrations.
  contact:
    name: Jawwws Bytes
    url: https://bytes.jawwws.com
servers:
  - url: https://api.jaww.ws/api/v1
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Activity Groups
    description: Resolve workspace Activity Groups for link assignment.
  - name: Links
    description: Create and manage short links.
  - name: Tags
    description: Manage workspace tags.
  - name: Link tags
    description: Attach and remove tags on links.
paths:
  /activity-groups:
    get:
      tags:
        - Activity Groups
      operationId: listActivityGroups
      summary: List Activity Groups
      description: |
        Lists Activity Groups in the workspace that owns the API key. Use the
        public ID or slug filters to resolve a group before assigning a link.
      parameters:
        - name: id
          in: query
          required: false
          schema:
            type: string
            pattern: "^[0-9A-HJKMNP-TV-Z]{26}$"
          description: Filter by immutable public Activity Group ID.
        - name: slug
          in: query
          required: false
          schema:
            type: string
            maxLength: 120
            pattern: "^[a-z0-9][a-z0-9-]*$"
          description: Filter by workspace Activity Group slug.
        - name: q
          in: query
          required: false
          schema:
            type: string
            maxLength: 150
          description: Search name, slug or description.
        - name: type
          in: query
          required: false
          schema:
            type: string
            enum:
              - campaign
              - always_on
              - project
              - event
              - content
              - other
          description: Filter by Activity Group type.
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - draft
              - scheduled
              - active
              - completed
              - archived
          description: Filter by effective Activity Group status.
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number.
        - name: per_page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: Activity Groups per page.
      responses:
        "200":
          description: Activity Groups returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActivityGroupListResponse"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          $ref: "#/components/responses/WorkspaceNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /links:
    get:
      tags:
        - Links
      operationId: listLinks
      summary: List short links
      description: Lists short links in the workspace that owns the API key.
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number.
        - name: per_page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: Links per page.
        - name: q
          in: query
          required: false
          schema:
            type: string
          description: Search slug, title or destination URL.
        - name: tag_id
          in: query
          required: false
          schema:
            type: string
            pattern: "^[0-9A-HJKMNP-TV-Z]{26}$"
          description: Filter by immutable public tag ID.
        - name: tag
          in: query
          required: false
          schema:
            type: string
          description: Filter by workspace tag slug.
      responses:
        "200":
          description: Links returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkListResponse"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          $ref: "#/components/responses/WorkspaceNotFound"
    post:
      tags:
        - Links
      operationId: createLink
      summary: Create a short link
      description: |
        Creates a short link in the workspace that owns the API key. Supply an
        Activity Group public ID or slug to create a primary assignment. When
        utm_campaign is omitted, the Activity Group default is used when present.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateLinkRequest"
            examples:
              printQr:
                summary: Printed QR placement
                value:
                  destination_url: https://example.com/cops-and-coffee/coffee-tasting
                  slug: cops-coffee-tasting
                  title: Cops and Coffee tasting event
                  activity_group_id: summer-events
                  channel: print
                  utm_source: poster
                  utm_medium: qr
                  utm_campaign: coffee-tasting
                  utm_content: poster-v1
      responses:
        "201":
          description: Short link created successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkCreatedResponse"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          $ref: "#/components/responses/WorkspaceNotFound"
        "422":
          description: Validation, Activity Group, limit, slug or destination safety error.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ValidationErrorResponse"
                  - $ref: "#/components/schemas/ActiveLinkLimitErrorResponse"
                  - $ref: "#/components/schemas/DestinationSafetyErrorResponse"
  /links/{id}:
    parameters:
      - $ref: "#/components/parameters/LinkId"
    get:
      tags:
        - Links
      operationId: getLink
      summary: Get a short link
      description: Returns one short link from the workspace that owns the API key.
      responses:
        "200":
          description: Link returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkResponse"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          description: Workspace or link not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                linkNotFound:
                  value:
                    message: Link not found.
    patch:
      tags:
        - Links
      operationId: updateLink
      summary: Update a short link
      description: |
        Updates one short link in the workspace that owns the API key. Supplying
        activity_group_id moves or reconciles its primary Activity Group assignment
        without creating duplicate active items.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateLinkRequest"
            examples:
              updateCampaign:
                summary: Update campaign metadata
                value:
                  title: Cops and Coffee tasting event
                  activity_group_id: summer-events
                  channel: print
                  utm_source: poster
                  utm_medium: qr
                  utm_campaign: coffee-tasting
                  utm_content: poster-v2
      responses:
        "200":
          description: Link updated successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkUpdatedResponse"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          description: Workspace or link not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                linkNotFound:
                  value:
                    message: Link not found.
        "422":
          description: Validation, Activity Group, limit, slug or destination safety error.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ValidationErrorResponse"
                  - $ref: "#/components/schemas/ActiveLinkLimitErrorResponse"
                  - $ref: "#/components/schemas/DestinationSafetyErrorResponse"
    delete:
      tags:
        - Links
      operationId: deleteLink
      summary: Delete a short link
      description: Soft-deletes one short link in the workspace that owns the API key.
      responses:
        "204":
          description: Link deleted successfully. No response body is returned.
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          description: Workspace or link not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                linkNotFound:
                  value:
                    message: Link not found.
        "422":
          description: Link cannot be deleted because it is attached to a live Offline Engagement asset.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ValidationErrorResponse"
  /tags:
    get:
      tags:
        - Tags
      operationId: listTags
      summary: List workspace tags
      description: Lists tags for the workspace that owns the API key.
      responses:
        "200":
          description: Tags returned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TagListResponse"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          $ref: "#/components/responses/WorkspaceNotFound"
    post:
      tags:
        - Tags
      operationId: createTag
      summary: Create a workspace tag
      description: Creates a tag in the workspace that owns the API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTagRequest"
            examples:
              eventTag:
                summary: Event tag
                value:
                  name: Coffee tasting
                  slug: coffee-tasting
                  colour: "#2563eb"
                  description: Links for coffee tasting events
      responses:
        "201":
          description: Tag created successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TagCreatedResponse"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          $ref: "#/components/responses/WorkspaceNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
  /tags/{id}:
    parameters:
      - $ref: "#/components/parameters/TagId"
    patch:
      tags:
        - Tags
      operationId: updateTag
      summary: Update a workspace tag
      description: Updates a tag in the workspace that owns the API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTagRequest"
            examples:
              updateDescription:
                summary: Update tag description
                value:
                  description: Updated event links
      responses:
        "200":
          description: Tag updated successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TagUpdatedResponse"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          description: Workspace or tag not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                tagNotFound:
                  value:
                    message: Tag not found.
        "422":
          $ref: "#/components/responses/ValidationError"
    delete:
      tags:
        - Tags
      operationId: deleteTag
      summary: Delete a workspace tag
      description: Deletes a tag from the workspace and removes it from links.
      responses:
        "204":
          description: Tag deleted successfully. No response body is returned.
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          description: Workspace or tag not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                tagNotFound:
                  value:
                    message: Tag not found.
  /links/{id}/tags:
    parameters:
      - $ref: "#/components/parameters/LinkId"
    post:
      tags:
        - Link tags
      operationId: attachTagsToLink
      summary: Attach tags to a link
      description: Attaches one or more workspace tags to a link.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AttachTagsRequest"
            examples:
              oneTag:
                summary: Attach one tag
                value:
                  tag_id: 01K0M5ABCD7EFGHJKMNPQRSTVW
              multipleTags:
                summary: Attach multiple tags
                value:
                  tag_ids:
                    - 01K0M5ABCD7EFGHJKMNPQRSTVW
                    - 01K0M5BCDE8FGHJKMNPQRSTVWX
      responses:
        "200":
          description: Tags attached successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AttachTagsResponse"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          description: Workspace or link not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                linkNotFound:
                  value:
                    message: Link not found.
        "422":
          $ref: "#/components/responses/ValidationError"
  /links/{id}/tags/{tagId}:
    parameters:
      - $ref: "#/components/parameters/LinkId"
      - $ref: "#/components/parameters/TagIdForLink"
    delete:
      tags:
        - Link tags
      operationId: detachTagFromLink
      summary: Remove a tag from a link
      description: Removes one workspace tag from one link.
      responses:
        "204":
          description: Tag removed from link. No response body is returned.
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          description: Workspace, link or tag not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                linkNotFound:
                  value:
                    message: Link not found.
                tagNotFound:
                  value:
                    message: Tag not found.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Bytes API key
      description: Use a workspace API key in the Authorization header.
  parameters:
    LinkId:
      name: id
      in: path
      required: true
      description: Immutable public Link ID.
      schema:
        type: string
        pattern: "^[0-9A-HJKMNP-TV-Z]{26}$"
      example: 01K0M4ZN8X7P9Q2R3S4T5V6W7X
    TagId:
      name: id
      in: path
      required: true
      description: Immutable public tag ID.
      schema:
        type: string
        pattern: "^[0-9A-HJKMNP-TV-Z]{26}$"
      example: 01K0M5ABCD7EFGHJKMNPQRSTVW
    TagIdForLink:
      name: tagId
      in: path
      required: true
      description: Immutable public tag ID.
      schema:
        type: string
        pattern: "^[0-9A-HJKMNP-TV-Z]{26}$"
      example: 01K0M5ABCD7EFGHJKMNPQRSTVW
  responses:
    Unauthenticated:
      description: Missing, invalid or revoked API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            unauthenticated:
              value:
                message: Unauthenticated.
    WorkspaceNotFound:
      description: Workspace not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            workspaceNotFound:
              value:
                message: Workspace not found.
    ValidationError:
      description: Validation error.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ValidationErrorResponse"
  schemas:
    LinkListResponse:
      type: object
      required:
        - workspace
        - summary
        - links
        - meta
      properties:
        workspace:
          $ref: "#/components/schemas/WorkspaceSummary"
        summary:
          $ref: "#/components/schemas/LinkSummary"
        links:
          type: array
          items:
            $ref: "#/components/schemas/Link"
        meta:
          $ref: "#/components/schemas/PaginationMeta"
    LinkResponse:
      type: object
      required:
        - link
      properties:
        link:
          $ref: "#/components/schemas/Link"
    CreateLinkRequest:
      type: object
      required:
        - destination_url
      additionalProperties: false
      properties:
        destination_url:
          type: string
          maxLength: 65535
          description: Full destination URL for the short link.
          example: https://example.com/cops-and-coffee/coffee-tasting
        slug:
          type: string
          nullable: true
          minLength: 3
          maxLength: 64
          pattern: "^[a-z0-9-]{3,64}$"
          description: Optional custom slug. Bytes generates one when omitted.
          example: cops-coffee-tasting
        title:
          type: string
          nullable: true
          maxLength: 190
          example: Cops and Coffee tasting event
        notes:
          type: string
          nullable: true
          maxLength: 255
          example: Used on the front door poster.
        is_active:
          type: boolean
          nullable: true
          default: true
          description: Whether the link is active immediately.
          example: true
        channel:
          type: string
          nullable: true
          enum:
            - social
            - email
            - print
            - ads
            - sms
            - other
          example: print
        expires_at:
          type: string
          nullable: true
          format: date-time
          description: Optional future expiry date and time.
          example: "2026-12-31T23:59:59Z"
        password:
          type: string
          nullable: true
          minLength: 6
          maxLength: 100
          writeOnly: true
          description: Optional password for the link. Never returned by the API.
        utm_source:
          type: string
          nullable: true
          maxLength: 100
          example: poster
        utm_medium:
          type: string
          nullable: true
          maxLength: 100
          example: qr
        utm_campaign:
          type: string
          nullable: true
          maxLength: 100
          example: coffee-tasting
        utm_term:
          type: string
          nullable: true
          maxLength: 100
          example: espresso
        utm_content:
          type: string
          nullable: true
          maxLength: 100
          example: poster-v1
        activity_group_id:
          type: string
          nullable: true
          maxLength: 120
          description: |
            Public Activity Group ID or slug in the API key workspace. When
            supplied and utm_campaign is omitted, the Activity Group default
            campaign is used when present.
          example: summer-events
    UpdateLinkRequest:
      type: object
      additionalProperties: false
      properties:
        destination_url:
          type: string
          maxLength: 65535
          example: https://example.com/cops-and-coffee/coffee-tasting
        slug:
          type: string
          nullable: true
          minLength: 3
          maxLength: 64
          pattern: "^[a-z0-9-]{3,64}$"
          example: cops-coffee-tasting
        title:
          type: string
          nullable: true
          maxLength: 190
          example: Cops and Coffee tasting event
        notes:
          type: string
          nullable: true
          maxLength: 255
          example: Updated placement notes.
        is_active:
          type: boolean
          nullable: true
          example: true
        channel:
          type: string
          nullable: true
          enum:
            - social
            - email
            - print
            - ads
            - sms
            - other
          example: print
        expires_at:
          type: string
          nullable: true
          format: date-time
          example: "2026-12-31T23:59:59Z"
        password:
          type: string
          nullable: true
          minLength: 6
          maxLength: 100
          writeOnly: true
          description: Optional replacement password. Send null or an empty value to clear.
        utm_source:
          type: string
          nullable: true
          maxLength: 100
          example: poster
        utm_medium:
          type: string
          nullable: true
          maxLength: 100
          example: qr
        utm_campaign:
          type: string
          nullable: true
          maxLength: 100
          example: coffee-tasting
        utm_term:
          type: string
          nullable: true
          maxLength: 100
          example: espresso
        utm_content:
          type: string
          nullable: true
          maxLength: 100
          example: poster-v2
        activity_group_id:
          type: string
          maxLength: 120
          description: |
            Public Activity Group ID or slug in the API key workspace. Reconciles
            the primary assignment without creating duplicate active items. When
            utm_campaign is omitted, the Activity Group default campaign is used
            when present.
          example: summer-events
    LinkCreatedResponse:
      type: object
      required:
        - message
        - link
      properties:
        message:
          type: string
          example: Short link created successfully.
        link:
          $ref: "#/components/schemas/Link"
    LinkUpdatedResponse:
      type: object
      required:
        - message
        - link
      properties:
        message:
          type: string
          example: Link updated successfully.
        link:
          $ref: "#/components/schemas/Link"
    Link:
      type: object
      required:
        - id
        - slug
        - destination_url
        - short_url
        - is_active
        - is_password_protected
        - tags
        - click_count
        - unique_click_count
      properties:
        id:
          type: string
          pattern: "^[0-9A-HJKMNP-TV-Z]{26}$"
          example: 01K0M4ZN8X7P9Q2R3S4T5V6W7X
        slug:
          type: string
          example: cops-coffee-tasting
        destination_url:
          type: string
          example: https://example.com/cops-and-coffee/coffee-tasting
        short_url:
          type: string
          example: https://jaww.ws/cops-coffee-tasting
        title:
          type: string
          nullable: true
          example: Cops and Coffee tasting event
        notes:
          type: string
          nullable: true
          example: Used on the front door poster.
        is_active:
          type: boolean
          example: true
        expires_at:
          type: string
          nullable: true
          format: date-time
          example: "2026-12-31T23:59:59Z"
        is_password_protected:
          type: boolean
          example: false
        channel:
          type: string
          nullable: true
          enum:
            - social
            - email
            - print
            - ads
            - sms
            - other
          example: print
        utm_source:
          type: string
          nullable: true
          example: poster
        utm_medium:
          type: string
          nullable: true
          example: qr
        utm_campaign:
          type: string
          nullable: true
          example: coffee-tasting
        utm_term:
          type: string
          nullable: true
          example: espresso
        utm_content:
          type: string
          nullable: true
          example: poster-v1
        activity_group:
          allOf:
            - $ref: "#/components/schemas/ActivityGroup"
          nullable: true
          description: Primary Activity Group assignment, when present.
        tags:
          type: array
          items:
            $ref: "#/components/schemas/Tag"
        click_count:
          type: integer
          example: 0
        unique_click_count:
          type: integer
          example: 0
        last_clicked_at:
          type: string
          nullable: true
          format: date-time
        created_at:
          type: string
          nullable: true
          format: date-time
        updated_at:
          type: string
          nullable: true
          format: date-time
        deleted_at:
          type: string
          nullable: true
          format: date-time
    ActivityGroupListResponse:
      type: object
      required:
        - workspace
        - activity_groups
        - meta
      properties:
        workspace:
          $ref: "#/components/schemas/WorkspaceSummary"
        activity_groups:
          type: array
          items:
            $ref: "#/components/schemas/ActivityGroup"
        meta:
          $ref: "#/components/schemas/PaginationMeta"
    ActivityGroup:
      type: object
      required:
        - id
        - name
        - slug
        - type
        - status
      properties:
        id:
          type: string
          pattern: "^[0-9A-HJKMNP-TV-Z]{26}$"
          example: 01KYNVZSNW6Y56W2SHB4ZZ9QKE
        name:
          type: string
          example: Summer events
        slug:
          type: string
          example: summer-events
        type:
          type: string
          enum:
            - campaign
            - always_on
            - project
            - event
            - content
            - other
          example: event
        description:
          type: string
          nullable: true
        status:
          type: string
          enum:
            - draft
            - scheduled
            - active
            - completed
            - archived
          example: active
        default_utm_campaign:
          type: string
          nullable: true
          example: summer-events
        starts_at:
          type: string
          nullable: true
          format: date-time
        ends_at:
          type: string
          nullable: true
          format: date-time
        archived_at:
          type: string
          nullable: true
          format: date-time
        created_at:
          type: string
          nullable: true
          format: date-time
        updated_at:
          type: string
          nullable: true
          format: date-time
        assignment:
          allOf:
            - $ref: "#/components/schemas/ActivityGroupAssignment"
          nullable: true
          description: Present when returned as a link assignment.
    ActivityGroupAssignment:
      type: object
      required:
        - role
      properties:
        role:
          type: string
          example: primary
        channel:
          type: string
          nullable: true
          example: print
    WorkspaceSummary:
      type: object
      required:
        - name
        - slug
      properties:
        name:
          type: string
          example: Cops and Coffee
        slug:
          type: string
          example: cops-and-coffee
        role:
          type: string
          nullable: true
          example: owner
    LinkSummary:
      type: object
      properties:
        total_links:
          type: integer
          example: 12
        active_links:
          type: integer
          example: 10
        active_link_limit:
          type: object
          additionalProperties: true
        total_clicks:
          type: integer
          example: 143
        total_unique_clicks:
          type: integer
          example: 98
        last_activity_at:
          type: string
          nullable: true
          format: date-time
    PaginationMeta:
      type: object
      required:
        - current_page
        - last_page
        - per_page
        - total
      properties:
        current_page:
          type: integer
          example: 1
        last_page:
          type: integer
          example: 3
        per_page:
          type: integer
          example: 20
        total:
          type: integer
          example: 42
    TagListResponse:
      type: object
      required:
        - tags
      properties:
        tags:
          type: array
          items:
            $ref: "#/components/schemas/Tag"
    CreateTagRequest:
      type: object
      required:
        - name
      additionalProperties: false
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          example: Coffee tasting
        slug:
          type: string
          nullable: true
          minLength: 1
          maxLength: 120
          pattern: "^[a-z0-9-]+$"
          description: Optional slug. Generated from the name when omitted.
          example: coffee-tasting
        colour:
          type: string
          nullable: true
          maxLength: 32
          example: "#2563eb"
        description:
          type: string
          nullable: true
          maxLength: 255
          example: Links for coffee tasting events
    UpdateTagRequest:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          example: Coffee tasting
        slug:
          type: string
          nullable: true
          minLength: 1
          maxLength: 120
          pattern: "^[a-z0-9-]+$"
          example: coffee-tasting
        colour:
          type: string
          nullable: true
          maxLength: 32
          example: "#2563eb"
        description:
          type: string
          nullable: true
          maxLength: 255
          example: Updated event links
    TagCreatedResponse:
      type: object
      required:
        - message
        - tag
      properties:
        message:
          type: string
          example: Tag created successfully.
        tag:
          $ref: "#/components/schemas/Tag"
    TagUpdatedResponse:
      type: object
      required:
        - message
        - tag
      properties:
        message:
          type: string
          example: Tag updated successfully.
        tag:
          $ref: "#/components/schemas/Tag"
    Tag:
      type: object
      required:
        - id
        - name
        - slug
      properties:
        id:
          type: string
          pattern: "^[0-9A-HJKMNP-TV-Z]{26}$"
          example: 01K0M5ABCD7EFGHJKMNPQRSTVW
        name:
          type: string
          example: Coffee tasting
        slug:
          type: string
          example: coffee-tasting
        colour:
          type: string
          nullable: true
          example: "#2563eb"
        description:
          type: string
          nullable: true
          example: Links for coffee tasting events
    AttachTagsRequest:
      type: object
      additionalProperties: false
      description: Supply tag_id, tag_ids, or both. At least one valid tag is required.
      properties:
        tag_id:
          type: string
          nullable: true
          pattern: "^[0-9A-HJKMNP-TV-Z]{26}$"
          example: 01K0M5ABCD7EFGHJKMNPQRSTVW
        tag_ids:
          type: array
          nullable: true
          items:
            type: string
            pattern: "^[0-9A-HJKMNP-TV-Z]{26}$"
          example:
            - 01K0M5ABCD7EFGHJKMNPQRSTVW
            - 01K0M5BCDE8FGHJKMNPQRSTVWX
    AttachTagsResponse:
      type: object
      required:
        - message
        - tags
      properties:
        message:
          type: string
          example: Tag attached successfully.
        tags:
          type: array
          items:
            $ref: "#/components/schemas/Tag"
    ErrorResponse:
      type: object
      required:
        - message
      properties:
        message:
          type: string
          example: Unauthenticated.
    ValidationErrorResponse:
      type: object
      required:
        - message
        - errors
      properties:
        message:
          type: string
          example: The destination_url field is required.
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
          example:
            destination_url:
              - The destination_url field is required.
    ActiveLinkLimitErrorResponse:
      type: object
      required:
        - message
        - code
        - limit
        - active_links
        - plan
      properties:
        message:
          type: string
          example: Active link limit reached for this workspace plan.
        code:
          type: string
          example: active_link_limit_reached
        limit:
          type: integer
          example: 100
        active_links:
          type: integer
          example: 100
        plan:
          type: string
          example: free
    DestinationSafetyErrorResponse:
      type: object
      required:
        - message
        - code
        - destination_safety
        - errors
      properties:
        message:
          type: string
          example: This destination URL has been blocked by Bytes destination safety rules.
        code:
          type: string
          example: destination_url_blocked
        destination_safety:
          type: object
          properties:
            result:
              type: string
              example: blocked
            reason:
              type: string
              nullable: true
              example: This destination URL is not allowed.
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
          example:
            destination_url:
              - This destination URL is not allowed.
