KirokuForms

Using KirokuForms Webhooks

Automate your workflows and build event-driven integrations by subscribing to real-time events from KirokuForms, such as new form submissions.

What are Webhooks?

Webhooks are a way for KirokuForms to send real-time notifications to your application when specific events occur. Instead of you repeatedly asking our API if something new has happened (polling), KirokuForms will proactively send an HTTP POST request with event data (a JSON payload) to a URL you provide.

Common use cases for webhooks include:

  • Triggering custom email or SMS notifications upon a new form submission.
  • Updating customer records in your CRM instantly.
  • Syncing submission data with your internal databases or spreadsheets.
  • Starting backend processes or workflows (e.g., order fulfillment, support ticket creation).

Note on HITL Callbacks

This page focuses on general webhooks you can configure per form (e.g., for new submissions). For specific callback URLs used in Human-in-the-Loop (HITL) tasks created via the MCP API or Python SDK, please see the Python SDK Documentation or the HITL API Guide as those are configured per-task.

Available Webhook Events

You can configure your webhook endpoints to subscribe to one or more event types. The event type for form submissions is:

  • form.submission.new: Triggered immediately after a new submission is successfully received for the subscribed form.

If you drive Human-in-the-Loop (HITL) approvals through the MCP API or Python SDK, three decision events fire as a task moves through review. Subscribe to them the same way, by naming them in the events array of a per-form webhook:

  • decision.requested: A human has been asked to review a task. Fires when the task is created.
  • decision.approved: The reviewer approved the task.
  • decision.rejected: The reviewer rejected the task.

Two lifecycle events track the task itself. They are not per-form webhook events (they carry no formId to route on), so you receive them over the authenticated event stream rather than a per-form webhook, and hitl.task.completed is also delivered to the task's own callback URL:

  • hitl.task.created: A HITL task was created and is awaiting a human.
  • hitl.task.completed: A HITL task was settled (submitted, approved, or rejected).

The events array in the webhook creation payload (see API setup below) specifies which events your endpoint will receive. See HITL and Decision Event Payloads below for the shape of each.

Setting Up Webhooks

You can manage webhooks for your forms through the KirokuForms dashboard or programmatically using our Management API.

Method 1: Using the KirokuForms Dashboard

  1. Log into your KirokuForms account and navigate to the "My Forms" page.
  2. Select the form for which you want to configure a webhook.
  3. Go to the form's "Settings" tab, and find the "Webhooks" or "Integrations" section.
  4. Click "Add Webhook" or "Create New Webhook".
  5. You'll typically configure the following:
    • Endpoint URL: Your server's publicly accessible HTTPS URL that will listen for incoming webhook POST requests from KirokuForms.
    • Event(s): Select the specific events you want this webhook to subscribe to (e.g., "New Submission").
    • Secret (Highly Recommended): A strong, unique string that KirokuForms will use to sign webhook requests. You'll use this same secret on your server to verify the authenticity of incoming requests. KirokuForms may auto-generate one for you if left blank, or you can provide your own. Store this secret securely.
    • Enable/Disable: A toggle to activate or deactivate the webhook.
  6. Save your configuration. KirokuForms might send a test ping or require endpoint validation depending on the setup.

(Consider adding a screenshot here showing the webhook configuration UI in your dashboard.)

Method 2: Using the KirokuForms API

For automated setup or integration into your own systems, you can manage webhooks via our API. Refer to the API Reference for complete endpoint details, including parameters for listing, updating, and deleting webhooks.

To create a webhook, you'll make a POST request to /api/forms/{formId}/webhooks. Here's an example using cURL:

Create Webhook via API
# Create a Webhook via API
# POST /api/forms/YOUR_FORM_ID/webhooks
# Requires API Key with appropriate scopes.

curl -X POST "https://www.kirokuforms.com/api/forms/YOUR_FORM_ID/webhooks" \
     -H "Authorization: Bearer YOUR_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
           "url": "https://your-server.com/your-webhook-listener-path",
           "events": ["form.submission.new"],
           "secret": "a-very-strong-secret-you-generate-and-store",
           "isEnabled": true
         }'

