chatrail-dev Messages API

Sending, delivery state and export.

Operations 15

POST /v1/messages/text Send a message #
GET /v1/messages List messages #
GET /v1/messages/{id} Get a message #
PATCH /v1/messages/{id} Edit a message #
POST /v1/messages/{id}/cancel Cancel a message #
POST /v1/messages/{id}/reactions React to a message #
POST /v1/messages/{id}/read Mark a message read #
POST /v1/connections/{connection}/presence Show typing in a chat #
POST /v1/messages/{id}/revoke Delete a message for everyone #
POST /v1/messages/{id}/reschedule Move a scheduled message #
GET /v1/messages/export Export delivery metadata as CSV #
GET /v1/numbers/check Check whether a number is on WhatsApp #
POST /v1/numbers/check Check up to 10 numbers #
GET /v1/messages/{id}/media Download an inbound attachment #
POST /v1/messages/{id}/correlation Correct which message a reply answers #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/chatrail-dev:chatrail-dev-messages-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

chatrail-dev-messages-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: ChatRail Messages API
  version: 1.0.0
  description: 'Send, schedule and receive WhatsApp messages through a linked-device session.


    **Two things the API is careful about, which shape every endpoint below.**


    *Accepted is not delivered.* A 202 means the message is durably stored and queued. Only the `status` field ever claims WhatsApp delivered anything, and `queued`, `submitted`, `sent`, `delivered` and `read` are deliberately distinct.


    *A retry must not send twice.* Send `Idempotency-Key` on every send. A replay returns the original response; a concurrent duplicate is told to retry rather than allowed to race.'
  contact:
    name: ChatRail
    url: https://www.chatrail.dev
servers:
- url: https://www.chatrail.dev
security:
- apiKey: []
tags:
- name: Messages
  description: Sending, delivery state and export.
