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

# Edit a message

> Changes the text of an iMessage you sent from this workspace. The edit also shows on the recipient's device.

Apple's limits apply: you can edit a message within 15 minutes of sending it, up to 5 times. Only text-only messages can be edited, so `part_index` is always `0`.

If the message already has the text you send, Blujay returns the message without editing it again. Send an `Idempotency-Key` so a retry after a lost response is safe.

To hear about every edit, including edits your contacts make, subscribe to the `message.edited` webhook.

If editing is switched off for your workspace, the request returns `400 FEATURE_NOT_ENABLED`.




## OpenAPI

````yaml /api-reference/openapi.yaml patch /v1/messages/{message_id}
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/messages/{message_id}:
    patch:
      tags:
        - Messages
      summary: Edit a message
      description: >
        Changes the text of an iMessage you sent from this workspace. The edit
        also shows on the recipient's device.


        Apple's limits apply: you can edit a message within 15 minutes of
        sending it, up to 5 times. Only text-only messages can be edited, so
        `part_index` is always `0`.


        If the message already has the text you send, Blujay returns the message
        without editing it again. Send an `Idempotency-Key` so a retry after a
        lost response is safe.


        To hear about every edit, including edits your contacts make, subscribe
        to the `message.edited` webhook.


        If editing is switched off for your workspace, the request returns `400
        FEATURE_NOT_ENABLED`.
      operationId: updatePublicMessage
      parameters:
        - $ref: '#/components/parameters/MessageId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageEditRequest'
            examples:
              editText:
                summary: Correct the text of a sent message
                value:
                  text: See you at 4 instead
      responses:
        '200':
          $ref: '#/components/responses/MessageMutated'
        '400':
          $ref: '#/components/responses/MessageMutationRejected'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/MessageMutationRejected'
        '502':
          $ref: '#/components/responses/MessageMutationRejected'
        '503':
          $ref: '#/components/responses/MessageMutationRejected'
      security:
        - PublicApiKey: []
