Webhooks
Send each new message to another service the moment it arrives. Add them under Integrations on any form, up to 5 per form.
Discord and Slack
Paste the webhook address you got from the channel settings. RavenMail formats the message for that app: the subject as the title, the message as the text, then the sender and your custom fields.
JSON
For your own server. RavenMail sends a POST with a JSON body to any public https:// address. The body arrives on a single line; here it is spread out to read.
{
"event": "submission.created",
"form": {"slug": "your-form", "title": "Say hi"},
"submission": {
"id": "5b0d3c8e-9a51-4f6e-b1d2-7c4a2e9f8d10",
"name": "Ada",
"email": "ada@example.com",
"subject": "Quick question",
"message": "Do you ship to Lisbon?",
"answers": {
"f_k7m2qa": {"label": "Company", "value": "Analytical Engines"}
},
"source": "hosted",
"page_url": "https://example.com/contact",
"created_at": "2026-10-21T14:03:12.481516+00:00",
"attachments": [
{"name": "floor-plan.pdf", "size": 48213, "type": "application/pdf"}
]
}
}
source is hosted, embed or endpoint. A checkbox answer has a list of strings as its value. The attachments are listed but not included; open the message in your dashboard to download them.
Checking the signature
Every JSON request carries a RavenMail-Signature header that looks like this:
RavenMail-Signature: t=1792591392,v1=3f9a0c...e41b
t is the Unix time the request was signed. v1 is the hex HMAC-SHA256 of the timestamp, a dot and the raw request body, made with the signing secret shown next to the webhook under Integrations. Compute the same thing on your side and compare. Checking the timestamp too keeps an old request from being replayed later.
import hashlib
import hmac
import time
def verify(secret, header, body, tolerance=300):
try:
parts = dict(item.split("=", 1) for item in header.split(","))
timestamp = int(parts["t"])
signature = parts["v1"]
except (KeyError, ValueError):
return False
if abs(time.time() - timestamp) > tolerance:
return False
signed = f"{timestamp}.".encode() + body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
Pass body as the raw bytes you received, before any JSON parsing, and header as the value of RavenMail-Signature. Parsing and re-encoding the JSON changes the bytes and breaks the signature.
Retries
Any 2xx answer counts as delivered. A 429, a 5xx, or no answer within 10 seconds gets retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, and then marked as failed. Any other answer fails right away. Each message in your dashboard shows how its deliveries went.
The "Send test" button under Integrations sends a sample message, so you can check your server before a real one arrives.