Replace YOUR_FORM_ID, YOUR_API_KEY (with forms:webhooks:manage or similar scope), and https://your-server.com/your-webhook-listener-path with your actual values. The secret should be a cryptographically strong random string that you generate and store securely on your server.

Understanding the Webhook Payload

When a subscribed event occurs, KirokuForms will send an HTTP POST request with a Content-Type: application/json header to your configured Endpoint URL. The request body will be a JSON object. For a form.submission.new event, the payload will look similar to this:

Webhook Payload Example
{
  "eventType": "form.submission.new",
  "formId": "frm_abc123xyz789",
  "submissionId": "sub_def456uvw012",
  "timestamp": "2025-07-15T10:30:00Z",
  "data": {
    "name": "Arthur Dent",
    "email": "arthur.dent@example.com",
    "feedback_topic": "General Inquiry",
    "message": "What is the answer to life, the universe, and everything?"
  },
  "metadata": {
    "submittedVia": "web",
    "ipAddress": "192.0.2.x",
    "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
  }
}

Key fields in the payload:

  • eventType: Identifies the event (e.g., "form.submission.new").
  • formId: The ID of the KirokuForm that triggered the event.
  • submissionId: For submission-related events, the unique ID of the submission.
  • timestamp: An ISO 8601 formatted string indicating when the event occurred on KirokuForms' servers.
  • data: An object containing the actual data related to the event. For form.submission.new, this object holds the submitted form field names and their values.
  • metadata: (Optional) Contains additional context about the event, like IP address or user agent, if captured and configured for inclusion.

HITL and Decision Event Payloads

HITL approvals emit the decision and lifecycle events listed above. This section shows the payload each one carries, so an agent can wire the full async path: a task is created (hitl.task.created), a human is asked to decide (decision.requested), the human approves or rejects (decision.approved / decision.rejected), and the task settles (hitl.task.completed).

Decision events

A decision.approved or decision.rejected event is delivered to any per-form webhook subscribed to it, with the same envelope as a submission event. The decision object names the outcome and the task it settled:

Decision Webhook Payload
{
  "eventType": "decision.approved",
  "timestamp": "2026-07-15T11:05:00Z",
  "data": {
    "formId": "tmpl_q3_invoice_review",
    "submissionId": "sub_def456uvw012",
    "decision": {
      "outcome": "approved",
      "taskId": "task_ghi789"
    }
  }
}

decision.rejected is identical with "outcome": "rejected". decision.requested carries only the formId in its data object (no decision block and no submissionId), because the human has not filled the task in yet. For a HITL task the formId is the task's template ID.

Task lifecycle events

hitl.task.created and hitl.task.completed arrive on the authenticated Server-Sent Events stream. Open the event endpoint /api/mcp/events/{eventType} with a GET request and your API key as a Bearer token. Each frame is the full event object (type, timestamp, and a data payload):

HITL Lifecycle Events (SSE Stream)
event: hitl.task.created
data: {"type":"hitl.task.created","timestamp":"2026-07-15T10:30:00.000Z","data":{"taskId":"task_ghi789","hitlTaskId":"clh0x9k2b0000","templateId":"tmpl_q3_invoice_review","userId":"user_owner_123","priority":"normal","expiresAt":"2026-07-16T10:30:00.000Z"}}

event: hitl.task.completed
data: {"type":"hitl.task.completed","timestamp":"2026-07-15T11:05:00.000Z","submissionId":"sub_def456uvw012","data":{"taskId":"task_ghi789","hitlTaskId":"clh0x9k2b0000","templateId":"tmpl_q3_invoice_review","userId":"user_owner_123","submissionId":"sub_def456uvw012","completedAt":"2026-07-15T11:05:00.000Z","submission":{"approved_amount":"5000","reviewer_note":"Matches the PO"}}}

When a task is created with a callbackUrl, KirokuForms also POSTs that URL once on completion. That callback carries the settled fields under formData:

