Skip to content

Set up a webhook

Tell your own system of every delivery, bounce, acknowledgement and constancia with a signed request.

This is the recipe the MCP server gives AI assistants, as is. It is in English because models follow it best that way; the assistant answers you in your language.

An assistant reads it with docs({uri:"docs://recipes/set-up-a-webhook"})

docs://recipes/set-up-a-webhook
docs://recipes/set-up-a-webhook — push the firm's delivery events to another system, signed

Use when the lawyer wants their ERP, CRM or spreadsheet told when a notification is sent,
delivered, bounced, opened, downloaded, acknowledged, or gets its constancia — instead of polling.
Needs a token with the webhooks:manage scope held by an OWNER (anyone else: X_FORBIDDEN).

1. whoami({})
   → orgId, role, scopes
   role is not "owner", or scopes lack "webhooks:manage"? Stop: an owner issues such a token.
   skip if you already hold orgId

2. Ask for the receiving URL. It must be https, on the default port 443 (no ":8443"), and a public
   host. localhost, *.local, *.internal and any host resolving to a private, link-local or CGNAT
   address are refused (X_WEBHOOK_URL_PRIVATE / _PORT / _NOT_HTTPS). Do not try to work around it.

3. manage_resource({resource:"webhooks", action:"createWebhookEndpoint", params:{orgId:"<orgId>", url:"<https URL>", description:"ERP", eventTypes:["delivery.delivered","delivery.bounced","constancia.issued"]}})
   eventTypes: 1 to 10 of notification.sent, delivery.failed, delivery.delivered,
   delivery.bounced, delivery.complained, recipient.opened, recipient.downloaded,
   recipient.acknowledged, recipient.replied, constancia.issued — ask which ones they want.
   → secret ("whsec_…", shown ONCE) and endpoint {id, status, …}
   Hand the secret to the person setting up the receiver now; it is never shown again. If lost:
   manage_resource({resource:"webhooks", action:"rotateWebhookSecret", params:{orgId:"<orgId>", endpointId:"<endpoint.id>"}})
   (both secrets sign for 24 hours).

4. manage_resource({resource:"webhooks", action:"sendTestWebhook", params:{orgId:"<orgId>", endpointId:"<endpoint.id>"}})
   → ok, statusCode, durationMs, error (timeout | connect | url_refused | http_status)
   ok false? Tell the user what the receiver answered; nothing else changes.

5. The receiver verifies each POST:
   Notificado-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, t + "." + raw body)>
   Refuse when t is more than 300 s off; accept when any v1 matches. Deduplicate on the
   Notificado-Event-Id header (the evidence event id). Answer 2xx within 10 s.

Done when the endpoint exists and a test answered ok, or you told the user why it did not.

Tips
- Only events from registration on are sent; history is not replayed.
- Failed deliveries retry up to 8 times over about 24 h. Each one is in
  manage_resource({resource:"webhooks", action:"listWebhookDeliveries", params:{orgId:"<orgId>", endpointId:"<endpoint.id>"}})
  A day of nothing but failures disables the endpoint and mails the owners;
  manage_resource({resource:"webhooks", action:"updateWebhookEndpoint", params:{orgId:"<orgId>", endpointId:"<endpoint.id>", state:"active"}})
  resumes it and delivers what waited. state:"paused" pauses it on purpose.
- recipient.opened is an indicio, never proof of reading — see docs://recipes/what-counts-as-delivered.
- The surtida date is not an event: read docs://recipes/track-deadlines after
  recipient.acknowledged or recipient.downloaded.
- A webhook is a notice, not evidence. The proof stays in the constancia and the evidence zip.
- The full guide, with Node and Python verification code: https://www.notificado.co/docs/webhooks

All recipes · Connect an assistant