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
-
Create an HTTPS endpoint in your backend that accepts
POSTrequests, verifies the signature, stores or enqueues the event, and returns2xx. -
Check which content types emit events for a Connected Profile.
GET /v1/connected-profiles/{connectedProfileId}/capabilities -
Create an Event Destination with the topics you want.
POST /v1/event-destinations -
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.webhookdelivers HTTPSPOSTs; queue and stream transports (SQS, Pub/Sub, Kafka, and others) are also available.GET /v1/event-destination-typeslists them with the schema of each one'sconfigandcredentials.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 needsurl; it may also setcustom_headers, a JSON-encoded object of headers to add to each delivery.credentials— write-only. For a webhook,secretis 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 thanscope.
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" }}idis stable per logical event: dedupe on it.topicsays what happened; the Events reference has the schema of each topic'smetadataanddata.metadatacarries routing context.modeisliveortest, so one endpoint can serve both modes. Acontent.*event's metadata also names the observing profile:connectedProfileId,providerId,contentType, andproviderProfileExternalId, the profile's own provider id.- A
content.*event'sdatais the content type's schema — the same shapeGET /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.