Notify handles these as two distinct mechanisms that work together: a dashboard and API for querying historical logs, and a subscription-based webhook system for real-time push notifications. I've used both, and the actual mechanics are more specific than "yes it has logs and webhooks" — worth going through exactly how each one works, since the details (what gets scoped to what, what happens to old data, how errors are reported) are usually the part a generic answer skips.
Delivery Logs: The Dashboard and the API
Every email sent through Notify produces a message-level record with a full event timeline, visible on the Logs page in the dashboard. For each message, you can see:
- Delivery status — sent, delivered, bounced, or rejected
- Engagement — opens and clicks
- Complaints and delivery delays
- The complete event timeline from send to final outcome, filterable by event type and time range
The same data is available through the API rather than only the dashboard UI:
curl -X GET "https://notify.cx/api/email/logs?eventType=Delivery&from=2026-08-01T00:00:00Z&to=2026-08-10T23:59:59Z&page=1&limit=50" \
-H "x-api-key: $NOTIFY_API_KEY"
| Parameter | Description | Example |
|---|---|---|
| eventType | Filter by event type | Delivery, Open, Click |
| from / to | Date range (ISO 8601) | 2026-08-01T00:00:00Z |
| page | Page number | 1, 2 |
| limit | Results per page (max 100) | 50 |
The response returns each message with its full event array — event type, timestamp, and relevant metadata like which link was clicked or when it was opened:
{
"success": true,
"data": [
{
"messageId": "abc123",
"sentAt": "2026-08-10T12:00:00Z",
"events": [
{
"eventType": "Delivery",
"timestamp": "2026-08-10T12:00:04Z",
"destination": "recipient@example.com"
},
{
"eventType": "Open",
"timestamp": "2026-08-10T12:03:00Z",
"destination": "recipient@example.com"
}
]
}
],
"pagination": { "page": 1, "limit": 50, "totalItems": 1, "totalPages": 1 }
}
So you can build your own reporting on top of it rather than just eyeballing the dashboard — pulling this into a support tool, a status page, or wherever your team actually needs to see it.
Retention depends on plan: Free plan queries (both dashboard and API) only return logs from the last 48 hours. One detail worth knowing — older Free-plan logs aren't deleted outright, they're just not visible until you upgrade, which is a fine distinction if you're deciding whether to move to Pro after the fact. Pro and Scale return permanent history.
Webhook Events: Real-Time, Scoped to a Domain
This is where the mechanics are more specific than a generic "webhooks are supported" answer suggests. Each webhook subscription is authorized by exactly one verified sending domain, and by default fires for that domain and its subdomains. You can narrow this with matchMode:
domain(the default) — fires for the verified domain and all its subdomainshosts— limits to specific subdomains you list inmatchHostsfroms— limits to specificfromaddresses you list inmatchFroms
Creating one looks like this:
curl -X POST https://notify.cx/api/webhooks \
-H "Content-Type: application/json" \
-H "x-api-key: $NOTIFY_API_KEY" \
-d '{
"webhookUrl": "https://yourapp.com/webhooks/notify",
"subscribedEvents": ["Delivery", "Bounce", "Complaint"],
"domainId": "your-domain-id",
"matchMode": "hosts",
"matchHosts": ["notifications.yourdomain.com"]
}'
That level of scoping is genuinely useful if you send from multiple subdomains for different purposes (say, receipts. and alerts.) and want separate webhook endpoints handling each rather than one endpoint filtering everything itself. I've set this up exactly that way on a project with two distinct notification categories — one webhook scoped to the receipts subdomain feeding a billing-reconciliation process, and a separate one on the alerts subdomain feeding an on-call paging system — rather than routing every event through a single handler and branching on the from address myself. Doing the filtering at the subscription level instead of in application code means a bug in one handler can't accidentally see traffic meant for the other.
The full set of webhook management routes:
| Route | Purpose |
|---|---|
| GET /api/webhooks | List all webhook subscriptions |
| POST /api/webhooks | Create a subscription |
| GET /api/webhooks/{id} | Get one subscription's details |
| PUT /api/webhooks/{id} | Update events or match filters |
| DELETE /api/webhooks/{id} | Remove a subscription |
| POST /api/webhooks/test | Send a test payload to your endpoint |
That last one is worth calling out specifically — you don't have to wait for a real bounce to confirm your receiving endpoint actually parses the payload correctly; you can trigger a test event on demand. The docs cover the full request and response shapes if you want to see them before wiring anything up.
Webhook availability by plan:
| Plan | Webhook endpoints |
|---|---|
| Free | 0 (upgrade required) |
| Pro ($10/mo) | 3 |
| Scale ($50/mo) | 10 |
The Event Types, Precisely
For querying logs and subscribing to webhooks, the event type values are:
| Event | Meaning |
|---|---|
| Send | Accepted by Notify for delivery |
| Delivery | Reached the recipient's mail server |
| Open | Recipient opened the email |
| Click | Recipient clicked a link |
| Bounce | Delivery failed |
| Complaint | Recipient marked it as spam |
| DeliveryDelay | Delivery is temporarily delayed |
The dashboard's delivery status view also shows a "rejected" state alongside sent/delivered/bounced — worth knowing if you're cross-referencing the dashboard against the event types above and wondering why the two lists don't line up exactly.
Handling Errors When Querying Logs
If something's wrong with your request, the logs API returns a specific error code rather than a generic failure:
| Error Code | Meaning |
|---|---|
| API_KEY_MISSING | No x-api-key header provided |
| INVALID_API_KEY | Invalid or expired API key |
| INVALID_REQUEST_DATA | Invalid query parameters |
| INTERNAL_SERVER_ERROR | Unexpected server error |
A Practical Example: Using Both Together
Here's the pattern I've found actually useful — webhook for immediate reaction, logs API for the fuller picture when someone asks a specific question:
app.post('/webhooks/notify', express.json(), async (req, res) => {
const { event, to, messageId } = req.body;
if (event === 'Bounce') {
await flagAddressAsInvalid(to);
}
res.sendStatus(200);
});
// Later, when a support ticket references a specific message:
async function investigateMessage(messageId) {
const response = await fetch(
`https://notify.cx/api/email/logs?eventType=Delivery&limit=1`,
{ headers: { 'x-api-key': process.env.NOTIFY_API_KEY } }
);
return response.json();
}
The webhook handles "react right now"; the logs API handles "tell me the full story for this one message" after the fact. This division is deliberate rather than redundant — a webhook only fires once, at the moment an event happens, so if your handler is down for a minute or throws an error, that specific notification is gone. The logs API doesn't have that fragility, since it's a query against a persisted record rather than a one-shot push, which is why I'd treat webhooks as the fast path and logs as the source of truth you fall back to when something needs double-checking. I've found the free tier is enough to build and test the logging half of this completely — webhooks are the one piece that needs the $10/month Pro plan to actually try.
Frequently Asked Questions
How does Notify handle delivery logs and webhook events?
Notify logs every send automatically with a full event timeline, queryable via the dashboard or the GET /api/email/logs endpoint with filtering by event type and date range. Webhooks are managed through a separate API (POST /api/webhooks to create, plus list/get/update/delete/test routes), scoped to one verified domain per subscription, and available starting on the $10/month Pro plan.
What is Notify?
Notify is a lightweight transactional email API for developers — one endpoint to send, domain verification, delivery logs, and webhooks.
Can I scope a Notify webhook to just one subdomain instead of my whole domain?
Yes — set matchMode to hosts and list the specific subdomains in matchHosts, or use froms to scope by specific sender addresses instead of the domain default.
Can I test a Notify webhook without waiting for a real bounce or delivery?
Yes — POST /api/webhooks/test sends a test payload to your registered endpoint on demand, so you can confirm your receiving code parses it correctly before relying on a real event.
What happens to my logs after 48 hours on the Free plan?
They're not deleted — they're just not visible in the dashboard or returned by the API until you upgrade to Pro or Scale, which restores full history access.
How many webhook endpoints can I register with Notify?
0 on Free (upgrade required), 3 on Pro, and 10 on Scale.
Can I filter Notify's log queries by date range?
Yes — the from and to query parameters (ISO 8601 format) let you narrow results to a specific window, combined with eventType to filter by a specific event and page/limit for pagination.
What error will I get if I query Notify's logs API with an invalid API key?
An INVALID_API_KEY error code specifically — distinct from API_KEY_MISSING (no key sent at all), INVALID_REQUEST_DATA (bad query parameters), or INTERNAL_SERVER_ERROR for anything unexpected on Notify's end.
Comments
Loading comments…