BETA
Skip to content

Webhooks

Receive real-time HTTP notifications when new form submissions arrive. Krafter Forms sends a signed POST request to your endpoint for every new submission.

Base URL: https://app.krafter.dev/api/v1

All webhook endpoints are nested under a form: /forms/:form_id/webhooks. Signing, retry policy, and auto-disable rules are shared across the platform — see Webhook delivery.

List Webhooks

Retrieve all webhooks for a form.

GET /forms/:form_id/webhooks

Required scope: forms:read

Example Request

bash
curl https://app.krafter.dev/api/v1/forms/FORM_ID/webhooks \
  -H "Authorization: Bearer kr_live_abc123def456"

Example Response

json
{
  "data": [
    {
      "id": "f1a2b3c4-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
      "url": "https://api.yourapp.com/webhooks/forms",
      "events": ["submission.created"],
      "active": true,
      "failure_count": 0,
      "last_triggered_at": "2025-01-15T10:30:00Z",
      "created_at": "2025-01-10T08:00:00Z"
    }
  ]
}

Create Webhook

Register a new webhook endpoint for a form.

POST /forms/:form_id/webhooks

Required scope: forms:write

Request Body

FieldTypeRequiredDescription
urlstringYesThe URL to receive webhook events. Must use https://lib/krafter/forms/form_webhook.ex:21 rejects http://, and Krafter.UrlValidator.validate_url/2 rejects loopback, RFC 1918 / link-local, cloud-metadata, and .local / .internal hosts to prevent SSRF.

Events default to ["submission.created"]. A signing secret is automatically generated (64-character hex string).

Example Request

bash
curl -X POST https://app.krafter.dev/api/v1/forms/FORM_ID/webhooks \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.yourapp.com/webhooks/forms"
  }'

Example Response

json
// 201 Created
{
  "id": "f1a2b3c4-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
  "url": "https://api.yourapp.com/webhooks/forms",
  "events": ["submission.created"],
  "active": true,
  "secret": "a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
  "failure_count": 0,
  "last_triggered_at": null,
  "created_at": "2025-01-15T10:00:00Z"
}

WARNING

The secret is only returned when the webhook is created. Store it securely -- you will need it to verify webhook signatures.


Webhook records are immutable

The Forms service does not expose a PATCH /forms/:form_id/webhooks/:id endpoint today, even though Surveys, Flags, and Push all do. To change a webhook's URL or to rotate its secret, you have to delete the webhook and create a new one — the new one will return a fresh secret in the create response (the only time a secret is ever returned). If you need a stable URL while rotating secrets, terminate TLS and route to your handler at a path that's owned by you, not by this webhook record.

Delete Webhook

Permanently delete a webhook endpoint.

DELETE /forms/:form_id/webhooks/:id

Required scope: forms:write

Example Request

bash
curl -X DELETE https://app.krafter.dev/api/v1/forms/FORM_ID/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer kr_live_abc123def456"

Example Response

204 No Content

Webhook Object

FieldTypeDescription
idstringUnique webhook identifier (UUID)
urlstringDestination URL for webhook delivery
eventsstring[]Event types this webhook subscribes to
activebooleanWhether the webhook is currently active
secretstringSigning secret (only returned on creation)
failure_countintegerNumber of consecutive delivery failures
last_triggered_atstring|nullISO 8601 timestamp of the last delivery attempt
created_atstringISO 8601 creation timestamp

Webhook Payload

When a new submission arrives, Krafter sends an HTTP POST request to your webhook URL with the following JSON payload:

json
{
  "event": "submission.created",
  "form_id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
  "form_name": "Contact Form",
  "submission": {
    "id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "data": {
      "name": "John Doe",
      "email": "john@example.com",
      "message": "Hello, I have a question."
    },
    "files": {},
    "metadata": {
      "ip": "192.168.1.1",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
      "referrer": "https://example.com/contact",
      "country": "US"
    },
    "created_at": "2025-01-15T10:30:00Z"
  }
}

Payload Fields

