Sources

Webhook sources

Receive JSON events from a service or a customer-supplied browser artifact, then route matching events to your agent.

When to use it

Choose a webhook source for service events, constrained row buttons, or forms that submit declared fields. Doorbell receives events; the receiving agent decides actions under its own permissions.

Example

Your order service sends order.status set to needs_review. The order-reviewer agent's input matches that field and shares the selected order details.

Set it up

  1. Open Sources and create a Webhook source. Name it, read a sample JSON object, and keep the scalar fields needed for matching and notification details. Nested arrays of objects use the first history item only: history_items.date means history_items[0].date.
  2. For a server sender that can send Authorization: Bearer, copy the source URL and reusable sender key into secure webhook settings. Keep this key out of HTML.
  3. For ClickUp, GitHub, Stripe, Slack, or Svix, choose that HMAC preset, paste the provider signing secret, and point the provider at /hooks/<pathToken> with no query and no Bearer. The secret is write-only after save. Never put it in a URL. Agents use set_webhook_signing_secret; update_webhook_endpoint never takes a secret.
  4. For browser publishing, open the source's browser settings and choose exact HTTPS origins, lifetime, budget, constraints, and optional dedupePaths.
  5. Mint a named disposable browser token with the approved or narrower settings. Embed only this browser token in your own artifact.
  6. Select the source as a named agent input. Choose conditions and shared fields, approve any capture changes, and review notification timing. Save sample previews, test the receiver, approve activation, and submit a fresh event.
  7. Revoke browser credentials when access should stop. Policy or payload-schema changes revoke tokens; mint replacements deliberately.

Important limits

  • A webhook source receives events; a receiver accepts outgoing notifications. Their URLs and keys serve different purposes. Webhook events use each agent's grouping, quiet periods, cooldown, and overflow settings. Several agents can independently receive one accepted event.
  • The schema declares optional scalar fields, not a required-body contract for server submissions. Raw bodies and unselected data are discarded. Explicitly approved bounded nested JSON capture is separate from declared scalars. A missing field does not reject a valid server POST, but a condition requiring it fails. Browser constraints can require fields, fix or allow values, bound numbers or strings, or require complete permitted row combinations. Credentials and submission IDs belong in headers, not the body.
  • Default ingress is Authorization: Bearer with the reusable sender key. HMAC presets verify a pasted signing secret on the exact JSON bytes. Replay of a signed delivery returns 200 and is dropped. HMAC and query modes allow 300 requests per minute per endpoint. Query mode uses a separate query secret named key, never the sender key. Slack Events JSON and url_verification are accepted; form-encoded Slack bodies are rejected.
  • Embedded tokens are extractable. Exact origins constrain browser use, not reviewer identity, private-page access, a URL path, or non-browser replay with forged Origin. Explicit portable mode removes origin protection for missing or opaque origins but retains all other restrictions. Your host must allow scripts and cross-origin POSTs; no hosting compatibility is guaranteed.
  • Defaults are 24 hours and 100 accepted unique submissions; ceilings are 168 hours and 1,000. Exact expiry timestamps apply, with at most 50 live tokens per endpoint. Ingress limits are 120 requests/minute per endpoint (durable) and per IP (process-local), including invalid requests and retries.
  • Each deliberate click uses a fresh Idempotency-Key. A network retry keeps the same ID and identical payload throughout the credential’s usable lifetime. Fresh accepted clicks spend budget even when collapsed or unmatched; identical accepted retries spend none.
  • Optional dedupePaths identify exact typed pending intent per credential, endpoint, and notifier. Suppression spans grouping, freezing, delivery retries, and unconsumed MCP wakeups. Handoff releases intent per delivered packet in drain mode or for the retained group including omitted details in pull mode. Reading details is not completion. Failure, expiry, cancellation, or grouping Drop also releases intent. A fresh click after release can match again; an old retry cannot.
  • Revocation stops new ingress, not accepted work. Replacing the source key stops the old key; update the sender before expecting further events. Source-key rotation is independent of browser tokens. Ordinary identical server POSTs remain distinct. Tokens and raw bodies do not enter wakeups. Doorbell provides no page builder or agent-result polling.

Troubleshooting

  • For a rejected server request, check the URL, Authorization header or signature header, JSON object, and request size. The source detail and list_webhook_ingress show a safe reject phrase when the path token resolved, including a failed sender key or signature. HTTP bodies stay generic.
  • For an accepted request with no notification, check the named agent's active settings, selected source, field names and types, conditions, and receiver health. Acceptance is not delivery.
  • For browser failures, inspect the exact origin and portable setting, credential expiry/revocation/budget, complete row combination, declared field types, and host scripting/CORS support. Do not send Doorbell session cookies.
  • For browser requests, generic 401 means invalid credentials; 400 invalid payload; 409 conflicting content under an accepted ID; 429 limits; 503 transient retry. Back off and reuse the same ID/body after network uncertainty or 503. Do not invent fresh IDs to bypass limits.
  • Browser success is always the minimal {ok:true}, whether new, retried, collapsed, or unmatched. It proves durable acceptance, not delivery or completed work, and reveals no routing state. Use authenticated settings and delivery history for diagnosis.
Technical reference

POST JSON up to 256 KiB (262,144 UTF-8 bytes). Declare up to 20 scalar fields with paths of at most three dot-separated segments. A nonempty array of records contributes the first history item under the array name. Source-specific matching clauses are ANDed and case-sensitive; operators include equals, not_equals, contains, not_contains, in, not_in, starts_with, not_starts_with, within_last_seconds, and not_within_last_seconds. Browser POSTs use /hooks/<pathToken>/browser; ordinary server POSTs use /hooks/<pathToken>. Bearer remains the default; HMAC presets paste a signing secret with set_webhook_signing_secret.

Tuple example: declare item.scope, item.id, and action as strings; constraints.fields can fix action to request_review, while constraints.rows is [{"item.scope":"a","item.id":"1"},{"item.scope":"b","item.id":"2"}]. Scope a/id 2 is rejected. For a form, use a required category allowed list, numeric priority min/max, and a note maxLength; leave other declared fields unconstrained. A bound also requires a value; use an empty string for an empty bounded text input. Action names are customer-defined data, not Doorbell commands.

Agent management uses get_webhook_browser_settings(endpointId) for safe metadata; configure_webhook_browser(endpointId, policy) and mint_webhook_browser_token(endpointId, name, optional lifetimeHours, budget, origins, portable, constraints) require configuration:write plus credentials:write. revoke_webhook_browser_tokens(endpointId, credentialId) requires those grants plus configuration:destructive; omit credentialId to revoke all. Reads never return saved secrets.

The policy contains enabled, origins, portable, lifetimeHours, budget, constraints:{fields:[],rows:[]}, and dedupePaths. Each field constraint has path, required, and optional fixed, allowed, min, max, maxLength. Rows map the same declared paths to permitted scalar tuples. Review complete changes before writing. The fetch example uses placeholders; retain clickId and body for uncertain retries, and generate both anew only for a deliberate new click.

const clickId = crypto.randomUUID();
const body = JSON.stringify({item:{scope:"a",id:"1"},action:"request_review"});
const response = await fetch("https://your-doorbell.example/hooks/<pathToken>/browser", {
  method: "POST", credentials: "omit",
  headers: {Authorization: "Bearer <browser-token>",
    "Idempotency-Key": clickId, "Content-Type": "application/json"},
  body
});
if (!response.ok) throw new Error(`Submission failed (${response.status})`);
const result = await response.json(); // {ok:true}: accepted, not completed
Open Agent Doorbell