Webhooks & Events
Receive real-time HTTP notifications for email events. Configure endpoints, verify signatures, and track deliveries, opens, clicks, bounces, and complaints.
Receive real-time HTTP notifications when something happens to your emails. Deliveries, opens, clicks, bounces, and complaints are all sent to your endpoint as they occur.
Events
| Event | Trigger |
|---|---|
email.sent | Email accepted for delivery |
email.delivered | Email delivered to recipient's mail server |
email.opened | Recipient opened the email |
email.clicked | Recipient clicked a link |
email.bounced | Delivery failed (permanent or transient) |
email.complained | Recipient marked as spam |
Quick Start
- Go to Settings > Webhooks
- Click Add Endpoint
- Enter a name, your HTTPS URL, and select the events you want
- Click Create and copy your signing secret (shown only once)
Save your signing secret immediately. It cannot be retrieved later. If you lose it, you can rotate to a new one.
Payload Format
Every webhook sends a POST request with a JSON body:
{
"event": "email.delivered",
"timestamp": "2026-02-10T10:30:00.000Z",
"data": {
"messageId": "msg_1a2b3c4d5e6f",
"recipient": "user@example.com",
"campaignId": "cmp_xxxxx",
"contactId": "cnt_xxxxx"
}
}The data object always includes messageId, recipient, campaignId, and contactId. Additional fields vary by event type.
Event-Specific Fields
email.delivered
| Field | Type | Description |
|---|---|---|
processingTimeMs | number | Time from send to delivery (ms) |
smtpResponse | string | Response from recipient's server |
email.bounced
| Field | Type | Description |
|---|---|---|
bounceType | string | permanent or transient |
bounceSubType | string | Sub-category (e.g., General, NoEmail) |
diagnosticCode | string | SMTP diagnostic from receiving server |
email.complained
| Field | Type | Description |
|---|---|---|
feedbackType | string | Complaint feedback type (e.g., abuse) |
email.opened
| Field | Type | Description |
|---|---|---|
userAgent | string | Recipient's email client |
ipAddress | string | Recipient's IP address |
email.clicked
| Field | Type | Description |
|---|---|---|
url | string | The link that was clicked |
userAgent | string | Recipient's browser/client |
ipAddress | string | Recipient's IP address |
email.sent includes only the base fields (no additional data).
Request Headers
Every webhook request includes these headers:
| Header | Example | Description |
|---|---|---|
Content-Type | application/json | Always JSON |
X-Xmit-Event | email.delivered | The event type |
X-Xmit-Timestamp | 2026-02-10T10:30:00.000Z | When the event occurred |
X-Xmit-Signature | sha256=a1b2c3... | HMAC-SHA256 signature |
Verifying Signatures
Every webhook is signed with your endpoint's secret using HMAC-SHA256. Always verify signatures to confirm the request came from Transmit.
The signature is computed over the raw JSON request body and sent in the X-Xmit-Signature header as sha256=<hex>.
Node.js
const crypto = require("crypto");
function verifyWebhook(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const received = signatureHeader.replace("sha256=", "");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(received)
);
}
// Express example
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-xmit-signature"];
const isValid = verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
console.log("Received:", event.event, event.data.messageId);
res.sendStatus(200);
});Python
import hmac
import hashlib
def verify_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()
received = signature_header.replace("sha256=", "")
return hmac.compare_digest(expected, received)Go
func verifyWebhook(body []byte, signatureHeader, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
received := strings.TrimPrefix(signatureHeader, "sha256=")
return hmac.Equal([]byte(expected), []byte(received))
}Always use constant-time comparison (timingSafeEqual, compare_digest, hmac.Equal) to prevent timing attacks.
Managing Endpoints
All webhook management is done from the dashboard at Settings > Webhooks.
Create an Endpoint
- Click Add Endpoint
- Give it a name (e.g., "Production")
- Enter your HTTPS URL
- Select the events you want to receive
- Click Create and copy the signing secret
Edit or Disable
Click on any endpoint to update its name, URL, or subscribed events. You can also toggle an endpoint on or off without deleting it. Re-enabling a disabled endpoint resets its failure count.
Rotate Secret
If your secret is compromised, click Rotate Secret on the endpoint. The old secret is invalidated immediately. Update your verification code with the new secret before processing further events.
Test Delivery
Click Send Test to deliver a sample email.delivered event to your endpoint. This lets you verify your server is receiving and processing webhooks correctly before going live.
Failure Handling
Transmit tracks consecutive delivery failures for each endpoint.
- Timeout: Your endpoint must respond within 5 seconds
- Success: Any
2xxstatus code. Resets the failure counter - Failure: Non-2xx response, timeout, or connection error. Increments the failure counter
- Auto-disable: After 10 consecutive failures, the endpoint is automatically disabled
Webhook deliveries are not retried. If your server is temporarily down, those events will be missed. For critical workflows, use the Messages API to backfill any gaps.
When an endpoint is auto-disabled, you will see the failure count and last error message in Settings > Webhooks. Fix the issue, then re-enable the endpoint to reset the counter.
Best Practices
- Respond quickly. Return
200immediately and process the event asynchronously. The 5-second timeout is strict. - Use a queue. Push incoming webhooks to a job queue (Redis, SQS, etc.) rather than doing heavy processing inline.
- Verify every request. Always check the
X-Xmit-Signatureheader to ensure the payload is authentic. - Handle duplicates. In rare cases, the same event may be delivered more than once. Use
messageId+ event type as a deduplication key. - Monitor failures. Check the failure count in your dashboard periodically. If an endpoint is auto-disabled, investigate and re-enable it.
Related
- Suppressions — Manage bounced and complained addresses
- Inbound Email — Receive and route incoming emails
- Send Email — Send emails programmatically