Receiving
Alerts
You should hear it from us before you hear it from your recipients. An alert watches one number — your complaint rate, your bounce rate, your monthly quota, your DNS — and when it crosses a threshold you set, we email the account owner and send a signed alert.raised event to your webhook endpoints.
It arrives once, not once per check
This is the part worth reading before anything else, because it is the part that decides whether you keep the alert or filter it. We evaluate every rule every five minutes. You are notified on the transition, not on the state. A complaint rate that stays bad for a fortnight produces one email on the day it went bad and one when it clears — not four thousand.
Three mechanisms, and they cover different things:
| Mechanism | What it stops |
|---|---|
| Edge triggering | A notification happens when a rule goes from not-firing to firing. While it stays firing nothing further is sent, however many times we look. |
| Hysteresis | A firing rule resolves only once the value has fallen 10% below its threshold. A rate resting exactly on the line would otherwise cross, clear and cross again, and every up-crossing is a genuine new edge. |
| Cooldown | A hard floor on the gap between any two notifications for one rule — a day by default. It bounds a condition that keeps coming back. It is not a reminder timer: we never re-send an unchanged fact. |
An account that goes quiet is not an account that got better. Rate rules need at least 100 settled messages in the window before they mean anything — three bounces out of five messages is 60% and says nothing. Below that floor a firing alert stays open rather than resolving, because “we cannot tell” must never be delivered as “it is fixed”.
What you can watch
| kind | Threshold means | Default | Why that number |
|---|---|---|---|
complaint_rate | Complaints ÷ settled mail over the window | 0.003 (0.30%) | Gmail’s own line. Above it a receiver starts filtering you, and we suspend at more than three times it — so this lands in the window where you can still prune a list rather than appeal a suspension. |
bounce_rate | Bounces ÷ settled mail over the window | 0.1 (10%) | The point at which your daily cap steps down on its own. A stale list looks to a receiver exactly like a bought one. |
quota_usage | Sent this calendar month ÷ your monthly allowance | 0.8 (80%) | With enough of the month left to do something about it. Above 1.0 is refused: by then the sends are already being turned away. |
dns_drift | Verified domains that have stopped publishing their DKIM record | 0 (any at all) | We recheck every verified domain twice a day and only call it drift after three consecutive misses, so the “do not cry wolf” judgement is already made. Your mail keeps flowing meanwhile — drift never changes a domain’s status. |
A rate threshold is refused above the point we would suspend the account anyway. A complaint-rate rule set at 5% could only ever fire after your API key was already stopped for the same numbers — an alert arriving with news you have had, which is worse than no alert. The ceilings are 1% for complaint_rate and 30% for bounce_rate.
The webhook payload
Alerts are delivered through the same machinery as delivery events: the same signature over the raw bytes, the same posthaste-delivery-id for de-duplication, the same seven-step retry ladder. Subscribe an endpoint to alert.raised and alert.resolved by name, or to nothing at all, which means everything.
POST /your/endpoint HTTP/1.1
content-type: application/json
user-agent: Posthaste-Webhooks/1.0
posthaste-signature: t=1760000000,v1=6f1b0a…
posthaste-delivery-id: evt_Nc7HrQ8T2Sf9pXbAZLm3kQ
posthaste-attempt: 1
{
"id": "evt_Nc7HrQ8T2Sf9pXbAZLm3kQ",
"type": "alert.raised",
"createdAt": "2026-08-26T02:14:07.318Z",
"data": {
"alertId": "evt_Nc7HrQ8T2Sf9pXbAZLm3kQ",
"ruleId": "alr_8T2Sf9pXbNc7HrQAZLm3kQ",
"kind": "complaint_rate",
"state": "firing",
"value": 0.0104,
"threshold": 0.003,
"windowDays": 7,
"summary": "complaint rate 1.04% over 7 days (52 of 5000 settled), against a threshold of 0.30%",
"detail": {
"kind": "complaint_rate",
"threshold": 0.003,
"windowDays": 7,
"settled": 5000,
"bounced": 61,
"complained": 52,
"sentThisMonth": 18402,
"monthlyAllowance": 300000,
"driftedDomains": 0
}
}
}| Field | Notes |
|---|---|
type | alert.raised or alert.resolved. These sit alongside the thirteen message event types and are the only two that are not about a message. |
data.messageId | Absent. An alert is a statement about the account, not about one message. If your handler assumes every event has a message id, this is the event that will find out. |
data.kind | Which signal — the same values as the table above. |
data.value | What we measured. A fraction for the rate and quota kinds; a count for dns_drift. |
data.threshold | The threshold as it stood when the alert fired. It stays what it was after you edit the rule. |
data.summary | One sentence with the numbers in it — the same sentence in the email and on your dashboard, so a paging integration can forward it verbatim. |
data.detail | Every signal we gathered on that pass, not only the one that fired. Read it defensively: fields are added here as new kinds arrive. |
Endpoints
/v1/alerts/rulesalerts:read/v1/alerts/rulesalerts:write/v1/alerts/rules/{'{id}'}alerts:write/v1/alerts/rules/{'{id}'}alerts:write/v1/alerts/eventsalerts:read| Field | Notes | |
|---|---|---|
kind | required | One of the four above. One rule per kind — a second answers 409, because two rules watching one signal means two emails about it. |
threshold | required | A fraction for the rate and quota kinds (0.003 is 0.3%), a whole number for dns_drift. |
windowDays | optional | 1–90, default 7. Applies to the two rate kinds; the others are not windowed. |
cooldownMinutes | optional | 5–43200, default 1440 (a day). The floor on the gap between notifications. |
notifyEmail, notifyWebhook | optional | Both default true. The email goes to the account owner. |
enabled | optional | Default true. Switching a rule off keeps its threshold, so you do not have to decide on a number twice. |
POST /v1/alerts/rules with an empty body creates the recommended set for every kind and is idempotent — call it twice and the second call changes nothing, including any threshold you have since tuned.
Editing a rule never re-arms it. Raising a threshold while an alert is open is not a way to be told about it a second time; the rule stays firing, and the next thing you hear is the resolution.
What fired, and where it went
Every raise and every clear is recorded with the numbers as they stood and which channels it was delivered on. delivery.webhooks: 0 is a real answer rather than a missing one — it means no active endpoint was subscribed at the time, which is the first thing to check when a webhook never arrived.
GET /v1/alerts/events
200 {
"nextCursor": null,
"data": [
{
"id": "evt_Nc7HrQ8T2Sf9pXbAZLm3kQ",
"ruleId": "alr_8T2Sf9pXbNc7HrQAZLm3kQ",
"kind": "complaint_rate",
"label": "Complaint rate",
"state": "firing",
"value": 0.0104,
"threshold": 0.003,
"summary": "complaint rate 1.04% over 7 days (52 of 5000 settled), …",
"delivery": {
"webhooks": 2,
"emailTo": "[email protected]",
"emailStatus": "sent"
},
"createdAt": "2026-08-26T02:14:07.318Z"
}
]
}NextDelivery analytics →Delivery rate and time to inbox, p50 and p95, for every receiving provider and for each of your own tags — with nothing invented where there is nothing to measure.