AeroChat Developer Docs
Two ways to integrate: a REST API to drive conversations and read data (you call us), and webhooks to get notified when things happen (we call you). Both share the same ids. Looking to connect an AI assistant instead? See the MCP and Skills tabs above.
Overview
| Base URL | https://app.aerochat.ai |
|---|---|
| Versioning | Version is in the path: /bot/v1/…. A future /bot/v2/… runs alongside it — choose a version by URL. |
| Auth | Header X-AeroChat-API-Key: <your_key> on every API request (server-side only). |
| Format | JSON. Errors are { "detail": { "type", "message" } } with a standard HTTP status. |
Concepts & terminology
The whole platform is built on a few objects and ids. These same ids appear in both the API and webhooks, so everything lines up.
| Term | What it means |
|---|---|
| Contact | A person who chats with your bot. Identified by contact_id. |
| Conversation | A thread of messages with a contact. Identified by conversation_id. (Today one per contact; designed so a contact can have several later.) |
| Message | A single message within a conversation. Identified by message_id. |
contact_id (ct_…) | Opaque, stable token for a contact — not a raw database id. Same value everywhere (API + webhooks), so you can store it and join records. AeroChat can resolve it back; you treat it as an opaque string. |
conversation_id (cv_…) | Opaque, stable token for a conversation/thread. Pass it back to continue a conversation. |
message_id | Unique id of one message. |
channel | Where the conversation happens: web, whatsapp, telegram, facebook, instagram, email, or api. |
direction | inbound (from the customer) or outbound (from the bot or a human agent). |
author | Who sent a message: { "type": "customer" | "bot" | "agent", "name"? }. No internal ids/emails are exposed. |
intent | The classified purpose of a turn (e.g. business_hours). |
status | Result of a sent message: bot (the bot replied) · queued_for_agent (a human will reply) · unavailable. |
| Event | A webhook notification of something that happened (e.g. message.inbound). |
| HMAC signature | A hash sent with each webhook (X-AeroChat-Signature) that proves it really came from AeroChat — see Verify signature. |
| Redaction profile | Per-webhook PII policy (passthrough vs strict) that controls whether email/phone are scrubbed before delivery. |
| API key | Your secret credential (the X-AeroChat-API-Key header). Generate it in the dashboard. |
Authentication
Generate a key in the dashboard: Integrations → Developer API → Generate API Key (shown once). Send it on every API request:
X-AeroChat-API-Key: <your_key>
Keep it server-side — never ship it in a browser or mobile app. Rotate/revoke from the dashboard. (Webhooks don't use this key; they're verified with the HMAC signature instead.)
Send a message
/bot/v1/conversations/messagesRequest body
| Field | Type | Notes |
|---|---|---|
message required | string | The customer's message. |
conversation_id | string | Omit on the first message (one is minted & returned); pass it to continue the same conversation. |
contact | object | Optional identity to attach: { "name", "email", "phone" }. |
curl -X POST https://app.aerochat.ai/bot/v1/conversations/messages \
-H "X-AeroChat-API-Key: <your_key>" \
-H "Content-Type: application/json" \
-d '{"message":"what are your opening hours?"}'
Response
{
"conversation_id": "cv_7Hq2...",
"contact_id": "ct_R2dE...",
"message_id": "9b3c0e7a...",
"status": "bot",
"reply": { "text": "We're open 9am-5pm on weekdays." },
"intent": "business_hours",
"language": "en",
"created_at": "2026-06-16T10:00:00Z"
}
Continue by sending the next message with "conversation_id": "cv_7Hq2...". When status is queued_for_agent, there's no reply — a human will answer (you'd receive that via the message.outbound webhook).
Get a contact
/bot/v1/contacts/{contact_id}curl https://app.aerochat.ai/bot/v1/contacts/ct_R2dE... \
-H "X-AeroChat-API-Key: <your_key>"
Returns contact_id, name, email, phone, channel, created_at, last_seen.
Get a conversation's messages
/bot/v1/conversations/{conversation_id}/messages?page=1&limit=50curl "https://app.aerochat.ai/bot/v1/conversations/cv_7Hq2.../messages?limit=50" \
-H "X-AeroChat-API-Key: <your_key>"
Each message: message_id, direction, author, text, attachments, language, created_at; the response also has a paging object (page, limit, total, has_more).
Webhooks
A webhook is the reverse of the API: instead of you asking us, we POST a JSON event to a URL you register whenever something happens (a new message, an escalation, a new contact…). Use them to sync conversations into your CRM or to receive human-agent replies for API-driven chats.
Subscribe
Dashboard → Integrations → Webhooks → Add Webhook: enter your https URL and tick the events you want. You get a signing secret (shown once) used to verify deliveries.
Delivery envelope
Every event is a POST with this body, plus headers:
{ "id": "evt_...", // unique event id — your idempotency key
"type": "message.inbound",
"created_at": "2026-06-16T10:00:00Z",
"data": { ...event-specific... } }
| Header | Meaning |
|---|---|
X-AeroChat-Signature | HMAC-SHA256 of the raw body (verify with your secret) |
X-AeroChat-Event | The event type |
X-AeroChat-Delivery | Unique id per delivery attempt (dedupe retries) |
Verify the signature
Always verify before trusting a webhook. Sign the raw request bytes (don't re-serialize the JSON — key order would differ):
import hmac, hashlib
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)
Delivery is at-least-once and unordered — dedupe on the event id. PII in the payload (email/phone) is governed by the subscription's redaction profile.
Event catalog
| Event | Fires when | Key fields in data |
|---|---|---|
message.inbound | A customer sends a message | conversation_id, contact_id, channel, author, text, language |
message.outbound | The bot or a human agent replies | …same, + author.type bot/agent, intent (bot) |
contact.created | A new contact appears | contact_id, name, email, phone, channel, source, geo |
contact.updated | A contact's name/email/phone changes | contact_id, changed_fields, name, email, phone |
conversation.escalated | A conversation is handed to a human | conversation_id, contact_id, contact_name, trigger_message, assigned_agent |
conversation.resolved | A conversation is closed | conversation_id, contact_id, contact_name |
Each event below: when it fires, what it's for, and a full example payload. The ct_/cv_ ids are identical to the ones the API returns, so events and API records join cleanly.
message.inbound — fires when a customer sends a message. Use it to log incoming messages or trigger your own automations; for an API-hosted chat it's how you receive the customer's turn server-side.
{
"id": "evt_a1b2c3",
"type": "message.inbound",
"created_at": "2026-06-16T10:00:00Z",
"data": {
"message_id": "9b3c0e7a4d5f4c8e9a1b2c3d",
"conversation_id": "cv_7Hq2Lm8pR1vXc0",
"contact_id": "ct_R2dE8nQ1pXkLZ0",
"channel": "whatsapp",
"direction": "inbound",
"author": { "type": "customer" },
"text": "where is my order?",
"language": "en",
"created_at": "2026-06-16T10:00:00Z"
}
}
message.outbound — fires when the bot or a human agent replies. author.type is bot (name = your brand) or agent (name = the agent). For API-hosted chats this is how you receive a human agent's reply.
{
"id": "evt_b2c3d4",
"type": "message.outbound",
"created_at": "2026-06-16T10:00:05Z",
"data": {
"message_id": "1f2e3d4c5b6a",
"conversation_id": "cv_7Hq2Lm8pR1vXc0",
"contact_id": "ct_R2dE8nQ1pXkLZ0",
"channel": "whatsapp",
"direction": "outbound",
"author": { "type": "bot", "name": "Acme Support" },
"text": "Your order #123 ships tomorrow.",
"intent": "order_status",
"language": "en",
"created_at": "2026-06-16T10:00:05Z"
}
}
Human-agent variant: "author": { "type": "agent", "name": "Sarah" } (no intent).
contact.created — fires when a new contact first appears. Use it to create the customer in your CRM. email/phone may be null until known; geo is present for web visitors.
{
"id": "evt_c3d4e5",
"type": "contact.created",
"created_at": "2026-06-16T09:59:50Z",
"data": {
"contact_id": "ct_R2dE8nQ1pXkLZ0",
"name": "Guest351497",
"email": null,
"phone": "+15551234567",
"channel": "whatsapp",
"source": "whatsapp",
"geo": { "city": "Pune", "country": "India" },
"created_at": "2026-06-16T09:59:50Z",
"updated_at": "2026-06-16T09:59:50Z"
}
}
contact.updated — fires when a contact's name, email, or phone changes (e.g. a guest later shares their email). changed_fields lists exactly what changed.
{
"id": "evt_d4e5f6",
"type": "contact.updated",
"created_at": "2026-06-16T10:02:00Z",
"data": {
"contact_id": "ct_R2dE8nQ1pXkLZ0",
"changed_fields": ["name", "email"],
"name": "Jane Doe",
"email": "jane@example.com",
"phone": null,
"channel": "web",
"updated_at": "2026-06-16T10:02:00Z"
}
}
conversation.escalated — fires when a conversation is handed to a human. trigger_message is the question that caused it; assigned_agent is the agent's display name (no id/email). Use it to alert your team or switch your UI to "an agent is helping".
{
"id": "evt_e5f6a7",
"type": "conversation.escalated",
"created_at": "2026-06-16T10:03:00Z",
"data": {
"conversation_id": "cv_7Hq2Lm8pR1vXc0",
"contact_id": "ct_R2dE8nQ1pXkLZ0",
"contact_name": "Guest351497",
"trigger_message": { "text": "I want to talk to a human" },
"assigned_agent": { "name": "Sarah" }
}
}
conversation.resolved — fires when a conversation is closed. Use it to mark the ticket resolved on your side.
{
"id": "evt_f6a7b8",
"type": "conversation.resolved",
"created_at": "2026-06-16T10:30:00Z",
"data": {
"conversation_id": "cv_7Hq2Lm8pR1vXc0",
"contact_id": "ct_R2dE8nQ1pXkLZ0",
"contact_name": "Guest351497"
}
}
Errors
| Status | Meaning |
|---|---|
400 | Invalid request (e.g. empty message) |
401 | Missing or invalid API key |
404 | Unknown contact / conversation (or not yours) |
413 | Message too long |
429 | Message limit reached / out of credits |