New: the MailCraft MCP server is live — connect your AI agent in one line. Connect your agent →

Webhooks

Webhook events

MailCraft sends a signed POST to your endpoint when something happens to an email or contact. Pick the event types you care about when you register the endpoint — you only receive those.

Register an endpoint

Add one under Webhooks in the dashboard, or with the webhooks API. The URL must be public HTTPS, and events lists the event types to deliver. The response includes the signing secret — it's shown only once, so store it.

terminal
curl https://api.mailcraft.host/v1/webhooks \
  -X POST \
  -H "Authorization: Bearer $MAILCRAFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/mailcraft",
    "events": ["email.delivered", "email.bounced", "email.clicked"]
  }'

Payload

Every delivery is JSON with the same envelope: type is the event type, created_at is when the delivery was created, and data depends on the event — see each type below.

POST https://yourapp.com/webhooks/mailcraft
{
  "type": "email.clicked",
  "created_at": "2026-09-22T14:31:08.000000Z",
  "data": { ... }
}

Verify signatures

Each request carries an X-Sendkit-Signature header like t=1695393068,v1=5f8c…. v1 is the hex HMAC-SHA256 of "{t}.{raw body}", keyed with your endpoint's secret. Recompute it and compare before trusting the payload.

import crypto from 'node:crypto';

// Use the raw request body — not a re-serialised JSON object.
export function verifyMailCraftSignature(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')));
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');

  const valid =
    expected.length === (parts.v1 ?? '').length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));

  // Reject replays of old deliveries.
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;

  return valid && fresh;
}

Responses and retries

Respond with any 2xx within 5 seconds. Anything else — an error, a timeout, or a redirect, which is never followed — counts as a failure. Each delivery is attempted up to five times in total, waiting this long before each retry:

  1. 1 minute
  2. 5 minutes
  3. 30 minutes
  4. 2 hours

Deliveries can arrive more than once and out of order, so make your handler idempotent — the email or contact id plus the type identify an event.

Event types

email.sentThe email was accepted for delivery by the mail server.
email.deliveredThe recipient's mail server accepted the email.
email.openedThe recipient opened the email. Sent every time it is opened; requires open tracking on the domain.
email.clickedThe recipient clicked a tracked link. Sent for every click; requires click tracking on the domain.
email.bouncedThe email bounced permanently. The address is added to your suppression list.
email.complainedThe recipient marked the email as spam. The address is added to your suppression list.
email.unsubscribedThe recipient unsubscribed from the email.
email.delayedDelivery is being retried after a temporary failure at the recipient’s server.
email.failedThe email could not be sent.
email.rejectedThe email was rejected before it was sent.
contact.createdA contact was added to the workspace.
contact.updatedA contact's details or properties changed.
contact.deletedA contact was deleted.

Email events

email.sent

The email was accepted for delivery by the mail server.

email.sent
{
  "type": "email.sent",
  "created_at": "2026-09-22T14:31:08.000000Z",
  "data": {
    "id": "01J9Z8K2M4Q6R8T0V2X4Y6Z8AB",
    "to": [
      "[email protected]"
    ],
    "subject": "Welcome to Acme",
    "metadata": null,
    "occurred_at": "2026-09-22T14:31:07.000000Z"
  }
}

email.delivered

The recipient's mail server accepted the email.

email.delivered
{
  "type": "email.delivered",
  "created_at": "2026-09-22T14:31:08.000000Z",
  "data": {
    "id": "01J9Z8K2M4Q6R8T0V2X4Y6Z8AB",
    "to": [
      "[email protected]"
    ],
    "subject": "Welcome to Acme",
    "metadata": null,
    "occurred_at": "2026-09-22T14:31:07.000000Z"
  }
}

email.opened

The recipient opened the email. Sent every time it is opened; requires open tracking on the domain.

email.opened
{
  "type": "email.opened",
  "created_at": "2026-09-22T14:31:08.000000Z",
  "data": {
    "id": "01J9Z8K2M4Q6R8T0V2X4Y6Z8AB",
    "to": [
      "[email protected]"
    ],
    "subject": "Welcome to Acme",
    "metadata": {
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
    },
    "occurred_at": "2026-09-22T14:31:07.000000Z"
  }
}

email.clicked

The recipient clicked a tracked link. Sent for every click; requires click tracking on the domain.

