API documentation

Messages API

Send messages on any channel with one endpoint — text, WhatsApp templates and marketing messages, and email with HTML and attachments — read delivery status, and check whose messaging window is open.

POST/v1/messagesmessages:sendThe API key needs the messages:send permission. Without it the call is refused with 403 insufficient_scope.Sign in to try

Send on any channel — checked now, delivered in the background

Reply in an existing conversation with conversation_id — on an Instagram comment thread that is a public reply under the customer's latest comment, or a private DM to them with reply_as: "dm" — or start one: WhatsApp with channel, to (a phone number) and an approved template; email with channel, to, from (one of your verified addresses) and subject. Everything the channel would refuse is refused here with a specific code. Attach files with attachments (uploaded file_ids or inline base64, 10 MB in all): any file on email and WhatsApp, images, video and audio on Instagram. An accepted message returns 202 with status: "queued"; its outcome arrives as a message.status webhook and on GET /messages/{id}. Send an Idempotency-Key header so a retry never sends twice.

Request
curl -X POST "https://api.iotabot.com/v1/messages" \
  -H "Authorization: Bearer $IOTABOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "conversation_id": "6a4b9e21c3f1a2b3c4d5e6f7",
  "text": "Your order #1042 shipped yesterday — it arrives Friday."
}' \
  -H "Idempotency-Key: order-1042-shipped"
Response · 202
{
  "data": {
    "id": "6a4ba0f1c3f1a2b3c4d5e733",
    "object": "message",
    "status": "queued",
    "channel": "whatsapp",
    "conversation_id": "6a4b9e21c3f1a2b3c4d5e6f7",
    "created_at": "2026-10-01T09:16:10.004Z"
  }
}
Errors
window_closedtemplate_not_approvedconversation_closedintegration_pausedprivate_reply_expiredprivate_reply_usedsender_not_verifiedsender_not_foundrecipient_suppressedrecipient_unsubscribedrecipient_opted_outinvalid_phoneunsupported_attachmentfile_already_usedattachments_too_largeapi_quota_exceededidempotency_key_reusedidempotency_in_progress
GET/v1/messages/{id}messages:readThe API key needs the messages:read permission. Without it the call is refused with 403 insufficient_scope.Sign in to try

One message and its delivery status

status is queued, sent, delivered, read or failed for messages you sent, and received for customer messages.

Parameters
idrequiredIn the path
Request
curl -X GET "https://api.iotabot.com/v1/messages/MESSAGE_ID" \
  -H "Authorization: Bearer $IOTABOT_API_KEY"
Response · 200
{
  "data": {
    "id": "6a4ba0f1c3f1a2b3c4d5e733",
    "object": "message",
    "conversation_id": "6a4b9e21c3f1a2b3c4d5e6f7",
    "channel": "whatsapp",
    "direction": "outbound",
    "sender": {
      "type": "api",
      "name": "API · CRM sync"
    },
    "type": "text",
    "text": "Your order #1042 shipped yesterday — it arrives Friday.",
    "html": null,
    "html_raw": null,
    "attachments": [],
    "status": "delivered",
    "delivery": {
      "status": "delivered",
      "at": "2026-10-01T09:16:12.410Z"
    },
    "created_at": "2026-10-01T09:15:02.881Z"
  }
}
Errors
message_not_found
GET/v1/messaging-windowconversations:readThe API key needs the conversations:read permission. Without it the call is refused with 403 insufficient_scope.Sign in to try

Everyone you can message right now, soonest-closing window first

WhatsApp and Instagram allow a free-form message only within 24 hours of the customer's last message. This lists every WhatsApp and Instagram DM conversation whose window is open now — opted-out customers and conversations where an agent paused integration replies are left out — with window_closes_at and seconds_left, sorted so the windows closing first come first. closes_within_minutes keeps only those closing soon. Use it to reach people before their window shuts; send with POST /messages. Instagram comment threads are not listed (their private reply has its own 7-day clock — check one with POST /messaging-window/check).

