DevelopersGuide
Outgoing webhooks
We call your URL when something happens. Here is how to trust the call.
2 min readUpdated View as Markdown
Add an endpoint in the dashboard's automations section, choose the events, and we send a signed POST to your URL each time one happens.
Events
| Event | When |
|---|---|
contact_form.submitted |
A contact form was submitted. |
contact_form.replied |
Staff replied, or the sender replied by email. |
contact_form.status_changed |
A submission's status changed. |
event.scheduled |
A calendar booking was created. |
email.received, email.sent, email.delivered, email.bounced |
Email activity. |
The request
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Dash4Devs-Webhooks/1.0
X-Dash4Devs-Event: contact_form.submitted
X-Dash4Devs-Delivery: …
X-Dash4Devs-Signature: t=1762531200,v1=5f2b…
{"created_at":"2026-11-07T15:00:00Z","data":{"submission_id":"…"},"event":"contact_form.submitted","id":"evt_…","organization_id":"org__…"}data holds ids, not personal details. Fetch what you need through the API with your own key. That keeps names and messages out of your logs and ours.
Verify the signature
The signature is HMAC-SHA256 of "<t>.<raw body>" with your secret, hex-encoded. Always verify against the raw body bytes, compare in constant time, and reject old timestamps.
// Node.js
import crypto from "node:crypto";
export function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");
const given = Buffer.from(parts.v1 ?? "", "hex");
const want = Buffer.from(expected, "hex");
return given.length === want.length && crypto.timingSafeEqual(given, want);
}# Python
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Retries
- Up to 5 attempts: immediately, then after 30 seconds, 3 minutes, 15 minutes and 1 hour.
- Any
2xxis success. Answer quickly and do slow work afterwards. X-Dash4Devs-Deliveryis the same on every retry: use it to ignore duplicates.- After 25 failures in a row the endpoint is switched off; turn it back on in the dashboard.
Safety rules we enforce
- HTTPS only.
- URLs that resolve to private, loopback or link-local addresses are refused, every attempt.
- Redirects are never followed.
- Secrets are shown masked after creation.
Keep reading
Something wrong or missing on this page? Email support@dashfordevs.com.