> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blujay.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Send a message

> Sends a message in an existing chat. Blujay returns `202` as soon as the message is accepted and sends it in the background. Follow its progress with message webhooks or `GET /v1/messages/{message_id}`.

A message can have `text`, `link`, `media` and `vcard` parts. A `link` part must be the only part.

Optional features:
- `reply_to` sends an iMessage threaded reply. It isn't available for SMS or RCS chats.
- `effect` adds an iMessage screen or bubble effect to a text message.
- `text_decorations` styles text in direct iMessage chats.
- `preferred_service` picks iMessage, RCS or SMS for a direct chat. Omit it to let Blujay choose. Group chats don't accept it.

If an effect or text style can't be delivered because the message goes out as SMS or RCS, the text is still sent and the message lists what was dropped in `degradations`.

These limits protect your number's reputation. Until the contact replies to this number with a text or a tapback:
- Links, media and contact cards return `400 PRE_REPLY_CONTENT_BLOCKED`.
- Text longer than 300 characters returns `400 PRE_REPLY_LENGTH_EXCEEDED`.
At any time, before or after a first reply, at most 6 texts go to a contact in a row without a response (a text or tapback from them, or a message from them in a group) on any of your numbers. Calls don't count as a response. The next returns `400 PRE_REPLY_LIMIT_REACHED`. With an API key, `pre_reply_override: true` sends a 7th once. The count starts again when the contact responds.
In a group, the content limits apply until someone in the group replies, and the 6-text limit counts the group's texts since anyone in it last wrote. These errors are never retryable and never `429`.

Send an `Idempotency-Key` header (or `message.idempotency_key`) so you can safely retry. Repeating a request with the same key returns `200` with the original response instead of sending again.

Errors worth handling:
- `400 PRE_REPLY_*`: the contact hasn't replied yet, or hasn't responded to your last 6 texts (see the limits above).
- `403 RECIPIENT_SUPPRESSED`: the recipient opted out.
- `403 CONTACT_NOT_VERIFIED`: on the Free Plan, the contact hasn't texted your Free Plan number to verify yet.
- `403 CONTACT_MUST_TEXT_FIRST`: the number is on the AI Agent plan: it only replies to people who text it first, and can't send to group chats.
- `429 RATE_LIMIT_EXCEEDED` or `429 QUEUE_FULL`: the sending number hit a limit. Wait `Retry-After` seconds. Each number's current limits are in `GET /v1/phone-numbers` under `limits`.
- `409 SENDER_RETIRED`: the chat's number was retired. Start a new chat from an active number.
- `503 QUEUE_UNAVAILABLE`: Blujay couldn't accept the message right now. Retry with the same `Idempotency-Key`.




## OpenAPI

````yaml /api-reference/openapi.yaml post /v1/chats/{chat_id}/messages
openapi: 3.1.0
info:
  title: Blujay API
  version: 1.0.0
  description: >-
    The Blujay API lets you send and receive iMessage, RCS and SMS messages from
    your

    Blujay phone numbers, manage contacts, and receive real-time webhooks.


    Every response uses the same JSON envelope and carries a `trace_id` you can
    share

    with support. Authenticate with your workspace API key.
servers:
  - url: https://api.blujay.io
    description: Production
security:
  - PublicApiKey: []
tags:
  - name: Auth
    description: Check which workspace and credential a request is using.
  - name: Chats
    description: >-
      Direct and group conversations between your Blujay numbers and your
      contacts.
  - name: Messages
    description: Send, read, edit, unsend and retry messages.
  - name: Phone Numbers
    description: >-
      Your Blujay numbers, whether each can send right now, and their live
      sending limits.
  - name: Capability
    description: >-
      Check whether a phone number can be reached by iMessage, RCS or SMS before
      you send.
  - name: Contacts
    description: Create, find, update, tag, assign and opt out contacts.
  - name: vCards
    description: >-
      Saved contact cards that you can send in a message. Recipients get a
      native contact card over iMessage, SMS or RCS.
  - name: Team Members
    description: People in your workspace you can assign contacts to.
  - name: Workflows
    description: Enroll contacts in workflows you start manually.
  - name: Attachments
    description: >-
      Upload files once and reuse them in messages, or download files from
      messages.
  - name: Webhooks
    description: Subscribe to events, inspect delivery attempts and verify signatures.