HITL Task Callback Payload
{
  "eventType": "hitl.task.completed",
  "timestamp": "2026-07-15T11:05:00Z",
  "taskId": "task_ghi789",
  "data": {
    "taskId": "task_ghi789",
    "status": "completed",
    "completedAt": "2026-07-15T11:05:00Z",
    "formData": {
      "approved_amount": "5000",
      "reviewer_note": "Matches the PO"
    },
    "metadata": {}
  }
}

For setting the per-task callbackUrl and the rest of the HITL flow, see the HITL API Guide and the Python SDK Documentation.

Securing Your Webhooks (Essential)

Since your webhook endpoint URL is publicly accessible, it's critical to verify that incoming requests genuinely originate from KirokuForms and haven't been tampered with. Here's how:

  1. Use HTTPS for Your Endpoint URL:

    Always use an https:// URL for your webhook listener. This encrypts the webhook payload in transit, protecting sensitive data.

  2. Verify Signatures using Your Webhook Secret:

    When you create a webhook in KirokuForms, you should configure a strong, unique "Secret". KirokuForms uses this secret to generate a cryptographic signature for each webhook request it sends. This signature is included in an HTTP header, typically named something like KirokuForms-Signature.

    The signature header often contains a timestamp (e.g., t=1678886400) and one or more signature versions (e.g., v1=a1b2c3d4...). The v1 signature is commonly an HMAC-SHA256 Hash-based Message Authentication Code using SHA-256 algorithm - a cryptographic method to verify both authenticity and integrity of messages hash.

    Your webhook listener endpoint must perform the following steps to verify the signature:

    • Extract the timestamp (t= value) and the provided signature (v1= value) from the KirokuForms-Signature header.
    • Important: You need the raw, unparsed request body as a string to verify the signature. If you're using a framework like Express that automatically parses JSON, ensure you have middleware to capture the raw body before JSON parsing.
    • Construct the "string-to-sign". This is typically formed by concatenating the timestamp, a literal dot (.), and the raw request body string. (e.g., timestamp_value + "." + raw_request_body_string).
    • Calculate the expected signature by computing an HMAC-SHA256 hash of the "string-to-sign" using your stored webhook secret as the key.
    • Compare the signature you calculated with the v1 signature provided in the header. Use a timing-safe comparison method to prevent timing attacks.
    • Additionally, check if the timestamp from the header is recent (e.g., within the last 5 minutes) to protect against replay attacks.
    • If the signature is valid and the timestamp is current, you can trust the request. Otherwise, reject it (e.g., with a 400 Bad Request or 401 Unauthorized status).

    Here's a conceptual Node.js example for signature verification (ensure the header name and string-to-sign construction matches KirokuForms' exact implementation):

    Webhook Signature Verification
    const crypto = require('crypto');
    
    /**
     * Verifies a KirokuForms webhook signature.
     * @param {string} rawRequestBody - The raw, unparsed request body string.
     * @param {string} signatureHeader - The value of the 'KirokuForms-Signature' HTTP header.
     * @param {string} webhookSecret - Your configured webhook secret.
     * @param {number} toleranceSeconds - Timestamp tolerance in seconds to prevent replay attacks.
     * @returns {boolean} True if the signature is valid, false otherwise.
     */
    function verifyKirokuSignature(rawRequestBody, signatureHeader, webhookSecret, toleranceSeconds = 300) {
      if (!signatureHeader) {
        console.error('Signature verification failed: Missing KirokuForms-Signature header.');
        return false;
      }
    
      const parts = signatureHeader.split(',');
      const timestampPart = parts.find(part => part.startsWith('t='));
      const signatureV1Part = parts.find(part => part.startsWith('v1='));
    
      if (!timestampPart || !signatureV1Part) {
        console.error('Signature verification failed: Invalid signature header format.');
        return false;
      }
    
      const requestTimestamp = parseInt(timestampPart.split('=')[1], 10);
      const providedSignature = signatureV1Part.split('=')[1];
    
      if (isNaN(requestTimestamp)) {
        console.error('Signature verification failed: Invalid timestamp in signature header.');
        return false;
      }
    
      // Check timestamp tolerance (e.g., 5 minutes = 300 seconds)
      const currentTimestampSeconds = Math.floor(Date.now() / 1000);
      if (Math.abs(currentTimestampSeconds - requestTimestamp) > toleranceSeconds) {
        console.warn('Webhook timestamp is outside the tolerance window (possible replay attack). Difference:', Math.abs(currentTimestampSeconds - requestTimestamp), 'seconds.');
        return false;
      }
    
      const stringToSign = requestTimestamp + '.' + rawRequestBody;
      
      const expectedSignature = crypto
        .createHmac('sha256', webhookSecret)
        .update(stringToSign)
        .digest('hex');
    
      if (crypto.timingSafeEqual(Buffer.from(providedSignature, 'hex'), Buffer.from(expectedSignature, 'hex'))) {
        return true;
      } else {
        console.warn('Signature verification failed: Computed signature does not match provided signature.');
        return false;
      }
    }
    
    // --- Example usage in an Express route ---
    // Ensure you use a middleware to get the raw body *before* any JSON parsing middleware.
    // For Express, you might need something like:
    // app.use('/webhook-listener-path', express.raw({ type: 'application/json' }));
    //
    // app.post('/webhook-listener-path', (req, res) => {
    //   const kirokuSignature = req.headers['kirokuforms-signature']; // Or your actual header name
    //   const webhookSecret = process.env.KIROKU_FORM_WEBHOOK_SECRET; // Store securely
    //
    //   // req.body will be a Buffer if using express.raw()
    //   const rawBody = req.body.toString('utf8'); 
    //
    //   if (verifyKirokuSignature(rawBody, kirokuSignature, webhookSecret)) {
    //     const eventPayload = JSON.parse(rawBody); // Now parse the JSON
    //     console.log('Webhook verified! Processing event:', eventPayload.eventType);
    //     // Process eventPayload.data
    //     res.status(200).send({ received: true });
    //   } else {
    //     console.warn('Webhook verification failed or invalid signature.');
    //     res.status(400).send('Invalid signature.');
    //   }
    // });
  3. Respond Quickly to Acknowledge Receipt:

    Your endpoint should process the request and return a 2xx HTTP status code (e.g., 200 OK or 204 No Content) as quickly as possible (ideally within 2-5 seconds). If your application needs to perform long-running tasks based on the webhook (like making further API calls, sending emails, complex database operations), do so asynchronously after sending the 2xx response. Add the webhook data to a queue or trigger a background job. This prevents timeouts on KirokuForms' side and ensures reliable webhook delivery.

