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.
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.
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"
}
}