email.clicked
{
  "type": "email.clicked",
  "created_at": "2026-09-22T14:31:08.000000Z",
  "data": {
    "id": "01J9Z8K2M4Q6R8T0V2X4Y6Z8AB",
    "to": [
      "[email protected]"
    ],
    "subject": "Welcome to Acme",
    "metadata": {
      "link_id": "01J9Z9C4E6G8J0L2N4Q6S8U0WY",
      "url": "https://acme.com/pricing",
      "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X)"
    },
    "occurred_at": "2026-09-22T14:31:07.000000Z"
  }
}

email.bounced

The email bounced permanently. The address is added to your suppression list.

email.bounced
{
  "type": "email.bounced",
  "created_at": "2026-09-22T14:31:08.000000Z",
  "data": {
    "id": "01J9Z8K2M4Q6R8T0V2X4Y6Z8AB",
    "to": [
      "[email protected]"
    ],
    "subject": "Welcome to Acme",
    "metadata": null,
    "occurred_at": "2026-09-22T14:31:07.000000Z"
  }
}

email.complained

The recipient marked the email as spam. The address is added to your suppression list.

email.complained
{
  "type": "email.complained",
  "created_at": "2026-09-22T14:31:08.000000Z",
  "data": {
    "id": "01J9Z8K2M4Q6R8T0V2X4Y6Z8AB",
    "to": [
      "[email protected]"
    ],
    "subject": "Welcome to Acme",
    "metadata": null,
    "occurred_at": "2026-09-22T14:31:07.000000Z"
  }
}

email.unsubscribed

The recipient unsubscribed from the email.

email.unsubscribed
{
  "type": "email.unsubscribed",
  "created_at": "2026-09-22T14:31:08.000000Z",
  "data": {
    "id": "01J9Z8K2M4Q6R8T0V2X4Y6Z8AB",
    "to": [
      "[email protected]"
    ],
    "subject": "Welcome to Acme",
    "metadata": null,
    "occurred_at": "2026-09-22T14:31:07.000000Z"
  }
}

email.delayed

Delivery is being retried after a temporary failure at the recipient’s server.

email.delayed
{
  "type": "email.delayed",
  "created_at": "2026-09-22T14:31:08.000000Z",
  "data": {
    "id": "01J9Z8K2M4Q6R8T0V2X4Y6Z8AB",
    "to": [
      "[email protected]"
    ],
    "subject": "Welcome to Acme",
    "metadata": null,
    "occurred_at": "2026-09-22T14:31:07.000000Z"
  }
}

email.failed

The email could not be sent.

email.failed
{
  "type": "email.failed",
  "created_at": "2026-09-22T14:31:08.000000Z",
  "data": {
    "id": "01J9Z8K2M4Q6R8T0V2X4Y6Z8AB",
    "to": [
      "[email protected]"
    ],
    "subject": "Welcome to Acme",
    "metadata": null,
    "occurred_at": "2026-09-22T14:31:07.000000Z"
  }
}

email.rejected

The email was rejected before it was sent.

email.rejected
{
  "type": "email.rejected",
  "created_at": "2026-09-22T14:31:08.000000Z",
  "data": {
    "id": "01J9Z8K2M4Q6R8T0V2X4Y6Z8AB",
    "to": [
      "[email protected]"
    ],
    "subject": "Welcome to Acme",
    "metadata": null,
    "occurred_at": "2026-09-22T14:31:07.000000Z"
  }
}

Contact events

contact.created

A contact was added to the workspace.

contact.created
{
  "type": "contact.created",
  "created_at": "2026-09-22T14:31:08.000000Z",
  "data": {
    "id": "01J9Z6Q4M7X2K9R3T5V8W1Y4ZB",
    "email": "[email protected]"
  }
}

contact.updated

A contact's details or properties changed.

contact.updated
{
  "type": "contact.updated",
  "created_at": "2026-09-22T14:31:08.000000Z",
  "data": {
    "id": "01J9Z6Q4M7X2K9R3T5V8W1Y4ZB",
    "email": "[email protected]"
  }
}

contact.deleted

A contact was deleted.

contact.deleted
{
  "type": "contact.deleted",
  "created_at": "2026-09-22T14:31:08.000000Z",
  "data": {
    "id": "01J9Z6Q4M7X2K9R3T5V8W1Y4ZB",
    "email": "[email protected]"
  }
}