# Hooks — full documentation > Hooks is a webhook gateway for AI agents and automation: one stable HTTPS ingress per endpoint verifies signatures, stores the raw payload, filters and reshapes events, then fans them out asynchronously to HTTP or agent targets with retries and a replayable delivery log. This file concatenates every documentation page in order. Each section is also published as its own Markdown file under https://hooks.events/docs/. Generated from `apps/web/public/docs/*.md` by `pnpm --filter web llms:build`. Do not edit by hand. # Quickstart Hooks receives webhooks on a stable HTTPS ingress URL, verifies the signature, stores the raw request, and forwards the event to one or more destinations. Most integrations are live in under five minutes. 1. **Create an endpoint.** Pick a provider (GitHub, Stripe, Slack, Linear, or generic HMAC) and copy its ingestion URL. 2. **Verify the signature.** Confirm the request really came from the source before trusting the payload. 3. **Add a destination.** Forward verified events to an HTTPS endpoint or an agent target. ## Send a test event POST JSON to the endpoint's ingestion URL: ```bash curl -X POST "https://api.hooks.events/in/whk_9f3k1a" \ -H "Content-Type: application/json" \ -H "X-Hooks-Signature: t=1739980800,v1=8f2c..." \ -d '{"type":"order.created","data":{"id":"ord_1042"}}' ``` Hooks stores the event immediately and returns `202 Accepted`; fan-out to destinations happens asynchronously in a queue. ## What happens next - Every delivery attempt is recorded with its request, response status, and response body. - Failed deliveries retry automatically with exponential backoff. - You can replay an event or retry a delivery from the console or the REST API. > The ingestion URL is a secret. Anyone with it can create events, so keep it out of client-side code and public repositories. ## Next steps - [Receive events](./receive-events.md) — headers, accepted content types, response behavior. - [Verify signatures](./signatures.md) — HMAC scheme and provider-specific verification. - [Destinations](./destinations.md) — where verified events go. --- # Receive events Hooks accepts JSON and form-encoded payloads over HTTPS. It records the raw body and headers before applying verification rules, so you can inspect exactly what arrived. ## Request headers | Header | Description | | ------------------- | ----------------------------------------------------------------------------- | | `X-Hooks-Signature` | Timestamp and HMAC signature for the raw body. Verify this before processing. | | `X-Hooks-Event` | Unique event ID, useful for logging and support. | | `X-Hooks-Delivery` | Unique delivery ID. Reuse it as an idempotency key at your destination. | | `Content-Type` | `application/json` or `application/x-www-form-urlencoded`. | Provider-specific headers are preserved on the event as well, for example `X-GitHub-Event`, `Stripe-Signature`, or `Linear-Delivery`. ## Response behavior Hooks returns `202` as soon as the event is persisted. It does not wait for downstream services. - `202 Accepted` — the event was stored and queued for fan-out. - `4xx` — the request was rejected and no event was created. Check the endpoint's verification rules and signature configuration. - `429` — the endpoint's ingest rate limit was exceeded. Honor `Retry-After`. > Use the raw request body when computing signatures. Re-serializing JSON can change whitespace and key order, producing a false mismatch. ## Body handling - JSON bodies are parsed and stored as structured data. - `application/x-www-form-urlencoded` bodies are parsed into fields; a GitHub-style `payload=` field is unpacked into the embedded JSON. - Any other body is stored as text and is not normalized into structured fields. --- # How delivery works Every event follows the same path: ingest, verify, persist, fan out, retry. Each step is observable in the console and through the API. 1. **Ingest** — the source sends an HTTPS request to the endpoint's ingestion URL. 2. **Verify** — the provider adapter checks the signature, timestamp, and any source-specific rules. 3. **Persist** — the raw body, sanitized headers, and normalized event are stored before responding `202`. 4. **Fan out** — the event is queued once per active destination. 5. **Deliver** — the target adapter sends the request and records the outcome; failures are retried with backoff. ## Delivery guarantees - **At-least-once delivery.** A destination may receive the same event more than once, especially after a timeout. Deduplicate with `X-Hooks-Delivery`. - **Ordering is not guaranteed.** Each destination is retried independently. Use event timestamps if order matters to your system. - **Paused endpoints stop new deliveries.** Events still arrive and are stored, but fan-out waits until the endpoint is resumed. ## Filters and templates - Destination filters run at fan-out time. A rejected destination gets no delivery row, while the event stays stored for the audit trail. - Templates reshape the payload per delivery without a proxy. ## Related - [Retries & idempotency](./retries.md) - [Destinations](./destinations.md) - [Payload templates](./templates.md) --- # Endpoints An endpoint is an ingestion URL plus verification and routing rules. Create one per source, not per destination — that keeps retries and logs easy to reason about. ## Create from the console 1. Open **Endpoints** and choose **Create endpoint**. 2. Pick a provider so Hooks pre-fills the right signature header. 3. Copy the ingestion URL into the provider's webhook settings. 4. Add at least one destination and send a test event. ## Endpoint fields | Field | Notes | | ---------------- | --------------------------------------------------------------------------------------------------------- | | `name` | Human-readable label shown in logs and alerts. | | `provider` | Preset for signature verification and event headers: `generic`, `github`, `stripe`, `slack`, or `linear`. | | `signing_secret` | Generated on creation. Rotate it from endpoint settings. | | `status` | `active` or `paused`. Paused endpoints keep receiving events. | ## Ingestion URL ``` https://api.hooks.events/in/{endpoint-public-id} ``` The public id is stable for the life of the endpoint. Treat the URL as a secret: anyone with it can create events. ## Providers | Provider | Signature header | Notes | | --------- | --------------------- | ---------------------------------------------------------------------- | | `generic` | `X-Hooks-Signature` | Timestamp + HMAC over the raw body. | | `github` | `X-Hub-Signature-256` | HMAC-SHA256 over the raw body. | | `stripe` | `Stripe-Signature` | `t=...,v1=...` with a 5-minute tolerance. | | `slack` | `X-Slack-Signature` | `v0=` HMAC over `v0::`. | | `linear` | `Linear-Signature` | Hex HMAC over the raw body plus a 60-second `webhookTimestamp` window. | ## Test events Queue a synthetic event instead of sending traffic through the public ingress: ```bash curl -X POST "https://api.hooks.events/v1/endpoints/$ENDPOINT_ID/test-events" \ -H "Authorization: Bearer $HOOKS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"eventType":"agent.check"}' ``` --- # Destinations Destinations are where verified events go. Add several to one endpoint when different systems need the same event — each gets its own retry schedule and delivery history. ## Destination types | Type | Status | Description | | ----------------------- | --------- | -------------------------------------------------------- | | HTTPS endpoint (`http`) | Supported | JSON or form body, custom headers, bearer or basic auth. | | Grok Bot (`grokbot`) | Supported | Dispatch the event to a Grok Bot agent. | | Slack (`slack`) | Supported | Post to an incoming webhook or `chat.postMessage`. | | T3 Code (`t3code`) | Supported | Trigger a scheduled task (webhook automation). | | Paseo (`paseo`) | Supported | Dispatch a Hub manual run (agent run id recorded). | ## Add a destination ```bash curl -X POST "https://api.hooks.events/v1/endpoints/ep_7a3k2/destinations" \ -H "Authorization: Bearer $HOOKS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.acme.com/hooks/orders", "method": "POST", "auth": { "type": "bearer", "token": "$DOWNSTREAM_TOKEN" } }' ``` ## Filters A destination can accept only some events: ```json { "allow": [ { "field": "event.type", "op": "equals", "value": "issues.opened" }, { "field": "event.subject.labels", "op": "contains", "value": "agent" } ] } ``` Rules support `equals`, `not_equals`, `in`, `contains`, `matches`, and `exists` on fields such as `event.type`, `event.source`, `event.actor.login`, `event.repo`, `event.project`, and `event.subject.*`. Bot actors are ignored by default; set `ignoreBots: false` to include them. > Inbound secrets and cookies are never forwarded. Configure a separate outbound credential for each destination. --- # Payload templates Reshape an event before it reaches a destination without writing a proxy. Templates render per delivery and can mix normalized event fields, raw payload paths, headers, and constants. ## Variables | Variable | Resolves to | | ------------------------------ | --------------------------------------------------------------- | | `{{event.id}}` | Relay event ID. | | `{{event.type}}` | Unified event type, for example `issues.opened`. | | `{{event.source}}` | Source id: `github`, `stripe`, `slack`, `linear`, or `generic`. | | `{{event.actor.login}}` | Actor login/handle. | | `{{event.subject.title}}` | Subject title. | | `{{event.subject.labels.0}}` | Array index into subject labels. | | `{{event.raw.issue.html_url}}` | Any path in the original parsed payload. | | `{{event.payload}}` | Raw inbound body text. | | `{{delivery.id}}` | Delivery ID, safe to use as an idempotency key. | ## Filters | Filter | Example | Result | | ---------- | -------------------------------------------- | ---------------------------------- | | `default` | `{{event.actor.login \| default:"unknown"}}` | Fallback when the path is missing. | | `truncate` | `{{event.subject.title \| truncate:60}}` | First 60 characters. | | `join` | `{{event.subject.labels \| join:", "}}` | Joins arrays. | Missing variables resolve to an empty string by default. Strict mode turns a missing path into a failed delivery with the missing paths named, which is useful while building a template. ## JSON bodies When the destination body is JSON, resolved values are escaped so a title containing quotes or newlines stays valid JSON. Templates apply to the body, URL, headers, query parameters, and auth values. > Use a test event before enabling a template in production. --- # Verify signatures Every request includes an `X-Hooks-Signature` header. Verify it against the raw body using your endpoint's signing secret before acting on the event. The header contains a timestamp and one or more signatures: `t=1739980800,v1=8f2c…`. Compute `HMAC-SHA256(secret, timestamp + "." + rawBody)` and compare it in constant time. ```js const crypto = require("node:crypto"); function verify(rawBody, header, secret) { const parts = Object.fromEntries(header.split(",").map((part) => part.split("="))); const expected = crypto .createHmac("sha256", secret) .update(`${parts.t}.${rawBody}`) .digest("hex"); return crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)); } ``` ## Provider-specific schemes When the endpoint uses a known provider, Hooks verifies the provider's own signature instead of the generic scheme: | Provider | Header | Scheme | | -------- | --------------------- | ------------------------------------------------------------------------------------ | | `github` | `X-Hub-Signature-256` | `sha256=`. | | `stripe` | `Stripe-Signature` | `t=,v1=.">`, 5-minute tolerance, multiple `v1` accepted. | | `slack` | `X-Slack-Signature` | `v0=:">`, 5-minute tolerance. | | `linear` | `Linear-Signature` | Hex HMAC of the raw body plus a body `webhookTimestamp` within 60 seconds. | Provider signatures are checked against the endpoint secret you configured in the console. > Never compare signatures with `==`. Use a constant-time function and reject requests whose timestamp is more than five minutes old to limit replay attacks. --- # Secrets & rotation Each endpoint has its own signing secret. Treat it like a password: store it in a secret manager, scope it to one environment, and rotate it on a schedule. ## Rotation without downtime 1. **Add the new secret** to your verifier and accept both old and new signatures. 2. **Rotate in Hooks** from the endpoint's security settings, or call `POST /v1/endpoints/{id}/rotate-secret`. 3. **Wait for in-flight retries** to finish, then remove the old secret from your verifier. A workspace-level secret can sign every endpoint unless overridden. Secrets are encrypted at rest and never displayed in full after creation. > If a secret leaks, rotate immediately and review delivery logs for unexpected destinations or replay attempts. --- # Retries & idempotency Failed deliveries retry automatically with exponential backoff. Retries use the same delivery ID, so your destination can safely deduplicate them. | Attempt | Timing | What happens | | ------- | ----------- | ---------------------------------------------- | | 1 | Immediately | Initial request to the destination. | | 2 | +30 seconds | Retried after a timeout or `5xx` response. | | 3 | +2 minutes | Backoff doubles each attempt. | | 4 | +10 minutes | Still using the same delivery ID. | | 5 | +1 hour | Final attempt, then marked failed and alerted. | ## Deduplicate at the destination Store `X-Hooks-Delivery` in a unique index before processing. If the same ID arrives again, return `200` without repeating side effects. A `2xx` response stops retries immediately. Rate limits and timeouts also trigger retries. Return a `4xx` only when retrying cannot help — for example, a malformed payload or an unknown account. ## Manual retry and replay - `POST /v1/deliveries/{id}/retry` re-queues a failed or stuck delivery. - `POST /v1/events/{id}/replay` replays an event to its destinations. - `POST /v1/deliveries/{id}/cancel` cancels a queued delivery. Only replay events when the destination state is known to be safe; a replay creates new delivery attempts against the same destinations. --- # Rate limits Limits protect shared infrastructure and keep delivery latency predictable. They apply per workspace and reset on a rolling window. | Operation | Limit | Window | | ----------------------------------------- | -------------- | ---------- | | Ingest events (`/in/*`) | 1,000 requests | 60 seconds | | Read and write API (`/v1/*`) | 600 requests | 60 seconds | | Manual retries (`/v1/deliveries/*/retry`) | 60 requests | 60 seconds | Every API response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. When a limit is exceeded, the API returns `429` with a `Retry-After` header. Respect `Retry-After` instead of retrying immediately. Sources that retry on `429` keep events flowing once the window resets; events rejected with `429` are not stored. --- # REST API The API uses predictable resource URLs, JSON bodies, and bearer authentication. All requests must use HTTPS. - Base URL: `https://api.hooks.events/v1` - Authentication: `Authorization: Bearer hks_...` - Scopes: `endpoints:read`, `endpoints:write`, `deliveries:read`, `deliveries:retry`, `history:purge`. - Session-authenticated requests can select a workspace with `X-Workspace-Id: `. Reads fall back to the caller's own workspace when the id no longer belongs to them (stale `localStorage`); mutating requests return `403 AUTHORIZATION_DENIED` instead of silently writing elsewhere. - Mutating requests with a body must send `Content-Type: application/json`. ## Routes | Method | Path | Purpose | | ------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------- | | GET | `/v1/whoami` | Current user and project. | | GET | `/v1/endpoints` | List endpoints. | | POST | `/v1/endpoints` | Create an endpoint; body accepts `name`, `provider`, `verificationMode`, `secret`, `destinations[]`. | | GET | `/v1/endpoints/{id}` | Read one endpoint. | | PATCH | `/v1/endpoints/{id}` | Update an endpoint. | | DELETE | `/v1/endpoints/{id}` | Delete an endpoint. | | POST | `/v1/endpoints/{id}/rotate-secret` | Rotate the signing secret. | | POST | `/v1/endpoints/{id}/test-events` | Queue a test event; body accepts `eventType`. | | GET | `/v1/events` | List events. | | GET | `/v1/events/{id}` | Read one event. | | GET | `/v1/events/{id}/payload` | Read the stored raw payload. | | POST | `/v1/events/{id}/replay` | Replay an event to its destinations. | | GET | `/v1/deliveries` | List delivery attempts. | | GET | `/v1/deliveries/{id}` | Read one delivery. | | GET | `/v1/deliveries/{id}/attempts` | List attempts for a delivery. | | POST | `/v1/deliveries/{id}/retry` | Retry a delivery. | | POST | `/v1/deliveries/{id}/cancel` | Cancel a queued delivery. | | GET, POST | `/v1/api-keys` | List or create API keys. | | DELETE | `/v1/api-keys/{id}` | Revoke an API key. | | GET, POST | `/v1/workspaces` | List or create workspaces. | | DELETE | `/v1/workspaces/{id}` | Delete a workspace. | | GET, PATCH | `/v1/project` | Read or update the current project. | | GET | `/v1/project/secret` | Read project secret metadata. | | POST | `/v1/project/secret/rotate` | Rotate the project secret. | | GET | `/v1/members` | List workspace members. | | PATCH, DELETE | `/v1/members/{userId}` | Update or remove a member. | | POST | `/v1/invitations` | Invite a teammate. | | GET | `/v1/stats` | Workspace delivery statistics. | List endpoints are paginated with `?limit=50&cursor=...`. The default page size is 25. ## Example ```bash curl "https://api.hooks.events/v1/endpoints" \ -H "Authorization: Bearer $HOOKS_API_KEY" ``` Create an API key in **Settings → API keys**. Keys are scoped and can be revoked at any time. --- # Errors Errors return a JSON body with a stable code, a human-readable message, and a request ID for support. Never parse the message; branch on the code. ```json { "error": { "code": "SIGNATURE_INVALID", "message": "The request signature did not match the endpoint secret.", "requestId": "req_8f2c9a" } } ``` ## Codes | Status | Code | Meaning | | ------ | -------------------------- | -------------------------------------------------------------------- | | 400 | `VALIDATION_ERROR` | The body or query parameters are malformed. | | 400 | `OUTBOUND_REQUEST_BLOCKED` | The destination URL is not allowed (for example, a private address). | | 400 | `REDIRECT_BLOCKED` | The destination redirected somewhere unsafe. | | 401 | `AUTHENTICATION_FAILED` | Missing or invalid API key or session. | | 401 | `SIGNATURE_INVALID` | The request signature did not match the endpoint secret. | | 403 | `AUTHORIZATION_DENIED` | The key lacks the required scope or workspace access. | | 404 | `ENDPOINT_NOT_FOUND` | The endpoint does not exist or was deleted. | | 404 | `DELIVERY_NOT_FOUND` | The delivery does not exist. | | 409 | `IDEMPOTENCY_CONFLICT` | The idempotency key was reused with a different request. | | 409 | `SLUG_TAKEN` | The workspace slug is already in use. | | 409 | `LAST_WORKSPACE` | A workspace cannot be deleted while it is the last one. | | 409 | `DELIVERY_NOT_RETRYABLE` | The delivery is in a state that cannot be retried. | | 409 | `DELIVERY_NOT_CANCELLABLE` | The delivery is already finished or in flight. | | 410 | `ENDPOINT_DISABLED` | The endpoint is paused or archived. | | 413 | `PAYLOAD_TOO_LARGE` | The request body exceeds the endpoint limit. | | 429 | `RATE_LIMIT_EXCEEDED` | Too many requests. Honor `Retry-After`. | | 500 | `INTERNAL_ERROR` | Unexpected server error; include the request ID when reporting. | | 500 | `DELIVERY_STUCK` | A delivery did not make progress and needs a manual retry. | | 500 | `OUTBOX_EXHAUSTED` | Fan-out retries were exhausted. | ## Ingest responses Ingest requests (`/in/*`) use the same envelope for rejections. A `202` means the event was stored; a `4xx` means it was rejected and no event exists. --- # For AI agents Coding agents and automation should read the machine-readable summary instead of scraping the HTML docs. It lists base URLs, authentication headers, route paths, and error codes in plain text. - Machine-readable summary: [`/llms.txt`](https://hooks.events/llms.txt) - Full documentation in one file: [`/llms-full.txt`](https://hooks.events/llms-full.txt) ## Recommended flow 1. Fetch `/llms.txt`, then read the REST API section. 2. Create a dedicated API key per agent in **Settings → API keys**, and load it from an environment variable. 3. Create an endpoint, queue a test event, then poll deliveries until they succeed or fail. ```bash curl -X POST "https://api.hooks.events/v1/endpoints" \ -H "Authorization: Bearer $HOOKS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "agent-test", "provider": "generic", "verificationMode": "hmac", "destinations": [{ "url": "https://example.com/hooks" }] }' # Queue a test event, then poll deliveries curl -X POST "https://api.hooks.events/v1/endpoints/$ENDPOINT_ID/test-events" \ -H "Authorization: Bearer $HOOKS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"eventType":"agent.check"}' ``` ## Guardrails - Never print API keys, signing secrets, or raw `Authorization` headers in logs, commits, or chat. - Prefer test events over replaying production events. - Treat the ingestion URL as a secret: anyone with it can create events. - Respect `429` responses and the `Retry-After` header instead of retrying immediately. --- # Support Stuck on a signature, a retry, or an integration? Send the event ID and we can trace the exact delivery path. - **Email support** — replies within one business day. - **Report a vulnerability** — encrypted reports welcome. - **Legal & privacy** — terms, data handling, and retention details: https://hooks.events/legal Include the `X-Hooks-Event` header value (or the event ID from the console) in any support request. Never include signing secrets or API keys in a report.