WidgetJarWebhooks logoDeveloper

Signed webhooks for every widget event

Send every submission, booking and visitor action to any HTTPS endpoint as JSON. Requests are signed with HMAC-SHA256, carry a delivery ID for deduplication and are retried when your server is down.

What arrives in Webhooks

A signed JSON POST with the answers and page context

Triggers

  • Form submissions
  • Bookings
  • Event calendar actions (ticket, calendar, share, details)
  • Detected button clicks

You will need

  • Endpoint URL
  • Signing secret (optional, recommended)

See it in action

A visitor submits your widget, WidgetJar delivers it in seconds. Press the button to send another one.

Your widget

Talk to our team

Webhooks logoWebhooksConnected

Waiting for the first delivery…

What teams do with WidgetJar + Webhooks

Detected form

Your own backend

Store leads in your database and trigger your own onboarding logic.

Popup

Serverless functions

Run a Cloudflare Worker or AWS Lambda for each sign-up.

Detected button

Internal tools

Feed button clicks and contact requests into your data warehouse.

Set it up in 3 steps

  1. 1

    Create an endpoint that accepts POST requests with a JSON body and answers with a 2xx status.

  2. 2

    In WidgetJar, add the Webhook integration with your URL and a long random secret.

  3. 3

    Verify the X-WidgetJar-Signature header on your server, then click “Send test”.

Integrations are set per widget. Open a widget in the WidgetJar panel and choose Integrations → Add integration. Every connected tool has a “Send test” button so you can check it before going live.

Webhook reference

Everything your endpoint needs to know to receive WidgetJar events safely.

Request

WidgetJar sends a POST with a JSON body. Your answers are in data, with keys named after your form fields.

HeaderValue
Content-Typeapplication/json
User-AgentWidgetJar-Webhook/1.0
X-WidgetJar-EventThe event type, for example submission
X-WidgetJar-DeliveryUnique delivery ID, the same on every retry
X-WidgetJar-TimestampUnix time in seconds when the request was signed
X-WidgetJar-Signaturesha256=<hex HMAC>, sent when you set a secret
Example body
{
  "id": "1842",
  "event_type": "submission",
  "widget_id": "0b8b6f8e-6f4f-4a52-9a5c-8f0f2b2f1c11",
  "timestamp": "2026-11-02 10:00:00.000",
  "data": {
    "name": "Ada Lovelace",
    "email": "[email protected]",
    "message": "Can we book a demo next week?"
  },
  "context": {
    "page": "https://example.com/contact",
    "country": "GB",
    "device": "desktop"
  }
}

Event types

event_typeMeaningSent by
submissionNew form submissionDetected form, popup and other form widgets
bookingNew bookingReservation widget
ticket_clickTicket link openedEvent calendar
calendar_addEvent added to a calendarEvent calendar
event_shareEvent link copiedEvent calendar
event_viewEvent details openedEvent calendar
button_clickButton clickedDetected button
contact_clickContact link clickedDetected button

Event calendar and detected button actions are only sent for the actions you tick on the integration in the panel.

Verify the signature

When you set a secret, WidgetJar computes an HMAC-SHA256 of {timestamp}.{raw body} and sends it as X-WidgetJar-Signature. Recompute it on your server, compare in constant time and reject requests older than five minutes.

Node.js · Express
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.WIDGETJAR_WEBHOOK_SECRET;

// Keep the raw body: the signature covers the exact bytes we sent.
app.post('/hooks/widgetjar', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.get('X-WidgetJar-Timestamp');
  const signature = req.get('X-WidgetJar-Signature') ?? '';
  const expected =
    'sha256=' +
    crypto.createHmac('sha256', SECRET).update(`${timestamp}.${req.body}`).digest('hex');

  const valid =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
  if (!valid || !fresh) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  // Use event.id to ignore a delivery you have already processed.
  console.log(event.event_type, event.data);
  res.sendStatus(200);
});
Python · Flask
import hashlib, hmac, os, time
from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["WIDGETJAR_WEBHOOK_SECRET"].encode()

@app.post("/hooks/widgetjar")
def widgetjar_hook():
    timestamp = request.headers.get("X-WidgetJar-Timestamp", "")
    signature = request.headers.get("X-WidgetJar-Signature", "")
    body = request.get_data()  # raw bytes
    expected = "sha256=" + hmac.new(
        SECRET, timestamp.encode() + b"." + body, hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        abort(401)
    if abs(time.time() - int(timestamp or 0)) > 300:
        abort(401)

    event = request.get_json()
    print(event["event_type"], event["data"])
    return "", 200

Delivery and retries

  • Answer with any 2xx status within 10 seconds. Anything else counts as a failure.
  • Each event is tried up to 5 times in total, with exponential backoff starting at 5 seconds.
  • Redirects are not followed. Point the integration at the final URL.
  • The delivery ID stays the same across retries, so you can safely drop duplicates.
  • Do slow work after you answer: put the event on a queue and return 200 right away.
  • “Send test” in the panel sends a sample submission with "test": true in the body.

Start sending leads to Webhooks today

Create a widget or add WidgetJar to the form you already have. Integrations are included on every plan.

Get started free

Related integrations