Channel webhooks
Everything that happens on your channels arrives as signed HTTP events. Register an endpoint once; consume idempotently.
Endpoints
curl -s -X POST https://api.agentsky.dev/v1/channels/webhooks \
-H "Authorization: Bearer $AST_TOKEN" -H 'content-type: application/json' \
-d '{"url": "https://your.app/hook", "events": ["message.received", "channel.connected"]}'
# -> returns the whsec_ signing secret ONCE — store itevents defaults to ["message.received"]. GET /v1/channels/webhooks lists endpoints; DELETE /v1/channels/webhooks/{id} removes one; PATCH /v1/channels/webhooks/{id} re-enables a failure-disabled endpoint (keeping its secret) or re-picks events. The URL must be https (http is allowed for localhost during development).
The developer console manages the same endpoints — the signing secret is shown exactly once, at creation:

Delivery format
Every delivery is a POST with a JSON body {id, type, createdAt, data} and these headers:
| Header | Meaning |
|---|---|
X-Asteroids-Event | the event type |
X-Asteroids-Delivery | unique delivery id — your dedupe key |
X-Asteroids-Timestamp | unix seconds, signed |
X-Asteroids-Signature | v1=<hex> — HMAC-SHA256 over {timestamp}.{rawBody} with your whsec_ secret |
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, timestamp, rawBody, signatureHeader) {
const expected = "v1=" + createHmac("sha256", secret).update(timestamp + "." + rawBody).digest("hex");
return timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}Failed deliveries retry at 0s/5s/30s. After ~20 consecutive failures the endpoint is disabled and events stop until you re-create it. Retries reuse the delivery id — consume idempotently by X-Asteroids-Delivery.
Event catalog
| Event | When |
|---|---|
message.received | an inbound message on a webhook-bound conversation — carries connection_id, thread_id, message_id, text, author, and your connection metadata |
message.reaction | an end user reacted to a message |
interaction.action | a button/select was tapped (platform spinners are acknowledged automatically) |
command.received | a slash command arrived on a connected surface |
channel.connected | a connect link was claimed — carries your metadata |
channel.needs_reauth | the platform surface needs re-authorization |
channel.disconnected | the surface was disconnected |
channel.unmapped_conversation | a message arrived on a connected surface with no active binding — bind or reply via the threads API |
turn.refused | a session-bound turn was refused (for example, out of credits) |
delivery.failed | a proactive delivery could not be posted |
Local development: the SSE mirror
curl -N https://api.agentsky.dev/v1/channels/events/stream \
-H "Authorization: Bearer $AST_TOKEN"GET /v1/channels/events/stream mirrors your webhook events as server-sent events — same payloads, no public URL or tunnel needed while developing. The developer console's Events tab renders the same stream live:

AgentSky