Building a Webhook Listener (Node.js/Express Example)

This example provides a basic structure for a webhook listener using Node.js and the Express framework. It demonstrates receiving a webhook, (conceptually) verifying its signature, and processing a form.submission.new event.

Complete Webhook Listener Example
const express = require('express');
const crypto = require('crypto'); // For signature verification

const app = express();
const port = process.env.PORT || 3000;

// Store your webhook secrets securely (e.g., in environment variables)
// Map form IDs to their secrets if you have multiple webhooks/secrets
const WEBHOOK_SECRETS = {
  'YOUR_FORM_ID_1': 'YOUR_SECRET_FOR_FORM_1',
  'YOUR_FORM_ID_2': 'YOUR_SECRET_FOR_FORM_2'
};

// Middleware to get the raw request body for signature verification,
// then parse JSON if signature is valid.
app.use('/your-webhook-listener-path', (req, res, next) => {
  let rawBody = '';
  req.on('data', chunk => {
    rawBody += chunk.toString();
  });
  req.on('end', () => {
    req.rawBody = rawBody; // Attach raw body for signature verification
    try {
      // Attempt to parse JSON after getting raw body, but verification uses rawBody
      req.body = JSON.parse(rawBody || '{}'); 
    } catch (e) {
      console.error("Webhook payload is not valid JSON:", e.message);
      req.body = {}; // Set to empty object if parsing fails
    }
    next();
  });
});

