Outgoing Webhooks
Outgoing webhooks push incoming email into your own systems. Whenever an email arrives in one of your mailboxes, SDP Email sends an HTTP POST with the full email as JSON — including download links for any attachments — to every notify URL you configure.
Typical uses: feeding a helpdesk or CRM, triggering automations, or archiving mail in your own storage.
Add a Notify URL
- Open Settings → Outgoing webhooks in the admin dashboard.
- Click Add notify URL.
- Enter the URL that should receive the
POST. It must be reachable from the internet over HTTPS. - Optionally set custom headers, a secret, and a recipient filter (explained below).
- Save. The webhook is active immediately.
You can add as many notify URLs as you need — each incoming email is delivered to every active URL whose filter matches.
Filtering Which Emails Notify a URL
Each notify URL has an optional filter — a list of entries the incoming email's recipient is matched against:
| Entry form | Example | Matches |
|---|---|---|
| Full mailbox address | [email protected] |
Only mail delivered to that exact mailbox |
| Bare domain | acme.com |
Mail delivered to any mailbox under that domain |
Rules:
- An empty filter means the URL is notified for every incoming email in your workspace.
- Entries are case-insensitive.
- A URL is notified once per email when any of its entries matches.
Verifying Requests with a Secret
If you set a secret on a notify URL, SDP Email sends it with every request as a header:
X-Webhook-Secret: your-secret-value
Your endpoint should compare the header against the value you configured and reject requests that don't match. This prevents anyone who discovers your endpoint URL from feeding it forged emails.
The secret is stored encrypted and is never shown again in the dashboard after you save it — you can only replace or remove it.
Custom Headers
Add header name/value pairs to each notify URL for endpoints that require authentication or other metadata. For example:
| Header name | Value |
|---|---|
Authorization |
Bearer your-token |
X-API-Key |
your-api-key |
Enter the complete value, including any authentication scheme such as Bearer or Basic. You can configure up to 20 headers. Names must be valid HTTP header names and unique regardless of case; values must be non-empty, single-line strings up to 8,192 characters.
Values are stored encrypted and loaded only when an administrator opens the edit form. Saved values appear in password inputs; click Show to reveal a value or Hide to mask it again. Enter a new value to replace it, leave a saved value blank to keep it, or remove its row to delete the header. Lists and request history keep values masked. Headers are sent alongside the optional X-Webhook-Secret. Transport headers, Content-Type, and X-Webhook-Secret are managed automatically and cannot be supplied as custom headers.
Use the final destination URL: redirects are not followed, so credentials are not forwarded to another endpoint.
Request Format
- Method:
POST - Content type:
application/json - Headers: Your configured custom headers, plus
X-Webhook-Secret(only when a secret is set)
Payload
{
"id": 481,
"message_id": "[email protected]",
"thread_id": 466,
"recipient": "[email protected]",
"from_name": "Jane Doe",
"from_email": "[email protected]",
"to_recipients": [{ "name": "Support", "email": "[email protected]" }],
"cc_recipients": [],
"subject": "Order #1042 arrived damaged",
"text_body": "Hi team, the package arrived damaged...",
"html_body": "<p>Hi team, the package arrived damaged...</p>",
"snippet": "Hi team, the package arrived damaged...",
"has_attachments": true,
"received_at": "2026-07-16T09:24:11+03:00",
"attachments": [
{
"filename": "damage-photo.jpg",
"content_type": "image/jpeg",
"size": 482113,
"url": "https://…signed-download-url…"
}
]
}
Field Reference
| Field | Type | Description |
|---|---|---|
id |
integer | SDP Email's ID for the stored email. |
message_id |
string | null | The email's Message-ID header, without angle brackets. |
thread_id |
integer | ID of the conversation thread the email belongs to. |
recipient |
string | The mailbox address the email was delivered to. This is the address the filter matched. |
from_name |
string | null | Sender display name. |
from_email |
string | null | Sender address. |
to_recipients |
array | To header addresses as { name, email } objects. |
cc_recipients |
array | Cc header addresses as { name, email } objects. |
subject |
string | null | Email subject. |
text_body |
string | null | Plain-text body. |
html_body |
string | null | HTML body. |
snippet |
string | null | Short plain-text preview of the body. |
has_attachments |
boolean | Whether the email carried attachments. |
received_at |
string | null | When the email was received, ISO-8601. |
attachments |
array | Attachment descriptors — always present, empty when the email has none. |
Attachments
Each entry in attachments:
| Field | Type | Description |
|---|---|---|
filename |
string | null | Original file name. |
content_type |
string | null | MIME type. |
size |
integer | null | Size in bytes. |
url |
string | Signed download URL, valid for 24 hours. |
Attachment contents are never embedded in the payload — download them from url. Because the link expires after 24 hours, fetch and store the file promptly if you need to keep it. In the rare case an attachment's file could not be stored when the email arrived, that attachment is omitted from the array (the email itself is still delivered).
Responses, Timeouts, and Retries
- Your endpoint must respond within 15 seconds.
- Any
2xxstatus counts as delivered. Keep processing fast — acknowledge first, do heavy work asynchronously. - Any other status, or a timeout, counts as a failure. Delivery is retried up to 3 attempts in total, waiting 30 seconds, 2 minutes, and 5 minutes between attempts. After the final failure the notification is dropped (the email itself is always kept in the mailbox).
- Deliveries can occasionally arrive more than once — use
idormessage_idto deduplicate.
Example Receiver
A minimal Laravel route that verifies the secret and reads the payload:
Route::post('/webhooks/incoming-email', function (Request $request) {
abort_unless(
hash_equals(config('services.email.webhook_secret'), (string) $request->header('X-Webhook-Secret')),
401
);
$email = $request->json()->all();
Log::info('Incoming email', [
'from' => $email['from_email'],
'subject' => $email['subject'],
'attachments' => count($email['attachments']),
]);
return response()->noContent();
});
The same in Express:
app.post('/webhooks/incoming-email', express.json(), (req, res) => {
if (req.get('X-Webhook-Secret') !== process.env.EMAIL_WEBHOOK_SECRET) {
return res.sendStatus(401);
}
const email = req.body;
console.log(`Incoming email from ${email.from_email}: ${email.subject}`);
res.sendStatus(204);
});
Request History and Testing
Click Details beside a notify URL to open its details popup. It shows the saved configuration and the latest 100 request attempts, including normal deliveries, retries, and manual tests. History begins when this feature is installed; older requests cannot be recovered.
Select a request to inspect its JSON payload, masked request headers, HTTP status, duration, response headers, and response body. Payloads and response bodies are encrypted at rest. Response previews are limited to 16 KB, and saved header values are redacted from history. Each retry creates a separate entry. Removing a webhook also removes its history.
Click Send test request to send a sample incoming email payload to the saved URL with its saved headers. This performs a real HTTP POST and can trigger processing at your endpoint. The payload uses sample example.test addresses, ID 0, no attachments, and a unique test message ID. It creates no email in your mailbox. Tests work for disabled webhooks and ignore recipient filters; they do not enable normal deliveries. A test makes one attempt and shows the result immediately, including HTTP errors or connection failures.
Managing Notify URLs
From Settings → Outgoing webhooks, open Details for a notify URL to:
- Edit a URL, its filter, or its active state at any time.
- Add, replace, or remove custom headers.
- Replace or remove the secret (leave the field blank while editing to keep the current one).
- Disable a URL without deleting it — deliveries stop immediately and resume when re-enabled.
- Delete a URL — deliveries stop immediately.