Callbacks overview
Callbacks are HTTP requests the IM service sends to your server, for two kinds of scenarios:
- Event callbacks: after something changes in the app (a user registers, two users become friends, someone joins a group, a message is sent, a call ends), your server is told about it. The copy sent for every message is called message delivery. Use them to sync business data, archive messages or tally call durations.
- Sync callbacks: before a user sends a message, adds a friend or joins a group from a client, your server is asked whether to allow it. Your server can allow, reject or rewrite the message content, for example to let only employees of the same company message each other, or to gate entry to a paid group.
Your server is the receiver: configure the receiving address in the console and implement one HTTPS endpoint; you don't need to call any API.
| You want to | See |
|---|---|
| Write a working receiver (Python, Go, Node.js) | Verify callback requests; already built into the Go, Python and Node.js server SDKs |
Look up when an event is sent and what its data holds | Event callbacks |
| Decide before messages, friend requests and group joins whether to allow them | Sync callbacks |
| Add addresses, choose events and turn on sync callbacks in the console | Callback configuration |
| Understand retries, deduplication, backlogs and automatic disabling | This page: Delivery and retries |
Two kinds of callbacks
| Event callbacks | Sync callbacks | |
|---|---|---|
| When sent | After the change is committed, usually within 1 to 2 seconds | Before the operation is written, while the caller waits for your response |
| Covers | Changes to users, sign-in, presence, friends, groups, messages (delivery, recall, edit), chat rooms, audio/video, moderation and files, see Event callbacks | Before sending messages, before sending chat room messages, before adding friends, before joining groups, see Sync callbacks |
| Your response | Any 2xx means received; the response body is ignored and can't undo what happened | 2xx with JSON that decides to allow, reject or rewrite |
| On failure | Retried automatically with backoff for up to 24 hours, then can be redelivered manually | Requested once with no retry; on timeout or error, allowed or rejected per your setting |
| Reliability | At least once, possibly duplicated, not ordered. Presence and chat room message delivery and recall are best effort, see Best-effort events | One request per operation; a client retrying the same operation triggers another request |
| Subscribing | Each callback address chooses the events it receives | Each sync callback chooses one callback address and is turned on |
Recommended order for setting up:
- Deploy a receiver at a publicly reachable HTTPS address, implementing signature verification and the URL verification below first.
- Create the callback address in the console and save the signing secret, which is shown only once. On saving, a URL verification is sent to the address right away, and the address becomes "Active" once it passes.
- Use "Test send" in the console to get signature verification and event handling working.
- Subscribe to the events you need. For sync callbacks, turn them on with "allow on failure" first, watch latency and failures in the console for a while, and switch to "reject on failure" only if needed once the receiver is stable.
Callback addresses
Each app can have up to 5 callback addresses. Each has its own signing secret, subscribed events, filters and rate settings, independent of the others: when two addresses subscribe to the same event, each delivers and retries on its own. A common setup is one address for business events, one for high-volume message delivery with batching on, and a separate fast address for sync callbacks.
| Setting | Default | Description |
|---|---|---|
| Name | — | 1 to 64 characters, to tell addresses apart in the console |
| URL | — | The receiving address, see Address requirements |
| Subscribed events | — | 1 to 50 items: full event types or prefixes like group.*, see Subscribing to events |
| Filters | None | Skip events caused by your server, skip events cascading from account deletion and so on, see Filters |
| Events per batch | 1 | The most events merged into one request, 1 to 100, see Multiple events in one request |
| Timeout | 5000 ms | How long to wait for an event callback response, 1000 to 10000 ms |
| Concurrency limit | 10 | Simultaneous event callback requests, 1 to 50 |
| Requests per second | 100 | 1 to 1000, no more than the app's limit (500 requests per second by default across all addresses, set by the platform) |
Events beyond the concurrency and rate limits wait in the queue and are just sent a little later, not lost. Setting changes usually take effect within a second, at most 30 seconds.
Address status
| Status | Meaning | Event callbacks | Sync callbacks |
|---|---|---|---|
| Pending verification | Just created or URL just changed, not yet through URL verification | No deliveries are created; events in this period aren't sent later | No requests; treated as failures |
| Active | Passed URL verification | Delivered normally | Called normally |
| Paused | You paused it in the console, for example while the receiver is down for maintenance | Queued as usual but not sent; sending resumes when unpaused | Called as usual |
| Automatically disabled | Disabled by the system after 24 hours of continuous failure, see Continuous failure and automatic disabling | No new deliveries; queued ones aren't sent | Called as usual |
| Disabled by the platform | The platform disabled the address (usually because it pointed at a third party that isn't yours); only the platform can restore it | No new deliveries | No requests; treated as failures |
Address status mainly controls event callbacks. When a sync callback's address is "Pending verification" or "Disabled by the platform", no request is sent and the sync callback's failure policy applies, see Handling failures; "Paused" and "Automatically disabled" don't affect sync callbacks.
Address requirements
The IM service sends requests outward on your behalf, so to prevent addresses being used to probe the platform's internal network or attack third parties, callback addresses must:
- Use
httpsonly, with a valid certificate (not self-signed, expired or mismatched) and TLS 1.2 or later. - Use a public domain name as host: not an IP address (including decimal, hex and other variant forms),
localhost, a single label without a dot, names ending in.local,.internalor.localhost, or domains the platform forbids. - Use port 443 or 1024 to 65535.
- Have no username and password (
user:pass@) and no#fragment; query parameters are allowed; at most 1024 characters. - Resolve only to public IPs when saved; each request also checks the IP actually being connected to and gives up on non-public addresses, recording a failure (
blocked_address), so pointing the domain at an internal address after saving doesn't work either. - Not rely on redirects: any
3xxis recorded as a failure (redirect).
Disallowed addresses include loopback, private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), link-local (169.254.0.0/16, including cloud metadata addresses), carrier-grade NAT (100.64.0.0/10), multicast, reserved and documentation ranges, and their IPv6 equivalents. If the receiver shares an internal network with your other services, receive callbacks through a public HTTPS address (such as a reverse proxy or load balancer).
Invalid URLs return 400 invalid_argument on saving, with details.reason url_not_allowed and the reason in details.rule:
rule | Cause |
|---|---|
scheme | Not https |
host | The host is an IP address or internal name, or the URL is malformed |
port | The port isn't 443 or 1024 to 65535 |
userinfo | Has a username and password |
fragment | Has # |
length | Longer than 1024 characters |
denied_host | A domain the platform forbids |
disabled_host | This host name has a callback address disabled by the platform |
private_address | The domain resolves to a non-public address |
resolve_failed | The domain couldn't be resolved |
masked_query | A query parameter's value is ***, see below |
Tokens in query parameters: you can put the receiver's own token in a URL query parameter (such as ?token=xxx). URLs shown in the console, operation logs and the platform replace each query parameter's value with ***, keeping only the names, so when changing a URL you must enter the full new URL rather than submitting the displayed one.
URL verification
After creating an address or changing its URL, the address is "Pending verification" and a URL verification request is sent right away to prove you control it. The address must return 2xx within 5 seconds with a JSON object body whose challenge equals the one in the request (other fields allowed, at most 4 KB read). Once it passes, the address becomes "Active", starts receiving events, and can be bound to sync callbacks.
A request (X-IM-Kind is verify):
POST /im/callback HTTP/1.1
Host: your-server.example.com
User-Agent: IM-Callback/1
Content-Type: application/json; charset=utf-8
X-IM-Kind: verify
X-IM-Request-Id: 100310966629040128
X-IM-Endpoint-Id: 100310966557736960
X-IM-Timestamp: 1791141598
X-IM-Nonce: c90a695d9294c5f757dd88350e0e3187
X-IM-Signature: v1=2e5cebe9c6c67af85d62c4cc0250579223acc3e89549eabf036d3c523cb08610
{"kind":"verify","request_id":"100310966629040128","app":"6195295143#doc-callback","endpoint_id":"100310966557736960","sent_at":"2026-10-04T19:19:58.132Z","challenge":"J9Kk_hKp_H74ZScznGSzU_-kL8b4r1MTACmzTFsNVP4"}Your receiver verifies the signature and returns:
{ "challenge": "J9Kk_hKp_H74ZScznGSzU_-kL8b4r1MTACmzTFsNVP4" }challenge is different every time. If verification fails (timeout, connection failure, non-2xx, wrong challenge), the address stays "Pending verification"; fix the receiver and click "Verify" in the console to send it again. URL verification and test sends share a limit of 20 per app per minute, including verifications triggered automatically by creating addresses and changing URLs; when it's used up, addresses still save but no verification is sent, so click "Verify" again later.
Egress IPs
The platform can send callback requests from fixed egress IPs, listed on the console's "Callbacks" page, so your receiver's firewall can allow only them. When no egress IPs are listed, requests come from varying IPs, so rely only on signatures to confirm where requests come from.
Request format
All callbacks are POST requests with UTF-8 JSON bodies.
Request headers
| Header | Description |
|---|---|
Content-Type | application/json; charset=utf-8 |
User-Agent | IM-Callback/1 |
X-IM-Kind | The kind of request: event event callback, hook sync callback, verify URL verification |
X-IM-Hook | The sync callback type, such as message.before_send; only when X-IM-Kind is hook |
X-IM-Request-Id | The request ID, different for every send (retries included), matching the console's request records, for troubleshooting |
X-IM-Endpoint-Id | The callback address ID |
X-IM-Timestamp | Unix time of sending, in seconds |
X-IM-Nonce | A random 32-hex-character value, different for every send |
X-IM-Signature | The signature, v1= plus 64 hex characters; two, comma-separated, during a secret rotation grace period, see Signature algorithm |
HTTP headers are case-insensitive, and some frameworks show them as X-Im-Kind or x-im-kind. The kind, request ID and address ID also appear in the body, protected by the signature; the header copies are only for routing without parsing the body, so base business decisions on the body.
Request body
All three kinds of request body share these fields:
| Field | Type | Description |
|---|---|---|
kind | String | event, hook or verify, the same as X-IM-Kind |
request_id | String | The request ID, the same as X-IM-Request-Id |
app | String | The app's AppKey, such as 6195295143#demo, to tell apps apart when they share a receiver |
endpoint_id | String | The callback address ID |
sent_at | String | When it was sent, see Time format |
Event callbacks also have an events array, sync callbacks hook, timeout_ms, test and data (see Sync callbacks), and URL verification challenge.
An event callback for someone joining a group:
POST /im/callback HTTP/1.1
Host: your-server.example.com
User-Agent: IM-Callback/1
Content-Type: application/json; charset=utf-8
X-IM-Kind: event
X-IM-Request-Id: 100311905645625344
X-IM-Endpoint-Id: 100310966557736960
X-IM-Timestamp: 1791141822
X-IM-Nonce: 36e818ae8f4cd9fdaa24db6ca521475d
X-IM-Signature: v1=f7be9e46c15142401297376147fdea08c762c6ca7364...{
"kind": "event",
"request_id": "100311905645625344",
"app": "6195295143#demo",
"endpoint_id": "100310966557736960",
"sent_at": "2026-10-04T19:23:42.011Z",
"events": [
{
"id": "100311903162597376",
"type": "group.members_added",
"occurred_at": "2026-10-04T19:23:41.419Z",
"attempt": 1,
"test": false,
"data": {
"origin": "client",
"group_id": "100311903108071424",
"member_version": 2,
"member_count": 2,
"members": [
{
"username": "carol",
"role": "member",
"joined_via": "create",
"inviter": "alice",
"joined_at": "2026-10-04T19:23:41.409Z"
}
],
"operator": "alice"
}
}
]
}Each item in events is one event:
| Field | Type | Description |
|---|---|---|
id | String | The event ID, the same across retries and redeliveries, and for every address subscribed to the event. Deduplicate by it |
type | String | The event type, such as group.members_added, see Event list |
occurred_at | String | When the event happened |
attempt | Number | Which send this is for the event, from 1; resets to 1 after redelivery |
test | Boolean | Whether it's a test send from the console. Test events use sample data, so receivers can skip business actions |
data | Object | The event content, with fields depending on the type, see Event callbacks |
Multiple events in one request
Events are always in the events array, batched or not. When an address's "events per batch" is above 1, a request carries up to that many due events with a body of at most 1 MB; events in a batch aren't guaranteed to be in order. A batched request succeeds or fails as a whole: on a non-2xx, every event in the batch is retried. From the 3rd send on, events are no longer batched and are sent one by one, so one event that fails to process doesn't hold back the rest.
Turn on batching for high-volume events like message delivery. For example, an app with 30,000 messages a minute has about 500 events a second, which takes only about 5 requests a second at 100 per batch.
How data is written
- Users are usernames, never internal user IDs; deleted users are
null. - IDs are strings, such as
group_id,message_idandcall_id. - Times are UTC with exactly three millisecond digits, such as
2026-10-04T19:23:41.409Z. The exception is sign-in session times (created_atinuser.session_created,revoked_beforeinuser.sessions_revoked,sessions_revoked_beforeinuser.relogin_required), which have six microsecond digits, such as2026-10-04T19:21:11.735215Z; don't truncate them when comparing. - Message
bodyandextare included as is, see Message format. - Fields and event types will be added, so receivers must ignore unknown fields and event types rather than returning errors.
- Size: an event's
datais at most 70 KB. If it would exceed that, content and list fields (such asbody,ext,members) are dropped, keeping only IDs, usernames, types, statuses, reasons, versions and times, with"truncated": trueadded. When you receive one, query the current state with OpenAPI using its IDs.
origin: who caused the event
Every event's data starts with origin, saying who caused the change:
origin | Meaning |
|---|---|
client | A user's action in a client, such as sending a message, adding a friend or leaving a group |
server | Your server (OpenAPI) or your actions in the console, and moderation actions your reviewers decided |
platform | A platform administrator's action, such as blocking a file or banning a user |
system | Something the system did automatically, such as cleaning up friends and group memberships after an account is deleted, automatic moderation actions, or the call record written after a call |
null | Only presence.changed has no origin |
You already know the results of changes your server caused. Turn on the address filter "skip events caused by the tenant's server" to stop receiving events with origin server (except recall events), see Filters.
Responses
| Request | Success condition | Response body read |
|---|---|---|
| Event callback | Any 2xx within the address's timeout (5 seconds by default); every event in the batch counts as delivered | Ignored; at most 16 KB read |
| URL verification | 2xx within 5 seconds with the correct challenge in the body | At most 4 KB; more counts as failure |
| Sync callback | 2xx with valid JSON within the wait time, see Sync callback responses | At most 64 KB; more counts as failure |
Everything else is a failure: 3xx (redirects aren't followed), 4xx, 5xx, timeouts, connection failures and TLS handshake failures. When signature verification fails, return 401.
When receiving event callbacks, verify the signature, deduplicate by event ID and store the event, return 2xx immediately, and process asynchronously in your own queue. Doing slow business logic inside the callback request makes it time out, the event gets resent as a failure, and the load on your receiver grows.
Delivery and retries
Retries
Failed event callbacks are retried at these intervals, each randomized by ±20% so many events don't retry at once:
| After failure | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Wait | 10 s | 30 s | 1 min | 5 min | 15 min | 30 min | 1 h | 2 h | 4 h | 8 h | 8 h |
- When the receiver returns
429or503with aRetry-Afterheader (seconds or an HTTP date), the larger of that (up to 1 hour) and the interval above is used. During maintenance, return503withRetry-Afterto have events come back later. - Events not delivered within 24 hours of being queued become delivery failed, after at most 12 sends. Failed events are kept for 7 days and can be redelivered in the console.
- Every retry is a new request: request ID, timestamp, nonce and signature are all regenerated; the event ID stays and
attemptincreases by 1.
Circuit breaker: after 5 consecutive failures, sending to an address pauses for 30 seconds, then one request is let through as a probe: success resumes sending, failure doubles the pause, up to 5 minutes. Events queue as usual during the pause and it doesn't count as sends. A receiver that's down entirely isn't hammered by requests bound to fail.
Duplicates and ordering
- At least once: the same event can arrive more than once (for example when the receiver processed it but didn't return in time). Deduplicate by event
id, keeping records for at least 24 hours; keep them for 7 days to be safe when redelivering events that failed days ago. - No ordering guarantee: retried events can arrive after later ones, and events in a batch aren't in order. Use the versions in events (such as
versioninfriend.added,member_versioningroup.members_added,versioninpresence.changed) andoccurred_atto tell old from new, and don't let old events overwrite newer state. - Events are snapshots: for example the nickname in
friend.addedis the one at the moment they became friends, and later changes don't update events already queued. Query OpenAPI for current state.
Best-effort events
Presence (presence.changed) and chat room message delivery (chatroom.message_sent) and recall (chatroom.message_recalled) are high volume, so they're buffered in server memory for about a second before being written to the delivery queue in bulk. If a service instance crashes or the buffer is full during that time, those events are lost and not resent. Once in the delivery queue, delivery and retries work like any other event. For accurate state, query OpenAPI (such as presence).
Backlog limit
Each address holds at most 500,000 pending events. When a receiver is down long enough to fill that, new events aren't queued but dropped, counted as "dropped" in console statistics, and the tenant's owner and admin are notified (at most once per 24 hours). An address with 500 events a second fills up in about 17 minutes of downtime, so turn on batching and raise concurrency for high-volume addresses. Lost message deliveries can be filled in with Export the app's messages.
Continuous failure and automatic disabling
- When an address has been failing since its last success for 1 hour without a single success, the tenant's
ownerand alladmins are notified (by email if verified, otherwise SMS) that it will be disabled in 24 hours. - After 24 hours without a success and with failed requests still in the last hour, the address is automatically disabled: no new deliveries are created for it, queued ones aren't sent (becoming delivery failed when due), and another notification is sent.
- Only event callback failures count; sync callback, URL verification and test send failures don't trigger warnings or disabling. Nothing is sent while paused, so that doesn't count as failure.
After fixing the receiver, click "Resume" in the console: a URL verification is sent first, and once it passes the address is active again; then redeliver events that failed before disabling. Events that happened while disabled weren't queued and aren't resent; fill them in with OpenAPI or Export the app's messages if needed.
Delivery records and redelivery
Callback address details in the console show:
| Record | Content | Kept |
|---|---|---|
| Delivery queue | Each event's delivery to this address: status (pending, delivered, failed), send count, latest error and status code; filter by event type, look up by event ID, and view the event's data (without body and ext for message events) | Delivered: 1 hour. Failed: 7 days |
| Request records | Each request sent: kind, result, status code, latency, the receiver's IP, and for failures the error and the start of the response. URL verifications, test sends, redeliveries and failed requests are nearly all recorded, successful ones sampled; event content isn't recorded | 7 days |
| Statistics | Hourly request counts, failures, timeouts, latency, delay from event to successful delivery, and per event type queued, succeeded, failed and dropped counts | 90 days |
Redelivery: failed events can be redelivered one by one in the console, or in bulk by failure time range (up to 7 days, optionally only some event types), up to 10,000 at a time; submit again for the rest. Redelivered events go back to pending, the send count resets and the 24-hour window restarts. The address must be "Active" or "Paused" (queued while paused and sent on resuming); automatically disabled addresses must be resumed first.
Message content retention: once delivered, message content is cleared from the delivery queue; for message events that failed, body and ext are cleared after 24 hours. When redelivered later, the current message content is looked up by message ID: if the message was recalled, erased or expired, body and ext are null and content_available is false. Chat room messages aren't stored long-term, so their content can't be found once cleared.
When callbacks aren't sent
- While the tenant is suspended (including for unpaid bills) or in the account-closure cooling-off period, or the app is disabled or suspended by the platform, no new deliveries are created: events in that period aren't sent, even after recovery; already queued ones pause and resume afterwards, becoming delivery failed after 24 hours in the queue. Read-only apps get callbacks as usual. See App and tenant status.
- Deleting a callback address stops delivery immediately; queued events and delivery records are then deleted and can't be restored.
- Deleting the app or closing the tenant stops all callbacks.
Channel callbacks
Standalone channels have 9 rtc_channel.* events covering channel creation, updates and closing, sessions joining and leaving, and ban changes. Signatures, retries, batching and deduplication work as on this page, with fields in Channel events. Join tickets are issued by your server, so there's no "before joining a channel" sync callback.
