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 URLhttps://app.aerochat.ai
VersioningVersion is in the path: /bot/v1/…. A future /bot/v2/… runs alongside it — choose a version by URL.
AuthHeader X-AeroChat-API-Key: <your_key> on every API request (server-side only).
FormatJSON. 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.

TermWhat it means
ContactA person who chats with your bot. Identified by contact_id.
ConversationA thread of messages with a contact. Identified by conversation_id. (Today one per contact; designed so a contact can have several later.)
MessageA 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_idUnique id of one message.
channelWhere the conversation happens: web, whatsapp, telegram, facebook, instagram, email, or api.
directioninbound (from the customer) or outbound (from the bot or a human agent).
authorWho sent a message: { "type": "customer" | "bot" | "agent", "name"? }. No internal ids/emails are exposed.
intentThe classified purpose of a turn (e.g. business_hours).
statusResult of a sent message: bot (the bot replied) · queued_for_agent (a human will reply) · unavailable.
EventA webhook notification of something that happened (e.g. message.inbound).
HMAC signatureA hash sent with each webhook (X-AeroChat-Signature) that proves it really came from AeroChat — see Verify signature.
Redaction profilePer-webhook PII policy (passthrough vs strict) that controls whether email/phone are scrubbed before delivery.
API keyYour 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

POST/bot/v1/conversations/messages

Request body

FieldTypeNotes
message requiredstringThe customer's message.
conversation_idstringOmit on the first message (one is minted & returned); pass it to continue the same conversation.
contactobjectOptional 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

GET/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

GET/bot/v1/conversations/{conversation_id}/messages?page=1&limit=50
curl "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... } }
HeaderMeaning
X-AeroChat-SignatureHMAC-SHA256 of the raw body (verify with your secret)
X-AeroChat-EventThe event type
X-AeroChat-DeliveryUnique 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

EventFires whenKey fields in data
message.inboundA customer sends a messageconversation_id, contact_id, channel, author, text, language
message.outboundThe bot or a human agent replies…same, + author.type bot/agent, intent (bot)
contact.createdA new contact appearscontact_id, name, email, phone, channel, source, geo
contact.updatedA contact's name/email/phone changescontact_id, changed_fields, name, email, phone
conversation.escalatedA conversation is handed to a humanconversation_id, contact_id, contact_name, trigger_message, assigned_agent
conversation.resolvedA conversation is closedconversation_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

StatusMeaning
400Invalid request (e.g. empty message)
401Missing or invalid API key
404Unknown contact / conversation (or not yours)
413Message too long
429Message limit reached / out of credits