Outgoing Webhooks
Outgoing webhooks push incoming email into your own systems. Whenever an email arrives in one of your mailboxes, Arawa Mail 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 a secret and a recipient filter (both 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, Arawa Mail 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.
Request Format
- Method:
POST - Content type:
application/json - Headers:
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 | Arawa Mail'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);
});
Managing Notify URLs
From Settings → Outgoing webhooks you can:
- Edit a URL, its filter, or its active state at any time.
- 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.