paths:
  /v1/chats/{chat_id}/messages:
    post:
      tags:
        - Messages
      summary: Send a message
      description: >
        Sends a message in an existing chat. Blujay returns `202` as soon as the
        message is accepted and sends it in the background. Follow its progress
        with message webhooks or `GET /v1/messages/{message_id}`.


        A message can have `text`, `link`, `media` and `vcard` parts. A `link`
        part must be the only part.


        Optional features:

        - `reply_to` sends an iMessage threaded reply. It isn't available for
        SMS or RCS chats.

        - `effect` adds an iMessage screen or bubble effect to a text message.

        - `text_decorations` styles text in direct iMessage chats.

        - `preferred_service` picks iMessage, RCS or SMS for a direct chat. Omit
        it to let Blujay choose. Group chats don't accept it.


        If an effect or text style can't be delivered because the message goes
        out as SMS or RCS, the text is still sent and the message lists what was
        dropped in `degradations`.


        These limits protect your number's reputation. Until the contact replies
        to this number with a text or a tapback:

        - Links, media and contact cards return `400 PRE_REPLY_CONTENT_BLOCKED`.

        - Text longer than 300 characters returns `400
        PRE_REPLY_LENGTH_EXCEEDED`.

        At any time, before or after a first reply, at most 6 texts go to a
        contact in a row without a response (a text or tapback from them, or a
        message from them in a group) on any of your numbers. Calls don't count
        as a response. The next returns `400 PRE_REPLY_LIMIT_REACHED`. With an
        API key, `pre_reply_override: true` sends a 7th once. The count starts
        again when the contact responds.

        In a group, the content limits apply until someone in the group replies,
        and the 6-text limit counts the group's texts since anyone in it last
        wrote. These errors are never retryable and never `429`.


        Send an `Idempotency-Key` header (or `message.idempotency_key`) so you
        can safely retry. Repeating a request with the same key returns `200`
        with the original response instead of sending again.


        Errors worth handling:

        - `400 PRE_REPLY_*`: the contact hasn't replied yet, or hasn't responded
        to your last 6 texts (see the limits above).

        - `403 RECIPIENT_SUPPRESSED`: the recipient opted out.

        - `403 CONTACT_NOT_VERIFIED`: on the Free Plan, the contact hasn't
        texted your Free Plan number to verify yet.

        - `403 CONTACT_MUST_TEXT_FIRST`: the number is on the AI Agent plan: it
        only replies to people who text it first, and can't send to group chats.

        - `429 RATE_LIMIT_EXCEEDED` or `429 QUEUE_FULL`: the sending number hit
        a limit. Wait `Retry-After` seconds. Each number's current limits are in
        `GET /v1/phone-numbers` under `limits`.

        - `409 SENDER_RETIRED`: the chat's number was retired. Start a new chat
        from an active number.

        - `503 QUEUE_UNAVAILABLE`: Blujay couldn't accept the message right now.
        Retry with the same `Idempotency-Key`.
      operationId: createPublicChatMessage
      parameters:
        - $ref: '#/components/parameters/ChatId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateMessageRequest'
            examples:
              followUpText:
                summary: Follow-up text message
                value:
                  message:
                    parts:
                      - type: text
                        text: Here is the follow-up we discussed.
              followUpLink:
                summary: Follow-up link message
                value:
                  message:
                    parts:
                      - type: link
                        value: https://example.com/schedule
                    idempotency_key: follow-up-link-001
              threadedReply:
                summary: iMessage threaded reply
                value:
                  message:
                    parts:
                      - type: text
                        text: Yes, that one works.
                    reply_to:
                      message_id: msg_target123
                      part_index: 0
                    idempotency_key: follow-up-reply-001
      responses:
        '200':
          description: >-
            This `Idempotency-Key` was already used for the same request. The
            response is the original one, and nothing was sent again.
          headers:
            X-Trace-ID:
              $ref: '#/components/headers/XTraceID'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    required:
                      - data
                    properties:
                      data:
                        $ref: '#/components/schemas/AcceptedMessage'
        '202':
          description: The message was accepted and will be sent shortly.
          headers:
            X-Trace-ID:
              $ref: '#/components/headers/XTraceID'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    required:
                      - data
                    properties:
                      data:
                        $ref: '#/components/schemas/AcceptedMessage'
              examples:
                followUpText:
                  summary: Follow-up text message accepted
                  value:
                    status: success
                    data:
                      chat_id: chat_abc123
                      message:
                        id: msg_jkl012
                        direction: outbound
                        delivery_status: queued
                        preferred_service: null
                        service: null
                        from: '+15555550100'
                        to:
                          - '+15555550123'
                        created_at: '2026-10-14T17:08:30.000Z'
                        parts:
                          - type: text
                            text: Here is the follow-up we discussed.
                    trace_id: trc_8f2a1c0e4b7d
                    timestamp: '2026-10-14T17:08:30.142Z'
                threadedReply:
                  summary: Threaded reply accepted
                  value:
                    status: success
                    data:
                      chat_id: chat_abc123
                      message:
                        id: msg_mno345
                        direction: outbound
                        delivery_status: queued
                        preferred_service: null
                        service: null
                        from: '+15555550100'
                        to:
                          - '+15555550123'
                        created_at: '2026-10-14T17:09:05.000Z'
                        parts:
                          - type: text
                            text: Yes, that one works.
                        reply_to:
                          message_id: msg_target123
                          part_index: 0
                          thread_root_message_id: msg_target123
                    trace_id: trc_8f2a1c0e4b7d
                    timestamp: '2026-10-14T17:09:05.120Z'
        '400':
          $ref: '#/components/responses/SendBadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SendForbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/MessageSendConflict'
        '429':
          $ref: '#/components/responses/SendRejected429'
        '503':
          $ref: '#/components/responses/QueueUnavailable'
      security:
        - PublicApiKey: []