// Your verifyKirokuSignature function from the example above would go here
// function verifyKirokuSignature(rawRequestBody, signatureHeader, webhookSecret, toleranceSeconds = 300) { ... }

app.post('/your-webhook-listener-path', (req, res) => {
  const formId = req.body.formId; // Assuming formId is in the payload
  const webhookSecret = formId ? WEBHOOK_SECRETS[formId] : null;
  const signature = req.headers['kirokuforms-signature']; // Adjust if header name is different

  // ** 1. Verify the signature (CRITICAL) **
  // if (!webhookSecret || !verifyKirokuSignature(req.rawBody, signature, webhookSecret)) {
  //   console.warn('Webhook signature verification failed for form ' + (formId || 'unknown') + '.');
  //   return res.status(400).send('Signature verification failed.');
  // }
  // console.log('Webhook signature VERIFIED for form ' + formId);

  // ** 2. Process the event **
  const { eventType, data, submissionId, timestamp } = req.body;
  console.log('Received event: ' + eventType + ' at ' + timestamp);

  switch (eventType) {
    case 'form.submission.new':
      console.log('New submission (' + submissionId + ') for form ' + formId + ':', data);
      // Add your business logic here:
      // - Save to your database
      // - Send a custom email
      // - Update a CRM
      // - Trigger a Slack notification
      break;
    // Add other event types if KirokuForms supports them
    // case 'form.updated':
    //   console.log('Form ' + formId + ' was updated.');
    //   break;
    default:
      console.log('Received unhandled event type: ' + eventType);
  }

  // ** 3. Acknowledge receipt quickly **
  res.status(200).json({ message: 'Webhook received successfully.' });
});

// app.listen(port, () => {
//   console.log('KirokuForms webhook listener running at http://localhost:' + port + '/your-webhook-listener-path');
//   console.log('Ensure this URL is publicly accessible and configured in KirokuForms.');
// });

To run this example, you would save it as a .js file, install Express (npm install express), replace placeholders with your actual webhook secret and logic, and run it (e.g., node your-listener-file.js). Your server would need to be publicly accessible via HTTPS for KirokuForms to reach it.

Delivery Attempts, Retries, and Error Handling

  • Acknowledgment: Your webhook endpoint MUST return a 2xx HTTP status code to signal successful receipt. Any other status code (or a timeout) will be considered a delivery failure by KirokuForms.
  • Retries: If KirokuForms doesn't receive a 2xx response, it will typically attempt to resend the webhook. This often follows an exponential backoff schedule (e.g., retrying after 1 minute, then 5 minutes, then 15 minutes, up to a certain number of attempts or duration).
  • Disabling Webhooks: If an endpoint consistently fails to respond successfully after multiple retries, KirokuForms may automatically disable the webhook to prevent further issues. You would usually receive an email notification if this happens and would need to re-enable it via the dashboard or API after fixing your listener.
  • Idempotency The property of an operation where performing it multiple times has the same effect as performing it once - critical for webhook handling since you may receive duplicates : Because of retries (and other potential network issues), your listener might occasionally receive the same webhook event more than once. Design your event processing logic to be idempotent, meaning processing the same event multiple times has the same effect as processing it once. This can be achieved by checking for unique event IDs or submission IDs before taking action.

Troubleshooting Common Issues

  • Webhooks Not Arriving:
    • Is your endpoint URL correct in KirokuForms and publicly accessible via HTTPS? Use a tool like webhook.site to test if your endpoint can receive POST requests from the internet.
    • Is the webhook enabled in KirokuForms?
    • Check your server logs for any incoming requests or errors.
    • Are there any firewalls (server-side or network) blocking requests from KirokuForms' IP addresses?
  • Signature Verification Failing:
    • Ensure the webhook secret stored on your server exactly matches the one configured for that webhook in KirokuForms. Even a small difference will cause failure.
    • Confirm you are using the correct raw request body (unparsed) for generating the signature. JSON parsing middleware can alter the body.
    • Double-check the algorithm (e.g., HMAC-SHA256) and the exact construction of the string-to-sign (timestamp, dot, body).
  • Endpoint Responding with Errors (4xx or 5xx):
    • Thoroughly debug your listener code. Check server logs for detailed error messages.
    • Remember to send a 2xx response quickly and process data asynchronously if needed.
  • Using KirokuForms Logs:
    • Check the webhook settings area in your KirokuForms dashboard. There might be delivery logs showing the status of recent attempts, response codes received from your server, and any error messages.