Parameters
channelQuery — whatsapp or instagram — both when left out
closes_within_minutesQuery — 1–1440: only windows closing within this many minutes
limitQuery — 1–100, default 25
starting_afterQuery — the next_cursor of the previous page
Request
curl -X GET "https://api.iotabot.com/v1/messaging-window?channel=instagram&closes_within_minutes=180" \
  -H "Authorization: Bearer $IOTABOT_API_KEY"
Response · 200
{
  "data": {
    "checked_at": "2026-10-02T09:00:58.000Z",
    "items": [
      {
        "conversation_id": "6a4b9e21c3f1a2b3c4d5e6f8",
        "channel": "instagram",
        "thread": "dm",
        "account_id": "6a4c02bb…",
        "contact": {
          "name": "priya.styles",
          "phone": null,
          "email": null,
          "external_id": "17841400000000001"
        },
        "window_closes_at": "2026-10-02T14:05:00.000Z",
        "seconds_left": 18342,
        "last_customer_message_at": "2026-10-01T14:05:00.000Z",
        "can_send": true,
        "reason": null
      }
    ],
    "has_more": false,
    "next_cursor": null
  }
}
Errors
invalid_channelinvalid_limitinvalid_cursor
POST/v1/messaging-window/checkconversations:readThe API key needs the conversations:read permission. Without it the call is refused with 403 insufficient_scope.Sign in to try

Can these people be messaged now, and until when?

Up to 100 people in one call, each named by one of conversation_id, instagram_username, instagram_user_id or phone (WhatsApp) — mix them freely. Add instagram_account_id or whatsapp_account_id to an item to look on one of your accounts only. Each row says can_send (a free-form message would be accepted now), window_closes_at, seconds_left and, when it cannot, reason: window_closed, no_conversation (they never wrote to you), opted_out, integration_paused, ambiguous (the username or number matches on several of your accounts — matches lists them), channel_not_allowed, invalid_phone, conversation_not_found, invalid_item (not exactly one identifier), and for comment threads private_reply_used or no_comment. On WhatsApp template_allowed says whether an approved template can still go — it can at any time, window or not. On an Instagram comment thread (thread: "comment") the window is the private reply's: once per comment, within 7 days. Usernames are matched as IotaBot last saw them, so a renamed account may show no_conversation — instagram_user_id (from webhooks) always matches. Email and website chat have no window (window_closes_at: null). Rows come back in the order sent; one bad item never fails the rest. Checks use no API quota.

Request
curl -X POST "https://api.iotabot.com/v1/messaging-window/check" \
  -H "Authorization: Bearer $IOTABOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "items": [
    {
      "instagram_username": "priya.styles"
    },
    {
      "phone": "+919812345678"
    },
    {
      "conversation_id": "6a4b9e21c3f1a2b3c4d5e6f7"
    }
  ]
}'
Response · 200
{
  "data": {
    "checked_at": "2026-10-02T09:00:58.000Z",
    "items": [
      {
        "input": {
          "instagram_username": "priya.styles"
        },
        "conversation_id": "6a4b9e21c3f1a2b3c4d5e6f8",
        "channel": "instagram",
        "thread": "dm",
        "account_id": "6a4c02bb…",
        "contact": {
          "name": "priya.styles",
          "phone": null,
          "email": null,
          "external_id": "17841400000000001"
        },
        "window_closes_at": "2026-10-02T14:05:00.000Z",
        "seconds_left": 18342,
        "last_customer_message_at": "2026-10-01T14:05:00.000Z",
        "can_send": true,
        "reason": null
      },
      {
        "input": {
          "phone": "+919812345678"
        },
        "conversation_id": "6a4b9e21c3f1a2b3c4d5e6f7",
        "channel": "whatsapp",
        "account_id": "6a4c01aa…",
        "contact": {
          "name": "Priya Sharma",
          "phone": "+919812345678",
          "email": null,
          "external_id": "919812345678"
        },
        "window_closes_at": null,
        "seconds_left": 0,
        "last_customer_message_at": "2026-09-28T09:15:02.881Z",
        "can_send": false,
        "reason": "window_closed",
        "template_allowed": true
      },
      {
        "input": {
          "instagram_username": "arjun.k"
        },
        "conversation_id": null,
        "channel": "instagram",
        "can_send": false,
        "window_closes_at": null,
        "seconds_left": null,
        "last_customer_message_at": null,
        "reason": "no_conversation"
      }
    ]
  }
}
Errors
invalid_request
POST/v1/whatsapp/marketing-messagesmessages:sendThe API key needs the messages:send permission. Without it the call is refused with 403 insufficient_scope.Sign in to try

