Subscribe to events

Receive provider content and Koil lifecycle events at your own endpoint.

An Event Destination tells Koil where to deliver events and which topics it receives. Koil delivers one signed stream per destination, whether a provider reported a change by webhook or Koil found it by polling.

Start here after you have connected at least one profile with Connect provider accounts.

How it works

  1. Create an HTTPS endpoint in your backend that accepts POST requests, verifies the signature, stores or enqueues the event, and returns 2xx.

  2. Check which content types emit events for a Connected Profile. GET /v1/connected-profiles/{connectedProfileId}/capabilities

  3. Create an Event Destination with the topics you want. POST /v1/event-destinations

  4. Store the returned destinationId.

Create a destination

curl -X POST "https://api.koil.co/v1/event-destinations" \  -H "Authorization: Bearer $KOIL_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "type": "webhook",  "topics": [    "content.instagram.media.comments.*",    "content.instagram.dm.messages.*",    "publish_request.*"  ],  "scope": {    "type": "connected_profiles",    "connectedProfileIds": [      "cp_123"    ]  },  "config": {    "url": "https://example.com/koil/events"  },  "credentials": {    "secret": "your-signing-secret"  }}'
  • type — the transport. webhook delivers HTTPS POSTs; queue and stream transports (SQS, Pub/Sub, Kafka, and others) are also available. GET /v1/event-destination-types lists them with the schema of each one's config and credentials.
  • topics — what this destination receives: exact topics, a family with a trailing .*, or * for everything.
  • scope — optional. Omit it, or send { "type": "organization" }, for every Connected Profile in the Organization, now and future. Send { "type": "connected_profiles", "connectedProfileIds": [...] } (at least one id) to receive only those profiles' events.
  • config — transport settings. A webhook needs url; it may also set custom_headers, a JSON-encoded object of headers to add to each delivery.
  • credentials — write-only. For a webhook, secret is the key deliveries are signed with. Choose it yourself and keep it in your secret store: no read returns it. If you omit it, one is generated, and you can see it in the Events dashboard of the admin console.
  • filter — optional, a match over the fields of the delivered event (topic, metadata, data) for finer routing than scope.

You can also create and manage destinations — and inspect deliveries, attempts, and retries — from the Events dashboard; it reads and writes the same destinations as the API.

A new destination's first events

A destination receives events from when Koil starts collecting what it covers, usually within a few minutes of creating it or widening its topics or scope. Content from before that is not delivered to it; use a backfill request for anything older.

Topics

The Events reference lists every topic Koil emits, with the schema of each delivery. Subscribe only to topics listed there; a topic that is not in the reference is never delivered. Subscribing to a family with .* also covers actions a content type gains later.

Event delivery is unlimited under fair use, and only content.* deliveries count toward it. Koil's lifecycle topics — publish_request.*, media.*, connection.*, connected_profile.*, event_destination.* — never count, so following your own publishes costs nothing.

The event

Every delivery is one JSON object with the same envelope, on every destination type — webhook, queue, or stream. This is the publish_request.succeeded event that follows the Quickstart's comment:

{  "id": "evt_47d81d80625d9958f21a275209d6d7ea127828bf",  "topic": "publish_request.succeeded",  "time": "2026-05-01T10:00:04.000Z",  "metadata": {    "organizationId": "org_01J9Z3QK7X2M4N8P6R5T0VWYAB",    "publishRequestId": "test_pub_7GhQ2dLkP0aZ1xYc9VwN",    "connectedProfileId": "test_cp_4T2cgH9gnSqEb9DrntKA",    "contentType": "youtube.videos.comments",    "status": "succeeded",    "mode": "test"  },  "data": {    "publishRequestId": "test_pub_7GhQ2dLkP0aZ1xYc9VwN",    "status": "succeeded",    "contentType": "youtube.videos.comments",    "connectedProfileId": "test_cp_4T2cgH9gnSqEb9DrntKA",    "externalId": "test_comment_1"  }}
  • id is stable per logical event: dedupe on it.
  • topic says what happened; the Events reference has the schema of each topic's metadata and data.
  • metadata carries routing context. mode is live or test, so one endpoint can serve both modes. A content.* event's metadata also names the observing profile: connectedProfileId, providerId, contentType, and providerProfileExternalId, the profile's own provider id.
  • A content.* event's data is the content type's schema — the same shape GET /v1/content/{contentType} returns — so an event and a read of the same item are interchangeable.

The routing fields are also sent beside the body — as x-koil-* headers on a webhook, as message attributes on a queue — for infrastructure that routes without parsing it. The body is complete without them.

How Koil learned of a change — a provider webhook, polling, or a backfill — is deliberately not part of the event: an event means the same thing however it was collected.

Koil delivers both sides of a conversation. Content the connected profile wrote itself — an agent replying in the provider's app, or a reply you published through Koil — arrives as a normal event with data.author.id === metadata.providerProfileExternalId. Inboxes should show it; automations that respond to events must skip it, or a bot will answer its own replies.

Receiving deliveries

Return 2xx only once the event is durably stored or enqueued; any other response, or a timeout, is retried. Delivery is at-least-once: retries, replays, and an item Koil observes again (a backfill, a provider resending a webhook) reuse the event id, and Koil suppresses a repeat only within a short window, so dedupe on the id. See Verify webhook deliveries for the signature check.

Persist what your product needs from the stream — inbox items, workflow state, analytics. Koil does not keep provider content, and there is no aggregate read across content types.

On this page