Inbound Webhooks: Sending Submissions Into KirokuForms

The events above flow out of KirokuForms. Inbound webhooks go the other way: an external system POSTs a payload to KirokuForms to create a submission. The submission is stored exactly as a web submission would be, so it also emits submission.created and fires any outbound webhooks and notifications you have configured on the same form.

Pro and Business plans

Inbound webhooks are part of the same webhook capability as outbound delivery. The form owner needs a Pro or Business plan; a request against a form on a Free or Starter plan returns 403 with code PLAN_RESTRICTED.

Endpoint

Send a POST request to /api/inbound/{formId}, where {formId} is the ID of the form that should receive the submission. The request body is the JSON object of field values to submit.

Authentication

Each inbound endpoint has its own bearer token (prefix kir_inb_). Create an endpoint in the form's Settings, Inbound section, or via POST /api/forms/{formId}/inbound. The plaintext token is shown exactly once on creation; KirokuForms stores only a hash of it, so save it when you see it.

Pass the token in the Authorization header as Bearer <token>. A token field in the JSON body also works for senders that cannot set headers.

Optional Signature

If the endpoint has a signing secret, sign the raw request body with HMAC-SHA256 and send the hex digest in the X-KirokuForms-Signature-256 header. An optional sha256= prefix is accepted, matching the outbound signing convention. KirokuForms recomputes the signature over the exact bytes it receives and rejects a mismatch with 401 and code INVALID_SIGNATURE. Endpoints without a secret skip this check.

Example Request

Send a Submission Inbound via API
# Send a submission INTO KirokuForms (inbound webhook)
# POST /api/inbound/YOUR_FORM_ID
# Auth: a per-endpoint bearer token. Create one in the form's
# Settings -> Inbound section, or via POST /api/forms/YOUR_FORM_ID/inbound
# (the plaintext token is shown exactly once on creation).

FORM_ID="YOUR_FORM_ID"
TOKEN="kir_inb_your_endpoint_token"       # shown once when the endpoint is created
SECRET="your-optional-signing-secret"     # only if the endpoint has a secret
BODY='{"name":"Arthur Dent","email":"arthur.dent@example.com","message":"Hello from my system"}'

# Optional: sign the exact raw body with HMAC-SHA256 (hex output).
# Omit this header entirely if the endpoint has no secret.
SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -X POST "https://www.kirokuforms.com/api/inbound/$FORM_ID" \
     -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json" \
     -H "X-KirokuForms-Signature-256: sha256=$SIGNATURE" \
     -d "$BODY"

Response

A valid request returns 202 Accepted with the new submission ID:

Inbound Submission Response (202 Accepted)
{
  "success": true,
  "message": "Submission accepted.",
  "data": {
    "submissionId": "sub_def456uvw012"
  }
}

Other responses you may receive:

  • 400 INVALID_BODY: the request body was not a JSON object.
  • 401 UNAUTHORIZED: the bearer token was missing or not valid for this form.
  • 401 INVALID_SIGNATURE: the endpoint has a secret and the signature did not match.
  • 403 ENDPOINT_DISABLED: the endpoint exists but is turned off.
  • 403 PLAN_RESTRICTED: the form owner's plan does not include webhooks.
  • 404 NOT_FOUND: no form exists with that ID.

Field Mapping

By default each top-level key in the payload maps to the form field of the same name. If your sender uses different key names, set a field mapping when you create the endpoint (inbound key to form field name) and KirokuForms rewrites the keys before saving.

Next Steps

With webhooks configured, you can build event-driven integrations.