# Outgoing webhooks

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

Source: https://docs.dashfordevs.com/v1/webhooks/outgoing-webhooks · Topic: Automations and webhooks · Updated: 2026-11-07

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](https://datatracker.ietf.org/doc/html/rfc2104)\-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.

```js
// 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.
