API Reference
API Reference
GitHub ↗
Webhooks and Events

Event Types

Available webhook event types

Seven events exist, and a subscription to anything else is refused with 400 when the webhook is created.

Email Events

EventFires when
email.queuedThe message was accepted and written to the delivery queue
email.sentIt was handed to an SMTP server successfully
email.failedDelivery failed and no attempt remains
email.suppressedEvery recipient was on a suppression list, so nothing was attempted
email.cancelledThe caller withdrew it before a worker claimed it

Campaign Events

EventFires when
campaign.startedThe audience was fanned out into the queue
campaign.completedEvery message in the campaign reached a terminal state

Inbound Events

EventFires when
inbound.receivedA message arrived for a verified domain

The Envelope

Every delivery has the same three top-level fields, and the per-event fields are inside data - never at the top level:

{
    "event": "email.sent",
    "timestamp": "2026-01-01T00:00:01Z",
    "data": {}
}

timestamp is when the event was emitted, RFC 3339 in UTC. The body is signed with the webhook’s secret - see Signature Verification .

Email Payloads

email.queued, email.sent, email.failed, email.suppressed and email.cancelled all carry the same data shape, so one handler can serve all four and switch on event or on data.status:

{
    "event": "email.failed",
    "timestamp": "2026-01-01T00:00:01Z",
    "data": {
        "id": "019ffb19-e600-7b7d-b8dd-17cff629d5c6",
        "project_id": "019ffb18-b5d6-7488-9c5e-4373a8c062dc",
        "sender": "noreply@example.com",
        "recipients": [
            "recipient@customer.example"
        ],
        "subject": "Your receipt",
        "template_name": "receipt",
        "status": "failed",
        "error_message": "smtp dial failed: dial tcp 203.0.113.9:587: connect: connection refused",
        "attempts": 3,
        "created_at": "2026-01-01T00:00:00Z",
        "sent_at": null
    }
}
FieldNotes
idThe email id. GET /api/v1/emails/{id} returns the full record, bodies included.
recipientsEvery recipient of this message, as an array. This is the address you want - id alone means nothing outside Mailyard.
senderThe From address as accepted, which may carry a display name.
template_nameEmpty string when the message was not sent from a template.
statusqueued, sent, failed or suppressed. Matches the event.
error_messageWhy delivery failed. Empty on the non-failure events.
attemptsAttempts made. On email.failed this is the last one.
sent_atnull until the message is sent.

No per-recipient event

One message produces one event, whatever the recipient count - recipients is an array for that reason. A message that failed for one address and succeeded for another is not representable here; use one send per recipient when you need that distinction.

Campaign Payloads

{
    "event": "campaign.completed",
    "timestamp": "2026-01-01T00:00:01Z",
    "data": {
        "id": "a1f5c3d2-6b48-4e97-9c21-3d8e7f0a5b64",
        "project_id": "019ffb18-b5d6-7488-9c5e-4373a8c062dc",
        "name": "Spring Newsletter",
        "from_email": "news@example.com",
        "list_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
        "counts": {
            "sent": 1180,
            "failed": 12,
            "skipped": 8
        }
    }
}

counts differs between the two events, and that is the only difference in shape:

  • On campaign.started it is {"recipients": N} - how many messages were queued.
  • On campaign.completed it is the per-status totals (sent, failed, skipped), and a status with no messages is absent rather than zero. The event waits for the last message to be delivered or refused, so the totals are final.

Inbound Payload

{
    "event": "inbound.received",
    "timestamp": "2026-01-01T00:00:01Z",
    "data": {
        "id": "7d3f9a12-4b2c-7e81-9a03-5f6d8c1e2b47",
        "domain": "mail.example.com",
        "status": "received",
        "sender": "Jane Doe <jane@example.com>",
        "bounce_address": "bounces@example.com",
        "recipients": [
            "inbox@mail.example.com"
        ],
        "subject": "Hello",
        "message_id": "<unique@mail.example.com>",
        "size": 14200,
        "received_at": "2026-01-01T00:00:00Z"
    }
}

status is received, or failed for a message that arrived but would not parse as MIME. A failed message is stored with its raw bytes and no sender, subject or Message-ID, so those fields are empty.

Metadata only: no bodies, no headers and no attachments. A received message can be tens of megabytes, and a webhook body that size is a delivery that times out rather than a convenience. Fetch what you need with GET /api/v1/inbound-emails/{id}, which returns the parsed message, and its attachment routes for the content.

sender is the From header, who the message says it is from. bounce_address is the SMTP MAIL FROM, where bounces for the message go, which is often not the sender.

message_id is the RFC 5322 header from the message itself, not one of ours, and it is empty when the sender did not set one.

Filters

A webhook may narrow its deliveries to matching sender addresses - exact (billing@example.com) or per-domain (*@example.com), matched case-insensitively against the bare address with any display name stripped.

The field matched is the sender, and which address that is depends on the direction:

  • On the email events it is the From address of the message you sent.
  • On the campaign events it is the campaign’s own from address.
  • On inbound.received it is the From address of the mail you received, which is somebody else’s. A filter of *@example.com on a webhook subscribed to inbound therefore means “only mail arriving from example.com”, not “only mail to my example.com domain”.

A message with no sender at all - no From header - passes every filter rather than none.