Status: stable, additive contract
Released: 2026-08-14
Base URL: https://dev.chataman.com for development and https://chataman.com for production
This guide is the canonical contract for server-to-server API keys, channel-aware message sending, organization data reads, webhook endpoints, signing, retries, and delivery observability. Existing /api/* clients and legacy webhook records remain supported.
Quickstart
- In Developer Tools → Access Tokens, create a key, select only the required scopes, optionally select a default channel, and save the full key immediately. Chat Aman stores only its SHA-256 hash and will not show the full value again.
- Discover channel IDs and slugs:
curl https://dev.chataman.com/api/v1/integrations/channels \
-H "Authorization: Bearer $CHATAMAN_API_KEY" \
-H "Accept: application/json"
- Send through a specific channel. Every v1 write requires a new
Idempotency-Keyfor the logical operation:
curl -X POST https://dev.chataman.com/api/v1/integrations/messages \
-H "Authorization: Bearer $CHATAMAN_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: order-7842-confirmation-v1" \
-d '{
"channel_slug": "support-telegram",
"contact_uuid": "CONTACT_UUID",
"message": "Your order is ready."
}'
- Register a signed webhook endpoint and store the returned
signing_secret. The secret is returned only on creation or rotation.
curl -X POST https://dev.chataman.com/api/v1/integrations/webhook-endpoints \
-H "Authorization: Bearer $CHATAMAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Order sync",
"url": "https://hooks.example.com/chataman",
"events": ["message.*", "contact.updated", "security.*"]
}'
Authentication and scopes
Send Authorization: Bearer <key> on every request. New keys are high-entropy, hashed at rest, organization-bound, revocable, and may have an expiry and default channel. Legacy plaintext keys remain valid until revoked or rotated; keys created before scopes existed are treated as wildcard keys for backward compatibility.
Available scopes:
channels:readcontacts:read,contacts:writegroups:read,groups:writemessages:read,messages:writecampaigns:writetemplates:readcanned_replies:read,canned_replies:writeusers:readpermissions:readevents:readwebhooks:read,webhooks:writeflow:read,flow:write*for a deliberately unrestricted key
Use one key per integration and environment. Keep keys in a secret manager, never in frontend code, rotate them after exposure or ownership changes, and choose the smallest useful scope set.
Channel selection
V1 send requests accept exactly one of:
channel_id: numeric ID returned by the channels endpoint.channel_slug: stable API-facing slug. Chat Aman matches an explicit configured slug, channel UUID, provider identifier, or normalized channel name.- neither: uses the API key’s configured default channel, then the organization’s default channel.
Supplying both returns 422. A channel from another organization, an inactive channel, a missing default, or a channel that cannot send external messages returns 422. Snapchat currently exposes lead-ingestion data but is returned with can_send_messages: false; the other current channel service types are sendable.
Successful send responses include the actual channel object so callers can audit where a message originated. Webhook envelopes use the same channel identity shape.
Endpoint catalogue
All endpoints are organization-scoped and use Bearer authentication.
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /api/v1/integrations/channels |
channels:read |
List channel IDs, UUIDs, slugs, type, status, send capability, and media support. |
| GET | /api/v1/integrations/messages |
messages:read |
Paginated channel-aware message history. |
| POST | /api/v1/integrations/messages |
messages:write |
Send text or interactive content; requires idempotency key. |
| POST | /api/v1/integrations/messages/media |
messages:write |
Send media; requires idempotency key. |
| POST | /api/v1/integrations/messages/template |
messages:write |
Send a provider template; requires idempotency key. |
| GET | /api/v1/integrations/users |
users:read |
List organization memberships, roles, and effective permissions. |
| GET | /api/v1/integrations/permissions |
permissions:read |
List the permission and API-scope catalogues. |
| GET | /api/v1/integrations/events |
events:read |
Read organization audit/admin/security events. |
| GET | /api/v1/integrations/webhook-events |
webhooks:read |
Read the authoritative event catalogue for this deployment. |
| GET/POST | /api/v1/integrations/webhook-endpoints |
webhooks:read / webhooks:write |
List or create endpoints. |
| GET/PUT/DELETE | /api/v1/integrations/webhook-endpoints/{uuid} |
webhooks:read / webhooks:write |
Read, update, or remove one endpoint. |
| POST | /api/v1/integrations/webhook-endpoints/{uuid}/rotate-secret |
webhooks:write |
Rotate and return a new secret once. |
| POST | /api/v1/integrations/webhook-endpoints/{uuid}/test |
webhooks:write |
Deliver an isolated integration.test event. |
| GET | /api/v1/integrations/webhook-deliveries |
webhooks:read |
Inspect attempts, state, response, error, and retry time. |
Legacy contact, group, canned reply, template, campaign, flow, and /api/send* routes remain available. Scoped keys must now hold the matching scope; pre-scope keys retain wildcard behavior.
Text request
{
"channel_id": 42,
"contact_uuid": "95cc16a4-cf69-41f8-b2c4-923de17d3fc6",
"message": "Hello from the billing system",
"type": "text"
}
contact_uuid is preferred. phone remains accepted for backward compatibility and is resolved only inside the authenticated organization.
Media request
{
"channel_slug": "sales-whatsapp",
"contact_uuid": "95cc16a4-cf69-41f8-b2c4-923de17d3fc6",
"media_type": "document",
"media_url": "https://cdn.example.com/invoices/7842.pdf",
"file_name": "invoice-7842.pdf",
"caption": "Invoice 7842"
}
Template request
{
"channel_slug": "sales-whatsapp",
"contact_uuid": "95cc16a4-cf69-41f8-b2c4-923de17d3fc6",
"template": {
"name": "order_ready",
"language": "en",
"components": []
}
}
Idempotency
POST /api/v1/integrations/messages* requires Idempotency-Key (1–120 characters). The key is isolated to the API credential. Repeating the same key and request returns the stored status and JSON body with Idempotency-Replayed: true. Reusing it with a different method, path, or body returns 409 idempotency_conflict; a still-running duplicate returns 409 idempotency_in_progress. Records are retained for seven days by default.
Webhook envelope
Modern and legacy consumers retain the original top-level event and data fields. V1 adds consistent metadata and channel identity:
{
"event": "message.sent",
"data": {
"message_id": 9182,
"contact_uuid": "95cc16a4-cf69-41f8-b2c4-923de17d3fc6",
"status": "sent"
},
"metadata": {
"event_id": "6e1eb478-2888-47aa-8dce-e6f830ead865",
"event_type": "message.sent",
"api_version": "v1",
"occurred_at": "2026-08-14T10:45:32+00:00",
"organization_id": 17
},
"channel": {
"id": 42,
"uuid": "dcb67374-4d88-4930-b834-86c64359745d",
"slug": "sales-whatsapp",
"type": "whatsapp",
"name": "Sales WhatsApp",
"identifier": "PROVIDER_CHANNEL_IDENTIFIER"
}
}
channel is null only when an event has no channel context. Use metadata.event_id as the consumer deduplication key. Event-specific data is additive: consumers must ignore unknown fields.
Full webhook event catalogue
The v1 catalogue contains 146 events. GET /api/v1/integrations/webhook-events is authoritative and includes events enabled by the deployed application. Endpoint subscriptions accept exact names, *, or namespace wildcards such as message.*.
- account:
account.email_verification_requested,account.email_verified,account.password_changed,account.password_reset_requested,account.registered - ai:
ai.agent_failed,ai.cost_threshold_reached,ai.knowledge_sync_completed,ai.knowledge_sync_failed,ai.quota_exceeded,ai.quota_low - audit:
audit.recorded - automation:
automation.failed - autoreply:
autoreply.created,autoreply.deleted,autoreply.updated - billing:
billing.auto_recharge_failed,billing.credit_critical,billing.credit_depleted,billing.credit_low,billing.credit_recharged,billing.invoice_approved,billing.invoice_due,billing.invoice_generated,billing.invoice_overdue,billing.invoice_paid,billing.invoice_sent,billing.payment_failed,billing.payment_refunded,billing.payment_succeeded - broadcast:
broadcast.completed,broadcast.failed - bulk_operation:
bulk_operation.completed,bulk_operation.failed - campaign:
campaign.completed,campaign.contact_replied,campaign.failed,campaign.paused,campaign.resumed,campaign.scheduled,campaign.started - channel:
channel.connected,channel.disconnected,channel.health_degraded,channel.health_restored,channel.token_expiring - commerce:
commerce.cart_abandoned,commerce.catalog_sync_failed,commerce.order_canceled,commerce.order_created,commerce.order_fulfilled,commerce.order_paid,commerce.order_refunded,commerce.order_status_changed,commerce.payment_link_created,commerce.store_disconnected - contact:
contact.created,contact.deleted,contact.updated - conversation:
conversation.agent_mentioned,conversation.assigned,conversation.closed,conversation.created,conversation.customer_replied,conversation.reassigned,conversation.reopened,conversation.sla_breached,conversation.sla_warning,conversation.unassigned - crm:
crm.deal_assigned,crm.deal_closing_soon,crm.deal_created,crm.deal_lost,crm.deal_rotting,crm.deal_stage_changed,crm.deal_won,crm.lead_assigned,crm.lead_converted,crm.lead_created,crm.lead_stale,crm.meeting_booked,crm.meeting_canceled,crm.meeting_reminder,crm.meeting_rescheduled,crm.quote_accepted,crm.quote_expired,crm.quote_rejected,crm.quote_sent,crm.quote_viewed,crm.sequence_completed,crm.sequence_failed,crm.task_assigned,crm.task_completed,crm.task_due,crm.task_overdue - drip_campaign:
drip_campaign.completed,drip_campaign.failed - feedback:
feedback.csat_low,feedback.csat_received - group:
group.created,group.deleted,group.updated - integration:
integration.sync_failed,integration.sync_restored,integration.test - message:
message.received,message.sent,message.status.update - organization:
organization.activated,organization.suspended - scheduled_message:
scheduled_message.canceled,scheduled_message.failed,scheduled_message.sent - security:
security.api_key_created,security.api_key_revoked,security.new_login,security.two_factor_changed - storage:
storage.migration_completed,storage.migration_failed,storage.threshold_reached - subscription:
subscription.canceled,subscription.downgraded,subscription.expired,subscription.expiring,subscription.renewed,subscription.started,subscription.upgraded - support:
support.ticket_assigned,support.ticket_created,support.ticket_reopened,support.ticket_replied,support.ticket_resolved - survey:
survey.completed - team:
team.invitation_accepted,team.invitation_created,team.invitation_expired,team.member_added,team.member_removed,team.role_changed - trial:
trial.expired,trial.expiring,trial.started - usage:
usage.quota_exceeded,usage.threshold_reached - webhook:
webhook.delivery_failed,webhook.endpoint_disabled
Signing and verification
Modern endpoints receive:
X-ChatAman-Event: event nameX-ChatAman-Delivery: durable delivery UUIDX-ChatAman-Timestamp: Unix timestamp used in the signatureX-ChatAman-Attempt: one-based attempt numberX-ChatAman-Signature:v1=<hex HMAC-SHA256>
The signed bytes are timestamp + "." + raw_request_body. Verify the raw bytes before JSON parsing, reject stale timestamps (five minutes is recommended), and use a constant-time comparison.
import crypto from 'node:crypto';
export function verifyChatAman(rawBody, headers, secret) {
const timestamp = headers['x-chataman-timestamp'];
const received = headers['x-chataman-signature'];
if (!timestamp || !received || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = `v1=${crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')}`;
return received.length === expected.length && crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}
Custom endpoint headers cannot override authorization, transport, content, cookie, host, or Chat Aman signature headers. Endpoint URLs must use public HTTPS on port 443; credentials, localhost/private/reserved IPs, internal domains, and DNS rebinding to non-public addresses are rejected. Secrets are encrypted at rest. Older module endpoints keep their unsigned payload contract until migrated.
Delivery, retries, and observability
Delivery is queued and persisted before the network call. A success is any 2xx response. Connection failures, 408, 425, 429, and 5xx are retried with backoff of 30 seconds, 5 minutes, and 30 minutes, up to four attempts by default. Other 4xx responses are permanent rejections. Repeated endpoint failures increment health state and may automatically disable the endpoint after ten failures; a later success resets the failure count.
Use GET /api/v1/integrations/webhook-deliveries?status=retrying&event=message.sent to inspect delivery_id, event ID, endpoint, attempt count, response status, error code/message, next attempt, and delivered timestamp. Delivery records are retained for 90 days by default. Alert on failed, retries_exhausted, and endpoint-disabled events. Do not log payloads, API keys, or signing secrets.
Errors and rate limits
Error responses use an HTTP status plus a stable error.code where integration middleware handles the failure.
| Status | Meaning | Action |
|---|---|---|
| 400 | Malformed JSON or request syntax | Correct the request; do not retry unchanged. |
| 401 | Missing, invalid, expired, or revoked key | Add, rotate, or replace the key. |
| 403 | insufficient_scope or organization access denied |
Grant only the required scope or correct the tenant. |
| 404 | Resource not found in the authenticated organization | Verify organization-bound IDs/UUIDs. |
| 409 | Idempotency conflict or operation still processing | Reuse only for the same logical request; retry in-progress later. |
| 422 | Validation, unsafe URL, invalid channel, or provider rule failure | Correct the fields; do not retry unchanged. |
| 429 | Rate limit exceeded | Respect Retry-After and use jittered exponential backoff. |
| 5xx | Temporary server/provider failure | Retry idempotently with capped exponential backoff. |
Default limits are 120 authenticated integration reads per minute, 30 sends per minute, and 10 endpoint tests per minute. Deployments may override these limits; clients must honor response headers rather than hard-code them.
Versioning and changelog
The contract uses a path version (/api/v1/integrations) and an envelope version (metadata.api_version). Additive fields and new events do not change the major version. Removing/renaming fields, changing signatures, or changing field meaning requires a new major path or a documented compatibility window. Consumers must ignore unknown fields and events.
2026-08-14 — v1
- Added explicit channel selection and per-key default-channel fallback.
- Added hashed API keys, granular scopes, expiry/revocation metadata, rate limits, and legacy-key compatibility.
- Added idempotent message writes.
- Added channel/message/user/permission/audit discovery endpoints.
- Unified modern and legacy webhook dispatch without duplicating same-URL deliveries.
- Added stable event metadata, originating channel identity, encrypted signing secrets, queued retries, delivery ledger, endpoint health, and SSRF protection.
- Expanded the catalogue to all current message, contact, group, campaign, channel, account, organization, team, security, audit, billing, CRM, commerce, AI, support, storage, and operational events.
Legacy routes are not removed. Migrate sends to /api/v1/integrations/messages*, create scoped keys, move legacy webhook records to signed endpoints, and verify signatures before depending on the v1 security guarantees.