Webhooks API

6 min read

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:

EventDescriptionTriggered when
form.submittedNew form submission receivedA visitor submits a form or a response is created via the API
response.receivedNew response recordedAlias of the submission event; subscribe to either to receive new submissions
form.updatedForm definition changedA form's fields or settings are updated
form.publishedForm publishedA form is published and starts accepting responses
form.deletedForm deletedA form is permanently deleted
quiz.completedQuiz completedA 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}, 200

Responding to webhooks

Your endpoint must return a 2xx status code within 10 seconds to acknowledge receipt. Best practice:

  • Return 200 OK immediately.
  • 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:

AttemptDelay
1st retry30 seconds after initial failure
2nd retry5 minutes after 1st retry
3rd retry30 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.

$37 · paid once

Ready to Build Forms That Actually Convert?

Start free and keep the five forms for good, or pay once and unlock all of it — unlimited forms, ten team members, AI, CRM, white label and the API. No renewal, ever.

14-day money-backInstant accessNo subscription, ever
Webhooks API