Webhooks
Webhooks send HTTP POST requests to your server when events happen. Pick the event types you care about and shipmail delivers a signed JSON payload for each one.
Last updated
The webhook object
{
"object": "webhook",
"id": "whk_abc123",
"url": "https://example.com/webhooks",
"events": ["message.received", "message.bounced"],
"active": true,
"description": "Production webhook",
"created_at": "2025-01-15T10:00:00.000Z",
"updated_at": "2025-01-15T10:00:00.000Z"
}Limits
- Max 10 webhooks per organization.
- URLs must use HTTPS.
Event types
| Event | Description |
|---|---|
| message.received | Someone sent an email to one of your mailboxes. |
| message.sent | An email you sent has left the server. |
| message.delivered | Your email was successfully delivered to the recipient. |
| message.bounced | Your email could not be delivered. The address may not exist. |
| message.complained | A recipient marked your email as spam. |
| domain.verified | Your domain's DNS records are correctly configured. |
| domain.verification_failed | One or more DNS records are missing or incorrect. |
| domain.degraded | A DNS record that was working has stopped resolving. |
| org.reputation_warning | Too many emails are bouncing or getting flagged as spam. |
| org.sending_throttled | Sending speed reduced to protect your reputation. |
| org.sending_suspended | Sending paused until bounce and complaint rates improve. |
| org.reputation_recovered | Your sending reputation is back to normal. |
Create a webhook
POST /v1/webhooksScope: webhooks:write. Rate limit tier: write.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | HTTPS endpoint URL. |
| events | string[] | Yes | At least one event type from the table above. |
| description | string | No | Description. Max 500 characters. |
Returns 201 with the webhook object plus a secret field. The secret is shown only once. Store it securely for signature verification.
{
"object": "webhook",
"id": "whk_abc123",
"url": "https://example.com/webhooks",
"events": ["message.received"],
"active": true,
"description": null,
"created_at": "2025-01-15T10:00:00.000Z",
"updated_at": "2025-01-15T10:00:00.000Z",
"secret": "whsec_..."
}List webhooks
GET /v1/webhooksScope: webhooks:read. Supports pagination.
Retrieve a webhook
GET /v1/webhooks/:idScope: webhooks:read. Returns the webhook object or 404. The secret is never returned after creation.
Update a webhook
PATCH /v1/webhooks/:idScope: webhooks:write. At least one field must be provided.
Request body
| Field | Type | Description |
|---|---|---|
| url | string | New HTTPS endpoint URL. |
| events | string[] | Replace subscribed events. |
| description | string | null | Update or clear description. |
| active | boolean | Enable or disable. |
Delete a webhook
DELETE /v1/webhooks/:idScope: webhooks:write. Returns 204 with no body.
Rotate secret
POST /v1/webhooks/:id/rotate-secretGenerates a new signing secret. The previous secret remains valid for a 24-hour grace period. During this window, deliveries include both X-ShipMail-Signature (new secret) and X-ShipMail-Signature-Previous (old secret).
{
"secret": "whsec_...",
"previous_secret_expires_at": "2025-01-16T10:00:00.000Z"
}Send test event
POST /v1/webhooks/:id/testQueues a webhook.test event for delivery. Returns 202 with the event ID. The test delivery follows the same signing and retry logic as real events.
{
"event_id": "evt_abc123"
}List deliveries
GET /v1/webhooks/:id/deliveriesScope: webhooks:read. Supports pagination and optional status (pending, delivered, failed) and event_type query filters.
The delivery object
{
"object": "webhook_delivery",
"id": "dlv_abc123",
"event_id": "evt_abc123",
"event_type": "message.received",
"status": "delivered",
"attempts": 1,
"last_status_code": 200,
"last_error": null,
"created_at": "2025-01-15T10:00:00.000Z",
"delivered_at": "2025-01-15T10:00:01.000Z"
}Retry schedule
ShipMail persists each delivery before enqueueing it for background processing. Temporary failures are retried up to 5 total attempts with increasing delays: 1 minute, 5 minutes, 30 minutes, then 4 hours. After 3 consecutive failed deliveries, the webhook is automatically disabled.
Verifying signatures
Every webhook delivery is signed. Verify the signature on your endpoint to confirm the request came from ShipMail and was not modified in transit. See the webhook verification guide for the signature scheme and code examples in TypeScript and Python.
Event payloads
Each webhook delivery contains an event object with the following structure. The data field varies by event type.
message.received
A new email was received by a mailbox.
{
"event_id": "evt_abc123",
"event_type": "message.received",
"created_at": "2025-01-15T10:30:00.000Z",
"data": {
"message_id": "msg_xyz789",
"mailbox_id": "mbx_def456",
"from": "sender@example.com",
"to": [
"hello@yourdomain.com"
],
"subject": "Hello from a customer"
}
}message.sent
An outbound email was accepted by the recipient's mail server.
{
"event_id": "evt_abc124",
"event_type": "message.sent",
"created_at": "2025-01-15T10:31:00.000Z",
"data": {
"message_id": "msg_xyz790",
"mailbox_id": "mbx_def456",
"to": [
"user@example.com"
],
"subject": "Your order confirmation"
}
}message.delivered
Delivery to the recipient was confirmed.
{
"event_id": "evt_abc125",
"event_type": "message.delivered",
"created_at": "2025-01-15T10:32:00.000Z",
"data": {
"message_id": "msg_xyz790",
"mailbox_id": "mbx_def456",
"to": [
"user@example.com"
]
}
}message.bounced
Delivery failed permanently. The recipient address may not exist.
{
"event_id": "evt_abc126",
"event_type": "message.bounced",
"created_at": "2025-01-15T10:33:00.000Z",
"data": {
"message_id": "msg_xyz791",
"mailbox_id": "mbx_def456",
"to": [
"invalid@example.com"
],
"bounce_type": "permanent"
}
}message.complained
The recipient marked the email as spam.
{
"event_id": "evt_abc127",
"event_type": "message.complained",
"created_at": "2025-01-15T10:34:00.000Z",
"data": {
"message_id": "msg_xyz792",
"mailbox_id": "mbx_def456",
"to": [
"user@example.com"
]
}
}domain.verified
All DNS records for a domain passed verification.
{
"event_id": "evt_abc128",
"event_type": "domain.verified",
"created_at": "2025-01-15T10:35:00.000Z",
"data": {
"domain_id": "dom_ghi012",
"verified_at": "2025-01-15T10:35:00.000Z"
}
}domain.verification_failed
DNS verification for a domain did not pass.
{
"event_id": "evt_abc129",
"event_type": "domain.verification_failed",
"created_at": "2025-01-15T10:36:00.000Z",
"data": {
"domain_id": "dom_ghi012",
"records": {
"mx": true,
"spf": false,
"dkim": true
},
"checked_at": "2025-01-15T10:36:00.000Z"
}
}domain.degraded
A previously verified domain has failing DNS records and may not be able to send or receive.
{
"event_id": "evt_abc130",
"event_type": "domain.degraded",
"created_at": "2025-01-15T10:37:00.000Z",
"data": {
"domain_id": "dom_ghi012",
"records": {
"mx": true,
"spf": false,
"dkim": true
},
"checked_at": "2025-01-15T10:37:00.000Z"
}
}org.reputation_warning
Bounce or complaint rates are elevated and may require attention.
{
"event_id": "evt_abc131",
"event_type": "org.reputation_warning",
"created_at": "2025-01-15T10:38:00.000Z",
"data": {
"previous_status": "healthy",
"new_status": "warning"
}
}org.sending_throttled
Sending has been throttled due to high bounce or complaint rates.
{
"event_id": "evt_abc132",
"event_type": "org.sending_throttled",
"created_at": "2025-01-15T10:39:00.000Z",
"data": {
"previous_status": "warning",
"new_status": "throttled"
}
}org.sending_suspended
Sending has been suspended due to very high bounce or complaint rates.
{
"event_id": "evt_abc133",
"event_type": "org.sending_suspended",
"created_at": "2025-01-15T10:40:00.000Z",
"data": {
"previous_status": "throttled",
"new_status": "suspended"
}
}org.reputation_recovered
Reputation has returned to healthy levels. Sending is no longer restricted.
{
"event_id": "evt_abc134",
"event_type": "org.reputation_recovered",
"created_at": "2025-01-15T10:41:00.000Z",
"data": {
"previous_status": "warning",
"new_status": "healthy"
}
}