Skip to content

Outgoing webhooks

We call your URL when something happens. Here is how to trust the call.

2 min readUpdated View as Markdown

On this page

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

HTTP
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.

JavaScript
// 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
# 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 2xx is success. Answer quickly and do slow work afterwards.
  • X-Dash4Devs-Delivery is 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.