Webhooks API
The Webhooks API lets you manage webhook subscriptions programmatically. List, create, and delete webhooks for each of your forms, and verify the signature on every delivery. Editing a webhook and retrying a failed delivery are done from the dashboard, not the API.
Base URL: https://wittyform.com/api/v1
Webhook events
WittyForm supports the following webhook events:
| Event | Description | Triggered when |
|---|---|---|
form.submitted | New form submission received | A visitor submits a form or a response is created via the API |
response.received | New response recorded | Alias of the submission event; subscribe to either to receive new submissions |
form.updated | Form definition changed | A form's fields or settings are updated |
form.published | Form published | A form is published and starts accepting responses |
form.deleted | Form deleted | A form is permanently deleted |
quiz.completed | Quiz completed | A respondent finishes a quiz |
Payload structure
Every webhook delivers a JSON payload with a top-level event, timestamp, formId (and quizId for quiz events), and a data object holding the submission details:
{
"event": "form.submitted",
"timestamp": "2025-09-15T14:32:08.000Z",
"formId": "b3f1c2d4-1a2b-4c3d-9e8f-001122334455",
"data": {
"responseId": "e6a4f5b7-4d5e-4f70-91b2-334455667788",
"formId": "b3f1c2d4-1a2b-4c3d-9e8f-001122334455",
"answers": {
"9a1b2c3d-4e5f-4061-8273-8495a6b7c8d9": "Jane Smith",
"1f2e3d4c-5b6a-4798-9a8b-7c6d5e4f3a2b": "[email protected]",
"7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f": "Hello!"
},
"submittedVia": "form"
}
}The event name is also sent in the X-Webhook-Event header and the timestamp in X-Webhook-Timestamp.
Signature verification
Every webhook request includes an X-Webhook-Signature header containing an HMAC-SHA256 signature of the request body, formatted as sha256=<hex>. Verify this to ensure the payload was sent by WittyForm and was not tampered with.
Node.js verification
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}
// Express.js example:
app.post('/webhooks/wittyform', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const isValid = verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log('Received:', event.event, event.data.responseId);
// Process the webhook asynchronously
res.status(200).json({ received: true });
});Python verification
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example:
@app.route('/webhooks/wittyform', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature', '')
if not verify_signature(request.data, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
event = request.get_json()
print(f"Received: {event['event']} {event['data']['responseId']}")
return {'received': True}, 200Responding to webhooks
Your endpoint must return a 2xx status code within 10 seconds to acknowledge receipt. Best practice:
- Return
200 OKimmediately. - Process the data asynchronously (e.g., queue it for background processing).
- Do not perform long-running operations before responding.
Retry behavior
If your endpoint returns a non-2xx status or times out, WittyForm retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 30 seconds after initial failure |
| 2nd retry | 5 minutes after 1st retry |
| 3rd retry | 30 minutes after 2nd retry |
After 3 failed retries, the delivery is marked as failed. You can manually retry a failed delivery from the dashboard. (Retrying a delivery is not available through the API.)
List webhooks
GET /api/v1/forms/:form_id/webhooks
Returns all webhooks configured for a form. Requires read scope.
Example request
curl -X GET "https://wittyform.com/api/v1/forms/b3f1c2d4-1a2b-4c3d-9e8f-001122334455/webhooks" \
-H "Authorization: Bearer wf_live_abc123"Example response
{
"success": true,
"data": {
"webhooks": [
{
"id": "2b3c4d5e-6f70-4812-93a4-66778899aabb",
"form_id": "b3f1c2d4-1a2b-4c3d-9e8f-001122334455",
"name": "Order notifier",
"url": "https://api.example.com/webhooks/wittyform",
"events": ["form.submitted"],
"is_active": true,
"max_retries": 3,
"created_at": "2025-08-01T10:00:00.000Z",
"updated_at": "2025-08-01T10:00:00.000Z"
}
]
}
}The signing secret is never returned by the list endpoint. It is only shown once, when the webhook is created.
Create a webhook
POST /api/v1/forms/:form_id/webhooks
Registers a new webhook endpoint for a form. Requires admin scope.
Request body
Provide a name (1 to 100 characters), a url, and one or more events. The URL must be a public HTTPS endpoint.
{
"name": "Order notifier",
"url": "https://api.example.com/webhooks/wittyform",
"events": ["form.submitted"]
}Example response (201 Created)
{
"success": true,
"data": {
"webhook": {
"id": "3c4d5e6f-7081-4923-a4b5-778899aabbcc",
"form_id": "b3f1c2d4-1a2b-4c3d-9e8f-001122334455",
"name": "Order notifier",
"url": "https://api.example.com/webhooks/wittyform",
"events": ["form.submitted"],
"is_active": true,
"max_retries": 3,
"created_at": "2025-09-15T19:00:00.000Z",
"updated_at": "2025-09-15T19:00:00.000Z",
"secret": "a1b2c3d4e5f6..."
}
}
}Save data.webhook.secret: it is only returned once, when the webhook is created. Use it to verify incoming webhook signatures.Delete a webhook
DELETE /api/v1/forms/:form_id/webhooks/:id
Removes a webhook subscription. Requires admin scope.
Example request
curl -X DELETE "https://wittyform.com/api/v1/forms/b3f1c2d4-1a2b-4c3d-9e8f-001122334455/webhooks/2b3c4d5e-6f70-4812-93a4-66778899aabb" \
-H "Authorization: Bearer wf_live_abc123"Example response (204 No Content)
An empty response body with status 204 indicates the webhook has been deleted.