Webhooks

Setup & verification

Receive form submissions in your own backend via webhooks.

Webhooks let you receive a real-time POST request to your server every time a form is submitted. Use them to sync leads to your CRM, trigger automations, or save data to your own database.

Setup

  1. Open the Vlozi dashboard → Forms → your form → SettingsWebhook.
  2. Enter your endpoint URL (must be publicly reachable via HTTPS).
  3. Save. Vlozi generates a unique signing secret for this form automatically.

Payload

Every webhook delivery is a POST with Content-Type: application/json:

{
  "event": "submission.created",
  "formId": "form_abc123",
  "formName": "Contact Us",
  "submissionId": "sub_xyz789",
  "submittedAt": "2026-06-28T10:00:00.000Z",
  "data": {
    "email": "visitor@example.com",
    "message": "Hello!"
  },
  "meta": {
    "referer": "https://example.com/contact",
    "utm_source": "newsletter"
  }
}

File fields in data include a url for the uploaded file:

{
  "resume": {
    "__file": true,
    "name": "resume.pdf",
    "size": 184320,
    "type": "application/pdf",
    "url": "https://cdn.vlozi.app/…/resume.pdf"
  }
}

Verifying the signature

Every delivery includes an X-Vlozi-Signature header. Always verify it before trusting the payload.

The signature is computed over `${timestamp}.${rawBody}` — the X-Vlozi-Timestamp header value, a literal ., then the exact request body. Binding the timestamp in lets you reject stale (replayed) deliveries.

import { createHmac, timingSafeEqual } from "crypto";
 
function verifyWebhook(
  rawBody: string,
  signature: string,
  timestamp: string,
  secret: string,
): boolean {
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`, "utf8")
    .digest("hex");
 
  const sigBuf = Buffer.from(signature);
  const expBuf = Buffer.from(expected);
  if (sigBuf.length !== expBuf.length) return false;
 
  return timingSafeEqual(sigBuf, expBuf);
}

IMPORTANT

Compute the HMAC over the raw request body bytes (prefixed with the timestamp) before calling JSON.parse. Always use timingSafeEqual to prevent timing attacks. Optionally reject deliveries whose X-Vlozi-Timestamp is more than a few minutes old.

Express example

import express from "express";
 
const app = express();
 
app.post("/vlozi-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["x-vlozi-signature"] as string;
  const ts = req.headers["x-vlozi-timestamp"] as string;
 
  if (!verifyWebhook(req.body.toString(), sig, ts, process.env.VLOZI_WEBHOOK_SECRET!)) {
    return res.status(401).send("Invalid signature");
  }
 
  const event = JSON.parse(req.body.toString());
  console.log("New submission:", event.submissionId);
 
  res.sendStatus(200); // respond quickly — Vlozi retries on non-2xx
});

Request headers

Header Value
Content-Type application/json
User-Agent Vlozi-Forms/1.0
X-Vlozi-Form-Id Your form's ID
X-Vlozi-Submission-Id The submission's ID
X-Vlozi-Timestamp Unix timestamp (ms) of the delivery attempt
X-Vlozi-Signature sha256=<hex> HMAC of `${timestamp}.${rawBody}`

Retries

If your endpoint returns a non-2xx status (or times out), Vlozi retries up to 3 times:

Attempt Delay
1st Immediate
2nd 1 second
3rd 3 seconds

After 3 failed attempts the delivery is marked as failed. You can view the delivery log in the dashboard under Forms → Settings → Webhook deliveries.

TIP

Respond with 200 as quickly as possible, then do your processing asynchronously. If your handler takes too long, the request may time out and trigger a retry.

Delivery log

Open the Vlozi dashboard → Forms → your form → SettingsWebhook deliveries to see the last 20 delivery attempts, their HTTP status codes, and response bodies.

Forms · WebhooksEdit on GitHub