> ## 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.

# Create a chat and send the first message

> Starts a chat from one of your Blujay numbers and sends its first message. One recipient in `to` creates a direct chat; two or more create a group chat.

For a direct chat you can leave out `from`, and Blujay picks the number. It uses the number this person already knows you on. If they've never texted any of your numbers, it uses your only number, or else the default line of the teammate the contact is assigned to. If none of those apply, the request returns `422 FROM_NUMBER_REQUIRED` and Blujay never guesses. A group chat always needs `from`.

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}`.

The first message has some limits (see the sending rules at https://docs.blujay.io/messaging/sending-rules):
- It can't contain a link. A link part, or a URL in the text, returns `400 FIRST_MESSAGE_LINK_NOT_ALLOWED`. Send links in follow-up messages after the contact replies.
- Until the contact replies, `media` and `vcard` parts return `400 PRE_REPLY_CONTENT_BLOCKED` and text is limited to 300 characters (`400 PRE_REPLY_LENGTH_EXCEEDED`). A first message is text only.
- `reply_to` isn't accepted (`400 VALIDATION_ERROR`), and message effects and text decorations return `400 FEATURE_NOT_ENABLED`.

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:
- `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.
- `422 FROM_NUMBER_REQUIRED`: you left out `from` and Blujay couldn't tell which number to use. `error.message` names the fix, such as setting a default line for the assigned teammate in Team Members.
- `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`.
- `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
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:
    post:
      tags:
        - Chats
      summary: Create a chat and send the first message
      description: >
        Starts a chat from one of your Blujay numbers and sends its first
        message. One recipient in `to` creates a direct chat; two or more create
        a group chat.


        For a direct chat you can leave out `from`, and Blujay picks the number.
        It uses the number this person already knows you on. If they've never
        texted any of your numbers, it uses your only number, or else the
        default line of the teammate the contact is assigned to. If none of
        those apply, the request returns `422 FROM_NUMBER_REQUIRED` and Blujay
        never guesses. A group chat always needs `from`.


        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}`.


        The first message has some limits (see the sending rules at
        https://docs.blujay.io/messaging/sending-rules):

        - It can't contain a link. A link part, or a URL in the text, returns
        `400 FIRST_MESSAGE_LINK_NOT_ALLOWED`. Send links in follow-up messages
        after the contact replies.

        - Until the contact replies, `media` and `vcard` parts return `400
        PRE_REPLY_CONTENT_BLOCKED` and text is limited to 300 characters (`400
        PRE_REPLY_LENGTH_EXCEEDED`). A first message is text only.

        - `reply_to` isn't accepted (`400 VALIDATION_ERROR`), and message
        effects and text decorations return `400 FEATURE_NOT_ENABLED`.


        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:

        - `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.

        - `422 FROM_NUMBER_REQUIRED`: you left out `from` and Blujay couldn't
        tell which number to use. `error.message` names the fix, such as setting
        a default line for the assigned teammate in Team Members.

        - `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`.

        - `503 QUEUE_UNAVAILABLE`: Blujay couldn't accept the message right now.
        Retry with the same `Idempotency-Key`.
      operationId: createPublicChat
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateChatRequest'
            examples:
              firstText:
                summary: First text message
                value:
                  from: '+15555550100'
                  to:
                    - '+15555550123'
                  message:
                    parts:
                      - type: text
                        text: Hi Ava, this is Sam from the Northwind support team.
                    idempotency_key: first-send-001
              automaticFrom:
                summary: First text without from, so Blujay picks the number
                value:
                  to:
                    - '+15555550123'
                  message:
                    parts:
                      - type: text
                        text: Thanks for reaching out. This is Sam from Northwind.
              firstGroupText:
                summary: First group text message
                value:
                  from: '+15555550100'
                  to:
                    - '+15555550123'
                    - '+15555550124'
                  message:
                    parts:
                      - type: text
                        text: >-
                          Hi both, this is Sam from Northwind. I've started this
                          thread for your order.
              firstMessageLinkRejected:
                summary: A link in the first message is rejected
                value:
                  from: '+15555550100'
                  to:
                    - '+15555550123'
                  message:
                    parts:
                      - type: link
                        value: https://example.com/schedule
      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:
                firstText:
                  summary: First text message accepted
                  value:
                    status: success
                    data:
                      chat_id: chat_abc123
                      message:
                        id: msg_abc123
                        direction: outbound
                        delivery_status: queued
                        preferred_service: null
                        service: null
                        from: '+15555550100'
                        to:
                          - '+15555550123'
                        created_at: '2026-10-14T17:01:00.000Z'
                        parts:
                          - type: text
                            text: >-
                              Hi Ava, this is Sam from the Northwind support
                              team.
                    trace_id: trc_8f2a1c0e4b7d
                    timestamp: '2026-10-14T17:01:00.184Z'
                firstGroupText:
                  summary: First group text message accepted
                  value:
                    status: success
                    data:
                      chat_id: chat_def456
                      message:
                        id: msg_ghi789
                        direction: outbound
                        delivery_status: queued
                        preferred_service: null
                        service: null
                        from: '+15555550100'
                        to:
                          - '+15555550123'
                          - '+15555550124'
                        created_at: '2026-10-14T15:20:00.000Z'
                        parts:
                          - type: text
                            text: >-
                              Hi both, this is Sam from Northwind. I've started
                              this thread for your order.
                    trace_id: trc_8f2a1c0e4b7d
                    timestamp: '2026-10-14T15:20:00.211Z'
        '400':
          $ref: '#/components/responses/SendBadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/SendForbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/IdempotencyConflict'
        '422':
          $ref: '#/components/responses/FromNumberRequired'
        '429':
          $ref: '#/components/responses/SendRejected429'
        '503':
          $ref: '#/components/responses/QueueUnavailable'
      security:
        - PublicApiKey: []
components:
  parameters:
    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:
    CreateChatRequest:
      type: object
      required:
        - to
        - message
      properties:
        from:
          type: string
          examples:
            - '+15555550100'
          description: >-
            Your Blujay number to send from, in E.164 (`+15555550100`) or US
            format (`(555) 555-0100`). Optional for a direct chat; Blujay then
            picks the number this person knows you on (see the operation
            description). Required for a group chat.
        to:
          type: array
          minItems: 1
          maxItems: 31
          items:
            type: string
            examples:
              - '+15555550123'
          description: >-
            Recipient phone numbers in E.164 or US format. One recipient creates
            a direct chat; two or more create a group chat. Contact IDs and
            email addresses aren't accepted.
        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`.
    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_-]+$
    ChatId:
      type: string
      examples:
        - chat_abc123
      pattern: ^chat_[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'
    IdempotencyConflict:
      description: >
        The `Idempotency-Key` conflicts with an earlier request:

        - `IDEMPOTENCY_REQUEST_MISMATCH`: the key was already used for a
        different request.

        - `IDEMPOTENCY_KEY_MISMATCH`: on sends, the `Idempotency-Key` header and
        `message.idempotency_key` don't match.

        - `IDEMPOTENCY_IN_PROGRESS`: a request with this key is still running.
        Retry shortly.
      headers:
        X-Trace-ID:
          $ref: '#/components/headers/XTraceID'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            mismatch:
              value:
                status: error
                error:
                  code: IDEMPOTENCY_REQUEST_MISMATCH
                  message: Idempotency key was reused with a different request shape
                  type: idempotency
                  http_status: 409
                  retryable: false
                  doc_url: https://docs.blujay.io/errors/idempotency-request-mismatch
                  details: []
                trace_id: trc_8f2a1c0e4b7d
                timestamp: '2026-10-14T00:00:00.000Z'
            keyMismatch:
              value:
                status: error
                error:
                  code: IDEMPOTENCY_KEY_MISMATCH
                  message: Idempotency key header and body value do not match
                  type: idempotency
                  http_status: 409
                  retryable: false
                  doc_url: https://docs.blujay.io/errors/idempotency-key-mismatch
                  details: []
                trace_id: trc_8f2a1c0e4b7d
                timestamp: '2026-10-14T00:00:00.000Z'
            inProgress:
              value:
                status: error
                error:
                  code: IDEMPOTENCY_IN_PROGRESS
                  message: An idempotent request with this key is already in progress
                  type: idempotency
                  http_status: 409
                  retryable: true
                  doc_url: https://docs.blujay.io/errors/idempotency-in-progress
                  details: []
                trace_id: trc_8f2a1c0e4b7d
                timestamp: '2026-10-14T00:00:00.000Z'
    FromNumberRequired:
      description: >-
        The request left out `from`, and Blujay couldn't tell which number to
        send from (`FROM_NUMBER_REQUIRED`). The person hasn't texted any of your
        numbers, you have several, and the contact isn't assigned to a teammate
        with an active default line. `error.message` names the fix. Send `from`,
        or fix the assignment or default line and retry.
      headers:
        X-Trace-ID:
          $ref: '#/components/headers/XTraceID'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            teammateHasNoDefault:
              value:
                status: error
                error:
                  code: FROM_NUMBER_REQUIRED
                  message: >-
                    This contact hasn't texted you yet, so Blujay can't tell
                    which of your numbers to use. Choose a From number, or set a
                    default line for Sam Rivera in Team Members.
                  type: validation
                  http_status: 422
                  retryable: false
                  doc_url: https://docs.blujay.io/errors/from-number-required
                  details: []
                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.