paths:
  /v1/messages/text:
    post:
      summary: Send a message
      description: 'Accepts a message for delivery and returns 202. **202 means accepted, never delivered** — the message is durably stored and queued, and only the `status` field ever claims delivery. Send `media` or `location` for an attachment, `send_at` to defer it. Supply `Idempotency-Key` so a retry cannot send twice. A first message to someone the number has never talked to is checked against WhatsApp and paced: 10 an hour in a number''s first week, 30 after, at least 20 seconds apart. Over the pace it stays `queued` and goes out later; nothing is refused. A message WhatsApp accepts but never confirms sending within 24 hours is failed with `not_confirmed`.'
      tags:
      - Messages
      operationId: post_v1_messages_text
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                connection:
                  type: string
                  minLength: 1
                  maxLength: 40
                to:
                  anyOf:
                  - type: string
                    minLength: 5
                    maxLength: 40
                  - type: object
                    properties:
                      phone:
                        type: string
                        minLength: 5
                        maxLength: 40
                    required:
                    - phone
                    additionalProperties: false
                  - type: object
                    properties:
                      group:
                        type: string
                        minLength: 5
                        maxLength: 40
                    required:
                    - group
                    additionalProperties: false
                body:
                  type: string
                  maxLength: 4096
                media:
                  type: object
                  properties:
                    url:
                      type: string
                      maxLength: 2048
                      format: uri
                    kind:
                      type: string
                      enum:
                      - image
                      - video
                      - audio
                      - document
                    mimetype:
                      type: string
                      minLength: 3
                      maxLength: 255
                    filename:
                      type: string
                      minLength: 1
                      maxLength: 255
                  required:
                  - url
                  additionalProperties: false
                location:
                  type: object
                  properties:
                    latitude:
                      type: number
                      minimum: -90
                      maximum: 90
                    longitude:
                      type: number
                      minimum: -180
                      maximum: 180
                    title:
                      type: string
                      minLength: 1
                      maxLength: 255
                  required:
                  - latitude
                  - longitude
                  additionalProperties: false
                contact:
                  type: object
                  properties:
                    name:
                      type: string
                      minLength: 1
                      maxLength: 255
                    phone:
                      type: string
                      minLength: 5
                      maxLength: 40
                    organization:
                      type: string
                      minLength: 1
                      maxLength: 255
                  required:
                  - name
                  - phone
                  additionalProperties: false
                poll:
                  type: object
                  properties:
                    question:
                      type: string
                      minLength: 1
                      maxLength: 255
                    options:
                      minItems: 2
                      maxItems: 12
                      type: array
                      items:
                        type: string
                        minLength: 1
                        maxLength: 100
                    multiple_answers:
                      type: boolean
                  required:
                  - question
                  - options
                  additionalProperties: false
                reply_to:
                  type: string
                  format: uuid
                  pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
                mentions:
                  minItems: 1
                  maxItems: 50
                  type: array
                  items:
                    type: string
                    minLength: 1
                    maxLength: 40
                link_preview:
                  type: boolean
                context:
                  type: object
                  propertyNames:
                    type: string
                  additionalProperties: {}
                metadata:
                  type: object
                  propertyNames:
                    type: string
                  additionalProperties: {}
                pause_ai:
                  type: boolean
                context_sensitivity:
                  type: string
                  enum:
                  - normal
                  - sensitive
                  - restricted
                send_at:
                  type: string
                  format: date-time
                  pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
                timezone:
                  type: string
                  minLength: 1
                  maxLength: 64
              required:
              - connection
              - to
              additionalProperties: false
            example:
              connection: main
              to: '+15551234567'
              body: Order CR-2048 has shipped.
              context:
                order_id: CR-2048
                status: dispatched
      responses:
        '202':
          description: Accepted and queued.
        '400':
          description: The body, recipient or attachment was rejected.
        '402':
          description: The plan does not include this. The message names what would allow it.
        '409':
          description: The connection is not ready, or an identical Idempotency-Key is in flight.
        '422':
          description: The Idempotency-Key was reused with a different body.
        '429':
          description: Quota or rate limit.
      security:
      - apiKey:
        - messages:write
      x-scopes:
      - messages:write
  /v1/messages:
    get:
      summary: List messages
      description: 'Newest first, cursor paginated. Offsets are not offered: they skip and duplicate.'
      tags:
      - Messages
      operationId: get_v1_messages
      parameters:
      - name: limit
        in: query
        required: false
        schema:
          default: 25
          type: integer
          minimum: 1
          maximum: 100
      - name: cursor
        in: query
        required: false
        schema:
          type: string
          maxLength: 200
      - name: connection
        in: query
        required: false
        schema:
          type: string
          minLength: 1
          maxLength: 40
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - scheduled
          - accepted
          - queued
          - submitting
          - submitted
          - sent
          - delivered
          - read
          - failed
          - canceled
      - name: direction
        in: query
        required: false
        schema:
          type: string
          enum:
          - inbound
          - outbound
      - name: q
        in: query
        required: false
        schema:
          type: string
          minLength: 1
          maxLength: 120
      responses:
        '200':
          description: A page of messages and a `next_cursor`.
      security:
      - apiKey:
        - messages:read
      x-scopes:
      - messages:read
  /v1/messages/{id}:
    get:
      summary: Get a message
      tags:
      - Messages
      operationId: get_v1_messages__id_
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Message id.
      responses:
        '200':
          description: The message and its delivery timeline.
        '404':
          description: No such message.
      security:
      - apiKey:
        - messages:read
      x-scopes:
      - messages:read
    patch:
      summary: Edit a message
      description: 'Replaces the text of a text message this number sent, within WhatsApp''s 15-minute window, and/or its `context`. Captions cannot be changed after sending. The first text is kept and audited. Editing the text does not change the context the AI answers from: send `context` too when the correction matters to it (a new gate, a new time). A context-only edit has no window and does not reach WhatsApp.'
      tags:
      - Messages
      operationId: patch_v1_messages__id_
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Message id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                body:
                  type: string
                  maxLength: 4096
                context:
                  type: object
                  propertyNames:
                    type: string
                  additionalProperties: {}
                context_sensitivity:
                  type: string
                  enum:
                  - normal
                  - sensitive
                  - restricted
              additionalProperties: false
            example:
              body: 'Corrected: viewing is at 3pm, not 2pm.'
              context:
                property:
                  address: 14 Mill Lane
                  viewing: '15:00'
      responses:
        '200':
          description: The message, with `edited_at` set.
        '404':
          description: No such message.
        '409':
          description: 'Not editable: received, not a text message, deleted, or past the window.'
      security:
      - apiKey:
        - messages:write
      x-scopes:
      - messages:write
  /v1/messages/{id}/cancel:
    post:
      summary: Cancel a message
      description: Only while it has not reached the provider. Once submitted the message exists on somebody's phone and cancelling would be a lie.
      tags:
      - Messages
      operationId: post_v1_messages__id__cancel
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Message id.
      responses:
        '200':
          description: Cancelled.
        '404':
          description: No such message.
        '409':
          description: Already handed to the provider.
      security:
      - apiKey:
        - messages:write
      x-scopes:
      - messages:write
  /v1/messages/{id}/reactions:
    post:
      summary: React to a message
      description: Sets this number's reaction on a message it sent or received; an empty `emoji` removes it. Works on any message that has reached WhatsApp.
      tags:
      - Messages
      operationId: post_v1_messages__id__reactions
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Message id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                emoji:
                  type: string
                  maxLength: 16
              required:
              - emoji
              additionalProperties: false
            example:
              emoji: 👍
      responses:
        '200':
          description: The message, with `reaction` set.
        '404':
          description: No such message.
        '409':
          description: Not possible for this message, or WhatsApp refused.
        '422':
          description: The connection's engine does not support reactions.
      security:
      - apiKey:
        - messages:write
      x-scopes:
      - messages:write
  /v1/messages/{id}/read:
    post:
      summary: Mark a message read
      description: 'Shows blue ticks to the sender of an inbound message. Idempotent: a second call sends nothing.'
      tags:
      - Messages
      operationId: post_v1_messages__id__read
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Message id.
      responses:
        '200':
          description: The message, with `seen_at` set.
        '404':
          description: No such message.
        '409':
          description: Only a received message can be marked read.
      security:
      - apiKey:
        - messages:write
      x-scopes:
      - messages:write
  /v1/connections/{connection}/presence:
    post:
      summary: Show typing in a chat
      description: Shows "typing…" or "recording…" to a person or group, or stops (`paused`). WhatsApp clears it by itself after a short while or when a message arrives. Nothing is stored.
      tags:
      - Messages
      operationId: post_v1_connections__connection__presence
      parameters:
      - name: connection
        in: path
        required: true
        schema:
          type: string
        description: Connection slug.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                to:
                  anyOf:
                  - type: string
                    minLength: 5
                    maxLength: 40
                  - type: object
                    properties:
                      phone:
                        type: string
                        minLength: 5
                        maxLength: 40
                    required:
                    - phone
                    additionalProperties: false
                  - type: object
                    properties:
                      group:
                        type: string
                        minLength: 5
                        maxLength: 40
                    required:
                    - group
                    additionalProperties: false
                state:
                  type: string
                  enum:
                  - typing
                  - recording
                  - paused
              required:
              - to
              - state
              additionalProperties: false
            example:
              to: '+447700900123'
              state: typing
      responses:
        '204':
          description: Shown.
        '404':
          description: No such connection.
        '409':
          description: The connection is not ready.
        '422':
          description: The connection's engine cannot show typing.
      security:
      - apiKey:
        - messages:write
      x-scopes:
      - messages:write
  /v1/messages/{id}/revoke:
    post:
      summary: Delete a message for everyone
      description: Removes a message this number sent from every phone in the chat, within WhatsApp's window of about two and a half days. The record stays in ChatRail, marked deleted, and the deletion is audited.
      tags:
      - Messages
      operationId: post_v1_messages__id__revoke
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Message id.
      responses:
        '200':
          description: The message, with `deleted_at` set.
        '404':
          description: No such message.
        '409':
          description: 'Not deletable: received, already deleted, or past the window.'
      security:
      - apiKey:
        - messages:write
      x-scopes:
      - messages:write
  /v1/messages/{id}/reschedule:
    post:
      summary: Move a scheduled message
      tags:
      - Messages
      operationId: post_v1_messages__id__reschedule
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Message id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                send_at:
                  type: string
                  format: date-time
                  pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
                timezone:
                  type: string
                  minLength: 1
                  maxLength: 64
              required:
              - send_at
              additionalProperties: false
            example:
              send_at: '2026-10-01T09:00:00Z'
              timezone: Europe/London
      responses:
        '200':
          description: Rescheduled.
        '409':
          description: No longer scheduled.
      security:
      - apiKey:
        - messages:write
      x-scopes:
      - messages:write
  /v1/messages/export:
    get:
      summary: Export delivery metadata as CSV
      description: Streamed row by row. **Message bodies are not included** — this is delivery metadata, not content.
      tags:
      - Messages
      operationId: get_v1_messages_export
      parameters:
      - name: connection
        in: query
        required: false
        schema:
          type: string
          minLength: 1
          maxLength: 40
      - name: status
        in: query
        required: false
        schema:
          type: string
          minLength: 1
          maxLength: 20
      - name: direction
        in: query
        required: false
        schema:
          type: string
          enum:
          - inbound
          - outbound
      - name: days
        in: query
        required: false
        schema:
          default: 30
          type: integer
          minimum: 1
          maximum: 365
      responses:
        '200':
          description: A CSV stream.
      security:
      - apiKey:
        - messages:read
      x-scopes:
      - messages:read
  /v1/numbers/check:
    get:
      summary: Check whether a number is on WhatsApp
      description: 'Answers `checked: false` when the engine cannot say. Treat that as "send anyway": refusing a message because a check failed turns a missing provider feature into a delivery failure. `chat_id` is WhatsApp''s own id for the chat when it gives one. An answer from the last 24 hours is reused (`cached: true`) without asking WhatsApp again. Lookups that do reach WhatsApp go through your connected number and are limited to 10 a minute per workspace, and 30 a day on Sandbox.'
      tags:
      - Messages
      operationId: get_v1_numbers_check
      parameters:
      - name: phone
        in: query
        required: true
        schema:
          type: string
          minLength: 5
          maxLength: 40
      - name: connection
        in: query
        required: true
        schema:
          type: string
          minLength: 1
          maxLength: 40
      responses:
        '200':
          description: 'The answer, or `checked: false`.'
        '404':
          description: No such connection.
        '429':
          description: No lookup budget left; `Retry-After` says how long to wait.
      security:
      - apiKey:
        - messages:read
      x-scopes:
      - messages:read
    post:
      summary: Check up to 10 numbers
      description: 'The same check for several numbers. Each gets its own line in `results`, in the order sent: a number that cannot be parsed has `reason: invalid_number`, and numbers a spent budget could not reach have `reason: minute_limit` or `daily_limit` instead of failing the request. Repeats in one request are looked up once.'
      tags:
      - Messages
      operationId: post_v1_numbers_check
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                connection:
                  type: string
                  minLength: 1
                  maxLength: 40
                phones:
                  minItems: 1
                  maxItems: 10
                  type: array
                  items:
                    type: string
                    minLength: 1
                    maxLength: 40
              required:
              - connection
              - phones
              additionalProperties: false
            example:
              connection: main
              phones:
              - '+923001234567'
              - '+2348031234567'
      responses:
        '200':
          description: One result per number.
        '400':
          description: No numbers, or more than 10.
        '404':
          description: No such connection.
      security:
      - apiKey:
        - messages:read
      x-scopes:
      - messages:read
  /v1/messages/{id}/media:
    get:
      summary: Download an inbound attachment
      description: 'Streams the file from the transport through ChatRail. Inbound only: an outbound attachment''s URL is one the caller supplied and already has. The response carries the stored content type, never the transport''s, and is always served as an attachment. Answers 404 when the message has no attachment, when the transport reported a file it does not serve, or when it no longer holds it.'
      tags:
      - Messages
      operationId: get_v1_messages__id__media
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Inbound message id.
      responses:
        '200':
          description: The file.
        '404':
          description: No downloadable media on this message.
        '503':
          description: This deployment cannot retrieve media from the transport.
      security:
      - apiKey:
        - messages:read
      x-scopes:
      - messages:read
  /v1/messages/{id}/correlation:
    post:
      summary: Correct which message a reply answers
      description: Re-points an inbound reply at the outbound message it actually answers, or unlinks it with `null`. The engine's original choice is preserved, so corrections measure the correlation rules rather than quietly overwriting them. The previously linked alert reopens for correlation; the newly linked one is marked answered.
      tags:
      - Messages
      operationId: post_v1_messages__id__correlation
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Inbound message id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                reply_to_message_id:
                  anyOf:
                  - type: string
                    format: uuid
                    pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
                  - type: 'null'
              required:
              - reply_to_message_id
              additionalProperties: false
            example:
              reply_to_message_id: 019bf4a1-0000-7000-8000-000000000000
      responses:
        '200':
          description: The corrected link, and what it overruled.
        '400':
          description: The target is not an outbound message in the same conversation.
        '404':
          description: No such message.
        '409':
          description: Only an inbound message has a correlation to correct.
      security:
      - apiKey:
        - messages:write
      x-scopes:
      - messages:write
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: 'A workspace API key, `cr_live_…`. Server-side only: it carries workspace authority and must never reach a browser.'
    session:
      type: apiKey
      in: cookie
      name: chatrail_session
      description: Dashboard session cookie. Set by `/v1/auth/verify`.