Webhooks & events
Account webhooks send what happens across your whole account to a URL you choose, as it happens: a new lead, a booked meeting, a deal moving stage. They're what the Zapier and n8n integrations use under the hood, and you can point them at your own endpoint too.
They sit alongside the project webhook on a single email's Responses page, which keeps working as before. A project webhook covers one email's interactions. An account webhook covers every email, plus the events below, and is signed the same way.
Add a webhook
- Go to Developers in the dashboard and find the Webhooks card.
- Enter the Endpoint URL (a public
https://URL) and tick the Events you want. - Click Add webhook and copy the signing secret. It's shown only this once.
Each webhook shows how it was created (Dashboard, API, Zapier or n8n), its status, and when it last delivered. Send test posts a sample of the chosen event, with "test": true in its data. Delete removes the webhook. An account can have up to 50.
Only the account owner can add or delete webhooks, and you may be asked to sign in again first.
Events
| Event | Fires when |
|---|---|
interaction.received | A recipient interacts with an email: a poll vote, rating, quiz answer, tracked click, purchase and so on. Repeats that are deduplicated don't fire. |
form.submitted | A form in an email is submitted (source: "email-form"), or a signup form is (source: "signup-form"). |
contact.created | A contact is added to a list. |
contact.updated | A contact's fields change. Saving identical data doesn't fire. |
lead.hot | A contact's engagement score crosses your hot-lead threshold on the way up. |
lead.new | A new lead arrives from a signup form or a form in an email. |
booking.created | A meeting is booked. |
booking.cancelled | A booking is cancelled, with cancelledBy. Rescheduling fires neither booking event. |
deal.created | A deal is created, from anywhere: the board, an auto-rule, a journey, a proposal or the API. |
deal.stage_changed | A deal moves stage, won and lost included. |
purchase.completed | A recipient pays through an in-email checkout. |
journey.completed | A contact reaches the end of a journey. A contact who exits early doesn't count. |
The contact events fire for imports, signup forms, the API, syncs and AI connectors. A single write of more than 500 contacts (a large CSV import, for instance) fires none, so an import doesn't start thousands of workflows. Edits in the contact grid, journey Update field steps and saved answers don't fire them either.
lead.hot and lead.new fire whatever your lead-alert switches and limits say: subscribing is the opt-in.
Payload
Every delivery is a JSON POST with the same envelope:
{
"id": "evt_8c1f…",
"type": "deal.stage_changed",
"createdAt": 1790000000000,
"data": {
"deal": {
"id": "deal_abc123",
"title": "Annual plan",
"value": 1200,
"currency": "USD",
"pipelineId": "owner_default",
"stageId": "proposal-sent",
"stage": "Proposal sent",
"status": "open",
"source": "manual",
"listId": "list_abc123",
"rowId": "3f6c1b0e2d…",
"email": "[email protected]"
},
"fromStageId": "meeting-booked"
}
}
id is the same for every delivery of one event, and is also sent as the X-MailInApp-Idempotency-Key header, so you can safely drop a redelivery you've already processed. createdAt is in epoch milliseconds.
To see the data of each event type, call GET /api/v1/events/sample or use Send test.
Verify the signature
Deliveries carry X-MailInApp-Timestamp and X-MailInApp-Signature headers, computed exactly like a project webhook's. Check them against the raw body with your signing secret, and reject timestamps more than a few minutes old. The project webhook docs have a ready-made Node.js function.
Retries
Your endpoint has 5 seconds to answer with a 2xx. Anything else (a timeout, an error status, a redirect) is a failure, and the delivery is retried automatically with backoff over several hours, with the same body and idempotency key, to the webhook's current URL. Answer quickly and do slow work afterwards.
- If your endpoint answers
410 Gone, the webhook is disabled. That's how REST Hooks clients unsubscribe. - Revoking an API key disables the webhooks created with it.
- Deleting a webhook stops its pending retries.
Subscribe over the API
Zapier, n8n and your own code can manage webhooks with an API key (Authorization: Bearer mia_live_…):
curl https://mailinapp.com/api/v1/webhooks \
-H "Authorization: Bearer mia_live_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://hooks.example.com/mailinapp", "events": ["lead.hot", "booking.created"]}'
The 201 response holds the subscription's id and its secret, which signs every delivery. Keep it: it isn't returned again. GET /api/v1/webhooks lists your subscriptions, and DELETE /api/v1/webhooks/{id} removes one. One unknown event name rejects the whole request. These routes allow 30 requests a minute per key.
The full contract is in the OpenAPI spec.