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

# Send a message

> Sends one message to one phone number from one of your lines, and returns it as queued or held.

* **Credential:** an API key, or a [trial key](/authentication#trial-keys), which sends text only, from the trial's line to the trial's paired number: see [Send with the trial key](/guides/trials#send-with-the-trial-key) and [With a trial key](#with-a-trial-key). See [The send request](/guides/sending-messages#the-send-request).
* **Line-scoped keys:** a [line-scoped key](/authentication#line-scoped-keys) gets `403 forbidden` unless `line_id` is in its list.
* **Test keys:** a send with an `isnap_test_` account key reaches a real recipient and counts against the same sending limits: see [Test keys](/authentication#test-keys).
* **`Idempotency-Key`:** honored. The key belongs to the line, and a repeat returns the message already created, as it is now. The checks on the line, and on the trial with a trial key, run before the key is read: a refusal of a repeat does not mean that the first request was refused. See [Retrying a send](/guides/sending-messages#retrying-a-send).
* **Events:** `message.queued`, or `message.scheduled` for a held message, then the events of [Following a message](/guides/sending-messages#following-a-message).
* **Personal data:** the message carries the handles in `to` and `from`, the text in `body`, the attachment file names and your `metadata`. Request it only when needed, do not copy it into logs, tickets or other tools, and refer to a contact by an id of your own.
* **Quiet hours:** `ignore_quiet_hours: true` lets the message leave during the quiet hours of the line. Use it for a recipient of your own, such as your own phone number: the API does not check the recipient. The sending limits, the spacing and the order per recipient still apply, and a shared line ignores it. See [Sending during quiet hours](/guides/sending-limits#sending-during-quiet-hours).
* **Response:** `202` means that iSnap accepted the message, never that it was sent. `status` is `queued`, or `scheduled` when iSnap holds the message: see [The response](/guides/sending-messages#the-response) and [Held messages](/guides/sending-limits#held-messages). `queue_info` is filled only in this response.
* **Headers:** with an account API key, the `202` carries `X-iSnap-Quota-Remaining` and `X-iSnap-Quota-Resets-At`: see [Headers on a send](/guides/sending-limits#headers-on-a-send).
* **Rate limit:** this operation and `GET /v1/messages` share the message-operations limit, 60 requests per rolling 60 seconds per network address, in addition to the request-rate limit: see [Rate limits](/errors-and-rate-limits#rate-limits).

## Errors

| Status | `error.code` | When |
| - | - | - |
| 400 | `validation_failed` | A required field is missing, a field is too long, `channel` is not `auto`, more than 10 attachments, an unknown effect, `metadata` over 20 keys or 8,192 bytes, or neither `body` nor an attachment. |
| 400 | `bad_request` | `to` is not a valid phone number, or is an email address. |
| 400 | `line_not_active` | The line is neither `active` nor `suspended`. |
| 403 | `forbidden` | The request uses a line-scoped key, and `line_id` is not in its list. |
| 403 | `line_suspended` | The line is suspended. |
| 403 | `outbound_first_forbidden` | The line is inbound-only, and the recipient never wrote to it. |
| 404 | `not_found` | The line does not exist or is not yours, `reply_to_message_id` is unknown or was deleted, or an attachment id is not one your account created. |
| 409 | `idempotency_conflict` | The `Idempotency-Key` was already used on this line with other content. |
| 422 | `reply_to_cross_line` | `reply_to_message_id` is a message of another line. |
| 429 | `minute_limit_exceeded`, `hourly_limit_exceeded`, `total_message_quota_exceeded`, `new_contact_quota_exceeded`, `known_contact_quota_exceeded` | A sending limit is reached on a line set to refuse. See [Refusals](/guides/sending-limits#refusals). |
| 429 | `queue_full` | The message would be held, and the line holds its maximum. See [Maximum number of held messages](/guides/sending-limits#maximum-number-of-held-messages). |
| 500 | `internal_error` | An unexpected error. It includes an `Idempotency-Key` longer than 255 characters, and the losing one of two simultaneous requests with the same new key: retry it with that key. |
| 503 | `line_pool_exhausted` | Shared line: no number can take a new contact. |
| 503 | `quota_state_unavailable` | Shared line: iSnap could not read the state of the line. Retry. |

### With a trial key

A send with a [trial key](/authentication#trial-keys) can also return these refusals:

| Status | `error.code` | When |
| - | - | - |
| 400 | `bad_request` | `to` is not a valid phone number. An email address is refused. |
| 402 | `trial_needs_inbound` | The trial is `dormant`: see [Unanswered-send limit](/guides/trials#unanswered-send-limit). A repeat of the send that made the trial `dormant` returns it too: iSnap accepted that earlier message, which you read with [`GET /v1/messages`](/api-reference/messages/list-messages). |
| 403 | `trial_scope_violation` | `line_id` or `to` is outside the trial, or `reply_to_message_id` names a message of another trial. |
| 403 | `line_not_active` | The pool line is not `active`. |
| 404 | `not_found` | `reply_to_message_id` is unknown. |
| 409 | `idempotency_conflict` | The `Idempotency-Key` was already used on the pool line with another body. |
| 422 | `trial_attachments_unsupported` | The request carries attachments. |
| 422 | `reply_to_cross_line` | `reply_to_message_id` names a message on another line. |

The errors that every operation shares are in [Errors](/errors-and-rate-limits#errors).


## OpenAPI

````yaml POST /v1/messages
openapi: 3.0.0
info:
  title: iSnap API
  version: 2.45.0
  description: >-
    The iSnap API sends and receives iMessage, RCS and SMS on real phone lines,
    manages those lines, and reports events through webhooks. Every message is
    sent to a real recipient, with an `isnap_live_` key and with an
    `isnap_test_` key alike. Retry a `429` after `Retry-After` seconds. Retry a
    `500` or a `503` up to 2 times, with exponential backoff. Do not retry any
    other `4xx`: fix the request. Retry a write only on an operation that honors
    `Idempotency-Key`, with the key of the first attempt. Responses carry
    personal data: handles, names and message text.
servers:
  - url: https://api.isnap.ai
security:
  - bearerAuth: []
paths:
  /v1/messages:
    post:
      summary: Send a message
      description: >-
        Accepts one message to one phone number on one of your lines. Returns
        `202` with the message as `queued`, or as `scheduled` when iSnap holds
        it: `scheduled_for` is then the planned departure time and
        `schedule_reason` names the cause. A `202` means that the message is
        accepted, not that it is sent. The line's device chooses the service
        (`iMessage`, `RCS` or `SMS`) when it sends, and `service` then shows the
        one it used. Body fields the API does not know are ignored. `POST
        /v1/messages` and `GET /v1/messages` share 60 requests per 60 seconds
        per network address.


        The message then produces events: `message.queued`, or
        `message.scheduled` while it is held and `message.queued` when it is
        released, then `message.sent` or `message.failed`. `message.delivered`
        and `message.read` follow only when the line's device reports them.
        `message.fallback_triggered` is sent when the device reports that the
        message went out, or was attempted, over SMS or RCS instead of iMessage.


        Accepts an API key or a trial key. An API key that starts with
        `isnap_test_` sends real messages, like one that starts with
        `isnap_live_`. A line-scoped key sends only on the lines in its
        `line_ids`. A trial key sends only from the trial's line to the phone
        number paired with the trial, text only: the sending limits and quiet
        hours do not apply to it, and it returns `402 trial_needs_inbound` while
        the trial is `dormant`.


        Send an `Idempotency-Key` header to make a retry safe
        (`X-Idempotency-Key` is read when it is absent). A retry with the same
        key and the same body on the same line sends nothing and returns `202`
        with the existing message in its current state, with `queue_info` set to
        `null`. The same key with a different body returns `409
        idempotency_conflict`. A key that the line already used for a reaction
        call returns `202` with that reaction entry, and the message is not
        sent: use a new key for every action. iSnap remembers a key for as long
        as its message exists, which is at least 72 hours. Keep the key at 255
        characters or fewer: a longer key returns `500 internal_error`. Send
        retries one after the other: of two simultaneous requests with the same
        new key, one is accepted and the other is either replayed or returns
        `500 internal_error`.


        On a shared line, the first message to a contact returns `503
        line_pool_exhausted` when no phone number is available to serve that
        contact, and `503 quota_state_unavailable` when that availability could
        not be read: retry the request.


        The response carries personal data: the handles in `to` and `from`, the
        text in `body`, the file names of the attachments and your `metadata`.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                line_id:
                  type: string
                  pattern: ^line_[0-9A-Za-z]{25}$
                  description: >-
                    The line that sends the message: `line_` followed by 25
                    letters or digits. It must be a line of your account whose
                    status is `active`.
                to:
                  type: string
                  minLength: 1
                  maxLength: 30
                  description: >-
                    Recipient phone number, 1 to 30 characters. E.164 is
                    preferred; a number without a country code is read as a US
                    number. iSnap normalizes it to E.164 and returns the
                    normalized value in `to` of the message. An email address or
                    an Apple ID returns `400 bad_request`, even when `POST
                    /v1/lookup` reports that it can receive iMessage. One
                    request has one recipient: there is no group send.
                body:
                  type: string
                  maxLength: 5000
                  description: >-
                    Message text, at most 5,000 characters. Required unless
                    `attachments` is not empty. With a trial key, iSnap appends
                    the footer of the trial to the text, after a blank line: the
                    `body` of the message returned and the message the recipient
                    receives both carry it.
                channel:
                  type: string
                  enum:
                    - auto
                  default: auto
                  description: >-
                    Only `auto` is accepted, and it is the default: the line's
                    device chooses the service.
                attachments:
                  type: array
                  items:
                    type: string
                    pattern: ^att_[0-9A-Za-z]{25}$
                    description: 'Attachment id: `att_` followed by 25 letters or digits.'
                  maxItems: 10
                  default: []
                  description: >-
                    Ordered list of at most 10 attachment ids (`att_` followed
                    by 25 letters or digits). On an iMessage line, the line's
                    device sends only the first attachment and still reports the
                    message `sent`: send one attachment per message. Create each
                    attachment with `POST /v1/attachments`, upload the file, and
                    wait until `GET /v1/attachments/{id}` shows `status: ready`:
                    an attachment that is not `ready` when the line's device
                    takes the message is left out of the message, without an
                    error. An id that does not exist, that belongs to another
                    account, or that names a file received on an inbound message
                    returns `404 not_found`. Do not repeat an id: a request that
                    lists the same id twice returns `500 internal_error`, and
                    nothing is sent. A request with no `body` and exactly one
                    attachment whose content type starts with `audio/` is sent
                    as a voice note, with `kind: voice`.
                effect:
                  oneOf:
                    - type: object
                      properties:
                        type:
                          type: string
                          enum:
                            - bubble
                          description: Always `bubble` for a bubble effect.
                        name:
                          type: string
                          enum:
                            - slam
                            - loud
                            - gentle
                            - invisible
                          description: Name of the bubble effect.
                      required:
                        - type
                        - name
                      description: 'A bubble effect: an effect on the message bubble.'
                    - type: object
                      properties:
                        type:
                          type: string
                          enum:
                            - screen
                          description: Always `screen` for a screen effect.
                        name:
                          type: string
                          enum:
                            - confetti
                            - balloons
                            - love
                            - lasers
                            - fireworks
                            - celebration
                            - shooting_star
                            - echo
                            - spotlight
                          description: Name of the screen effect.
                      required:
                        - type
                        - name
                      description: 'A screen effect: a full-screen effect.'
                  description: >-
                    iMessage effect of the message: a bubble effect or a screen
                    effect. iSnap passes it to the line's device and does not
                    report whether the recipient saw it.
                reply_to_message_id:
                  type: string
                  pattern: ^msg_[0-9A-Za-z]{25}$
                  description: >-
                    Id of the message this message replies to (`msg_` followed
                    by 25 letters or digits): a message on the same line,
                    received or sent. An id that does not exist returns `404
                    not_found`, including the id of a message iSnap has deleted.
                    A message on another line, of your account or of another
                    account, returns `422 reply_to_cross_line`.
                use_typing_indicator:
                  type: boolean
                  description: >-
                    Whether the line's device shows the typing indicator to the
                    recipient before it sends this message. When omitted, the
                    line's `default_use_typing_indicator` applies, which is
                    `true` unless you changed it. Always `false` on a line that
                    is not an iMessage line. The response returns the value in
                    effect.
                ignore_quiet_hours:
                  type: boolean
                  description: >-
                    `true` to let this message leave during the quiet hours of
                    the line: the hours whose weight is 0 in the line's
                    `activity_curve`, counted in the recipient's local time. Use
                    it for a recipient of your own, such as an alert to your own
                    phone number. The default is `false`. The message still
                    waits for the sending limits of the line, for the spacing
                    between messages and behind an earlier message to the same
                    recipient that iSnap holds. No effect on a shared line. The
                    response returns the value in effect.
                metadata:
                  type: object
                  additionalProperties:
                    nullable: true
                  description: >-
                    Your own keys and values for this message: at most 20 keys
                    and 8,192 bytes once serialized as JSON, otherwise the API
                    returns `400 validation_failed`. iSnap stores the object as
                    sent and returns it in `metadata` of the message, in every
                    response and in the events of the message.
                  example:
                    crm_account_id: acc_123
              required:
                - line_id
                - to
              description: >-
                The message to send. It needs a non-empty `body` or at least one
                attachment.
      responses:
        '202':
          description: >-
            The message, accepted as `queued` or `scheduled`. iSnap holds a
            message when a sending limit of the line is reached and
            `quota_overflow_behavior` of the line is `queue`, the default. A
            message is also held by the spacing between messages of the line, by
            quiet hours, and behind an earlier message to the same recipient
            that has not left. A `202` returned to an API key carries two
            headers: `X-iSnap-Quota-Remaining`, the number of new contacts the
            line can still message in the rolling 24 hours, and
            `X-iSnap-Quota-Resets-At`, the next midnight UTC. The limits are
            rolling, so that second header is not the time a full limit frees:
            read `GET /v1/lines/{id}/quota`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageV1Envelope'
        '400':
          description: >-
            Invalid request. `error.code` is `validation_failed` (a field is
            missing or malformed, or the request has neither a non-empty `body`
            nor an attachment: `error.issues` lists each problem), `bad_request`
            (`to` is not a valid phone number, or is an email address) or
            `line_not_active` (the line's status is neither `active` nor
            `suspended`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: No valid credential. `error.code` is `unauthorized`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: >-
            Refused. `error.code` is `forbidden` (the request carries neither an
            API key in its `Authorization` header nor the header
            `X-Requested-With: XMLHttpRequest`, or the key is line-scoped and
            `line_id` is not in its `line_ids`), `line_suspended` (the line is
            suspended) or `outbound_first_forbidden` (the line is inbound-only
            and the recipient has never written to it). With a trial key:
            `trial_scope_violation` (`line_id` or `to` is outside the trial) or
            `line_not_active` (the trial's line is not active).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: >-
            Not found. `error.code` is `not_found`: the line or an attachment id
            does not exist or does not belong to your account, or the message
            named by `reply_to_message_id` does not exist. The id of a file
            received on an inbound message is refused the same way.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          description: >-
            Conflict. `error.code` is `idempotency_conflict`: the
            `Idempotency-Key` was already used on this line for a message with a
            different body. A key that the line already used for a reaction call
            does not conflict: the API returns `202` with that reaction entry,
            and the message is not sent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: >-
            The request breaks a rule of this operation. `error.code` is
            `reply_to_cross_line` (`reply_to_message_id` names a message of
            another line, including a line of another account) or, with a trial
            key, `trial_attachments_unsupported` (the request carries
            attachments).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: >-
            Too many requests. `error.code` is `rate_limit_exceeded` (more than
            60 requests came from the same network address in 60 seconds to
            `POST /v1/messages` and `GET /v1/messages`, or more than 100 to the
            whole API), `minute_limit_exceeded`, `hourly_limit_exceeded`,
            `total_message_quota_exceeded`, `new_contact_quota_exceeded` or
            `known_contact_quota_exceeded` (a sending limit is reached and the
            line's `quota_overflow_behavior` is `drop` or `fail`, so nothing is
            stored; with `queue`, the default, the message is accepted and
            held), or `queue_full` (the message would be held and the line
            already holds its maximum number of held messages, or no departure
            time exists in the next 8 days). `Retry-After` gives the seconds to
            wait: 60 for the minute limit, 3,600 for the hourly limit, and the
            seconds until the next midnight UTC for the 24-hour limits and
            `queue_full`, which is a hint and not the time a rolling limit
            frees.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: >-
            Unexpected failure. `error.code` is `internal_error`. An operation
            that takes a JSON body also returns it when the body, sent as
            `application/json`, is not valid JSON. Quote `trace_id` when you
            report it to iSnap.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
      security:
        - bearerAuth: []
components:
  schemas:
    MessageV1Envelope:
      type: object
      properties:
        success:
          type: boolean
          enum:
            - true
          description: Always `true` on a success response.
        data:
          $ref: '#/components/schemas/MessageV1'
        trace_id:
          type: string
          description: >-
            Trace id of the request that produced this response: 32 lowercase
            hexadecimal characters. Quote it when you report a problem to iSnap.
        request_id:
          type: string
          description: >-
            Id of the request that produced this response: `req_` followed by 25
            letters or digits.
      required:
        - success
        - data
        - trace_id
        - request_id
    ErrorEnvelope:
      type: object
      properties:
        success:
          type: boolean
          enum:
            - false
          description: Always `false` on an error response.
        data:
          nullable: true
          description: Always `null` on an error response.
        error:
          type: object
          properties:
            status:
              type: integer
              description: The HTTP status of the response.
            code:
              type: string
              description: >-
                Stable machine-readable code that names the refusal. Branch on
                this value.
            message:
              type: string
              description: >-
                Text for a person. It can change between versions: do not branch
                on it.
            retry_after:
              type: integer
              nullable: true
              description: >-
                Seconds to wait before the next attempt. Set on the `429`
                responses that know a delay, where the `Retry-After` header
                carries the same value. When it is above 60 seconds, do not wait
                in a request: schedule the call, or report the time. `null` on
                every other error.
            retry_at:
              type: string
              nullable: true
              description: >-
                ISO 8601 timestamp at which the refusal ends. Set only on `429
                rebind_cap_reached`. `null` on every other error.
            issues:
              type: array
              nullable: true
              items:
                type: object
                properties:
                  path:
                    type: array
                    items:
                      anyOf:
                        - type: string
                        - type: number
                    description: >-
                      Where the problem is: the field names and array indexes
                      that lead to it.
                  message:
                    type: string
                    description: Text for a person.
                  code:
                    type: string
                    description: >-
                      Kind of problem. On `validation_failed`, for example
                      `invalid_type`, `invalid_value`, `invalid_format`,
                      `invalid_union`, `invalid_key`, `invalid_element`,
                      `too_small`, `too_big`, `not_multiple_of`,
                      `unrecognized_keys` or `custom`. The list is open: read
                      `path` and `message`.
                  min:
                    type: integer
                    description: >-
                      Smallest accepted value. Present only on
                      `limit_out_of_bounds`, `mac_quantity_not_a_pack`,
                      `byop_reduction_strands_mac_sessions` and
                      `byop_reduction_strands_byod_lines`.
                  max:
                    type: integer
                    description: >-
                      Largest accepted value. Present only on
                      `limit_out_of_bounds` and
                      `mac_sessions_exceed_byop_lines`, and on the `too_big`
                      issue of an `Idempotency-Key` header longer than 255
                      characters.
                required:
                  - path
                  - message
                  - code
              description: >-
                Problems found in the request, one entry each. Set on most `400
                validation_failed` responses and on the refusals that name a
                field: a refused `handle` of `POST /v1/lookup` has `issues:
                null`. `null` on every other error.
          required:
            - status
            - code
            - message
            - retry_after
            - retry_at
            - issues
          description: >-
            What went wrong. The six fields are always present; `retry_after`,
            `retry_at` and `issues` are `null` when they do not apply.
        trace_id:
          type: string
          description: >-
            Trace id of the request that produced this response: 32 lowercase
            hexadecimal characters. Quote it when you report a problem to iSnap.
        request_id:
          type: string
          description: >-
            Id of the request that produced this response: `req_` followed by 25
            letters or digits.
      required:
        - success
        - data
        - error
        - trace_id
        - request_id
      description: The body of an error response.
    MessageV1:
      type: object
      properties:
        id:
          type: string
          description: 'Message id: `msg_` followed by 25 letters or digits.'
        line_id:
          type: string
          pattern: ^line_[0-9A-Za-z]{25}$
          description: Id of the line that sent or received the message.
        direction:
          $ref: '#/components/schemas/MessageDirection'
        kind:
          $ref: '#/components/schemas/MessageKind'
        to:
          type: string
          description: >-
            On an outbound message sent through the API, the recipient's phone
            number in E.164. On a message typed on the line's device, the
            contact's handle as the device reports it. On an inbound message,
            the line's number: on a shared line, the number that serves the
            contact. A contact's number is personal data.
        from:
          type: string
          description: >-
            On an outbound message, the line's number: an empty string when the
            line has no number, and on a shared line the number that serves the
            contact. On an inbound message, the sender's handle, a phone number
            or an email address of up to 255 characters: exactly as the line's
            device stores it, without normalization, or normalized on a shared
            line (E.164 for a phone number). A phone number can come in another
            form than E.164: before you compare it with a number you send to,
            bring both to E.164. A contact's handle is personal data.
        body:
          type: string
          nullable: true
          description: >-
            Text of the message. `null` when the message has no text: a message
            with attachments only, or a reaction. The text of an inbound message
            is cut to 10,000 characters.
        channel:
          $ref: '#/components/schemas/MessageChannel'
        service:
          $ref: '#/components/schemas/MessageService'
        status:
          $ref: '#/components/schemas/MessageStatus'
        effect:
          oneOf:
            - type: object
              properties:
                type:
                  type: string
                  enum:
                    - bubble
                  description: Always `bubble` for a bubble effect.
                name:
                  type: string
                  enum:
                    - slam
                    - loud
                    - gentle
                    - invisible
                  description: Name of the bubble effect.
              required:
                - type
                - name
              description: 'A bubble effect: an effect on the message bubble.'
            - type: object
              properties:
                type:
                  type: string
                  enum:
                    - screen
                  description: Always `screen` for a screen effect.
                name:
                  type: string
                  enum:
                    - confetti
                    - balloons
                    - love
                    - lasers
                    - fireworks
                    - celebration
                    - shooting_star
                    - echo
                    - spotlight
                  description: Name of the screen effect.
              required:
                - type
                - name
              description: 'A screen effect: a full-screen effect.'
            - nullable: true
          description: >-
            The iMessage effect of the message, or `null` when it has none. On
            an outbound message, the effect you sent. On an inbound message, the
            effect the sender used, when the line's device reports it.
        reply_to_message_id:
          type: string
          nullable: true
          description: >-
            Id of the message this message replies to, or `null`. It becomes
            `null` when iSnap deletes the message replied to.
        use_typing_indicator:
          type: boolean
          description: >-
            On an outbound message, whether the line's device shows the typing
            indicator to the recipient before it sends the message. A message
            sent with an API key on a line that is not an iMessage line always
            has `false`. It has no meaning on an inbound message.
        metadata:
          type: object
          nullable: true
          additionalProperties:
            nullable: true
          description: >-
            The `metadata` you sent with the message, unchanged. `null` when you
            sent none, and on an inbound message.
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/MessageAttachmentV1'
          description: >-
            The files of the message, in order. Empty when the message has none.
            Files received with an inbound message are listed here too.
        reactions:
          type: array
          items:
            $ref: '#/components/schemas/MessageReactionV1'
          description: >-
            The reactions on the message, at most one per author. Empty when the
            message has none, and always empty on an entry whose `kind` is
            `reaction`. The line's own reaction appears as soon as the API
            accepts it, and disappears as soon as the API accepts its removal,
            before the line's device confirms either. It stays listed when the
            device fails to place it.
        scheduled_for:
          type: string
          nullable: true
          description: >-
            On a held message (`status: scheduled`), the planned departure time.
            On an outbound message that was not held, or that has left its hold,
            the same value as `queued_at`: read `sent_at` for the time the
            message was sent. `null` on an inbound message and on a message
            typed on the device of the line.
        schedule_reason:
          type: string
          nullable: true
          description: >-
            Why iSnap holds the message, while its `status` is `scheduled`.
            `minute_limit_exceeded`, `hourly_limit_exceeded`,
            `total_message_quota_exceeded`, `new_contact_quota_exceeded` or
            `known_contact_quota_exceeded`: a sending limit of the line is
            reached. `inter_message_delay` or `inter_new_contact_delay`: the
            spacing between two messages of the line. `quiet_hours`: the message
            was accepted outside the line's sending hours, counted in the
            recipient's local time. `carrier_retry`: on a shared line, the
            number that serves the contact is busy or offline. `pending_retry`:
            iSnap is retrying the release. A message held behind an earlier
            message to the same recipient takes the `schedule_reason` of that
            earlier message while the earlier one is held, and
            `inter_message_delay` (`carrier_retry` on a shared line) while the
            earlier one waits for the line's device: `schedule_reason` alone
            does not tell you that a message waits for an earlier one. `null` on
            a message that was never held, and `null` again once the message is
            released. Treat a value you do not know as a hold: more values can
            be added.
        reply_lane:
          type: boolean
          description: >-
            `true` when the message is in the reply lane: it answers a contact
            who wrote to the line in the last 24 hours, within the limits of the
            reply lane. Such a message is not held by the hourly limit, by the
            three 24-hour limits or by quiet hours. Set when the message is
            accepted. A held message enters the reply lane when its contact
            writes to the line while it waits, whatever held it, oldest first
            and within the same limits: the value then changes from `false` to
            `true`, once and never back, and the message is released without
            waiting for its `scheduled_for`, still after the earlier messages to
            the same recipient and within the per-minute limit and the spacing
            of the line.
        ignore_quiet_hours:
          type: boolean
          description: >-
            `true` when the message was sent with `ignore_quiet_hours: true` on
            a line that is not a shared line: quiet hours do not hold it.
            `false` otherwise, on a shared line, on an inbound message and on a
            message typed on the device of the line. Set when the message is
            accepted and never changed after that.
        queue_info:
          type: object
          nullable: true
          properties:
            queue_position:
              type: integer
              minimum: 0
              description: >-
                0 when the message leaves immediately. Otherwise the rank of the
                message among the line's held messages, starting at 1.
            is_new_contact:
              type: boolean
              description: >-
                `true` when the recipient is a new contact for the line: that
                number has not written to the line in the last 30 days, which is
                the default length of that period. `false` when the recipient is
                a known contact.
            daily_new_contacts_used:
              type: integer
              minimum: 0
              description: >-
                Distinct new contacts counted for the line in the last 24 hours,
                once this request is accepted.
            daily_new_contact_limit:
              type: integer
              minimum: 0
              description: The line's limit of distinct new contacts per rolling 24 hours.
            daily_known_contacts_used:
              type: integer
              minimum: 0
              description: >-
                Distinct known contacts counted for the line in the last 24
                hours, once this request is accepted.
            daily_known_contact_limit:
              type: integer
              minimum: 0
              description: >-
                The line's limit of distinct known contacts per rolling 24
                hours.
            daily_messages_used:
              type: integer
              minimum: 0
              description: >-
                Messages counted for the line in the last 24 hours, once this
                request is accepted. A held message counts when it is released,
                not when it is accepted.
            daily_total_message_limit:
              type: integer
              minimum: 0
              description: The line's limit of messages per rolling 24 hours.
            reply_lane:
              type: boolean
              description: The same value as `reply_lane` of the message.
            daily_replies_used:
              type: integer
              minimum: 0
              description: >-
                Reply-lane messages counted for the line in the last 24 hours,
                once this request is accepted.
            daily_reply_limit:
              type: integer
              minimum: 0
              description: The line's limit of reply-lane messages per rolling 24 hours.
            messages_ahead_in_queue:
              type: integer
              minimum: 0
              description: >-
                Number of held messages of the line planned to leave before this
                one. Present only when the message is held.
            daily_slot_of:
              type: string
              description: >-
                Date of the planned departure in the recipient's local time, as
                `YYYY-MM-DD`. Present only when iSnap placed the held message at
                a time in the line's sending hours.
            target_activity_hour:
              type: integer
              minimum: 0
              maximum: 23
              description: >-
                Hour of the planned departure in the recipient's local time, 0
                to 23. Present only with `daily_slot_of`.
            next_slot_available_at:
              type: string
              description: >-
                The planned departure time: the same value as `scheduled_for` of
                the message. Present only when the message is held.
            last_new_contact_sent_at:
              type: string
              description: >-
                When the line last sent a first message to a new contact.
                Present only when `schedule_reason` is
                `inter_new_contact_delay`.
            min_inter_new_contact_seconds:
              type: integer
              minimum: 0
              description: >-
                The line's minimum number of seconds between two first messages
                to new contacts. Present only when `schedule_reason` is
                `inter_new_contact_delay`.
            jitter_applied_seconds:
              type: integer
              minimum: 0
              description: >-
                Random number of seconds added to that minimum for this message.
                Present only when `schedule_reason` is
                `inter_new_contact_delay`.
          required:
            - queue_position
            - is_new_contact
            - daily_new_contacts_used
            - daily_new_contact_limit
            - daily_known_contacts_used
            - daily_known_contact_limit
            - daily_messages_used
            - daily_total_message_limit
            - reply_lane
            - daily_replies_used
            - daily_reply_limit
          description: >-
            The state of the line's sending limits when the message was
            accepted. Filled only in the response of `POST /v1/messages` to a
            new request: `null` on every read, in the response to a retry with
            the same `Idempotency-Key`, and in events.
        created_at:
          type: string
          description: >-
            When iSnap recorded the message. On an inbound message it can be
            later than `delivered_at`, when the line's device was offline.
        queued_at:
          type: string
          nullable: true
          description: >-
            When an outbound message entered the queue. `null` on an inbound
            message and on a message typed on the device of the line.
        sent_at:
          type: string
          nullable: true
          description: >-
            When the line's device confirmed that it sent the message. `null`
            until then, and on an inbound message.
        delivered_at:
          type: string
          nullable: true
          description: >-
            On an outbound message, the delivery time the line's device
            reported: `null` until it reports one. On an inbound message, the
            time the device received the message.
        read_at:
          type: string
          nullable: true
          description: >-
            On an outbound message, the time the line's device reported that the
            recipient read it. On an inbound message, the time it was marked as
            read with `POST /v1/chats/{chat_id}/read`. `null` until then.
        failed_at:
          type: string
          nullable: true
          description: >-
            When the message became `failed`. `null` otherwise. When the device
            of an iMessage line reports a failure after `sent`, `failed_at`
            holds the time of the send, not the time of the failure.
        error_code:
          type: string
          nullable: true
          description: >-
            `null` unless the message is `failed`. When iSnap failed the message
            itself: `carrier_unavailable` (the message waited 48 hours without
            reaching a device), `no_terminal_ack` (the line's device took the
            message and never reported an outcome) or `carrier_terminated` (the
            line was terminated while the message was waiting). `null` when the
            line's device reported the failure: read `error_message`.
        error_message:
          type: string
          nullable: true
          description: >-
            `null` unless the message is `failed`. A text that describes the
            failure. When the line's device reported the failure, it is the text
            the device gave, or `null` when it gave none.
        cancelled_at:
          type: string
          nullable: true
          description: When the message was canceled. `null` otherwise.
      required:
        - id
        - line_id
        - direction
        - kind
        - to
        - from
        - body
        - channel
        - service
        - status
        - effect
        - reply_to_message_id
        - use_typing_indicator
        - metadata
        - attachments
        - reactions
        - scheduled_for
        - schedule_reason
        - reply_lane
        - ignore_quiet_hours
        - queue_info
        - created_at
        - queued_at
        - sent_at
        - delivered_at
        - read_at
        - failed_at
        - error_code
        - error_message
        - cancelled_at
      description: >-
        A message sent or received by a line. The response of `POST
        /v1/messages`, the reads, the list items, the cancel response and the
        `message` field of the `message.*` events all carry this object. It has
        no chat id: the chat id is given by `GET /v1/chats` and by events. It
        carries personal data: the handles in `to` and `from`, the text in
        `body`, the file names of `attachments`, and what you put in `metadata`.
        Timestamps are ISO 8601 in UTC.
    MessageDirection:
      type: string
      enum:
        - outbound
        - inbound
      description: >-
        `outbound`: a message the line sent, through the API or typed on the
        line's device. `inbound`: a message the line received.
    MessageKind:
      type: string
      enum:
        - message
        - voice
        - reaction
      description: >-
        `message`: a message with text, attachments or both. `voice`: a voice
        note. `reaction`: a reaction, recorded as its own entry.
    MessageChannel:
      type: string
      enum:
        - auto
        - iMessage
        - SMS
        - RCS
      description: >-
        `auto` on a message sent through the API. On an inbound message and on a
        message typed on the line's device, the same value as `service`.
        `iMessage` on a reaction.
    MessageService:
      type: string
      nullable: true
      enum:
        - iMessage
        - SMS
        - RCS
        - null
      description: >-
        The service the line's device reports it used: `iMessage`, `SMS` or
        `RCS`. On an outbound message it is `null` until the device confirms the
        send, and it stays `null` when the send fails or when the device does
        not report the service. On an inbound message, the service the message
        arrived on.
    MessageStatus:
      type: string
      enum:
        - queued
        - scheduled
        - sent
        - delivered
        - read
        - failed
        - cancelled
      description: >-
        `queued`: accepted and waiting for the line's device, or with the
        device, which has not reported the outcome. `scheduled`: held by iSnap
        until `scheduled_for`. `sent`: the device reported that it sent the
        message. `delivered`: the device reported a delivery receipt; every
        inbound message, and every message typed on the line's device, is
        `delivered` from the start. `read`: the device reported that the
        recipient read an outbound message. `failed`: the message failed, or
        iSnap stopped waiting for its outcome (`error_code: no_terminal_ack`),
        in which case it can have reached the recipient. iSnap never resends it.
        `cancelled`: canceled with `DELETE /v1/messages/{id}` before the device
        took it. An outbound message moves from `scheduled` to `queued`, `sent`,
        `delivered` and `read`, and no status is guaranteed to follow `sent`,
        `delivered` or `read`. On an iMessage line, a message sent over SMS or
        RCS gets no `delivered` and no `read`, and a `failed` can still follow
        its `sent`. `failed` can follow any status except `cancelled`, including
        `delivered` and `read`. `cancelled` is final. Treat `failed` as final: a
        send confirmation that arrives after a failure reported by the device
        sets `status` back to `sent` while `failed_at` stays set. A delivery
        receipt can arrive before the device confirms the send, and `status`
        then returns to `sent` while `delivered_at` stays set: read the
        timestamps, not only `status`.
    MessageAttachmentV1:
      type: object
      properties:
        id:
          type: string
          pattern: ^att_[0-9A-Za-z]{25}$
          description: >-
            Attachment id: `att_` followed by 25 letters or digits. Read the
            file with `GET /v1/attachments/{id}`.
        content_type:
          type: string
          minLength: 1
          maxLength: 100
          description: >-
            Content type of the file, as declared when the attachment was
            created. `audio/x-caf` for an audio file that iSnap converted.
        filename:
          type: string
          minLength: 1
          maxLength: 255
          description: File name. It can carry personal data.
        size_bytes:
          type: integer
          minimum: 1
          description: >-
            Size in bytes, as declared when the attachment was created. For an
            audio file that iSnap converted, the size after conversion.
        duration_ms:
          type: integer
          nullable: true
          minimum: 0
          description: >-
            Duration in milliseconds of an audio file that iSnap converted.
            `null` on every other file.
      required:
        - id
        - content_type
        - filename
        - size_bytes
        - duration_ms
      description: >-
        A file of a message. It carries no download URL: call `GET
        /v1/attachments/{id}` to get one.
    MessageReactionV1:
      type: object
      properties:
        reaction_type:
          type: string
          enum:
            - love
            - like
            - dislike
            - laugh
            - emphasize
            - question
            - custom
          description: >-
            `love`, `like`, `dislike`, `laugh`, `emphasize` or `question`, or
            `custom` for an emoji reaction from the contact.
        reaction_emoji:
          type: string
          nullable: true
          description: >-
            The emoji of the reaction. For `custom`, the emoji the contact used,
            or `null` when the line's device did not report it.
        from_handle:
          type: string
          description: >-
            Handle of the author of the reaction: the contact's handle when
            `direction` is `inbound`, the line's number when it is `outbound`. A
            contact's handle is personal data.
        direction:
          type: string
          enum:
            - outbound
            - inbound
          description: >-
            `outbound`: the line's own reaction. `inbound`: a reaction from the
            contact.
        created_at:
          type: string
          description: When iSnap recorded the reaction.
      required:
        - reaction_type
        - reaction_emoji
        - from_handle
        - direction
        - created_at
      description: >-
        A reaction on a message. A message carries at most one reaction per
        author.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        An API key created in the iSnap dashboard, or a trial key, sent as
        `Authorization: Bearer <key>`. An API key starts with `isnap_live_` or
        `isnap_test_`; both act on the same account, lines and billing. A trial
        key acts on its trial only.

````

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