Skip to content
Browse documentation

Build

Embeddable portal

Give each end customer a branded portal to manage their own receiving URLs, read their message log, retry deliveries, and verify signatures with the Standard Webhooks library.

The embedded portal is the multi-tenant mode of Hooks, in the spirit of the Svix App Portal. You keep one application per end customer. Each application has:

  • its own signing secret, so a tenant can verify their deliveries without seeing anyone else’s;
  • its own ingest URL, where you send the messages for that customer;
  • a portal link you hand to the customer, where they add, edit and pause their own receiving URLs.

Deliveries reuse the normal queue, retry policy and signature format. The only difference is which key signs them.

Terminal window
curl -X POST https://api.hooks.events/v1/applications \
-H "Authorization: Bearer $HOOKS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Acme Inc.", "uid": "acme", "brandColor": "#22d3ee"}'

The response includes id and ingestUrl. uid is your own customer identifier and must be unique in the workspace; reuse it to look the application up in your own systems.

Terminal window
curl -X POST https://api.hooks.events/v1/applications/$APPLICATION_ID/messages \
-H "Authorization: Bearer $HOOKS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"eventType": "invoice.paid",
"payload": {"invoiceId": "inv_42", "amount": 4200},
"idempotencyKey": "invoice-42-paid"
}'

eventType must look like <resource>.<action>. A repeated idempotencyKey returns the original event with status: "duplicate" instead of sending again. ingestUrl accepts the same payloads through the normal ingest pipeline if you prefer to POST there directly.

Each active tenant receiving URL whose eventTypes include the event type (or that subscribes to everything) gets one delivery.

Terminal window
curl -X POST https://api.hooks.events/v1/applications/$APPLICATION_ID/portal-tokens \
-H "Authorization: Bearer $HOOKS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "billing@acme.com", "expiresInSeconds": 3600}'

The response contains portalUrl, of the form https://app.hooks.events/portal#token=hkt_…. The token is a short-lived capability: it is stored hashed, can be revoked, and grants access to exactly one application. It travels in the URL fragment, so it is never sent to the web host in a request.

Embed the link in your own dashboard:

<iframe
src="https://app.hooks.events/portal#token=hkt_…"
title="Webhook portal"
style="width: 100%; height: 720px; border: 0"
allow="clipboard-write"
></iframe>

The portal is served with Content-Security-Policy: frame-ancestors * and no X-Frame-Options, so any origin can frame it. Its API (/portal/v1/*) accepts any origin and authenticates only with the bearer token, never with cookies.

Inside the portal the customer can:

  • add, edit, pause and delete their receiving URLs;
  • filter each URL to specific event types;
  • read the recent message log and retry failed deliveries;
  • reveal the application’s signing secret.

Every portal request is scoped to the application the token was minted for. Listing, editing or retrying anything outside it returns 404.

Tenant deliveries use the Standard Webhooks format signed with the application secret, not the workspace secret. The portal shows that secret to the tenant; your own code can read it with GET /v1/applications/{id}/secret.

// npm install standardwebhooks
import { Webhook } from "standardwebhooks";
const webhook = new Webhook(process.env.APPLICATION_SIGNING_SECRET);
export function verify(rawBody, headers) {
return webhook.verify(rawBody, headers);
}

POST /v1/applications/{id}/secret/rotate issues a new key; for a transition window the previous key keeps verifying, so tenants can roll over without dropped deliveries.

DELETE /v1/applications/{id} archives the application, pauses its backing endpoint and revokes every portal token. Delivery history is kept.