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.
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 |