components:
  parameters:
    MessageId:
      name: message_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/MessageId'
    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:
    MessageEditRequest:
      type: object
      additionalProperties: false
      required:
        - text
      properties:
        text:
          type: string
          examples:
            - See you at 4 instead
          minLength: 1
          maxLength: 10000
          description: >-
            The new text. Must contain a non-whitespace character and differ
            from the current text.
        part_index:
          type: integer
          enum:
            - 0
          default: 0
          description: >-
            Only text-only messages can be edited, so the only editable part is
            0.
    MessageId:
      type: string
      examples:
        - msg_abc123
      pattern: ^msg_[A-Za-z0-9_-]+$
    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.
    PublicMessage:
      type: object
      required:
        - message_id
        - chat_id
        - phone_number_id
        - direction
        - preferred_service
        - service
        - status
        - parts
        - degradations
        - reply_to
        - sent_at
        - delivered_at
        - read_at
        - failed_at
        - failure
        - edited_at
        - edit_count
        - unsent_at
        - unsent_scope
        - created_at
        - updated_at
      properties:
        message_id:
          $ref: '#/components/schemas/MessageId'
        chat_id:
          $ref: '#/components/schemas/ChatId'
        contact_id:
          allOf:
            - $ref: '#/components/schemas/ContactId'
          description: >-
            The contact in a direct chat. Omitted for group messages, which
            carry `sender` instead.
        sender:
          allOf:
            - $ref: '#/components/schemas/ReceivedMessageWebhookSender'
          description: >-
            Who sent an inbound message in a group chat. Omitted for direct
            chats and for your own messages.
        phone_number_id:
          $ref: '#/components/schemas/PhoneNumberId'
        direction:
          type: string
          enum:
            - inbound
            - outbound
          description: >-
            `outbound` for messages your workspace sent, `inbound` for messages
            from a contact.
        preferred_service:
          type:
            - string
            - 'null'
          enum:
            - iMessage
            - RCS
            - SMS
            - null
          description: The service you asked for, or null if the message used Auto.
        service:
          type:
            - string
            - 'null'
          enum:
            - iMessage
            - RCS
            - SMS
            - null
          description: >-
            The service that actually carried the message, or null until it's
            confirmed.
        status:
          $ref: '#/components/schemas/MessageStatus'
        failure_code:
          type: string
          enum:
            - ATTACHMENT_FETCH_FAILED
            - ATTACHMENT_TOO_LARGE
            - UNSUPPORTED_ATTACHMENT_TYPE
            - SENDER_RETIRED
            - REPLY_REQUIRES_IMESSAGE
          description: >
            Present only when the message failed for a reason you can act on:

            - `ATTACHMENT_FETCH_FAILED`: a media URL couldn't be downloaded.

            - `ATTACHMENT_TOO_LARGE`: a file is too large to send, including
            SMS/MMS messages whose files total more than 5 MiB.

            - `UNSUPPORTED_ATTACHMENT_TYPE`: a file type can't be sent by the
            service the message went out on (for example a PDF over SMS) or in
            that group.

            - `SENDER_RETIRED`: the sending number was retired. Send from an
            active number.

            - `REPLY_REQUIRES_IMESSAGE`: a threaded reply could only go out as
            SMS or RCS, so it wasn't sent. Send it again without `reply_to`.
        parts:
          type: array
          items:
            $ref: '#/components/schemas/MessageOutputPart'
        effect:
          description: >-
            The requested effect. See `degradations` for whether it was
            delivered.
          allOf:
            - $ref: '#/components/schemas/MessageEffect'
        degradations:
          type: array
          description: >-
            Requested features that weren't delivered because the message went
            out as SMS or RCS. Empty when nothing was dropped. Message webhooks
            carry the same entries.
          items:
            $ref: '#/components/schemas/MessageDegradation'
        reply_to:
          description: Threaded reply info, or null when the message is not a reply.
          oneOf:
            - $ref: '#/components/schemas/MessageReplyToInfo'
            - type: 'null'
        sent_at:
          type:
            - string
            - 'null'
          format: date-time
          examples:
            - '2026-10-14T17:01:02.000Z'
          description: >-
            When the message left your number. Null until then, and always null
            for inbound messages.
        delivered_at:
          type:
            - string
            - 'null'
          format: date-time
          examples:
            - '2026-10-14T17:01:03.000Z'
          description: >-
            When the recipient's device confirmed delivery, where the service
            reports it.
        read_at:
          type:
            - string
            - 'null'
          format: date-time
          description: When the recipient read the message, where the service reports it.
        failed_at:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            When the message last failed. Null if it never failed. A retried
            message keeps it, so check `status` for the current state.
        failure:
          description: >-
            Why the message failed. Null unless `status` is `failed`. Message
            webhooks carry the same object.
          oneOf:
            - $ref: '#/components/schemas/MessageFailure'
            - type: 'null'
        retryable:
          type: boolean
          description: >-
            Present only when `status` is `failed`. True means `POST
            /v1/messages/{message_id}/retry` will be accepted: the message
            failed before it was handed to your phone number, for a reason a
            retry can fix. False means retry returns `RETRY_NOT_ALLOWED`, either
            because the message may already have reached the recipient (check
            the conversation before you send a replacement) or because a retry
            would fail the same way, such as unsupported content; fix the
            message and send a new one instead.
        client_reference:
          type: string
          maxLength: 255
          description: The `client_reference` you sent with the message, when you sent one.
        edited_at:
          examples:
            - '2026-10-14T17:01:00.000Z'
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            When the iMessage was last edited, or null if it was never edited.
            `parts` holds the current text.
        edit_count:
          type: integer
          examples:
            - 0
          minimum: 0
          description: How many times the iMessage was edited.
        edit_history:
          type: array
          description: >-
            Earlier versions of the text, oldest first. Returned only with
            `include=edit_history`.
          items:
            $ref: '#/components/schemas/MessageEditHistoryEntry'
        unsent_at:
          examples:
            - '2026-10-14T17:01:00.000Z'
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            When the iMessage was unsent, or null. An unsent message has no text
            in `parts`, and the unsent text is never returned.
        unsent_scope:
          type:
            - string
            - 'null'
          enum:
            - all
            - text
            - null
          description: >-
            `all` when the whole message was unsent (`parts` is empty); `text`
            when only a caption was unsent and attachments remain.
        trace_id:
          $ref: '#/components/schemas/TraceId'
        created_at:
          type: string
          examples:
            - '2026-10-14T17:01:00.000Z'
          format: date-time
        updated_at:
          type: string
          examples:
            - '2026-10-14T17:01:00.000Z'
          format: date-time
    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.
    TraceId:
      type: string
      examples:
        - trc_8f2a1c0e4b7d
      pattern: ^trc_[A-Za-z0-9_-]+$
    ChatId:
      type: string
      examples:
        - chat_abc123
      pattern: ^chat_[A-Za-z0-9_-]+$
    ContactId:
      type: string
      examples:
        - contact_abc123
      pattern: ^contact_[A-Za-z0-9_-]+$
    ReceivedMessageWebhookSender:
      type: object
      additionalProperties: false
      required:
        - type
        - address
      description: >-
        The person in the group who sent the message. `contact_id` is present
        only when the sender matches a contact in your workspace.
      properties:
        type:
          type: string
          enum:
            - phone
            - email
        address:
          type: string
          minLength: 1
        contact_id:
          $ref: '#/components/schemas/ContactId'
    PhoneNumberId:
      type: string
      examples:
        - phone_abc123
      pattern: ^phone_[A-Za-z0-9_-]+$
    MessageStatus:
      type: string
      enum:
        - received
        - accepted
        - sending
        - sent
        - delivered
        - read
        - failed
    MessageOutputPart:
      oneOf:
        - $ref: '#/components/schemas/TextPart'
        - $ref: '#/components/schemas/LinkPart'
        - $ref: '#/components/schemas/MediaOutputPart'
      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
    MessageDegradation:
      type: object
      required:
        - code
        - reason
      properties:
        code:
          type: string
          enum:
            - effect_not_delivered
            - text_decorations_not_delivered
        reason:
          type: string
          examples:
            - >-
              The iMessage effect was not delivered because the message was sent
              as SMS or RCS.
          description: >-
            Human-readable explanation, for example "The iMessage effect was not
            delivered because the message was sent as SMS or RCS."
    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`.
    MessageFailure:
      type: object
      additionalProperties: false
      required:
        - code
        - reason
      description: >-
        Why a message failed. `code` is stable; `reason` is for people and may
        change.
      properties:
        code:
          type: string
          enum:
            - recipient_unavailable
            - unsupported_content
            - reply_requires_imessage
            - service_mismatch
            - sender_unavailable
            - sender_retired
            - network_rejected
            - dispatch_unavailable
            - delivery_failed
        reason:
          type: string
          examples:
            - The messaging network rejected the message.
    MessageEditHistoryEntry:
      type: object
      additionalProperties: false
      required:
        - text
        - replaced_at
      properties:
        text:
          type: string
          examples:
            - See you at 3
        replaced_at:
          type: string
          examples:
            - '2026-10-14T17:01:00.000Z'
          format: date-time
    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.
    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.
    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_-]+$
  responses:
    MessageMutated:
      description: >-
        The message after the edit or unsend. If the same change was already in
        place, the message is returned unchanged.
      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/PublicMessage'
    MessageMutationRejected:
      description: >
        The edit or unsend was refused, and the message is unchanged.


        `400`:

        - `VALIDATION_ERROR`: the request is invalid.

        - `EDIT_UNCHANGED`: the new text is the same as the current text.


        `409`:

        - `MESSAGE_NOT_EDITABLE`: the message isn't a text-only iMessage you
        sent from this workspace, or it was already unsent.

        - `EDIT_WINDOW_EXPIRED`: more than 15 minutes have passed since it was
        sent.

        - `EDIT_LIMIT_REACHED`: it has already been edited 5 times.

        - `UNSEND_WINDOW_EXPIRED`: more than 2 minutes have passed since it was
        sent.

        - `SENDER_UNAVAILABLE`: the number that sent it is no longer available.

        - `MESSAGE_NOT_SENT_YET`: the message hasn't finished sending yet. Retry
        shortly.

        - `IDEMPOTENCY_REQUEST_MISMATCH` or `IDEMPOTENCY_IN_PROGRESS`: see
        `Idempotency-Key`.


        `502 CHANGE_REJECTED`: the change was refused and didn't take effect.


        `503 CHANGE_UNCONFIRMED`: Blujay couldn't confirm the change. It may
        still apply, so retry to check.
      headers:
        X-Trace-ID:
          $ref: '#/components/headers/XTraceID'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    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'
  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'
  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.