{"id":51848,"plugin_id":"plugin_asdk_app_6ac773e202f0819199f20c6665fcfcde","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-24T13:47:45.308Z","digest":"d469febf9d79fdcc8707f5ec5d653502892b4d09e1e613e9384998596ea3d07b","against":null,"payload":{"description":"This skill should be used when the user asks to \"set up an Emailit webhook\", \"receive bounce notifications\", \"verify the webhook signature\", \"handle email.delivered events\", \"my webhook is failing\", \"retry failed webhooks\", or \"sync Emailit events to my database\". Covers creating webhooks, choosing events and filters, verifying X-Emailit-Signature, handling batched payloads, retries, and debugging.","included_files":[{"relative_path":"references/events.md","size_in_bytes":3893},{"relative_path":"references/verify-signature.md","size_in_bytes":6007}],"name":"emailit-webhooks","skill_md_contents":"---\nname: emailit-webhooks\ndescription: This skill should be used when the user asks to \"set up an Emailit webhook\", \"receive bounce notifications\", \"verify the webhook signature\", \"handle email.delivered events\", \"my webhook is failing\", \"retry failed webhooks\", or \"sync Emailit events to my database\". Covers creating webhooks, choosing events and filters, verifying X-Emailit-Signature, handling batched payloads, retries, and debugging.\n---\n\n# Emailit webhooks\n\nEmailit sends HTTPS POST requests to the user's endpoint when events happen: email delivery outcomes, opens and clicks, domain, contact, audience, subscriber, template, suppression, verification, and campaign changes.\n\n## Create a webhook\n\n1. Ask for the endpoint URL (HTTPS, publicly reachable) and which events the app needs. Do not subscribe to everything by default; fewer events mean less load.\n2. Call `create-webhook` with `name`, `url`, and either `events: [...]` or `all_events: true`. Event names are in `references/events.md`.\n3. Optional `filter` delivers only matching events:\n\n```json\n{ \"match\": \"all\", \"rules\": [{ \"field\": \"meta.app\", \"operator\": \"equals\", \"value\": \"billing\" }] }\n```\n\n   A `field` is a dotted path inside the event's `object` (shown below), so write `meta.app`, `status`, or `to`, not `data.meta.app`. Loads and clicks nest the email, so use `email.meta.app` there. Operators: `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `ends_with`, `greater_than`, `less_than`, `is_set`, `is_not_set`, `in`, `not_in`. Up to 25 rules. Filters need the Pro or Business plan; `get-webhook` shows `filters_allowed`.\n\n4. The response includes `secret` (`whsec_...`); `get-webhook` returns it too. Tell the user to store it as `EMAILIT_WEBHOOK_SECRET` in their app's secret store. Do not repeat it in chat more than needed, and never write it into code or a commit. If it leaks, `reset-webhook-secret` issues a new one and the old one stops working at once.\n5. `test-webhook` sends a sample event to the URL. Confirm first, since it calls the user's server.\n\n## The request\n\n```\nPOST <your url>\nContent-Type: application/json\nX-Emailit-Signature: <hex HMAC-SHA256>\nX-Emailit-Timestamp: <unix seconds>\n```\n\nThe body is a **JSON array** of events. Emailit batches events, so loop over every item:\n\n```json\n[\n  {\n    \"event_id\": \"evt_...\",\n    \"type\": \"email.delivered\",\n    \"object\": { \"id\": \"em_...\", \"object\": \"email\", \"to\": \"user@example.com\", \"status\": \"delivered\", \"meta\": { \"order_id\": \"1234\" } },\n    \"data\": {\n      \"object\": { \"id\": \"em_...\", \"object\": \"email\", \"to\": \"user@example.com\", \"status\": \"delivered\", \"meta\": { \"order_id\": \"1234\" } }\n    }\n  }\n]\n```\n\nEach event carries the resource twice: at `data.object` and, as a copy, at the top level `object`. Read it from `data.object` (for example `event.data.object.meta.order_id`). Fields per event type are in `references/events.md`.\n\n## Verify every request\n\nSignature = hex of HMAC-SHA256, keyed with the webhook secret, over the string `\"{timestamp}.{raw body}\"`.\n\n1. Read the **raw** body bytes before any JSON parsing. Re-serialized JSON will not match.\n2. Compute the HMAC and compare with `X-Emailit-Signature` in constant time.\n3. Reject timestamps more than 300 seconds from now, to block replays.\n4. Return 401 on failure.\n\nCode for Node, Next.js, Python, PHP, and Go, and the SDK helpers (`WebhookSignature.verify`) are in `references/verify-signature.md`.\n\n## Handle events safely\n\n- **Respond fast**: return 2xx within a few seconds, then process in a background job. Requests time out after 30 seconds.\n- **Deduplicate** on `event_id`. Retries and batching mean the same event can arrive more than once.\n- **Order is not guaranteed**. Use the event's own timestamps and the email's current status rather than arrival order. A `delivered` can arrive after `loaded`.\n- **Correlate** with your own data through `meta` set on `send-email` (for example `data.object.meta.order_id`).\n\n## Retries and failures\n\nAny non-2xx response or timeout is a failure. Emailit retries each request with backoff: 5 minutes, 30 minutes, 2 hours, 5 hours, then every 12 hours, up to 11 attempts. The owner is alerted after the third failed attempt. A webhook with no successful delivery for 3 days is disabled.\n\n- Check state: `get-webhook` shows `enabled` and `last_used_at`. `list-events` shows the events that were generated. Per request response codes are on the webhook page in the dashboard.\n- Resend failed requests: `retry-failed-webhook-requests` queues again every request that failed permanently (all attempts used) in the last 7 days. If it finds any, it also re-enables the webhook, so fix the endpoint first. It returns `retried` with the count; 0 means nothing was resent and the webhook was left as it was.\n- Resend one request: `retry-webhook-request` with the request ID from the dashboard. Only permanently failed requests can be retried, and it re-enables the webhook too.\n- Turn a webhook back on without resending: `update-webhook` with `enabled: true`.\n\n## Debugging checklist\n\n| Symptom | Check |\n| --- | --- |\n| Signature never matches | Using the raw body, the right secret (it changes on reset), and `timestamp + \".\" + body` |\n| 401 or 403 from the endpoint | Auth middleware, CSRF protection, or a firewall blocking the POST |\n| Timeouts | Do the work after responding |\n| Only one event handled | Body is an array; loop over it |\n| No requests at all | `enabled` is false, the event is not subscribed, or the filter excludes it |\n\n## Without MCP\n\nREST endpoints: `POST /v2/webhooks`, `POST /v2/webhooks/:id` (update), `POST /v2/webhooks/:id/test`, `POST /v2/webhooks/:id/reset-secret`, `POST /v2/webhooks/:id/retry-failed`, `POST /v2/webhooks/:id/requests/:requestId/retry`.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}