Appearance
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/webhooksRequired 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/webhooksRequired scope: forms:write
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The 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/:idRequired 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 ContentWebhook Object
| Field | Type | Description |
|---|---|---|
id | string | Unique webhook identifier (UUID) |
url | string | Destination URL for webhook delivery |
events | string[] | Event types this webhook subscribes to |
active | boolean | Whether the webhook is currently active |
secret | string | Signing secret (only returned on creation) |
failure_count | integer | Number of consecutive delivery failures |
last_triggered_at | string|null | ISO 8601 timestamp of the last delivery attempt |
created_at | string | ISO 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
| Field | Type | Description |
|---|---|---|
event | string | Event type (submission.created) |
form_id | string | UUID of the form |
form_name | string | Display name of the form |
submission.id | string | UUID of the submission |
submission.data | object | Key-value pairs of submitted fields |
submission.files | object | File metadata for uploaded files |
submission.metadata.ip | string | Submitter's IP address |
submission.metadata.user_agent | string | Submitter's User-Agent header |
submission.metadata.referrer | string | Referring URL |
submission.metadata.country | string | Two-letter country code (GeoIP) |
submission.created_at | string | ISO 8601 timestamp |
Request Headers
Every webhook request includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Krafter-Event | Event type (e.g., submission.created) |
X-Krafter-Signature | HMAC-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
- Extract the
X-Krafter-Signatureheader value. - Remove the
sha256=prefix to get the hex digest. - Compute an HMAC-SHA256 of the raw request body using your webhook secret.
- 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}), 200Automatic 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
activefield is set tofalse. - The
failure_countreflects 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.idfield 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/.internalto prevent SSRF (form_webhook.ex:21-23). Existinghttp://rows from before this enforcement keep working until theirurlfield 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.