INTEGRATION GUIDE
Webhooks
Receive signed event notifications from Aunoa on your own server.
Set up your integration
Webhooks let your server receive notifications when something happens in your Aunoa project. You provide a receiving URL and choose the events you need; Aunoa sends signed HTTPS POST requests to that URL. You do not need to poll for updates.
Send your Aunoa contact:
- Project: the project/bot to connect and a technical contact for the integration.
- Receiving URL: a public HTTPS endpoint on port 443, with a valid TLS certificate. It must accept JSON POST requests without redirects. URLs with embedded credentials or fragments, private networks and local addresses are not supported.
- Events: the exact names from the catalog below. Choose only the events your integration needs.
Aunoa configures the subscription, provides your signing secret securely and coordinates a test delivery. Subscription management is currently handled by Aunoa; this guide describes the notifications your server receives. You do not need an Aunoa API key to receive them: authenticate incoming requests using their signature.
Subscriptions send only while the project is in production mode. There is no historical backfill, and changes can take up to 30 seconds to reach event producers. Events from projects outside production are discarded. Disabling production also discards queued deliveries and retries; re-enabling it does not restore them. A request already in flight may finish.
What your server receives
For every matching event, Aunoa sends a request to the URL you provided. The path below is an example on your server, not an Aunoa API endpoint.
POST /integrations/aunoa HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: Aunoa-Webhooks/1
X-Aunoa-Delivery-Id: 2ce9cc67-fd24-44df-8737-6fa57738e2a7
X-Aunoa-Timestamp: 1791115200
X-Aunoa-Signature: v1=<64 hexadecimal characters>
{
"version": 1,
"eventId": "b3d7a8a2-824a-460e-bde6-df13e8c54026",
"deliveryId": "2ce9cc67-fd24-44df-8737-6fa57738e2a7",
"event": "onCcareStart",
"occurredAt": "2026-10-04T12:00:00.000Z",
"idapp": 123,
"data": {
"message": 456,
"payload": { "idconversation": 456 },
"keyword": "",
"response": null,
"idchannel": null,
"channel": null,
"sender": null,
"recipient": null,
"extraparams": null
}
}
The signature above is a placeholder. Each real signature covers the exact bytes of the JSON body sent in that request. The example uses an API-origin customer-care event; other events have different payloads.
Envelope and identifiers
version, eventId, deliveryId, event, occurredAt (UTC ISO 8601 with milliseconds, for example 2026-10-04T12:34:56.789Z) and idapp identify the notification. data preserves the event fields message, payload, keyword and response, plus available idchannel, channel, sender, recipient and extraparams metadata.
Data types vary by event: a payload can be an object, number, string, array or null. Empty strings, false and zero are preserved; absent values become null. JSON-looking strings are not parsed, identifiers are not renamed and data timestamps retain their original units. Only the envelope time is standardized. Additional fields may appear; receivers should ignore unknown fields.
requestId is not a universal webhook field. Voice events include it at data.payload.requestId when available. For individual API calls it correlates with the id returned by POST /bots/voice/calls. For other call origins it uses the original call ID. It is not an idempotency key. Other events do not acquire a synthetic request ID.
Verify the signature
Aunoa provides a signing secret when your subscription is set up. Store it securely on your server and agree secret rotation with your Aunoa contact. Never send it in the callback URL or expose it in browser code. It is a string: use its UTF-8 bytes as the HMAC key, not a hex-decoded value. Each attempt contains:
X-Aunoa-Delivery-Id: delivery identifier.X-Aunoa-Timestamp: Unix timestamp in seconds for this attempt.X-Aunoa-Signature:v1=followed by HMAC-SHA256 in hexadecimal.
The signed value is timestamp + "." + rawRequestBody. Verify the exact raw body before parsing JSON, compare in constant time, and reject timestamps more than five minutes away from your clock. Each retry gets a fresh timestamp and signature. Secret rotation applies to subsequent attempts, including pending retries; briefly allow the previous secret for requests already in flight.
Node.js verification (run before your JSON parser; rawBody is a Buffer):
const crypto = require('node:crypto');
function verify(secret, timestamp, header, rawBody) {
if (!/^\d+$/.test(timestamp || '') ||
Math.abs(Date.now() / 1000 - Number(timestamp)) > 300 ||
!/^v1=[0-9a-f]{64}$/.test(header || '')) return false;
const expected = crypto.createHmac('sha256', secret)
.update(timestamp + '.').update(rawBody).digest();
return crypto.timingSafeEqual(expected, Buffer.from(header.slice(3), 'hex'));
}
Acknowledge, deduplicate and retry
- Read the raw request body and validate its timestamp and signature.
- Store the notification durably and deduplicate by
deliveryIdbefore scheduling work. Make storing the event and claiming its ID atomic in your application. - Return HTTP 200 OK promptly. For a duplicate you already stored, return 200 again without repeating its effects. Other statuses, including 201, 202 and 204, are not accepted as acknowledgements.
- Process lengthy work asynchronously. The full request has a 3-second budget, including connection setup; do not wait for your business workflow to finish before acknowledging.
| Receiver result | Delivery behavior |
|---|---|
| HTTP 200 | Acknowledged; no automatic retry. |
| Network error, timeout, 408, 429 or 5xx | Retried, within the attempt budget. |
| Other statuses, including other 2xx and redirects | Final failure; not retried automatically. |
We schedule up to six attempts: the initial delivery and up to five retries after 2, 5, 10, 20 and 30 minutes from the previous attempt. These are retry delays, not exact delivery appointments: load can delay the next attempt. A valid Retry-After can extend the wait, capped at one hour. Crashes or uncertain acknowledgements can produce additional duplicate HTTP requests; this budget counts scheduled attempts.
Automatic retries preserve deliveryId, eventId and body. Each attempt has a fresh timestamp and signature. A lost response can cause a repeat even if you processed the first request. Delivery order is not guaranteed. Use your own business identifiers and event state to handle out-of-order updates.
Retry delays are minimum waiting periods, not guaranteed delivery times. Backlog or an automatic pause can delay delivery further. A connection failure or restart can cause an additional duplicate even when a retry has already been scheduled; always deduplicate by deliveryId.
The same event may reach several subscriptions. eventId identifies that emission. If your business action must happen only once across destinations, also deduplicate at that level using eventId. Manual replay is not available. After automatic retries are exhausted, the delivery is recorded as failed.
Automatic pause: after three consecutive failed attempts to the same webhook, deliveries to that destination pause for two minutes. Timeouts, connection errors and every HTTP status other than 200 count as failures. Events already admitted to the delivery queue remain stored during the pause; waiting does not consume attempts. Each destination has a bounded queue (64 MiB). If that queue is full or admission is unavailable, new notifications can be rejected and are not stored for later delivery. There is no manual replay. Requests already in flight may finish.
After the pause, Aunoa allows one due delivery to test recovery. Only HTTP 200 reopens normal delivery; another failure starts a new two-minute pause. Other destinations continue independently. A connection test also respects the pause. These checks can happen later than the minimum pause because delivery retry times and capacity still apply.
Automatic recovery starts once a notification has been persisted by Aunoa. A failure before persistence does not stop the underlying conversation or call and cannot be replayed automatically.
Test and troubleshoot
Ask your Aunoa contact to send a connection test before enabling business actions. A test is signed exactly like other notifications and uses event="webhook.test", data.message="Webhook connection test" and data.payload={"test":true}. It is not a selectable business event and has no voice request ID. Validate and acknowledge it without triggering business side effects.
Confirm signature verification, successful HTTP 200 responses and duplicate handling. Test a temporary 503 response to observe retry behavior. For troubleshooting, provide your project, event name, deliveryId, eventId, approximate time and the HTTP status your receiver returned. Never share the signing secret in logs or support messages. Delivery results, payloads and attempt responses are available for ten days after creation. Deliveries appear in the history once they finish and are archived; this can take a few seconds. There is no manual resend.
If you change the receiving URL, coordinate the change first. Pending deliveries to the previous URL are cancelled, not silently redirected. Disabling the subscription cancels pending deliveries; requests already in flight may finish.
Data handling
Notifications may contain conversation content, user identifiers and channel metadata. Subscribe only to what you need and apply your own access controls, retention and erasure policy to the data you receive. Aunoa cannot recall copies already delivered to your systems.
Events are limited to 1 MiB. Avoid returning personal data or secrets in your acknowledgement: Aunoa records response bodies up to 16 KiB for troubleshooting. Delivery history is retained for ten days from delivery creation. Deleting a user or channel does not cancel pending webhook deliveries or remove their history. User, channel and project deletion is not propagated to the archive; it expires through its retention policy. Disabling or removing a webhook destination still cancels its pending deliveries.
Event catalog
Only generic internal events are available. State-specific events and CustomEvents are excluded; onError is not available. Payloads vary by event and origin, as shown below.
The examples below show the event-specific part of data. The four fields message, payload, keyword and response are present in actual notifications; empty or null values retain their original meaning. Channel metadata is populated when available.
onMessageReceived
A user message enters bot processing. Synthetic messages from event/cloud/trigger/campaign paths are excluded by existing loop guards.
message is the incoming message (text, profile, channel data and original provider data as available); payload and keyword are the current bot values.
Illustrative data (optional fields omitted for readability):
{
"message": {
"id": "message-uuid",
"text": "Hello",
"payload": "",
"profile": {
"name": "Ana"
}
},
"payload": "",
"keyword": "Hello",
"response": null
}
onMessageSent
The bot processes an outgoing message, before continuing the outgoing flow. This is not a provider delivery receipt.
The outgoing content is in response; message and payload are empty strings.
Illustrative data (optional fields omitted for readability):
{
"message": "",
"payload": "",
"keyword": "welcome",
"response": {
"text": "Hello Ana"
}
}
onNewUser
A new user is created, including creation during identity remapping.
Same dispatch shape as an incoming message. Profile fields depend on the channel.
Illustrative data (optional fields omitted for readability):
{
"message": {
"id": "message-uuid",
"text": "Hello",
"profile": {
"name": "Ana"
}
},
"payload": "",
"keyword": "Hello",
"response": null
}
onCcareStart
A customer-care conversation opens.
Bot origin: payload contains idconversation, idchannel, takeadmin, adminame and adminemail; message is the conversation ID. API origin: message is the conversation ID and payload contains idconversation. adminame is the existing field spelling.
Illustrative data (optional fields omitted for readability):
{
"message": 456,
"payload": {
"idconversation": 456,
"idchannel": 25,
"takeadmin": 7,
"adminame": "Agent",
"adminemail": "agent@example.com"
},
"keyword": "",
"response": ""
}
onCcareEnd
A customer-care conversation closes for a reason other than the bot timeout event.
payload is the conversation record. Bot origin has an empty message; API origin puts the close reason in message and adds adminemail and adminame to the record. See conversation variants below.
Illustrative data (optional fields omitted for readability):
{
"message": "",
"payload": {
"idconversation": 456,
"status": 0,
"close_reason": "resolved"
},
"keyword": "",
"response": ""
}
onCcareExpired
The bot closes a customer-care conversation because its timeout expires.
payload is the bot conversation record; message is empty.
Illustrative data (optional fields omitted for readability):
{
"message": "",
"payload": {
"idconversation": 456,
"status": 0,
"close_reason": "timeout"
},
"keyword": "",
"response": ""
}
onCcareAgentAssigned
An agent is assigned when a conversation opens or through a later assignment.
message is the conversation ID. Payload fields are idconversation, idchannel, takeadmin, adminame, adminemail.
Illustrative data (optional fields omitted for readability):
{
"message": 456,
"payload": {
"idconversation": 456,
"idchannel": 25,
"takeadmin": 7,
"adminame": "Agent",
"adminemail": "agent@example.com"
},
"keyword": "",
"response": ""
}
onCcareMessage
The bot customer-care message path emits a message event, unless that emission is explicitly suppressed.
payload is the conversation ID, not the message object. message is text or an object with message/payload and attachment fields where available. This event does not itself guarantee the message is public.
Illustrative data (optional fields omitted for readability):
{
"message": "Hello",
"payload": 456,
"keyword": "",
"response": null
}
onCcareMessageAgent
An agent message is recorded through the API.
message is the outgoing agent content; payload is the API conversation record with adminemail. extraparams is passed separately. This path can also handle private notes; use subscriptions according to the intended audience.
Illustrative data (optional fields omitted for readability):
{
"message": "How can I help?",
"payload": {
"idconversation": 456,
"adminemail": "agent@example.com"
},
"keyword": "",
"response": ""
}
onHandoverPassControl
Facebook or Instagram reports passing conversation control.
message carries the incoming event, including original provider data; other arguments are empty strings.
Illustrative data (optional fields omitted for readability):
{
"message": {
"channel": "FACEBOOK",
"original": {
"entry": [
{
"messaging": [
{
"pass_thread_control": {
"new_owner_app_id": "123"
}
}
]
}
]
}
},
"payload": "",
"keyword": "",
"response": ""
}
onHandoverRequestControl
Facebook or Instagram reports a request for conversation control.
Same shape as pass-control, with provider request_thread_control data.
Illustrative data (optional fields omitted for readability):
{
"message": {
"channel": "INSTAGRAM",
"original": {
"entry": [
{
"messaging": [
{
"request_thread_control": {
"requested_owner_app_id": "123"
}
}
]
}
]
}
},
"payload": "",
"keyword": "",
"response": ""
}
onHandoverTakeControl
Facebook or Instagram reports taking conversation control.
Same shape as pass-control, with provider take_thread_control data.
Illustrative data (optional fields omitted for readability):
{
"message": {
"channel": "FACEBOOK",
"original": {
"entry": [
{
"messaging": [
{
"take_thread_control": {
"previous_owner_app_id": "123"
}
}
]
}
]
}
},
"payload": "",
"keyword": "",
"response": ""
}
onHandoverDoTakeControl
The bot leaves standby through its customer-care exit keyword.
message is the current incoming message; the other arguments are empty strings.
Illustrative data (optional fields omitted for readability):
{
"message": {
"text": "end",
"channel": "FACEBOOK"
},
"payload": "",
"keyword": "",
"response": ""
}
onCallStarted
An answered call enters the voice flow, including supported manual outbound calls.
message is BOT_CALL_ANSWER; payload is the common voice object described below. end is the snapshot time at this emission, not proof that the call ended.
Illustrative data (optional fields omitted for readability):
{
"message": "BOT_CALL_ANSWER",
"payload": {
"requestId": "request-uuid",
"callId": "call-123",
"answered": true,
"direction": "outbound"
},
"keyword": "",
"response": null
}
onCallFinished
The voice flow finishes, including successful external REFER handoff.
message is BOT_CALL_FINISHED; payload is the common voice object with closure/transfer information. A REFER handoff can finish the bot leg while the external call continues.
Illustrative data (optional fields omitted for readability):
{
"message": "BOT_CALL_FINISHED",
"payload": {
"requestId": "request-uuid",
"callId": "call-123",
"duration": 42,
"ended_by": 0,
"ended_by_label": "user",
"hangup_cause": 16
},
"keyword": "",
"response": null
}
onCallTransfered
A call transfer is confirmed: agent, external REFER or three-way fallback.
The existing name has one r in Transfered. Common voice payload plus optional transfer fields; agent transfer also supplies agent. This is distinct from agent-to-agent onAgentTransfer.
Illustrative data (optional fields omitted for readability):
{
"message": "BOT_CALL_TRANSFERED",
"payload": {
"requestId": "request-uuid",
"callId": "call-123",
"referred": true,
"transfer_type": "agent",
"transfer_result": "success",
"agent": "7"
},
"keyword": "",
"response": null
}
onCallRejected
A call is rejected by a limit or fails in the individual outgoing path.
Outgoing: requestId, reason, hangup_cause, whatsapp, direction, country_prefix, country_name, callId, sender, recipient, ring_duration. Limit variant: requestId, reason=limit, current, limit, soft_limit, channel/country/direction and call/interlocutor fields. A pre-call rejection can lack a call ID.
Illustrative data (optional fields omitted for readability):
{
"message": "BOT_CALL_REJECTED",
"payload": {
"requestId": "request-uuid",
"callId": "",
"reason": "failed",
"hangup_cause": "expired",
"direction": "outbound"
},
"keyword": "",
"response": null
}
onAgentTransfer
An agent-to-agent transfer attempt closes with its result.
Payload contains requestId, idconversation, from_agent, to_agent, result, ccare_desync (boolean). The result must be checked before assuming success.
Illustrative data (optional fields omitted for readability):
{
"message": "BOT_AGENT_TRANSFER",
"payload": {
"requestId": "request-uuid",
"idconversation": 456,
"from_agent": 7,
"to_agent": 8,
"result": "ok",
"ccare_desync": false
},
"keyword": "",
"response": null
}
Conversation record variants
Common fields: idconversation, idbot, idchannel, userid, status, tags, topics, created, takeadmin, closedby, close_reason, finished, lastfrom, lastmessage, channel, botname, adminname, isnew, sender, recipient. Database timestamps in these records are Unix seconds. Types follow the producing path: IDs may be numbers or numeric strings, and absent information may be null.
Bot-origin records also contain adminemail; tags and topics may remain serialized JSON strings. API-origin records parse JSON tags/topics when possible and include username, extraparams and user. The user object can contain userid, channel, sender, recipient, name, locale, avatar, logged, msisdn, email. API end/message events add the acting administrator's email. Do not assume all conversation variants are identical.
Common voice object
Used by onCallStarted, onCallFinished and onCallTransfered:
| Fields | Meaning |
|---|---|
requestId, callId, idconversation |
Original request/call identity and customer-care conversation (0 if unavailable). |
start, end |
Unix milliseconds; end is the emission snapshot. |
duration, ring_duration |
Seconds. |
answered, whatsapp |
Boolean flags. |
by, ended_by, ended_by_label |
Closure metadata: 0/user, 1/system, 2/timeout, 3/trigger. Before closure these are defaults. |
hangup_cause, extension, direction |
Cause, extension and call direction; may be empty. Cause may be numeric or a string. |
country_prefix, country_name |
Country metadata, empty when unavailable. |
provider, model, idPrompt |
AI provider/model/prompt metadata where applicable. |
input_tokens, output_tokens, total_tokens |
Token totals. |
input_text_tokens, input_audio_tokens, input_cached_tokens, output_text_tokens, output_audio_tokens |
Token breakdown, zero when unavailable. |
referred, transfer_type, transfer_result, redirect_destination, refer_uri, transfer_call_id |
Optional transfer block. Types include agent, sip_refer, three_way. |
agent |
Assigned agent for the agent-transfer variant. |