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

EventDescription
message.receivedSomeone sent an email to one of your mailboxes.
message.sentAn email you sent has left the server.
message.deliveredYour email was successfully delivered to the recipient.
message.bouncedYour email could not be delivered. The address may not exist.
message.complainedA recipient marked your email as spam.
domain.verifiedYour domain's DNS records are correctly configured.
domain.verification_failedOne or more DNS records are missing or incorrect.
domain.degradedA DNS record that was working has stopped resolving.
org.reputation_warningToo many emails are bouncing or getting flagged as spam.
org.sending_throttledSending speed reduced to protect your reputation.
org.sending_suspendedSending paused until bounce and complaint rates improve.
org.reputation_recoveredYour sending reputation is back to normal.

Create a webhook

POST /v1/webhooks

Scope: webhooks:write. Rate limit tier: write.

Request body

FieldTypeRequiredDescription
urlstringYesHTTPS endpoint URL.
eventsstring[]YesAt least one event type from the table above.
descriptionstringNoDescription. 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/webhooks

Scope: webhooks:read. Supports pagination.

Retrieve a webhook

GET /v1/webhooks/:id

Scope: webhooks:read. Returns the webhook object or 404. The secret is never returned after creation.

Update a webhook

PATCH /v1/webhooks/:id

Scope: webhooks:write. At least one field must be provided.

Request body

FieldTypeDescription
urlstringNew HTTPS endpoint URL.
eventsstring[]Replace subscribed events.
descriptionstring | nullUpdate or clear description.
activebooleanEnable or disable.

Delete a webhook

DELETE /v1/webhooks/:id

Scope: webhooks:write. Returns 204 with no body.

Rotate secret

POST /v1/webhooks/:id/rotate-secret

Generates 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/test

Queues 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/deliveries

Scope: 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" } }