Send a WhatsApp marketing template through Meta's Marketing Messages API

For promotions, offers and announcements: an approved MARKETING template (see GET /whatsapp/templates — category), to a phone number with to or in an existing WhatsApp conversation with conversation_id. Meta delivers it through its Marketing Messages API, with delivery optimised for marketing — the same route IotaBot campaigns use. A number not yet onboarded to it (marketing_messages on GET /channels) still sends: Meta falls back to the Cloud API. Utility and authentication templates are refused with template_not_marketing; send those with POST /messages. Everything else is as POST /messages: checked now, 202 with status: "queued", the outcome as a message.status webhook and on GET /messages/{id}, and an Idempotency-Key header makes retries safe.

Request
curl -X POST "https://api.iotabot.com/v1/whatsapp/marketing-messages" \
  -H "Authorization: Bearer $IOTABOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "+919812345678",
  "whatsapp_account_id": "6a4c01aa…",
  "template": {
    "name": "festive_offer",
    "language": "en_US",
    "parameters": [
      "Priya"
    ]
  }
}'
Response · 202
{
  "data": {
    "id": "6a4ba0f1c3f1a2b3c4d5e735",
    "object": "message",
    "status": "queued",
    "channel": "whatsapp",
    "via": "marketing_messages",
    "conversation_id": "6a4b9e21c3f1a2b3c4d5e6f7",
    "created_at": "2026-10-01T09:16:10.004Z"
  }
}
Errors
template_not_marketingtemplate_not_approvedrecipient_opted_outinvalid_phonewhatsapp_account_requiredno_whatsapp_accountintegration_pausedapi_quota_exceededidempotency_key_reusedidempotency_in_progress
GET/v1/whatsapp/templatesmessages:sendThe API key needs the messages:send permission. Without it the call is refused with 403 insufficient_scope.Sign in to try

Approved WhatsApp templates you can send

Request
curl -X GET "https://api.iotabot.com/v1/whatsapp/templates" \
  -H "Authorization: Bearer $IOTABOT_API_KEY"
Response · 200
{
  "data": {
    "items": [
      {
        "name": "order_update",
        "language": "en_US",
        "category": "UTILITY",
        "whatsapp_account_id": "6a4c01aa…",
        "inputs": {
          "body": [
            "order number",
            "delivery date"
          ]
        }
      },
      {
        "name": "festive_offer",
        "language": "en_US",
        "category": "MARKETING",
        "whatsapp_account_id": "6a4c01aa…",
        "inputs": {
          "body": [
            "customer name"
          ]
        }
      }
    ]
  }
}
GET/v1/email/sendersmessages:sendThe API key needs the messages:send permission. Without it the call is refused with 403 insufficient_scope.Sign in to try

Email addresses you can send from

Request
curl -X GET "https://api.iotabot.com/v1/email/senders" \
  -H "Authorization: Bearer $IOTABOT_API_KEY"
Response · 200
{
  "data": {
    "items": [
      {
        "address": "support@acme.com",
        "website_id": "6a4b3fbbe291bce5bf701c5c",
        "domain": "acme.com",
        "can_send": true
      }
    ]
  }
}
POST/v1/filesmessages:sendThe API key needs the messages:send permission. Without it the call is refused with 403 insufficient_scope.Sign in to try

Upload an attachment (base64, up to 10 MB) and use its id in POST /messages

Request
curl -X POST "https://api.iotabot.com/v1/files" \
  -H "Authorization: Bearer $IOTABOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "filename": "invoice.pdf",
  "content_type": "application/pdf",
  "content_base64": "JVBERi0xLjQK…"
}'
Response · 201
{
  "data": {
    "id": "6a4ba1c0c3f1a2b3c4d5e741",
    "object": "file",
    "filename": "invoice.pdf",
    "content_type": "application/pdf",
    "size": 48213,
    "created_at": "2026-10-01T09:17:00.000Z"
  }
}
Errors
file_rejected