# Chatmaid Developers — Full Documentation
> WhatsApp messaging API for developers and AI agents. This is the complete concatenated documentation.
---
# Account
Source: https://www.chatmaid.net/docs/account
Description: Read account profile and usage data via API key.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
## Overview
Account metadata and usage are available via standard API-key endpoints:
- `GET /v1/account`
- `GET /v1/account/usage`
Integration host: `https://developers-api.chatmaid.net`.
## Get Account
### GET /v1/account
Get account profile for the API key owner.
### Get Account Auth and Status Codes
| Item | Value |
| --- | --- |
| Recommended scope | `account:read` |
| Also accepted scopes | `messages:read, messages:send, messages:write` |
| Success code | `200` |
| Error codes | `401, 403, 429` |
```bash
curl -X GET https://developers-api.chatmaid.net/v1/account \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const response = await fetch("https://developers-api.chatmaid.net/v1/account", {
headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY },
});
const payload = await response.json();
```
```python
import os
import requests
response = requests.get(
"https://developers-api.chatmaid.net/v1/account",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"accountId": "acc_abc123def456",
"name": "Acme Inc",
"email": "developer@acme.com",
"subscriptionStatus": "active",
"stats": {
"apiKeys": 2,
"phoneNumbers": 1,
"messages": 328
}
}
}
```
## Usage
### GET /v1/account/usage
Get account usage metrics for day, week, or month.
### Usage Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `period` | Query | `string` | No | day \| week \| month, default month |
| Required scope | - | `account:read` | No | messages:* scopes are also accepted. |
| Status codes | - | `200 \| 401 \| 403 \| 429` | Yes | Typical usage endpoint responses. |
```bash
curl -X GET "https://developers-api.chatmaid.net/v1/account/usage?period=month" \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const response = await fetch("https://developers-api.chatmaid.net/v1/account/usage?period=month", {
headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY },
});
const payload = await response.json();
```
```python
import os
import requests
response = requests.get(
"https://developers-api.chatmaid.net/v1/account/usage",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
params={"period": "month"},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"period": "month",
"startDate": "2026-01-07T14:20:00.000Z",
"endDate": "2026-02-06T14:20:00.000Z",
"messages": {
"total": 328,
"pending": 0,
"sent": 314,
"failed": 14
},
"apiRequests": {
"total": 1242,
"successful": 1221,
"failed": 21
}
}
}
```
---
# API Reference
Source: https://www.chatmaid.net/docs/api-reference
Description: Endpoint-level parameters and response fields for integration APIs.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Base URL:
- `https://developers-api.chatmaid.net`
Integration host: `https://developers-api.chatmaid.net`.
## POST /v1/messages/send
### POST /v1/messages/send
Create a new outbound message.
### Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `fromPhoneId` | Body | `string` | Yes | Sender phone, given as its E.164 number (recommended) or its dashboard ID. |
| `to` | Body | `string` | Yes | E.164 phone number (example: +15551234567), or a group JID (example: 120363043211234567@g.us) from GET /v1/groups. |
| `content` | Body | `string` | No | Max length 4096. Required if mediaUrls is empty. |
| `mediaUrls` | Body | `string[]` | No | Public HTTPS URLs only. Required if content is empty. |
| `idempotencyKey` | Body | `string` | No | Max length 64. Reuse returns the original message. |
### Authorization and Scopes
| Requirement | Value |
| --- | --- |
| Auth header | `Authorization: Bearer sk_test_* \| sk_live_*` |
| Any required scope | `messages:send` |
| Alternative accepted scope | `messages:write` |
### Status Codes
| Code | Meaning |
| --- | --- |
| `201` | Message accepted. |
| `400` | Validation error or phone not connected for live key. |
| `401` | Missing/invalid/expired API key. |
| `403` | API key does not include required scope. |
| `404` | fromPhoneId not found in account. |
| `429` | Rate limit exceeded for this API key. |
### Response Fields
| Field | Type | Notes |
| --- | --- | --- |
| `data.id` | `string` | Message ID (msg_*). |
| `data.isGroup` | `boolean` | True when the recipient is a WhatsApp group. |
| `data.groupId` | `string \| null` | Full group JID when isGroup is true. |
| `data.status` | `pending\|sent\|delivered\|read\|failed` | Status after a successful send is sent; transitions to delivered/read as WhatsApp receipts arrive. Failed sends are marked failed. |
| `data.sentAt` | `string \| null` | Set when message is marked sent. |
| `data.deliveredAt` | `string \| null` | Set when WhatsApp confirms delivery to the recipient. |
| `data.readAt` | `string \| null` | Set when the recipient reads the message (requires read receipts). |
| `data.errorCode` | `string \| null` | Stable error code, populated when status is failed. See [Error codes](#error-codes). |
| `data.errorMessage` | `string \| null` | Human-readable explanation, populated when status is failed. |
### Error codes
When a message ends in `failed`, `errorCode` is one of the following stable values. `errorMessage` provides a human-readable explanation. These are the only values that will ever appear — internal service details are never surfaced.
| `errorCode` | Meaning |
| --- | --- |
| `NOT_CONNECTED` | The sending phone number is not connected to WhatsApp. |
| `INVALID_RECIPIENT` | The recipient is not a valid WhatsApp user or group. |
| `RECIPIENT_NOT_REACHABLE` | The message could not be delivered. The recipient may need to message you first, or may not be reachable on WhatsApp. |
| `NOT_GROUP_MEMBER` | The sender is not a member of the group or is not allowed to post in it. |
| `RATE_LIMITED` | Sending is temporarily rate-limited by WhatsApp. Retry later. |
| `NOT_AUTHORIZED` | The WhatsApp session is no longer authorized. Reconnect the phone number. |
| `MEDIA_ERROR` | The media could not be fetched or is not a supported type. |
| `TIMEOUT` | WhatsApp did not respond in time; delivery is indeterminate. |
| `SERVICE_UNAVAILABLE` | The WhatsApp service is temporarily unavailable. Retry shortly. |
| `INTERNAL_ERROR` | An unexpected error occurred. |
```bash
curl -X POST https://developers-api.chatmaid.net/v1/messages/send \
-H "Authorization: Bearer sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{"fromPhoneId":"+15551234567","to":"+15557654321","content":"Hello"}'
```
```ts
const response = await fetch("https://developers-api.chatmaid.net/v1/messages/send", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.CHATMAID_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
fromPhoneId: "+15551234567",
to: "+15557654321",
content: "Hello",
}),
});
const payload = await response.json();
```
```python
import os
import requests
response = requests.post(
"https://developers-api.chatmaid.net/v1/messages/send",
headers={
"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}",
"Content-Type": "application/json",
},
json={
"fromPhoneId": "+15551234567",
"to": "+15557654321",
"content": "Hello",
},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"id": "msg_abc123def456",
"from": "+15551234567",
"to": "+15557654321",
"isGroup": false,
"groupId": null,
"content": "Hello",
"mediaUrls": [],
"status": "sent",
"createdAt": "2026-02-06T14:18:33.000Z",
"sentAt": "2026-02-06T14:18:33.120Z",
"failedAt": null,
"errorCode": null,
"errorMessage": null
}
}
```
For live keys, the response is returned with `status: "pending"` and is updated to `sent` or `failed` once the WhatsApp microservice confirms delivery, then to `delivered` / `read` as recipient receipts arrive (track via webhooks or by polling `GET /v1/messages/:messageId`).
## GET /v1/messages
### GET /v1/messages
Paginated list of messages for the authenticated account.
### Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `phoneNumberId` | Query | `string` | No | Filter by sender phone ID. |
| `status` | Query | `string` | No | One of pending, sent, delivered, read, failed. |
| `page` | Query | `number` | No | Integer >= 1. Default 1. |
| `limit` | Query | `number` | No | Integer 1..100. Default 20. |
Results are scoped to the API key's environment (`sk_live_` or `sk_test_`) automatically.
### Authorization and Scopes
| Requirement | Value |
| --- | --- |
| Auth header | `Authorization: Bearer sk_test_* \| sk_live_*` |
| Required scope | `messages:read` |
### Status Codes
| Code | Meaning |
| --- | --- |
| `200` | Page returned. |
| `401` | Missing/invalid/expired API key. |
| `403` | API key does not include required scope. |
| `429` | Rate limit exceeded for this API key. |
### Response Fields
| Field | Type | Notes |
| --- | --- | --- |
| `data.data[]` | `Message[]` | Array sorted by createdAt desc. |
| `data.pagination.page` | `number` | Current page index (1-based). |
| `data.pagination.limit` | `number` | Requested page size. |
| `data.pagination.total` | `number` | Total matching messages. |
| `data.pagination.totalPages` | `number` | Computed page count. |
```bash
curl -X GET "https://developers-api.chatmaid.net/v1/messages?status=sent&page=1&limit=20" \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const query = new URLSearchParams({
status: "sent",
page: "1",
limit: "20",
});
const response = await fetch(
"https://developers-api.chatmaid.net/v1/messages?" + query.toString(),
{ headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY } }
);
const payload = await response.json();
```
```python
import os
import requests
response = requests.get(
"https://developers-api.chatmaid.net/v1/messages",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
params={"status": "sent", "page": 1, "limit": 20},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"data": [
{
"id": "msg_abc123def456",
"from": "+15551234567",
"to": "+15557654321",
"content": "Hello",
"status": "sent",
"createdAt": "2026-02-06T14:18:33.000Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1
}
}
}
```
## GET /v1/messages/:messageId
### GET /v1/messages/:messageId
Get one message and its lifecycle timestamps.
### Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `messageId` | Path | `string` | Yes | Message ID returned by send endpoint (msg_*). |
### Authorization and Scopes
| Requirement | Value |
| --- | --- |
| Auth header | `Authorization: Bearer sk_test_* \| sk_live_*` |
| Required scope | `messages:read` |
### Status Codes
| Code | Meaning |
| --- | --- |
| `200` | Message returned. |
| `401` | Missing/invalid/expired API key. |
| `403` | API key does not include required scope. |
| `404` | Message ID not found in this account. |
| `429` | Rate limit exceeded for this API key. |
```bash
curl -X GET https://developers-api.chatmaid.net/v1/messages/msg_abc123def456 \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const response = await fetch(
"https://developers-api.chatmaid.net/v1/messages/msg_abc123def456",
{ headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY } }
);
const payload = await response.json();
```
```python
import os
import requests
response = requests.get(
"https://developers-api.chatmaid.net/v1/messages/msg_abc123def456",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"id": "msg_abc123def456",
"from": "+15551234567",
"to": "+15557654321",
"isGroup": false,
"groupId": null,
"content": "Hello",
"mediaUrls": [],
"status": "delivered",
"errorCode": null,
"errorMessage": null,
"createdAt": "2026-02-06T14:18:33.000Z",
"sentAt": "2026-02-06T14:18:33.120Z",
"deliveredAt": "2026-02-06T14:18:35.000Z",
"readAt": null,
"failedAt": null
}
}
```
## GET /v1/messages/inbound
### GET /v1/messages/inbound
Paginated list of messages received by the account's connected phone numbers. Inbound messages exist for live connections only — sandbox (`sk_test_*`) keys always receive an empty list.
### Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `phoneNumberId` | Query | `string` | No | Filter by receiving phone ID. |
| `page` | Query | `number` | No | Integer >= 1. Default 1. |
| `limit` | Query | `number` | No | Integer 1..100. Default 20. |
### Authorization and Scopes
| Requirement | Value |
| --- | --- |
| Auth header | `Authorization: Bearer sk_live_*` |
| Required scope | `messages:read` |
### Status Codes
| Code | Meaning |
| --- | --- |
| `200` | Page returned. |
| `401` | Missing/invalid/expired API key. |
| `403` | API key does not include required scope. |
| `429` | Rate limit exceeded for this API key. |
```bash
curl -X GET "https://developers-api.chatmaid.net/v1/messages/inbound?page=1&limit=20" \
-H "Authorization: Bearer sk_live_xxx"
```
```json
{
"success": true,
"data": {
"data": [
{
"id": "inmsg_abc123def456",
"from": "+15557654321",
"to": "+15551234567",
"content": "Thanks, got it!",
"type": "text",
"isGroup": false,
"groupId": null,
"receivedAt": "2026-02-06T14:21:08.000Z",
"createdAt": "2026-02-06T14:21:09.000Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1
}
}
}
```
## GET /v1/messages/inbound/:messageId
### GET /v1/messages/inbound/:messageId
Get one inbound message by its identifier.
### Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `messageId` | Path | `string` | Yes | Inbound message ID (inmsg_*). |
### Status Codes
| Code | Meaning |
| --- | --- |
| `200` | Inbound message returned. |
| `401` | Missing/invalid/expired API key. |
| `403` | API key does not include required scope. |
| `404` | Inbound message ID not found in this account. |
| `429` | Rate limit exceeded for this API key. |
```bash
curl -X GET https://developers-api.chatmaid.net/v1/messages/inbound/inmsg_abc123def456 \
-H "Authorization: Bearer sk_live_xxx"
```
## GET /v1/groups
### GET /v1/groups
List the WhatsApp groups a sender phone can post to. Each `id` is a full group JID, valid as `to` in POST /v1/messages/send. See [Groups](/docs/groups) for details.
### Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `fromPhoneId` | Query | `string` | Yes | Sender phone, given as its E.164 number (recommended) or its dashboard ID. |
### Authorization and Scopes
| Requirement | Value |
| --- | --- |
| Auth header | `Authorization: Bearer sk_test_* \| sk_live_*` |
| Any required scope | `groups:read` |
| Alternative accepted scopes | `messages:read`, `messages:send`, `messages:write` |
### Status Codes
| Code | Meaning |
| --- | --- |
| `200` | Group list returned. |
| `400` | Phone not connected for live key. |
| `401` | Missing/invalid/expired API key. |
| `403` | API key does not include required scope. |
| `404` | fromPhoneId not found in account. |
| `429` | Rate limit exceeded for this API key. |
| `502` | WhatsApp group fetch failed — retry after a moment. |
```bash
curl -X GET "https://developers-api.chatmaid.net/v1/groups?fromPhoneId=%2B15551234567" \
-H "Authorization: Bearer sk_live_xxx"
```
```json
{
"success": true,
"data": [
{
"id": "120363043211234567@g.us",
"name": "Team Updates",
"isCommunityAnnounce": false
}
]
}
```
## GET /v1/phone-numbers
### GET /v1/phone-numbers
List phone numbers available to this account.
### Authorization and Scopes
| Requirement | Value |
| --- | --- |
| Auth header | `Authorization: Bearer sk_test_* \| sk_live_*` |
| Recommended scope | `phone_numbers:read` |
| Backward-compatible scope accepted | `phone-numbers:read` |
### Status Codes
| Code | Meaning |
| --- | --- |
| `200` | Phone list returned. |
| `401` | Missing/invalid/expired API key. |
| `403` | API key does not include required scope. |
| `429` | Rate limit exceeded for this API key. |
```bash
curl -X GET https://developers-api.chatmaid.net/v1/phone-numbers \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const response = await fetch("https://developers-api.chatmaid.net/v1/phone-numbers", {
headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY },
});
const payload = await response.json();
```
```python
import os
import requests
response = requests.get(
"https://developers-api.chatmaid.net/v1/phone-numbers",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
)
payload = response.json()
```
```json
{
"success": true,
"data": [
{
"id": "507f1f77bcf86cd799439011",
"phoneNumber": "+15551234567",
"displayName": "Primary Sender",
"connectionStatus": "connected",
"lastConnectedAt": "2026-02-06T14:10:00.000Z",
"lastDisconnectedAt": null,
"createdAt": "2026-02-01T10:20:00.000Z",
"updatedAt": "2026-02-06T14:10:00.000Z"
}
]
}
```
## GET /v1/phone-numbers/:id
### GET /v1/phone-numbers/:id
Get one phone number by internal ID or URL-encoded E.164 value.
### Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `id` | Path | `string` | Yes | Accepts phone ID or URL-encoded E.164 value (example: %2B15551234567). |
### Status Codes
| Code | Meaning |
| --- | --- |
| `200` | Phone returned. |
| `401` | Missing/invalid/expired API key. |
| `403` | API key does not include required scope. |
| `404` | Phone reference not found in this account. |
| `429` | Rate limit exceeded for this API key. |
```bash
curl -X GET "https://developers-api.chatmaid.net/v1/phone-numbers/%2B15551234567" \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const phoneRef = encodeURIComponent("+15551234567");
const response = await fetch(`https://developers-api.chatmaid.net/v1/phone-numbers/${phoneRef}`, {
headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY },
});
const payload = await response.json();
```
```python
import os
import requests
from urllib.parse import quote
phone_ref = quote("+15551234567", safe="")
response = requests.get(
f"https://developers-api.chatmaid.net/v1/phone-numbers/{phone_ref}",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"id": "507f1f77bcf86cd799439011",
"phoneNumber": "+15551234567",
"displayName": "Primary Sender",
"connectionStatus": "connected",
"lastConnectedAt": "2026-02-06T14:10:00.000Z",
"lastDisconnectedAt": null,
"createdAt": "2026-02-01T10:20:00.000Z",
"updatedAt": "2026-02-06T14:10:00.000Z"
}
}
```
## GET /v1/phone-numbers/:id/status
### GET /v1/phone-numbers/:id/status
Get live connection state by internal ID or URL-encoded E.164 value.
### Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `id` | Path | `string` | Yes | Accepts phone ID or URL-encoded E.164 value (example: %2B15551234567). |
### Status Codes
| Code | Meaning |
| --- | --- |
| `200` | Status returned. |
| `401` | Missing/invalid/expired API key. |
| `403` | API key does not include required scope. |
| `404` | Phone reference not found in this account. |
| `429` | Rate limit exceeded for this API key. |
```bash
curl -X GET "https://developers-api.chatmaid.net/v1/phone-numbers/%2B15551234567/status" \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const phoneRef = encodeURIComponent("+15551234567");
const response = await fetch(`https://developers-api.chatmaid.net/v1/phone-numbers/${phoneRef}/status`, {
headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY },
});
const payload = await response.json();
```
```python
import os
import requests
from urllib.parse import quote
phone_ref = quote("+15551234567", safe="")
response = requests.get(
f"https://developers-api.chatmaid.net/v1/phone-numbers/{phone_ref}/status",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"id": "507f1f77bcf86cd799439011",
"phoneNumber": "+15551234567",
"connectionStatus": "connected",
"lastConnectedAt": "2026-02-06T14:10:00.000Z",
"lastDisconnectedAt": null,
"updatedAt": "2026-02-06T14:10:00.000Z"
}
}
```
## GET /v1/account
### GET /v1/account
Read account profile and aggregate counters.
### Authorization and Scopes
| Requirement | Value |
| --- | --- |
| Auth header | `Authorization: Bearer sk_test_* \| sk_live_*` |
| Recommended scope | `account:read` |
| Other accepted scopes | `messages:read, messages:send, messages:write` |
### Status Codes
| Code | Meaning |
| --- | --- |
| `200` | Account data returned. |
| `401` | Missing/invalid/expired API key. |
| `403` | API key does not include required scope. |
| `429` | Rate limit exceeded for this API key. |
```bash
curl -X GET https://developers-api.chatmaid.net/v1/account \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const response = await fetch("https://developers-api.chatmaid.net/v1/account", {
headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY },
});
const payload = await response.json();
```
```python
import os
import requests
response = requests.get(
"https://developers-api.chatmaid.net/v1/account",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"accountId": "acc_abc123def456",
"name": "Acme Inc",
"email": "developer@acme.com",
"subscriptionStatus": "active",
"stats": {
"apiKeys": 2,
"phoneNumbers": 1,
"messages": 328
}
}
}
```
## GET /v1/account/usage
### GET /v1/account/usage
Read account usage metrics for day, week, or month.
### Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `period` | Query | `string` | No | One of day, week, month. Default month. |
### Status Codes
| Code | Meaning |
| --- | --- |
| `200` | Usage returned. |
| `401` | Missing/invalid/expired API key. |
| `403` | API key does not include required scope. |
| `429` | Rate limit exceeded for this API key. |
```bash
curl -X GET "https://developers-api.chatmaid.net/v1/account/usage?period=month" \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const response = await fetch("https://developers-api.chatmaid.net/v1/account/usage?period=month", {
headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY },
});
const payload = await response.json();
```
```python
import os
import requests
response = requests.get(
"https://developers-api.chatmaid.net/v1/account/usage",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
params={"period": "month"},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"period": "month",
"startDate": "2026-01-07T14:20:00.000Z",
"endDate": "2026-02-06T14:20:00.000Z",
"messages": {
"total": 328,
"pending": 0,
"sent": 314,
"failed": 14
},
"apiRequests": {
"total": 1242,
"successful": 1221,
"failed": 21
}
}
}
```
## Response Schema
```json
{
"success": true,
"data": {}
}
```
```json
{
"success": false,
"error": "Unauthorized",
"message": ["Invalid API key"],
"statusCode": 401,
"timestamp": "2026-02-06T14:22:30.000Z",
"path": "/v1/messages"
}
```
```json
{
"success": false,
"error": "Rate limit exceeded",
"message": ["Rate limit exceeded"],
"statusCode": 429,
"timestamp": "2026-02-06T14:22:30.000Z",
"path": "/v1/messages/send",
"retryAfter": 60
}
```
---
# Authentication
Source: https://www.chatmaid.net/docs/authentication
Description: API key authentication and environment behavior.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
## API Key Model
Integration endpoints use API key authentication with Bearer tokens.
- **Test keys** start with `sk_test_` and simulate delivery.
- **Live keys** start with `sk_live_` and send real WhatsApp traffic. Requires an active subscription.
### Key Prefix Behavior
| Prefix | Behavior | Host |
| --- | --- | --- |
| `sk_test_*` | Sandbox behavior (simulated delivery lifecycle) | `https://developers-api.chatmaid.net` |
| `sk_live_*` | Production behavior (real WhatsApp delivery) | `https://developers-api.chatmaid.net` |
## Key Format and Use
### POST /v1/messages/send
Representative protected endpoint using API key Bearer auth.
```bash
curl -X GET https://developers-api.chatmaid.net/v1/messages \
-H "Authorization: Bearer sk_test_xxxxxxxxxxxxxxxxxxxxxxxx"
```
```ts
const response = await fetch("https://developers-api.chatmaid.net/v1/messages", {
headers: {
Authorization: "Bearer sk_test_xxxxxxxxxxxxxxxxxxxxxxxx",
},
});
```
```python
import requests
response = requests.get(
"https://developers-api.chatmaid.net/v1/messages",
headers={"Authorization": "Bearer sk_test_xxxxxxxxxxxxxxxxxxxxxxxx"},
)
```
```json
{
"success": false,
"error": "Unauthorized",
"message": ["Invalid API key"],
"statusCode": 401,
"timestamp": "2026-02-06T14:22:30.000Z",
"path": "/v1/messages"
}
```
## Scopes and Limits
Common scopes:
- `messages:send` (or `messages:write`)
- `messages:read`
- `groups:read` (optional, group-listing endpoint; any messages scope also grants it)
- `phone_numbers:read` (optional, phone-number endpoints)
- `account:read` (optional, account endpoints)
Rate limit is enforced per API key. Default is 100 requests per minute unless account-specific limits are configured.
Environment mapping:
- Host: `https://developers-api.chatmaid.net` for both environments
- `sk_test_*` -> sandbox behavior
- `sk_live_*` -> production behavior
Environment is inferred from key prefix; do not pass environment as request input.
Integration host: `https://developers-api.chatmaid.net`.
### Endpoint to Scope Matrix
| Endpoint | Required Scope | Also Accepted |
| --- | --- | --- |
| `POST /v1/messages/send` | `messages:send` | `messages:write` |
| `GET /v1/messages` | `messages:read` | - |
| `GET /v1/messages/:messageId` | `messages:read` | - |
| `GET /v1/phone-numbers*` | `phone_numbers:read` | `phone-numbers:read, messages:*` |
| `GET /v1/account*` | `account:read` | `messages:read, messages:send, messages:write` |
---
# Cookbook
Source: https://www.chatmaid.net/docs/cookbook
Description: End-to-end implementation workflow from setup to production.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Callout } from 'fumadocs-ui/components/callout';
## Architecture
This cookbook covers the full runtime loop for an integration service using API-key auth only:
1. Load API key and set a single base URL.
2. Fetch connected sender phone numbers.
3. Send outbound message requests.
4. Track status via polling and optional webhooks.
5. Apply retry/idempotency strategy and promote to production.
Integration host: `https://developers-api.chatmaid.net`.
## Bootstrap Client
Standardize your HTTP client once and reuse it for all endpoints.
```bash
export CHATMAID_BASE_URL="https://developers-api.chatmaid.net"
export CHATMAID_API_KEY="sk_test_xxx"
curl -X GET "$CHATMAID_BASE_URL/v1/account" \
-H "Authorization: Bearer $CHATMAID_API_KEY"
```
```ts
const chatmaid = {
baseUrl: process.env.CHATMAID_BASE_URL || "https://developers-api.chatmaid.net",
apiKey: process.env.CHATMAID_API_KEY!,
async request(path: string, init: RequestInit = {}) {
const response = await fetch(this.baseUrl + path, {
...init,
headers: {
Authorization: "Bearer " + this.apiKey,
"Content-Type": "application/json",
...(init.headers || {}),
},
});
return response.json();
},
};
```
```python
import os
import requests
class ChatmaidClient:
def __init__(self):
self.base_url = os.getenv("CHATMAID_BASE_URL", "https://developers-api.chatmaid.net")
self.api_key = os.environ["CHATMAID_API_KEY"]
def request(self, method: str, path: str, **kwargs):
headers = kwargs.pop("headers", {})
headers.update({
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
})
response = requests.request(method, self.base_url + path, headers=headers, **kwargs)
return response.json()
```
## Send and Track
Use this operational sequence inside your application flow.
### 1) Resolve Sender Phone ID
### GET /v1/phone-numbers
Read available phone numbers and select a connected sender.
```bash
curl -X GET https://developers-api.chatmaid.net/v1/phone-numbers \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const phones = await chatmaid.request("/v1/phone-numbers");
const connected = phones.data.find((phone: any) => phone.connectionStatus === "connected");
```
```python
phones = client.request("GET", "/v1/phone-numbers")
connected = next((p for p in phones["data"] if p["connectionStatus"] == "connected"), None)
```
### 2) Send Message
### POST /v1/messages/send
Create outbound message. Use optional idempotency key only for retry-heavy workflows.
```bash
curl -X POST https://developers-api.chatmaid.net/v1/messages/send \
-H "Authorization: Bearer sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{
"fromPhoneId": "+15551234567",
"to": "+15557654321",
"content": "Order 7784 confirmed"
}'
```
```ts
const sent = await chatmaid.request("/v1/messages/send", {
method: "POST",
body: JSON.stringify({
fromPhoneId: "+15551234567",
to: "+15557654321",
content: "Order 7784 confirmed",
}),
});
const messageId = sent.data.id;
```
```python
sent = client.request(
"POST",
"/v1/messages/send",
json={
"fromPhoneId": "+15551234567",
"to": "+15557654321",
"content": "Order 7784 confirmed",
},
)
message_id = sent["data"]["id"]
```
```json
{
"success": true,
"data": {
"id": "msg_abc123def456",
"status": "sent",
"createdAt": "2026-02-06T14:18:33.000Z"
}
}
```
### 3) Poll Status Until Terminal
### GET /v1/messages/:messageId
Poll until status reaches sent or failed.
```bash
curl -X GET https://developers-api.chatmaid.net/v1/messages/msg_abc123def456 \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
async function waitForTerminal(messageId: string) {
while (true) {
const current = await chatmaid.request("/v1/messages/" + messageId);
const status = current.data.status;
if (["sent", "failed"].includes(status)) return current;
await new Promise((resolve) => setTimeout(resolve, 1500));
}
}
```
```python
import time
def wait_for_terminal(message_id: str):
while True:
current = client.request("GET", f"/v1/messages/{message_id}")
status = current["data"]["status"]
if status in ["sent", "failed"]:
return current
time.sleep(1.5)
```
## Reliability Patterns
- Treat `messageId` as your canonical correlation key across systems.
- Use webhook events for push updates; keep polling as fallback.
- Use `idempotencyKey` only when your retry policy can repeat sends.
- Capture and alert on `failed` statuses with retry in your domain logic.
Retry only transport failures (timeout/network/5xx) with exponential backoff. Do not blindly retry business errors (4xx) without fixing request payload or permissions.
## Go Live Runbook
1. Keep the same API host and verify all examples/routes still match your code.
2. Swap credentials to `sk_live_*` key in production secret store.
3. Validate connected production phone status via `GET /v1/phone-numbers/:id/status`.
4. Send a low-risk real message and verify status progression.
5. Enable webhook verification and alerting for failed deliveries.
---
# Environments
Source: https://www.chatmaid.net/docs/environments
Description: Single-host model, key-based environment behavior, and go-live path.
import { Callout } from 'fumadocs-ui/components/callout';
## Base URL
- `https://developers-api.chatmaid.net`
All integration endpoints are under `/v1`, for example: `https://developers-api.chatmaid.net/v1/messages/send`.
Integration host: `https://developers-api.chatmaid.net`.
## Behavior Differences
- Use `sk_test_*` keys for sandbox behavior and `sk_live_*` keys for production behavior.
- Environment is inferred from API key prefix. Do not send environment in request body or query.
- Sandbox simulates message delivery status progression. Test keys work without a subscription.
- Production sends real WhatsApp messages via connected numbers. **Live keys require an active subscription.**
- Webhook payload structure is identical across both environments.
Never reuse production API keys in sandbox or test keys in production. Keep keys and webhook endpoints separated by environment.
## Go-Live Checklist
1. Validate full flow using `sk_test_*` keys.
2. Keep the same API host and switch credentials to `sk_live_*` keys.
3. Point webhooks to production receiver endpoints and verify signatures.
4. Monitor initial traffic for rate limits and delivery failures.
---
# Errors
Source: https://www.chatmaid.net/docs/errors
Description: Error response format and common troubleshooting paths.
import { Callout } from 'fumadocs-ui/components/callout';
## Error Envelope
Most API errors follow this envelope.
```json
{
"success": false,
"error": "Unauthorized",
"message": ["Invalid API key"],
"statusCode": 401,
"timestamp": "2026-02-06T14:22:30.000Z",
"path": "/v1/messages/send"
}
```
Validation errors return the same shape, with multiple entries in `message`.
```json
{
"success": false,
"error": "Bad Request",
"message": [
"to must be an E.164 phone number (e.g., +1987654321) or a group JID (e.g., 120363043211234567@g.us)",
"to must be a string"
],
"statusCode": 400,
"timestamp": "2026-02-06T14:22:30.000Z",
"path": "/v1/messages/send"
}
```
## Common Errors
### Common Error Matrix
| Code | Cause | Typical Fix |
| --- | --- | --- |
| `400` | Invalid payload or query values | Fix request format, required fields, or value constraints. |
| `401` | Missing, invalid, or expired API key | Send a valid Bearer API key from dashboard. |
| `403` | Missing scope or inactive subscription | Use a key with correct scope and ensure account is active. |
| `404` | Unknown message/phone resource | Verify identifiers belong to your account. |
| `429` | Per-key rate limit exceeded | Back off and retry after retryAfter seconds. |
| `500` | Unexpected internal error | Retry safely and contact support with messageId/request context. |
```json
{
"success": false,
"error": "Rate limit exceeded",
"message": ["Rate limit exceeded"],
"statusCode": 429,
"timestamp": "2026-02-06T14:18:33.000Z",
"path": "/v1/messages/send",
"retryAfter": 60
}
```
## Debug Workflow
1. Verify `Authorization: Bearer sk_live_*` or `sk_test_*` header format.
2. Check key scopes include required permissions (for phone endpoints, include `phone_numbers:read`).
3. Confirm sender phone is connected in dashboard.
4. If using phone number in path, URL-encode it (for example `%2B15551234567`).
5. Use idempotency keys and inspect message status endpoint.
Log `messageId` values from send responses and use them as the canonical correlation key across polling, webhook processing, and support tickets.
---
# Getting Started
Source: https://www.chatmaid.net/docs/getting-started
Description: End-to-end quickstart from account creation to first message.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Callout } from 'fumadocs-ui/components/callout';
## Prerequisites
- Account setup completed in Chatmaid dashboard
- At least one connected sender phone in dashboard
- API key generated in dashboard with message scopes
## First Message Flow
1. Store your API key in your server secrets.
2. Fetch available sender phone IDs from standard API endpoints.
3. Send a message with API key auth.
Current base URL: `https://developers-api.chatmaid.net`.
```bash
curl -X GET https://developers-api.chatmaid.net/v1/phone-numbers \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const response = await fetch("https://developers-api.chatmaid.net/v1/phone-numbers", {
headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY },
});
const phones = await response.json();
```
```python
import os
import requests
response = requests.get(
"https://developers-api.chatmaid.net/v1/phone-numbers",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
)
phones = response.json()
```
4. Send your first message.
```bash
curl -X POST https://developers-api.chatmaid.net/v1/messages/send \
-H "Authorization: Bearer sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{
"fromPhoneId": "+15551234567",
"to": "+15557654321",
"content": "Hello from Chatmaid"
}'
```
```ts
const response = await fetch("https://developers-api.chatmaid.net/v1/messages/send", {
method: "POST",
headers: {
"Authorization": "Bearer sk_test_xxx",
"Content-Type": "application/json"
},
body: JSON.stringify({
fromPhoneId: "+15551234567",
to: "+15557654321",
content: "Hello from Chatmaid"
})
});
const payload = await response.json();
```
```python
import requests
response = requests.post(
"https://developers-api.chatmaid.net/v1/messages/send",
headers={
"Authorization": "Bearer sk_test_xxx",
"Content-Type": "application/json",
},
json={
"fromPhoneId": "+15551234567",
"to": "+15557654321",
"content": "Hello from Chatmaid",
},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"id": "msg_abc123def456",
"from": "+15551234567",
"to": "+15557654321",
"isGroup": false,
"groupId": null,
"content": "Hello from Chatmaid",
"mediaUrls": [],
"status": "sent",
"createdAt": "2026-02-06T14:18:33.000Z",
"sentAt": "2026-02-06T14:18:33.120Z",
"failedAt": null,
"errorCode": null,
"errorMessage": null
}
}
```
5. Poll status until terminal state.
```bash
curl -X GET https://developers-api.chatmaid.net/v1/messages/msg_abc123def456 \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const response = await fetch(
"https://developers-api.chatmaid.net/v1/messages/msg_abc123def456",
{ headers: { "Authorization": "Bearer sk_test_xxx" } }
);
const payload = await response.json();
```
```python
import requests
response = requests.get(
"https://developers-api.chatmaid.net/v1/messages/msg_abc123def456",
headers={"Authorization": "Bearer sk_test_xxx"},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"id": "msg_abc123def456",
"status": "sent",
"sentAt": "2026-02-06T14:18:34.000Z",
"failedAt": null,
"errorCode": null,
"errorMessage": null
}
}
```
If your app retries failed HTTP requests aggressively, include an `idempotencyKey` generated from your own operation ID to avoid duplicate sends.
## Production Checklist
- Keep the same base URL and switch to `sk_live_*` credentials in production.
- Use `sk_live_*` keys only in production systems.
- Configure webhook destination in dashboard and verify signatures in your receiver.
- Monitor failed delivery events and implement retries in your system.
---
# Groups
Source: https://www.chatmaid.net/docs/groups
Description: List the WhatsApp groups a connected phone can post to.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Callout } from 'fumadocs-ui/components/callout';
## List Groups
Current base URL: `https://developers-api.chatmaid.net`.
### GET /v1/groups
List the WhatsApp groups the given sender phone can post to. Each group's `id` is a full group JID — pass it as `to` in [`POST /v1/messages/send`](/docs/messages#send-to-a-whatsapp-group) to message that group.
Groups where the phone cannot post (for example announcement-only groups where it is not an admin) are excluded from the result.
### Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `fromPhoneId` | Query | `string` | Yes | Sender phone, given as its E.164 number (recommended) or its dashboard ID. Must belong to your account. |
### Auth and Status Codes
| Item | Value |
| --- | --- |
| Required scope | `groups:read` |
| Alternative scopes | `messages:read`, `messages:send`, `messages:write` |
| Success code | `200` |
| Error codes | `400, 401, 403, 404, 429, 502` |
For `sk_test_*` keys, a static sandbox group list is returned so the list-then-send flow can be exercised end to end. For `sk_live_*` keys, the phone must be connected; groups are fetched from WhatsApp on demand, so expect the call to take a few seconds for phones in many groups. A `502` means the WhatsApp fetch failed — retry after a moment.
```bash
curl -X GET "https://developers-api.chatmaid.net/v1/groups?fromPhoneId=%2B15551234567" \
-H "Authorization: Bearer sk_live_xxx"
```
```ts
const params = new URLSearchParams({ fromPhoneId: "+15551234567" });
const response = await fetch(
"https://developers-api.chatmaid.net/v1/groups?" + params.toString(),
{ headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY } }
);
const payload = await response.json();
```
```python
import os
import requests
response = requests.get(
"https://developers-api.chatmaid.net/v1/groups",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
params={"fromPhoneId": "+15551234567"},
)
payload = response.json()
```
```json
{
"success": true,
"data": [
{
"id": "120363043211234567@g.us",
"name": "Team Updates",
"isCommunityAnnounce": false
},
{
"id": "120363098765432109@g.us",
"name": "Product Community (announcements)",
"isCommunityAnnounce": true
}
]
}
```
### Response Fields
| Field | Type | Notes |
| --- | --- | --- |
| `data[].id` | `string` | Full group JID. Valid as `to` in POST /v1/messages/send. |
| `data[].name` | `string` | Group subject as shown in WhatsApp. |
| `data[].isCommunityAnnounce` | `boolean` | True for a community's announcement channel — sends reach all community members. |
Inbound group messages already carry the group JID in their `groupId` field (see the `message.received` webhook and `GET /v1/messages/inbound`). To reply to a group, pass that `groupId` as `to` — no groups lookup needed.
---
# Overview
Source: https://www.chatmaid.net/docs
Description: Introduction to the Chatmaid Developers platform and API contract.
import { Callout } from 'fumadocs-ui/components/callout';
## Platform
Chatmaid Developers provides a WhatsApp messaging API plus a dashboard for key management, billing, phone connections, and delivery observability.
This documentation focuses on the integration contract used by API consumers. Public APIs are versioned and stable under `/v1`.
Base URL:
- `https://developers-api.chatmaid.net`
Integration host: `https://developers-api.chatmaid.net`.
Integrators should only call documented message endpoints. Dashboard setup actions (billing, phone setup, webhook configuration) happen in the UI. This docs site focuses on API-key authenticated endpoints for runtime integrations.
## API Contract
Integration endpoints for API consumers:
- `POST /v1/messages/send`
- `GET /v1/messages`
- `GET /v1/messages/:messageId`
- `GET /v1/phone-numbers`
- `GET /v1/phone-numbers/:id`
- `GET /v1/phone-numbers/:id/status`
- `GET /v1/account`
- `GET /v1/account/usage`
## Integration Flow
1. Create account and complete setup in dashboard.
2. Develop and validate first with `sk_test_*` keys.
3. Generate API key in dashboard.
4. Discover phone IDs via `GET /v1/phone-numbers`.
5. Send messages via `POST /v1/messages/send`.
6. Track status via polling or webhooks.
7. Promote by using `sk_live_*` keys in production systems.
---
# Build with AI agents
Source: https://www.chatmaid.net/docs/mcp
Description: Two ways to make Claude Code, Cursor, Windsurf, and Claude Desktop send WhatsApp messages: point them at our markdown docs, or install the Chatmaid MCP server.
import { Callout } from "fumadocs-ui/components/callout";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
There are two ways to give an AI agent the ability to send WhatsApp through Chatmaid. Pick whichever fits your tooling — they're complementary.
## 1. Point your agent at the docs (recommended start)
Every agent that can read a URL can use Chatmaid in seconds. We publish the entire developer site as plain markdown following the [llms.txt](https://llmstxt.org) standard, so you don't have to copy-paste examples.
```bash
curl https://www.chatmaid.net/llms-full.txt
```
The complete concatenated documentation in one file. Pipe it into your agent's context, attach it to a Claude Project, or stream it via stdin. This is what most agents need.
```bash
curl https://www.chatmaid.net/llms.txt
```
A lightweight index linking to every page's markdown source. Ideal for agents that fetch selectively rather than loading everything at once.
```bash
curl https://www.chatmaid.net/raw-docs/getting-started
```
Prefix any docs slug with `/raw-docs/` to get the raw markdown source for that page.
Chatmaid's REST API is small and orthogonal — eleven endpoints under `/v1`. Once an
agent has the docs, it can compose any flow you describe in natural language.
### Example prompt
> "Read https://www.chatmaid.net/llms-full.txt, then send a WhatsApp from my business number to +14155551234 saying the order shipped. Use my key in `CHATMAID_API_KEY`."
## 2. Install the Chatmaid MCP server
If you'd rather give the agent first-class tool calls (structured arguments, autocomplete in some clients, no parsing of curl output), install the MCP server. It's a thin wrapper around the same REST API.
MCP is the open standard Anthropic introduced for AI agents to call external
tools. It's supported by Claude Code, Claude Desktop, Cursor, Windsurf, Zed,
and many other clients.
### Claude Desktop
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
```json
{
"mcpServers": {
"chatmaid": {
"command": "npx",
"args": ["-y", "@chatmaid/mcp"],
"env": {
"CHATMAID_API_KEY": "sk_test_xxx_or_sk_live_xxx"
}
}
}
}
```
Restart Claude Desktop. You should see "chatmaid" in the connected-MCP list.
### Cursor
Edit `~/.cursor/mcp.json` (create the file if missing):
```json
{
"mcpServers": {
"chatmaid": {
"command": "npx",
"args": ["-y", "@chatmaid/mcp"],
"env": { "CHATMAID_API_KEY": "sk_test_xxx_or_sk_live_xxx" }
}
}
}
```
### Claude Code
From any terminal inside your project:
```bash
claude mcp add chatmaid \
--env CHATMAID_API_KEY=sk_test_xxx \
-- npx -y @chatmaid/mcp
```
### Windsurf, Zed, other clients
Any MCP-compatible client can use the same `npx -y @chatmaid/mcp` command. Set `CHATMAID_API_KEY` in the client's environment configuration.
### Available tools
Once connected, the agent can call these tools automatically:
| Tool | What it does |
| -------------------- | ----------------------------------------------------------------------- |
| `send_message` | Send a WhatsApp message — `fromPhoneId` (discovered via `list_phone_numbers`), `to` as an E.164 number or a group JID (from `list_groups`), plus `content` and/or `mediaUrls`, optional `idempotencyKey`. |
| `list_groups` | List the WhatsApp groups a connected phone can post to; returned `id` values are group JIDs usable as `to` in `send_message`. |
| `list_messages` | List recent messages with optional `status` / `phoneNumberId` filter and `page` / `limit` pagination. |
| `get_message` | Fetch a single message and its final delivery status. |
| `list_inbound_messages` | List messages received by connected phone numbers (live only) with optional `phoneNumberId` filter and `page` / `limit` pagination. |
| `get_inbound_message` | Fetch a single inbound (received) message by its `inmsg_*` ID. |
| `list_phone_numbers` | Show all phone numbers registered to the current account. |
| `get_phone_number` | Get details for one phone number (accepts internal ID or E.164). |
| `get_phone_status` | Check whether a phone number is currently connected to WhatsApp (accepts internal ID or E.164). |
| `get_account` | Account profile (accountId, name, email, subscription status). |
| `get_usage` | Usage stats for `period` = `day` \| `week` \| `month` (defaults to month). |
The MCP server is open-source at [github.com/chatmaid/mcp](https://github.com/chatmaid/mcp). PRs welcome.
## Get an API key
1. Sign in at [developers.chatmaid.net/dashboard](https://developers.chatmaid.net/dashboard).
2. Go to **API Keys** and generate a `sk_test_*` key.
3. Paste it into your agent's environment.
While an agent is learning, use a `sk_test_*` key. Messages sent with test
keys run through Chatmaid's sandbox — nothing reaches real WhatsApp numbers.
Promote to `sk_live_*` only after you've confirmed behavior.
## Why the API is agent-friendly
Chatmaid's HTTP surface is built for autonomous use, regardless of which approach above you pick:
- **Idempotency keys** — safe retries after a timeout or tool-call interruption.
- **Structured error envelopes** — errors return a consistent `{ success, error, message[], statusCode }` shape, so agents know whether to retry, swap inputs, or escalate.
- **Sandbox keys** — `sk_test_*` simulates the full delivery lifecycle without touching WhatsApp.
- **Predictable status lifecycle** — messages go `pending → sent → delivered → read` (or `failed`), so an agent can poll confidently.
See the [API Reference](/docs/api-reference) for the full contract and the [Cookbook](/docs/cookbook) for end-to-end agent integration patterns.
---
# Messages
Source: https://www.chatmaid.net/docs/messages
Description: Send, list, and inspect WhatsApp messages via API.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Callout } from 'fumadocs-ui/components/callout';
## Send Message
Current base URL: `https://developers-api.chatmaid.net`.
### POST /v1/messages/send
Send text or media WhatsApp message from a connected phone.
### Send Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `fromPhoneId` | Body | `string` | Yes | Sender phone, given as its E.164 number (recommended) or its dashboard ID. Must belong to your account. |
| `to` | Body | `string` | Yes | E.164 phone number (example: +15557654321), or a group JID (example: 120363043211234567@g.us) from [`GET /v1/groups`](/docs/groups) or the `groupId` of an inbound group message. |
| `content` | Body | `string` | No | Max length 4096. Required if mediaUrls is empty. |
| `mediaUrls` | Body | `string[]` | No | Public HTTPS URLs. Required if content is empty. |
| `idempotencyKey` | Body | `string` | No | Max length 64. Same key returns original message. |
### Send Auth and Status Codes
| Item | Value |
| --- | --- |
| Required scope | `messages:send` |
| Alternative scope | `messages:write` |
| Success code | `201` |
| Error codes | `400, 401, 403, 404, 429` |
For `sk_test_*` keys, sandbox simulates synchronous delivery — the response status is already `sent`. For `sk_live_*` keys, the response returns `pending` and transitions to `sent` (then `delivered` / `read` as WhatsApp receipts arrive) or `failed` — track via webhooks or by polling `GET /v1/messages/:messageId`.
```bash
curl -X POST https://developers-api.chatmaid.net/v1/messages/send \
-H "Authorization: Bearer sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{
"fromPhoneId": "+15551234567",
"to": "+15557654321",
"content": "Order received. We will update you soon."
}'
```
```ts
const response = await fetch("https://developers-api.chatmaid.net/v1/messages/send", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.CHATMAID_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
fromPhoneId: "+15551234567",
to: "+15557654321",
content: "Order received. We will update you soon.",
}),
});
const payload = await response.json();
```
```python
import os
import requests
response = requests.post(
"https://developers-api.chatmaid.net/v1/messages/send",
headers={
"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}",
"Content-Type": "application/json",
},
json={
"fromPhoneId": "+15551234567",
"to": "+15557654321",
"content": "Order received. We will update you soon.",
},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"id": "msg_abc123def456",
"from": "+15551234567",
"to": "+15557654321",
"isGroup": false,
"groupId": null,
"content": "Order received. We will update you soon.",
"status": "sent",
"createdAt": "2026-02-06T14:18:33.000Z"
}
}
```
## Send to a WhatsApp Group
Pass a group JID as `to` to message a group instead of an individual. Group JIDs come from [`GET /v1/groups`](/docs/groups) (all groups the sender phone can post to) or from the `groupId` field of an inbound group message — so replying to a group from a `message.received` webhook is a single send call.
```bash
curl -X POST https://developers-api.chatmaid.net/v1/messages/send \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"fromPhoneId": "+15551234567",
"to": "120363043211234567@g.us",
"content": "Team update: deploy completed."
}'
```
The response carries `isGroup: true` and `groupId` set to the group JID. Requirements and behavior:
- The sender phone must be a member of the group and allowed to post in it (announcement-only groups require the phone to be an admin).
- Delivery statuses work like individual sends: `sent` on WhatsApp acceptance, then `delivered` / `read` receipts as group members receive and read the message.
- Sends to a group the phone cannot post to are accepted by the API and complete as `failed` — the send response may already carry `status: "failed"`, and a `message.failed` webhook fires either way.
## List and Read
### GET /v1/messages
List messages with pagination and optional filters.
### GET /v1/messages/:messageId
Get full message detail and status timestamps.
### List Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `phoneNumberId` | Query | `string` | No | Filter by sender phone ID. |
| `status` | Query | `string` | No | pending \| sent \| delivered \| read \| failed |
| `page` | Query | `number` | No | Integer >= 1. Default 1. |
| `limit` | Query | `number` | No | Integer 1..100. Default 20. |
Results are automatically scoped to the environment of the API key used (`sk_live_` or `sk_test_`).
### Get Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `messageId` | Path | `string` | Yes | Message identifier (msg_*). |
| Required scope | - | `messages:read` | Yes | Required for both list and get endpoints. |
| Status codes | - | `200 \| 401 \| 403 \| 404 \| 429` | Yes | Common read endpoint responses. |
```bash
curl -X GET "https://developers-api.chatmaid.net/v1/messages?status=sent&page=1&limit=20" \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const params = new URLSearchParams({
status: "sent",
page: "1",
limit: "20",
});
const response = await fetch(
"https://developers-api.chatmaid.net/v1/messages?" + params.toString(),
{ headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY } }
);
const payload = await response.json();
```
```python
import os
import requests
response = requests.get(
"https://developers-api.chatmaid.net/v1/messages",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
params={"status": "sent", "page": 1, "limit": 20},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"data": [
{
"id": "msg_abc123def456",
"to": "+15557654321",
"status": "sent",
"createdAt": "2026-02-06T14:18:33.000Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1
}
}
}
```
```bash
curl -X GET https://developers-api.chatmaid.net/v1/messages/msg_abc123def456 \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const response = await fetch(
"https://developers-api.chatmaid.net/v1/messages/msg_abc123def456",
{ headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY } }
);
const payload = await response.json();
```
```python
import os
import requests
response = requests.get(
"https://developers-api.chatmaid.net/v1/messages/msg_abc123def456",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"id": "msg_abc123def456",
"from": "+15551234567",
"to": "+15557654321",
"isGroup": false,
"groupId": null,
"status": "delivered",
"createdAt": "2026-02-06T14:18:33.000Z",
"sentAt": "2026-02-06T14:18:34.000Z",
"deliveredAt": "2026-02-06T14:18:36.000Z",
"readAt": null,
"failedAt": null
}
}
```
## Inbound Messages
Messages received by your connected phone numbers. Inbound messages exist for live connections only — sandbox (`sk_test_*`) keys always receive an empty list. For real-time processing, subscribe to the `message.received` webhook event instead of polling.
### GET /v1/messages/inbound
List inbound messages with pagination.
### GET /v1/messages/inbound/:messageId
Get a single inbound message by its `inmsg_*` identifier.
### Inbound Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `phoneNumberId` | Query | `string` | No | Filter by receiving phone ID. |
| `page` | Query | `number` | No | Integer >= 1. Default 1. |
| `limit` | Query | `number` | No | Integer 1..100. Default 20. |
| Required scope | - | `messages:read` | Yes | Required for both endpoints. |
```bash
curl -X GET "https://developers-api.chatmaid.net/v1/messages/inbound?page=1&limit=20" \
-H "Authorization: Bearer sk_live_xxx"
```
```json
{
"success": true,
"data": {
"data": [
{
"id": "inmsg_abc123def456",
"from": "+15557654321",
"to": "+15551234567",
"content": "Thanks, got it!",
"type": "text",
"isGroup": false,
"groupId": null,
"receivedAt": "2026-02-06T14:21:08.000Z",
"createdAt": "2026-02-06T14:21:09.000Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1
}
}
}
```
The `type` field is one of `text`, `image`, `video`, `audio`, `document`, `sticker`, `location`, `contact`, `other`. For media messages, `content` carries the caption when present; media payloads themselves are not exposed through the API.
## Status Lifecycle
Message states typically progress in this order:
- `pending`
- `sent`
- `delivered` (recipient device acknowledged the message)
- `read` (recipient read the message)
- `failed` (terminal failure path)
`delivered` and `read` are driven by WhatsApp receipts. `read` may never arrive if the recipient has read receipts disabled, and `delivered` can be skipped straight to `read`.
### Media and Idempotency Notes
- Use `mediaUrls` with public HTTPS links for media messages.
- Send `content` + `mediaUrls` for media with caption.
- Optionally include `idempotencyKey` (max 64 characters) when your client retries requests. Keys are scoped per account. Reusing a key returns the original message with its current data.
Phone registration, WhatsApp connection, and API key creation are handled in dashboard UI. At runtime, integration clients can fetch connected phone IDs via `/v1/phone-numbers`.
---
# Phone Numbers
Source: https://www.chatmaid.net/docs/phone-numbers
Description: List connected phone numbers and inspect connection status.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Callout } from 'fumadocs-ui/components/callout';
## Overview
Phone number data is available through standard API-key endpoints. Environment is inferred from your API key prefix on a single host.
- `GET /v1/phone-numbers`
- `GET /v1/phone-numbers/:id`
- `GET /v1/phone-numbers/:id/status`
The `:id` path parameter accepts either the internal phone ID or an E.164 phone number. When using a phone number in path, URL-encode it (for example `%2B15551234567`).
Integration host: `https://developers-api.chatmaid.net`.
## List Numbers
### GET /v1/phone-numbers
List all phone numbers in the account that owns the API key.
### List Auth and Status Codes
| Item | Value |
| --- | --- |
| Recommended scope | `phone_numbers:read` |
| Backward-compatible scope | `phone-numbers:read` |
| Success code | `200` |
| Error codes | `401, 403, 429` |
```bash
curl -X GET https://developers-api.chatmaid.net/v1/phone-numbers \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const response = await fetch("https://developers-api.chatmaid.net/v1/phone-numbers", {
headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY },
});
const payload = await response.json();
```
```python
import requests
import os
response = requests.get(
"https://developers-api.chatmaid.net/v1/phone-numbers",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
)
payload = response.json()
```
```json
{
"success": true,
"data": [
{
"id": "507f1f77bcf86cd799439011",
"phoneNumber": "+15551234567",
"displayName": "Primary Sender",
"connectionStatus": "connected",
"lastConnectedAt": "2026-02-06T14:10:00.000Z",
"lastDisconnectedAt": null,
"createdAt": "2026-02-01T10:20:00.000Z",
"updatedAt": "2026-02-06T14:10:00.000Z"
}
]
}
```
## Get One Number
### GET /v1/phone-numbers/:id
Get a single phone number using either internal ID or URL-encoded E.164 number.
### Read One Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `id` | Path | `string` | Yes | Accepts phone ID or URL-encoded E.164 (example: %2B15551234567). |
| Required scope | - | `phone_numbers:read` | Yes | Also accepts phone-numbers:read. |
| Status codes | - | `200 \| 401 \| 403 \| 404 \| 429` | Yes | Typical read endpoint responses. |
```bash
curl -X GET "https://developers-api.chatmaid.net/v1/phone-numbers/%2B15551234567" \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const phoneRef = encodeURIComponent("+15551234567");
const response = await fetch(`https://developers-api.chatmaid.net/v1/phone-numbers/${phoneRef}`, {
headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY },
});
const payload = await response.json();
```
```python
import os
import requests
from urllib.parse import quote
phone_ref = quote("+15551234567", safe="")
response = requests.get(
f"https://developers-api.chatmaid.net/v1/phone-numbers/{phone_ref}",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"id": "507f1f77bcf86cd799439011",
"phoneNumber": "+15551234567",
"displayName": "Primary Sender",
"connectionStatus": "connected",
"lastConnectedAt": "2026-02-06T14:10:00.000Z",
"lastDisconnectedAt": null,
"createdAt": "2026-02-01T10:20:00.000Z",
"updatedAt": "2026-02-06T14:10:00.000Z"
}
}
```
## Connection Status
### GET /v1/phone-numbers/:id/status
Get up-to-date WhatsApp connection state by internal ID or URL-encoded E.164 number.
### Status Request Contract
| Parameter | In | Type | Required | Rules |
| --- | --- | --- | --- | --- |
| `id` | Path | `string` | Yes | Accepts phone ID or URL-encoded E.164 (example: %2B15551234567). |
| Required scope | - | `phone_numbers:read` | Yes | Also accepts phone-numbers:read. |
| Status codes | - | `200 \| 401 \| 403 \| 404 \| 429` | Yes | Typical status endpoint responses. |
```bash
curl -X GET "https://developers-api.chatmaid.net/v1/phone-numbers/%2B15551234567/status" \
-H "Authorization: Bearer sk_test_xxx"
```
```ts
const phoneRef = encodeURIComponent("+15551234567");
const response = await fetch(`https://developers-api.chatmaid.net/v1/phone-numbers/${phoneRef}/status`, {
headers: { Authorization: "Bearer " + process.env.CHATMAID_API_KEY },
});
const payload = await response.json();
```
```python
import os
import requests
from urllib.parse import quote
phone_ref = quote("+15551234567", safe="")
response = requests.get(
f"https://developers-api.chatmaid.net/v1/phone-numbers/{phone_ref}/status",
headers={"Authorization": f"Bearer {os.environ['CHATMAID_API_KEY']}"},
)
payload = response.json()
```
```json
{
"success": true,
"data": {
"id": "507f1f77bcf86cd799439011",
"phoneNumber": "+15551234567",
"connectionStatus": "connected",
"lastConnectedAt": "2026-02-06T14:10:00.000Z",
"lastDisconnectedAt": null,
"updatedAt": "2026-02-06T14:10:00.000Z"
}
}
```
Phone registration and connect/disconnect lifecycle remain dashboard-managed. Standard API endpoints are read-focused for integration runtime discovery.
---
# Webhooks
Source: https://www.chatmaid.net/docs/webhooks
Description: Consume webhook events, verify signatures, and handle retries.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Callout } from 'fumadocs-ui/components/callout';
## Webhook Overview
Webhooks are optional. You can poll message status using `GET /v1/messages/:messageId`, or receive push updates on your webhook endpoint.
Webhook destination and subscribed events are configured in the Chatmaid dashboard.
Use a single API domain; environment is inferred from key prefix:
- `https://developers-api.chatmaid.net` + `sk_test_*` for sandbox behavior
- `https://developers-api.chatmaid.net` + `sk_live_*` for production behavior
Integration host: `https://developers-api.chatmaid.net`.
## Events and Signatures
Supported events include:
- `message.sent` — a message you sent was accepted by WhatsApp
- `message.failed` — a message you sent could not be delivered
- `message.received` — one of your connected phone numbers received an inbound message
- `message.delivered` — a message you sent was delivered to the recipient's device
- `message.read` — a message you sent was read by the recipient
- `phone.connected` — a phone number connected to WhatsApp
- `phone.disconnected` — a phone number disconnected from WhatsApp
`message.delivered` and `message.read` are driven by WhatsApp receipts: `read` may never fire if the recipient has read receipts disabled, and a message can jump from `sent` straight to `read`.
Each webhook includes these headers:
- `Content-Type: application/json`
- `X-Chatmaid-Event`
- `X-Chatmaid-Signature`
```ts
import crypto from "crypto";
function verifySignature(rawBody: string, signatureHeader: string, secret: string) {
// signature format: t=1700000000,v1=
const pairs = Object.fromEntries(
signatureHeader.split(",").map((part) => {
const idx = part.indexOf("=");
return [part.slice(0, idx), part.slice(idx + 1)];
})
);
const timestamp = pairs.t;
const expected = crypto
.createHmac("sha256", secret)
.update(timestamp + "." + rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(pairs.v1), Buffer.from(expected));
}
```
```python
import hmac
import hashlib
def verify_signature(raw_body: str, signature_header: str, secret: str) -> bool:
# signature format: t=1700000000,v1=
parts = dict(part.split("=", 1) for part in signature_header.split(","))
payload = (parts["t"] + "." + raw_body).encode("utf-8")
expected = hmac.new(secret.encode("utf-8"), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(parts.get("v1", ""), expected)
```
```bash
# Webhook requests are sent by Chatmaid to your server.
# Use cURL locally only to simulate payload delivery to your endpoint:
curl -X POST https://your-receiver.example/webhooks/chatmaid \
-H "Content-Type: application/json" \
-H "X-Chatmaid-Event: message.sent" \
-H "X-Chatmaid-Signature: t=1700000000,v1=" \
-d '{"event":"message.sent","timestamp":"2026-02-06T14:20:00.000Z","data":{"messageId":"msg_abc123"}}'
```
## Payloads
Message lifecycle events (`message.sent`, `message.delivered`, `message.read`, `message.failed`) share the same `data` shape:
```json
{
"event": "message.sent",
"timestamp": "2026-02-06T14:20:00.000Z",
"data": {
"messageId": "msg_abc123def456",
"from": "+15551234567",
"to": "+15557654321",
"isGroup": false,
"groupId": null,
"status": "sent",
"sentAt": "2026-02-06T14:19:12.000Z",
"deliveredAt": null,
"readAt": null,
"failedAt": null,
"errorCode": null,
"errorMessage": null
}
}
```
On a `message.failed` event, `errorCode` is a stable value from the [error codes reference](/docs/api-reference#error-codes) (e.g. `RECIPIENT_NOT_REACHABLE`) and `errorMessage` is a human-readable explanation. Internal service details are never included.
`message.received` carries the inbound message:
```json
{
"event": "message.received",
"timestamp": "2026-02-06T14:21:09.000Z",
"data": {
"messageId": "inmsg_abc123def456",
"from": "+15557654321",
"to": "+15551234567",
"content": "Thanks, got it!",
"type": "text",
"isGroup": false,
"groupId": null,
"receivedAt": "2026-02-06T14:21:08.000Z"
}
}
```
For outbound events, `isGroup` is true and `groupId` carries the group JID when the message was sent to a WhatsApp group. For `message.received`, `from` is the external sender and `to` is your connected phone number; when the message arrived in a group, `isGroup` is true and `groupId` is the group JID — pass it as `to` in `POST /v1/messages/send` to reply to that group. `type` is one of `text`, `image`, `video`, `audio`, `document`, `sticker`, `location`, `contact`, `other`; for media messages, `content` carries the caption when present.
## Retries and Debugging
Failed webhook deliveries are retried up to 3 attempts with exponential backoff: 1 minute, 5 minutes, then 15 minutes after the initial attempt. Your endpoint should return HTTP 2xx quickly and process events asynchronously.
Your webhook handler must be idempotent. Retries can deliver the same event more than once.