App events
An OAuth app can be told when an account installs it, changes what
it granted, disconnects it, or loses API access. Without these, an uninstall
reaches you only as a 401 on your next call.
The same URL can also receive the account events — calls, appointments, contacts, services, resources, locations, documents and account settings, working hours included — of every account that installed the app, so one receiver serves all your customers.
App events are separate from an account’s webhooks: they go to the app’s URL, not to one the customer configured, and no customer plan is involved.
Set up
Section titled “Set up”In the console, set the app’s events URL. The
same rules as a webhook endpoint apply: https://, port 443, public addresses
only. The signing secret (whsec_…) is generated the first time a URL is set and
shown once.
Changing the events URL does not send the app back to review.
Installation events
Section titled “Installation events”Always sent while the app has an events URL; there is nothing to subscribe to.
| Event | When | Extra data |
|---|---|---|
installation.created |
An account installs the app, or reinstalls it after a disconnect | — |
installation.scopes_changed |
An account reconnects with a different set of scopes. A reconnect with the same scopes sends nothing | previous_scopes |
installation.revoked |
The installation is disconnected | reason |
installation.access_suspended |
The account can no longer use the API | reason |
installation.access_restored |
It can again | — |
Every event carries data.installation:
{ "object": "installation", "id": "5d0e8a2c-7b41-4f7e-9c3a-1e2f4b6d8a90", "client_id": "talki_cid_4Hq2mZ8rVt1LxN3pYw7KcA", "account_id": "f7e39b9a-1ac8-45e4-93da-2600f17cb286", "scopes": ["appointments:read", "calls:read"], "status": "active", "access": "active", "livemode": true, "created_at": "2026-09-15T09:14:31.204518+00:00", "revoked_at": null}| Field | Type | Meaning |
|---|---|---|
object |
string | Always installation |
id |
string | The installation: one account’s connection to your app. Equal to the envelope’s installation_id |
client_id |
string | Your app’s client ID |
account_id |
string | The Talki account that installed the app |
scopes |
array of strings | The scopes the account granted, sorted |
status |
string | active, or revoked once the installation is disconnected |
access |
string | suspended while the account cannot use the API, otherwise active |
livemode |
boolean | false for a sandbox account’s installation |
created_at |
string | When the account first installed the app |
revoked_at |
string or null | When the installation was disconnected; null while it is active |
created_at and revoked_at are ISO 8601 with a +00:00 offset and may carry
microseconds, like the envelope’s created_at, not the Z form of the REST
API. Parse them as instants.
installation.scopes_changed adds previous_scopes, the scopes granted before
the reconnect, sorted; the new ones are in installation.scopes.
reason
Section titled “reason”On installation.revoked:
reason |
Who disconnected |
|---|---|
customer |
The account owner, from the Talki app |
partner |
Your app, by calling /oauth/revoke with one of its tokens |
account_deleted |
The account was closed |
On installation.access_suspended, reason is the error code your API calls
now receive: subscription_inactive, plan_upgrade_required or
account_closed. Stop syncing until installation.access_restored; the grant and
its tokens survive a suspension.
An access event can arrive up to 15 minutes after the change, and is sent only when the state actually changes.
Account events
Section titled “Account events”Subscribe
Section titled “Subscribe”In the console, open the app’s Events and tick the types it should receive. Each type is opt-in, and there is no wildcard: a type added to the catalogue later is not sent until you tick it.
A type is sent for an account only while its installation grants the type’s scope. A type whose scope the app does not request cannot be ticked.
| Event | Scope |
|---|---|
call.ringing |
calls:read |
call.ended |
calls:read |
call.transferred |
calls:read |
call.updated |
calls:read |
call.deleted |
calls:read |
appointment.created |
appointments:read |
appointment.updated |
appointments:read |
appointment.cancelled |
appointments:read |
contact.created |
contacts:read |
contact.updated |
contacts:read |
contact.deleted |
contacts:read |
pending_contact.created, .updated, .deleted |
contacts:read |
tag.created, .updated, .deleted |
contacts:read |
caller_rule.created, .updated, .deleted |
contacts:read |
unresolved_request.updated |
unresolved:read |
document.created, .updated, .deleted |
knowledge:read |
account.updated |
account:read |
working_hours.updated |
account:read |
closure.created, .updated, .deleted |
account:read |
service.created, .updated, .deleted |
services:read |
resource.created, .updated, .deleted |
resources:read |
location.created, .updated, .deactivated, .reactivated |
account:read |
location.settings_updated, location.working_hours_updated |
account:read |
data is the same as on a webhook; see the
event catalogue. usage.* and ping are not
offered to apps. A billing partner’s app receives usage.* for the accounts it
pays for without subscribing; see Managed billing.
The grant is checked when the event is written and again before each attempt. Once an installation is revoked or no longer grants the scope, or the account loses API access, its queued events are not sent.
Envelope
Section titled “Envelope”{ "id": "evt_3c7a1f0e9d2b4c6a8e5f1b0d7a2c4e6f", "type": "call.ended", "created_at": "2026-09-20T10:41:07.118204+00:00", "api_version": "2026-10-01", "account_id": "f7e39b9a-1ac8-45e4-93da-2600f17cb286", "livemode": true, "origin": { "source": "voice", "credential_id": null, "idempotency_key": null }, "installation_id": "5d0e8a2c-7b41-4f7e-9c3a-1e2f4b6d8a90", "data": { "object": { "id": "38d23d84-02d2-4418-b169-3608aa04a8f8", "…": "…" } }}installation_idis the connection the event belongs to: look up the customer and its token by it.account_idis the Talki account the event happened on.livemodeisfalsefor a sandbox installation.
Events can arrive out of order, and an account event can arrive before its
installation.created. Key on installation_id, and order events as on
webhooks: by the object’s updated_at, or the
envelope’s created_at for an object without one, never by arrival.
Echo detection
Section titled “Echo detection”A change your app makes through the API comes back as an event with
origin.source api, origin.credential_id set to your installation_id, and
origin.idempotency_key set to the request’s key. Drop the events whose
origin.credential_id is the installation’s own ID.
Other apps installed on the same account receive that event with
origin.source api and both credential_id and idempotency_key null.
Catching up
Section titled “Catching up”Events are not written while delivery is paused, while the app has no events URL, or for a type not yet subscribed. After a gap, read each installation’s change feeds from its last stored cursor, with that installation’s token, and upsert by Talki ID.
The same URL as a customer webhook
Section titled “The same URL as a customer webhook”If an account also has a webhook endpoint with the app’s events
URL, both deliver: the same change arrives twice, with different evt_ IDs.
Delete those endpoints once the app receives the events itself.
Installation events envelope
Section titled “Installation events envelope”The same envelope as webhooks, with two differences: account_id is the
installed account, and installation_id is added. origin is null.
{ "id": "evt_9b2f6c1e4a7d4e0f8c3b5a2d1e6f7a8b", "type": "installation.revoked", "created_at": "2026-09-20T16:02:11.482913+00:00", "api_version": "2026-10-01", "account_id": "f7e39b9a-1ac8-45e4-93da-2600f17cb286", "livemode": true, "origin": null, "installation_id": "5d0e8a2c-7b41-4f7e-9c3a-1e2f4b6d8a90", "data": { "installation": { "object": "installation", "status": "revoked", "…": "…" }, "reason": "customer" }}api_version is the app’s pin, on every app event.
Signatures and delivery
Section titled “Signatures and delivery”Signed and delivered exactly like webhooks:
the same headers, the same X-Talki-Signature, the same retries and timeouts.
The key is the app’s events secret. Use the same verifier, and accept any v1.
Rotating the events secret stops the old one at once unless you ask for an overlap (up to 7 days) when you rotate.
When nothing arrives
Section titled “When nothing arrives”- After 20 consecutive failed deliveries, event delivery to the app pauses,
and the app’s owner is emailed. Only failures at your receiver count: a non-2xx
answer (a
410 Goneincluded), the 10-second timeout, a connection, TLS or DNS failure, or a URL that no longer resolves to a public address. A delivery counts once all six attempts are spent, and one success resets the counter. Resume from the app’s events in the console. - Events from a pause, or from before an events URL is set, are not sent
later. For account events, catch up from the change feeds.
For installation events, the next access check after resuming reports the
current access state; installs and disconnects from the gap are not sent.
Reconcile them by calling the API with each installation’s token: a
disconnected one answers
401 invalid_credentials. - A disabled app receives nothing.
Delivery log
Section titled “Delivery log”Deliveries on the app, in the console, lists what was sent to the events URL in the last 30 days, newest first, filterable by status and installation. Each row shows the event ID and type, the installation, the attempts, and either your receiver’s status with the first 500 characters of its body, or an error class. The payload is not shown: fetch the object through the API.
Account events add three classes. None is retried, and none counts toward the pause:
| Class | Meaning |
|---|---|
installation_inactive |
The installation was revoked, or no longer grants the event’s scope |
event_unsubscribed |
The app no longer subscribes to the event type |
account_refused |
The account can no longer use the API |
Redeliver sends the stored event again with a fresh set of six attempts and
the same id. It answers 409 while the delivery is still being attempted,
while the app is paused, disabled or has no events URL, and for an account event
that one of the three classes above would stop now.
Daily digest
Section titled “Daily digest”A receiver that fails for one account while serving the others never pauses the app. Once a day, the app’s owner is emailed the installations whose deliveries failed at the receiver, each with a count, the last error and a link to its log. Live and sandbox installations are listed separately, and no digest is sent while the app is paused.
A digest covers the events created 36 to 12 hours before it is sent, when every
retry has finished, and 61 to 37 hours before for expired events. Refusals are
not counted.