components:
  parameters:
    ChatId:
      name: chat_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/ChatId'
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        minLength: 1
        maxLength: 255
      description: >
        A unique value you choose (1–255 characters) that makes this request
        safe to retry. Send the same key with the same request and Blujay
        returns the original response instead of doing the work again. Use a new
        key for each new action.


        Blujay keeps keys for at least 24 hours. Keys are separate for each API
        key and each route family (for example every `POST
        /v1/chats/{chat_id}/messages` shares one). Reusing a key with a
        different request returns `409 IDEMPOTENCY_REQUEST_MISMATCH`. Sending it
        while the first request is still running returns `409
        IDEMPOTENCY_IN_PROGRESS`; retry shortly.
  schemas:
    CreateMessageRequest:
      type: object
      required:
        - message
      properties:
        message:
          $ref: '#/components/schemas/MessageInput'
        pre_reply_override:
          type: boolean
          description: >-
            API keys only. Sends a 7th text in a row to a contact who hasn't
            responded, once. A later text before they respond still returns
            `PRE_REPLY_LIMIT_REACHED`. It never lifts
            `PRE_REPLY_CONTENT_BLOCKED` or `PRE_REPLY_LENGTH_EXCEEDED`, and
            connected apps such as Zapier can't use it.
    SuccessEnvelope:
      type: object
      description: Wraps every successful response. The result is in `data`.
      required:
        - status
        - trace_id
        - timestamp
      properties:
        status:
          type: string
          enum:
            - success
        trace_id:
          $ref: '#/components/schemas/TraceId'
        timestamp:
          type: string
          examples:
            - '2026-10-14T17:01:00.000Z'
          format: date-time
        client_request_id:
          type: string
          description: >-
            Your `X-Request-ID` header value, echoed back when you send one. Not
            included on the send responses (`POST /v1/chats`, `POST
            /v1/chats/{chat_id}/messages`, `POST
            /v1/messages/{message_id}/retry`); keep your own mapping from your
            request ID to the returned message ID there.
    AcceptedMessage:
      type: object
      required:
        - chat_id
        - message
      properties:
        chat_id:
          $ref: '#/components/schemas/ChatId'
        message:
          type: object
          additionalProperties: false
          required:
            - id
            - direction
            - delivery_status
            - service
            - from
            - to
            - created_at
            - parts
          properties:
            id:
              $ref: '#/components/schemas/MessageId'
            direction:
              type: string
              enum:
                - outbound
            delivery_status:
              type: string
              enum:
                - queued
            preferred_service:
              type:
                - string
                - 'null'
              enum:
                - iMessage
                - RCS
                - SMS
                - null
              description: The service you asked for, or null for Auto.
            service:
              type:
                - string
                - 'null'
              enum:
                - iMessage
                - RCS
                - SMS
                - null
              description: >-
                The service that actually carried the message, once confirmed.
                Null until then.
            from:
              type: string
              examples:
                - '+15555550100'
              description: Your Blujay number that sends this message.
            to:
              type: array
              items:
                type: string
                examples:
                  - '+15555550123'
              description: The recipients' phone numbers.
            created_at:
              type: string
              examples:
                - '2026-10-14T17:01:00.000Z'
              format: date-time
            parts:
              type: array
              items:
                $ref: '#/components/schemas/MessageOutputPart'
            reply_to:
              $ref: '#/components/schemas/MessageReplyToInfo'
              description: Present only when the request included `reply_to`.
    ChatId:
      type: string
      examples:
        - chat_abc123
      pattern: ^chat_[A-Za-z0-9_-]+$
    MessageInput:
      type: object
      required:
        - parts
      properties:
        preferred_service:
          type: string
          enum:
            - iMessage
            - RCS
            - SMS
          description: >-
            The service to use in a direct chat. Leave it out to let Blujay
            choose (Auto). `iMessage` sends only as iMessage. `RCS` and `SMS`
            both mean "not iMessage": the message goes out as RCS or SMS,
            whichever the recipient can receive. Values are case-sensitive.
            Group chats reject this field with `VALIDATION_ERROR`.
        parts:
          type: array
          description: >-
            1 to 100 parts, of which at most 40 can be media. Each text part can
            be up to 10,000 characters, and a message can have up to 40,000
            characters of text in total.
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/MessagePart'
        effect:
          $ref: '#/components/schemas/MessageEffect'
        reply_to:
          $ref: '#/components/schemas/MessageReplyTo'
        idempotency_key:
          type: string
          examples:
            - order-4821-first-message
          minLength: 1
          maxLength: 255
          description: >-
            Alternative to the `Idempotency-Key` header. If you send both, they
            must match, or you get `409 IDEMPOTENCY_KEY_MISMATCH`.
        client_reference:
          type: string
          examples:
            - order-4821
          description: >-
            Your own reference for this message, such as an ID from your system.
            It isn't a Blujay trace ID.
    TraceId:
      type: string
      examples:
        - trc_8f2a1c0e4b7d
      pattern: ^trc_[A-Za-z0-9_-]+$
    MessageId:
      type: string
      examples:
        - msg_abc123
      pattern: ^msg_[A-Za-z0-9_-]+$
    MessageOutputPart:
      oneOf:
        - $ref: '#/components/schemas/TextPart'
        - $ref: '#/components/schemas/LinkPart'
        - $ref: '#/components/schemas/MediaOutputPart'
      discriminator:
        propertyName: type
    MessageReplyToInfo:
      type: object
      additionalProperties: false
      required:
        - message_id
        - part_index
        - thread_root_message_id
      description: >-
        What a message replies to. For a reply sent through Blujay, `message_id`
        is the message it named. For a reply that arrived over iMessage, Apple
        reports only the start of the thread, so `message_id` equals
        `thread_root_message_id`. IDs are null when Blujay doesn't have that
        message (for example, it was sent before your history was imported).
      properties:
        message_id:
          oneOf:
            - $ref: '#/components/schemas/MessageId'
            - type: 'null'
        part_index:
          type: integer
          examples:
            - 0
          minimum: 0
          description: Which part of the replied-to message the reply targets (0-based).
        thread_root_message_id:
          oneOf:
            - $ref: '#/components/schemas/MessageId'
            - type: 'null'
          description: >-
            The message that started the thread. Use it to group a thread
            without an extra call, or page through the thread with `GET
            /v1/messages/{message_id}/thread`.
    ErrorEnvelope:
      type: object
      description: >-
        Wraps every error response. Branch on `error.code`, and include
        `trace_id` when you contact support.
      required:
        - status
        - error
        - trace_id
        - timestamp
      properties:
        status:
          const: error
        error:
          $ref: '#/components/schemas/PublicError'
        trace_id:
          $ref: '#/components/schemas/TraceId'
        timestamp:
          type: string
          examples:
            - '2026-10-14T17:01:00.000Z'
          format: date-time
        client_request_id:
          type: string
          description: Your `X-Request-ID` header value, echoed back when you send one.
    MessagePart:
      oneOf:
        - $ref: '#/components/schemas/TextPart'
        - $ref: '#/components/schemas/LinkPart'
        - $ref: '#/components/schemas/MediaPart'
        - $ref: '#/components/schemas/VcardPart'
      discriminator:
        propertyName: type
    MessageEffect:
      type: object
      description: >
        An iMessage screen or bubble effect. Accepted on text follow-up messages
        in direct chats (with any `preferred_service`) and in established
        iMessage group chats.


        Effects are delivered only over iMessage. If a direct message goes out
        as SMS or RCS, the text is sent without the effect and `degradations`
        includes `effect_not_delivered`. With `preferred_service: iMessage`, the
        message still fails if it can't go out as iMessage.


        Effects on a chat's first message, on messages with media, and in groups
        that aren't established iMessage groups return `FEATURE_NOT_ENABLED`
        before anything is sent.
      oneOf:
        - type: object
          required:
            - type
            - name
          properties:
            type:
              const: screen
            name:
              type: string
              enum:
                - confetti
                - fireworks
                - lasers
                - sparkles
                - celebration
                - hearts
                - love
                - balloons
                - happy_birthday
                - echo
                - spotlight
        - type: object
          required:
            - type
            - name
          properties:
            type:
              const: bubble
            name:
              type: string
              enum:
                - slam
                - loud
                - gentle
                - invisible
    MessageReplyTo:
      type: object
      additionalProperties: false
      required:
        - message_id
      description: >
        Makes the message an iMessage threaded reply. Accepted on `POST
        /v1/chats/{chat_id}/messages` only;

        `POST /v1/chats` returns `VALIDATION_ERROR`. The target must be a sent
        message (not a reaction) in

        this chat, on this chat's phone number. These errors are returned before
        anything is sent:

        - `REPLY_TARGET_NOT_FOUND` (404): the message doesn't exist, or belongs
        to another workspace or phone number.

        - `REPLY_TARGET_NOT_IN_CHAT` (400): the message is in a different chat.

        - `REPLY_TARGET_NOT_REPLYABLE` (400): the message is a reaction, was
        unsent, or was sent or received as SMS or RCS.

        - `REPLY_TARGET_NOT_SENT` (409, retryable): the message hasn't been sent
        yet.

        - `UNSUPPORTED_CHAT_OPERATION` (400): `preferred_service` is SMS or RCS,
        or the group isn't an established iMessage group.


        A reply is never sent without its thread. If a direct reply using Auto
        can only go out as SMS or RCS,

        the message fails with `failure_code` `REPLY_REQUIRES_IMESSAGE`.
      properties:
        message_id:
          $ref: '#/components/schemas/MessageId'
        part_index:
          type: integer
          minimum: 0
          default: 0
          description: >-
            Which part of the target to reply to (0-based), numbered the same
            way as `part_index` in `reaction.added`. For a media message with a
            caption, it may differ from the position in the message's `parts`
            array.
    TextPart:
      type: object
      required:
        - type
        - text
      properties:
        type:
          const: text
        text:
          type: string
          examples:
            - Hi Ava, this is Sam from the Northwind support team.
          minLength: 1
          maxLength: 10000
        text_decorations:
          type: array
          maxItems: 100
          description: >-
            Bold, italic, strikethrough or underline styling for ranges of the
            text. Styles work in text-only follow-up messages in direct iMessage
            chats. Animations, styles on a first message, and styles in other
            chats return `FEATURE_NOT_ENABLED`.
          items:
            $ref: '#/components/schemas/TextDecoration'
    LinkPart:
      type: object
      required:
        - type
        - value
      description: >-
        A link that recipients see as a rich preview. It must be the only part
        in the message, and it can't be in a chat's first message.
      properties:
        type:
          const: link
        value:
          type: string
          examples:
            - https://example.com/schedule
          format: uri
          pattern: ^https://
          maxLength: 2048
    MediaOutputPart:
      type: object
      additionalProperties: false
      required:
        - type
        - attachment_id
      properties:
        type:
          const: media
        attachment_id:
          $ref: '#/components/schemas/AttachmentId'
        filename:
          type: string
          examples:
            - receipt.jpg
        content_type:
          type: string
          examples:
            - image/jpeg
        size_bytes:
          type: integer
          examples:
            - 284512
          minimum: 1
        download_url:
          type: string
          examples:
            - /v1/attachments/att_abc123/content
          pattern: ^/v1/attachments/att_.+/content$
          description: >-
            Path to download the file with your API credentials. Present once
            the file is ready.
    PublicError:
      type: object
      required:
        - code
        - message
        - type
        - http_status
        - retryable
        - doc_url
        - details
      properties:
        code:
          type: string
          description: Stable, machine-readable error code. Branch on this value.
          examples:
            - RATE_LIMIT_EXCEEDED
            - QUEUE_UNAVAILABLE
            - FEATURE_NOT_ENABLED
        message:
          type: string
          description: Human-readable explanation. It can change, so don't parse it.
        type:
          type: string
          description: Broad error category.
          examples:
            - validation
            - auth
            - idempotency
            - rate_limit
            - queue
            - feature_gate
        http_status:
          type: integer
          description: The HTTP status code of the response.
          enum:
            - 400
            - 401
            - 403
            - 404
            - 409
            - 413
            - 415
            - 422
            - 429
            - 500
            - 502
            - 503
        retryable:
          type: boolean
          description: Whether sending the same request again later can succeed.
        doc_url:
          type: string
          format: uri
          description: Link to documentation for this error code.
        details:
          type: array
          description: >-
            Extra structured context, such as the fields that failed validation
            or the limit you hit.
          items:
            type: object
            additionalProperties: true
        retry_after:
          type: integer
          minimum: 1
          description: >-
            Seconds to wait before retrying. Also sent as the `Retry-After`
            header.
        reset_at:
          type: string
          examples:
            - '2026-10-14T17:01:00.000Z'
          format: date-time
          description: When the limit you hit resets.
    MediaPart:
      type: object
      required:
        - type
      additionalProperties: false
      description: >-
        A file to send. Set exactly one of `attachment_id` or `url`. An
        `attachment_id` must be an uploaded attachment in your workspace that is
        ready. A `url` must be public HTTPS; Blujay downloads it after accepting
        the message and before sending it.
      oneOf:
        - required:
            - attachment_id
        - required:
            - url
      properties:
        type:
          const: media
        attachment_id:
          $ref: '#/components/schemas/AttachmentId'
        url:
          type: string
          examples:
            - https://example.com/photos/receipt.jpg
          format: uri
          pattern: ^https://
          maxLength: 2048
          description: >-
            Public HTTPS URL of an image, a video, or a vCard (`.vcf`) file, up
            to 10 MiB. Upload other files first and send their `attachment_id`.
            URLs with credentials, private addresses, unsafe redirects, or files
            disguised as another type are rejected. If Blujay can't fetch the
            file, you get `ATTACHMENT_FETCH_FAILED`, either as an error response
            or as the failed message's `failure_code`.
    VcardPart:
      type: object
      required:
        - type
        - vcard_id
      additionalProperties: false
      description: >-
        Sends a saved vCard as a native contact card. Blujay creates the card
        file when the message is accepted. It counts as a media part, can be
        combined with text but not a link, and follows the same rules as any
        media part over iMessage, SMS or RCS. Message reads and webhooks return
        it as a media part with content type `text/vcard`.
      properties:
        type:
          const: vcard
        vcard_id:
          $ref: '#/components/schemas/VcardId'
    TextDecoration:
      type: object
      required:
        - range
        - style
      description: >-
        A text style applied to a range of the text, measured in UTF-16 code
        units.
      properties:
        range:
          type: array
          description: Character range `[start, end)` in UTF-16 code units.
          minItems: 2
          maxItems: 2
          prefixItems:
            - type: integer
              minimum: 0
            - type: integer
              minimum: 0
        style:
          type: string
          enum:
            - bold
            - italic
            - strikethrough
            - underline
    AttachmentId:
      type: string
      examples:
        - att_abc123
      pattern: ^att_[A-Za-z0-9_-]+$
    VcardId:
      type: string
      examples:
        - vcard_abc123
      pattern: ^vcard_[A-Za-z0-9_-]+$
  headers:
    XTraceID:
      description: >-
        Unique ID for this request. It matches `trace_id` in the body. Include
        it when you contact Blujay support.
      schema:
        $ref: '#/components/schemas/TraceId'
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
        minimum: 1
    RateLimitLimit:
      description: Size of the limit you hit.
      schema:
        type: integer
    RateLimitRemaining:
      description: How much of that limit is left in the current window.
      schema:
        type: integer
    RateLimitReset:
      description: Unix time, in seconds, when that limit resets.
      schema:
        type: integer
  responses:
    SendBadRequest:
      description: >-
        The message was rejected and nothing was sent. Common codes are
        `VALIDATION_ERROR`, `FIRST_MESSAGE_LINK_NOT_ALLOWED`,
        `FEATURE_NOT_ENABLED` (an option this message or chat doesn't support),
        `UNSUPPORTED_CHAT_OPERATION`, the `REPLY_TARGET_*` codes, and the limits
        before a reply (`PRE_REPLY_CONTENT_BLOCKED`,
        `PRE_REPLY_LENGTH_EXCEEDED`, `PRE_REPLY_LIMIT_REACHED`). The limits
        before a reply are never retryable and never `429`; they lift when the
        contact replies.
      headers:
        X-Trace-ID:
          $ref: '#/components/headers/XTraceID'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            validation:
              value:
                status: error
                error:
                  code: VALIDATION_ERROR
                  message: Request validation failed
                  type: validation
                  http_status: 400
                  retryable: false
                  doc_url: https://docs.blujay.io/errors/validation-error
                  details: []
                trace_id: trc_8f2a1c0e4b7d
                timestamp: '2026-10-14T00:00:00.000Z'
            preReplyLimitReached:
              value:
                status: error
                error:
                  code: PRE_REPLY_LIMIT_REACHED
                  message: >-
                    This contact hasn't replied to your last 6 texts. You can
                    text again after they reply.
                  type: validation
                  http_status: 400
                  retryable: false
                  doc_url: https://docs.blujay.io/errors/pre-reply-limit-reached
                  details:
                    - rule: limit
                      unanswered_count: 6
                      limit: 6
                trace_id: trc_8f2a1c0e4b7d
                timestamp: '2026-10-14T00:00:00.000Z'
            firstMessageLinkNotAllowed:
              value:
                status: error
                error:
                  code: FIRST_MESSAGE_LINK_NOT_ALLOWED
                  message: Links are not allowed in the first message
                  type: validation
                  http_status: 400
                  retryable: false
                  doc_url: https://docs.blujay.io/errors/first-message-link-not-allowed
                  details: []
                trace_id: trc_8f2a1c0e4b7d
                timestamp: '2026-10-14T00:00:00.000Z'
            featureNotEnabled:
              value:
                status: error
                error:
                  code: FEATURE_NOT_ENABLED
                  message: >-
                    Message effects are not available on the first message of a
                    new chat
                  type: feature_gate
                  http_status: 400
                  retryable: false
                  doc_url: https://docs.blujay.io/errors/feature-not-enabled
                  details: []
                trace_id: trc_8f2a1c0e4b7d
                timestamp: '2026-10-14T00:00:00.000Z'
    Unauthorized:
      description: >-
        The API key is missing or invalid (`UNAUTHORIZED`), revoked
        (`API_KEY_REVOKED`) or expired (`API_KEY_EXPIRED`).
      headers:
        X-Trace-ID:
          $ref: '#/components/headers/XTraceID'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    SendForbidden:
      description: >-
        The recipient opted out (`RECIPIENT_SUPPRESSED`), the contact hasn't
        verified for your Free Plan number yet (`CONTACT_NOT_VERIFIED`), the
        number only replies to people who text it first
        (`CONTACT_MUST_TEXT_FIRST`), or sending is suspended for this number
        (`SENDER_SUSPENDED`).
      headers:
        X-Trace-ID:
          $ref: '#/components/headers/XTraceID'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            recipientSuppressed:
              value:
                status: error
                error:
                  code: RECIPIENT_SUPPRESSED
                  message: Recipient is suppressed for outbound messaging
                  type: authorization
                  http_status: 403
                  retryable: false
                  doc_url: https://docs.blujay.io/errors/recipient-suppressed
                  details: []
                trace_id: trc_8f2a1c0e4b7d
                timestamp: '2026-10-14T00:00:00.000Z'
    NotFound:
      description: The resource doesn't exist or isn't in your workspace.
      headers:
        X-Trace-ID:
          $ref: '#/components/headers/XTraceID'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    MessageSendConflict:
      description: >-
        An `Idempotency-Key` conflict, or the chat's number was retired
        (`SENDER_RETIRED`). For a retired number, start a new chat from an
        active number.
      headers:
        X-Trace-ID:
          $ref: '#/components/headers/XTraceID'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            senderRetired:
              value:
                status: error
                error:
                  code: SENDER_RETIRED
                  message: >-
                    This phone number was retired. Select an active phone
                    number.
                  type: feature_gate
                  http_status: 409
                  retryable: false
                  doc_url: https://docs.blujay.io/errors/sender-retired
                  details:
                    - action: select_active_phone_number
                trace_id: trc_8f2a1c0e4b7d
                timestamp: '2026-10-14T00:00:00.000Z'
    SendRejected429:
      description: >
        The message wasn't accepted because the sending number hit a limit. Wait
        `Retry-After` seconds, then retry with the same `Idempotency-Key`.


        - `RATE_LIMIT_EXCEEDED`: a sending limit or request limit.
        `details[0].limit_type` says which. Each number's current sending limits
        are in `GET /v1/phone-numbers` under `limits`.

        - `QUEUE_FULL`: the number has too many messages waiting to go out.
      headers:
        X-Trace-ID:
          $ref: '#/components/headers/XTraceID'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            newContactLimit:
              $ref: '#/components/examples/NewContactLimitExceeded'
            queueFull:
              $ref: '#/components/examples/QueueFull'
    QueueUnavailable:
      description: >-
        Blujay can't accept the request right now (`QUEUE_UNAVAILABLE`). Retry
        after `Retry-After` seconds; for sends, reuse the same
        `Idempotency-Key`.
      headers:
        X-Trace-ID:
          $ref: '#/components/headers/XTraceID'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            unavailable:
              value:
                status: error
                error:
                  code: QUEUE_UNAVAILABLE
                  message: Sending is temporarily unavailable
                  type: queue
                  http_status: 503
                  retryable: true
                  doc_url: https://docs.blujay.io/errors/queue-unavailable
                  details: []
                  retry_after: 30
                trace_id: trc_8f2a1c0e4b7d
                timestamp: '2026-10-14T00:00:00.000Z'
  examples:
    NewContactLimitExceeded:
      summary: New-contact limit reached
      value:
        status: error
        error:
          code: RATE_LIMIT_EXCEEDED
          message: New-contact limit exceeded
          type: rate_limit
          http_status: 429
          retryable: true
          doc_url: https://docs.blujay.io/errors/rate-limit-exceeded
          details:
            - limit_type: new_contact_daily
              scope: phone_number
              phone_number_id: phone_abc123
              limit: 50
              used: 50
              remaining: 0
              reset_at: '2026-10-15T07:00:00.000Z'
          retry_after: 48600
          reset_at: '2026-10-15T07:00:00.000Z'
        trace_id: trc_8f2a1c0e4b7d
        timestamp: '2026-10-14T17:30:00.000Z'
    QueueFull:
      summary: Send queue full
      value:
        status: error
        error:
          code: QUEUE_FULL
          message: This phone number's send queue is full
          type: queue
          http_status: 429
          retryable: true
          doc_url: https://docs.blujay.io/errors/queue-full
          details:
            - limit_type: phone_number_queue
              scope: phone_number
              phone_number_id: phone_abc123
              remaining: 0
              reset_at: '2026-10-14T00:05:00.000Z'
          retry_after: 300
          reset_at: '2026-10-14T00:05:00.000Z'
        trace_id: trc_8f2a1c0e4b7d
        timestamp: '2026-10-14T00:00:00.000Z'
  securitySchemes:
    PublicApiKey:
      type: http
      scheme: bearer
      bearerFormat: blu_...
      description: >
        Your workspace API key, sent as `Authorization: Bearer blu_...`.
        Workspace admins can find it in the Blujay app under **API**.


        An API key has full access to your workspace and works on every
        endpoint. There is no test mode: every key sends real messages and
        changes real data, so keep it secret and use it only from your server.


        Each API key can make 600 read requests and 120 write requests per
        minute, plus 60 attachment requests per minute. Over a limit you get
        `429 RATE_LIMIT_EXCEEDED` with `Retry-After`.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.