Developers
Webhooks
Get notified in real time, verify signatures and handle retries.
Webhooks (Pro) send an HTTPS POST to your endpoint when something happens.
Events
| Event | When |
|---|---|
response.created | A response was submitted (web, embed or API) |
response.approval_changed | An owner overrode or reset an approval decision |
response.deleted | A response was deleted |
ping | You clicked Send test |
Payload
{
"id": "6650d0e9…",
"type": "response.created",
"created_at": "2026-10-18T09:24:12Z",
"data": {
"form": { "id": "6650…", "slug": "k3x9m2pq7a", "title": "Job application" },
"response": {
"id": "6650c1f2…",
"form_version": 3,
"created_at": "2026-10-18T09:24:11.502Z",
"source": "web",
"answers": { "email": "ada@example.com" },
"approval": { "status": "approved", "score": 5, "max_score": 6, "percent": 83.33 },
"url": "https://your-domain/dashboard/forms/6650…/responses/6650c1f2…"
}
}
}
Headers: X-CreateAForm-Event, X-CreateAForm-Delivery (unique id, use it to de-duplicate) and X-CreateAForm-Signature.
Verify the signature
The signature header looks like t=1729243452,v1=5257a869…. Compute HMAC-SHA256(secret, "{t}.{raw body}") in hex and compare it in constant time; reject requests older than 5 minutes.
import hmac, hashlib, time
def verify(secret: str, header: str, body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
Always verify against the raw request body, before parsing JSON.
Retries
Respond with any 2xx within 10 seconds. Otherwise the delivery is retried up to 8 times with increasing delays (30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h, 12 h). After 25 consecutive failures the endpoint is disabled; re-enable it in the dashboard.
Deliveries may arrive more than once or out of order — use X-CreateAForm-Delivery and created_at.
Security rules
- Endpoints must use
https://and resolve to a public IP address (private, loopback and cloud metadata addresses are refused, even via DNS tricks). - Redirects are not followed.
- Only signed-in users can manage webhooks; API keys cannot.