API v1.0

SendPigeon Documentation

Everything you need to send transactional emails. SDKs for Node.js, Python, Go, PHP and REST API.

Inbound Email

Receive emails on your domain via webhook or forwarding.

Paid plan required. Inbound email is available on Starter and Pro plans.

1. Add MX Record

Point your domain's MX record to SendPigeon to receive emails.

TypePriorityValue
MX10inbound.sendpigeon.dev

2. Choose Mode

Configure how inbound emails are handled in your domain settings.

ModeDescription
forwardForward emails to another address
webhookPOST email data to your endpoint

Webhook Payload

When an email arrives, we POST the following to your webhook URL:

Webhook Payloadjson
{
"event": "email.received",
"timestamp": "2025-01-15T10:30:00Z",
"data": {
"id": "in_abc123",
"from": "sender@example.com",
"to": "hello@yourdomain.com",
"subject": "Hello!",
"text": "Plain text body...",
"html": "<p>HTML body...</p>",
"attachments": [
{
"filename": "document.pdf",
"contentType": "application/pdf",
"size": 12345,
"url": "https://..."
}
],
"rawUrl": "https://..."
}
}

Attachment URLs are presigned and expire after 1 hour.

Verify Signature

Verify the webhook signature to ensure requests are from SendPigeon.

TypeScripttypescript
import { verifyInboundWebhook } from "sendpigeon";
app.post("/inbound", async (req, res) => {
const result = await verifyInboundWebhook({
payload: req.body,
signature: req.headers["x-webhook-signature"],
timestamp: req.headers["x-webhook-timestamp"],
secret: process.env.INBOUND_WEBHOOK_SECRET,
});
if (!result.valid) {
return res.status(400).json({ error: result.error });
}
const { from, to, subject, text, html, attachments } = result.payload.data;
console.log(`Email from ${from}: ${subject}`);
res.status(200).json({ received: true });
});

Webhook Headers

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature of the payload
X-Webhook-TimestampUnix timestamp when signature was created

Manual Verification

If not using the SDK, verify signatures manually:

TypeScripttypescript
import crypto from "crypto";
function verifySignature(payload, signature, timestamp, secret) {
const data = `${timestamp}.${JSON.stringify(payload)}`;
const expected = crypto
.createHmac("sha256", secret)
.update(data)
.digest("hex");
// Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}

Rate limit: 100 emails/minute per domain. Retry: Failed webhook deliveries are retried 3 times with exponential backoff.