FieldTypeDescription
eventstringEvent type (submission.created)
form_idstringUUID of the form
form_namestringDisplay name of the form
submission.idstringUUID of the submission
submission.dataobjectKey-value pairs of submitted fields
submission.filesobjectFile metadata for uploaded files
submission.metadata.ipstringSubmitter's IP address
submission.metadata.user_agentstringSubmitter's User-Agent header
submission.metadata.referrerstringReferring URL
submission.metadata.countrystringTwo-letter country code (GeoIP)
submission.created_atstringISO 8601 timestamp

Request Headers

Every webhook request includes these headers:

HeaderDescription
Content-Typeapplication/json
X-Krafter-EventEvent type (e.g., submission.created)
X-Krafter-SignatureHMAC-SHA256 signature: sha256=<hex>

Signature Verification

Every webhook request is signed using HMAC-SHA256 with your webhook secret. The signature is sent in the X-Krafter-Signature header in the format sha256=<hex_digest>.

You should always verify the signature before processing the payload to ensure the request is authentic and has not been tampered with.

Verification Steps

  1. Extract the X-Krafter-Signature header value.
  2. Remove the sha256= prefix to get the hex digest.
  3. Compute an HMAC-SHA256 of the raw request body using your webhook secret.
  4. Compare the computed digest to the received digest using a constant-time comparison.

Node.js

javascript
import crypto from "node:crypto";

function verifySignature(payload, signature, secret) {
  const digest = signature.replace("sha256=", "");
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(digest),
    Buffer.from(expected),
  );
}

// Express handler example
app.post("/webhooks/forms", (req, res) => {
  const signature = req.headers["x-krafter-signature"];
  const rawBody = req.rawBody; // Ensure access to the raw body

  if (!verifySignature(rawBody, signature, process.env.KRAFTER_WEBHOOK_SECRET)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const event = JSON.parse(rawBody);
  console.log(`New submission: ${event.submission.id}`);
  console.log(`Form: ${event.form_name}`);
  console.log(`Data:`, event.submission.data);

  // Process the submission...

  res.status(200).json({ received: true });
});

Python

python
import hmac
import hashlib
import json

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    digest = signature.replace("sha256=", "")
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(digest, expected)

# Flask handler example
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/webhooks/forms", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-Krafter-Signature", "")
    raw_body = request.get_data()

    if not verify_signature(raw_body, signature, WEBHOOK_SECRET):
        return jsonify({"error": "Invalid signature"}), 401

    event = json.loads(raw_body)
    print(f"New submission: {event['submission']['id']}")
    print(f"Form: {event['form_name']}")
    print(f"Data: {event['submission']['data']}")

    # Process the submission...

    return jsonify({"received": True}), 200

Automatic Disabling

Webhooks are automatically disabled after 10 consecutive delivery failures. A delivery is considered failed when:

  • Your endpoint returns a non-2xx HTTP status code.
  • The request times out.
  • The endpoint is unreachable.

When a webhook is disabled:

  • The active field is set to false.
  • The failure_count reflects the number of consecutive failures.
  • No further deliveries are attempted until the webhook is re-enabled.
  • An email is sent to the team owner with the webhook URL, failure count, and a link to the Forms dashboard so the disable doesn't go unnoticed.

TIP

Monitor the failure_count field to catch issues before a webhook is disabled. You can check webhook status via the List Webhooks endpoint or in the dashboard.

Best Practices

  • Always verify the signature before processing webhook events.
  • Respond with 2xx quickly. Process submissions asynchronously if needed. The webhook delivery expects a response within a reasonable timeout.
  • Handle duplicate deliveries. Use the submission.id field to deduplicate. The same event may be sent more than once in edge cases.
  • HTTPS is required. The schema rejects http:// URLs and any host that resolves to loopback, RFC 1918 / link-local, cloud-metadata, or .local / .internal to prevent SSRF (form_webhook.ex:21-23). Existing http:// rows from before this enforcement keep working until their url field is changed; new webhooks must use HTTPS.
  • Store the secret securely. The webhook secret is only returned once at creation time. Keep it in environment variables or a secret manager.

Built by Krafter Studio