Mailactor
Webhooks

Receive webhook notifications

Let Mailactor notify your application when new email arrives.

A webhook sends an HTTPS request to your application after an inbound message is available. The current event type is message.received. Outbound delivery status is read through submissions.

Required scope: webhook:manage. Your application also needs mailbox:read to fetch message content.

Set MAILACTOR_API_URL and MAILACTOR_API_KEY as shown in quickstart. Examples below use illustrative IDs and values; use the values returned for your organization.

Prepare your receiver

Use a public HTTPS endpoint on port 443. Localhost, private addresses, redirects, embedded URL credentials, and URL fragments are not supported. Your receiver must preserve the raw request body for signature verification and persist verified event IDs for deduplication.

If you do not have a public receiver yet, poll your inbox.

Register an endpoint

export WEBHOOK_KEY="webhook-$(uuidgen)"

curl --fail-with-body --silent --show-error \
  "$MAILACTOR_API_URL/v1/webhooks" \
  -H "x-api-key: $MAILACTOR_API_KEY" \
  -H "idempotency-key: $WEBHOOK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-app.example.com/webhooks/mailactor","events":["message.received"]}'

Replace the URL with your actual receiver. Store the returned id, signingSecret, and signingSecretReplayUntil securely. Avoid running the command in shared or logged terminals because its response contains the signing secret.

An exact retry with the same idempotency key and input can recover the same secret only until signingSecretReplayUntil. Listing webhooks does not return it. After that deadline, replace an endpoint whose secret you have lost.

Request and response

FieldRequiredShape
urlYesPublic HTTPS URL on port 443, at most 2,048 characters.
eventsYesExactly ["message.received"]; other event types are not supported.
Request: createWebhook
{
  "url": "https://your-app.example.com/webhooks/mailactor",
  "events": ["message.received"]
}

New registration returns 201 with the full object below. Exact replay returns 200, the same ID and secret, and replayed: true. The replay deadline shown is illustrative: use the returned signingSecretReplayUntil, never a hardcoded duration.

Response: createWebhook 201
{
  "id": "whk_0123456789abcdef01234567",
  "url": "https://your-app.example.com/webhooks/mailactor",
  "events": ["message.received"],
  "status": "active",
  "createdAt": "2026-09-08T12:00:00.000Z",
  "updatedAt": "2026-09-08T12:00:00.000Z",
  "signingSecret": "whsec_EXAMPLE_ONLY_DO_NOT_USE",
  "signingSecretReplayUntil": "2026-09-08T12:15:00.000Z",
  "replayed": false
}

Process an event

The JSON payload contains id, type, createdAt, and data with inboxId, threadId, and messageId. It contains no subject, sender address, message body, or attachments.

Event: message.received
{
  "id": "evt_0123456789abcdef01234567",
  "type": "message.received",
  "createdAt": "2026-09-08T12:01:00.000Z",
  "data": {
    "messageId": "msg_0123456789abcdef01234567",
    "inboxId": "inb_0123456789abcdef01234567",
    "threadId": "thr_0123456789abcdef01234567"
  }
}
  1. Verify the signature and timestamp against the exact raw body.
  2. Persist the verified event using a unique constraint on your endpoint identity plus the signed event ID. Record the delivery ID for tracing; that header alone is not a safe deduplication key.
  3. Return 2xx after durable acceptance. A duplicate already persisted should also receive 2xx.
  4. In a background job, fetch GET /v1/threads/{threadId} and locate data.messageId, following pagination as needed.

Delivery is at least once. Transport failures and non-2xx responses are retried with exponential backoff. Design side effects, including automatic replies, to be idempotent.

List and remove endpoints

GET /v1/webhooks returns endpoints and nextCursor. Use DELETE /v1/webhooks/{webhookId} to remove an endpoint.

curl --fail-with-body --silent --show-error \
  "$MAILACTOR_API_URL/v1/webhooks?limit=20" \
  -H "x-api-key: $MAILACTOR_API_KEY"

# WEBHOOK_ID is the saved endpoint id (whk_...), not an event or delivery id.
curl --fail-with-body --silent --show-error -X DELETE \
  "$MAILACTOR_API_URL/v1/webhooks/$WEBHOOK_ID" \
  -H "x-api-key: $MAILACTOR_API_KEY"

List success is 200; its full response contains no signing secret. Delete success is 204 with no body. There is no individual webhook GET or update endpoint; verify removal by paging the complete list.

Deletion can return 409 while a delivery is in flight. Retry with bounded backoff and confirm absence from the complete endpoint inventory. To change a URL or replace a secret, create a replacement at a distinct receiver URL (a new path is enough), store its secret, then delete the old endpoint. A URL must be unique within the organization. If you must reuse the exact URL, delete the old endpoint first, then register it with a fresh key; reconcile the resulting notification gap by polling. During replacement, endpoint-level deduplication alone cannot prevent both endpoints from triggering the same action. Add a business-action key such as organization + event ID + action name, shared by both receivers, and preserve the same send/reply idempotency key. Use polling to reconcile any gap. If recovery returns 409 webhook_secret_replay_expired, replace the endpoint with a new key; never invent or rotate a secret locally.

Webhook creation reference

Registration conflicts and expiry

Status and errorAction
409 idempotency_key_conflictRestore the original persisted input for that key.
409 webhook_endpoint_existsThe URL is already registered; list endpoints to reconcile.
409 webhook_secret_replay_expiredThe secret recovery window expired. Replace the endpoint if you lost its secret.
410 webhook_endpoint_deletedThat key belongs to a deleted endpoint. Register a new endpoint with a fresh key.
Response: createWebhook 409
{
  "error": "webhook_secret_replay_expired",
  "message": "The signing-secret replay window for this idempotency key has expired"
}
Response: createWebhook 410
{
  "error": "webhook_endpoint_deleted",
  "message": "The webhook endpoint created by this idempotency key was deleted"
}

Delivery retry boundary

Webhook retries are finite. Request timeout and maximum attempts are deployment settings; the public contract does not promise an exact retry schedule or delivery deadline. After attempts are exhausted, that delivery stops retrying. There is no public delivery replay or dead-letter inspection endpoint. A receiver must not rely on callbacks as its only record of incoming mail: periodically reconcile inbox threads and messages, including after outages. Arrival order is not guaranteed.

On this page