WebhooksForm Submissions

Form Submissions

When someone submits a contact form on a deployed Warpweb site, the data POSTs to the URL you configured via POST /v1/sites/:id/webhooks/forms.

Outbound request

POST https://api.yourapp.com/webhooks/warpweb-leads
Content-Type: application/json
User-Agent: Warpweb-Webhook/1.0
X-Warpweb-Event-Id: 7d0b368c-b452-cf9a-1234-56789abcdef0
X-Warpweb-Site-Id: 8f3c2a1b-5d47-4c9e-b820-1f8a3e7d9c4f
X-Warpweb-Signature: sha256=a3f9e2c1d4b5a6f7e8d9c0b1a2f3e4d5c6b7a8f9e0d1c2b3a4f5e6d7c8b9a0f1

Headers

HeaderDescription
X-Warpweb-Event-IdUUID for this delivery. Use as your idempotency key. Duplicates can arrive if your previous 2xx response was lost in transit.
X-Warpweb-Site-IdThe originating site’s ID. Context aid; doesn’t replace looking up which secret to verify against.
X-Warpweb-SignatureHMAC-SHA256 of the raw body, hex-encoded, with sha256= prefix. Same scheme as Stripe. See Verifying Signatures.

Body

{
  "event_id": "7d0b368c-b452-cf9a-1234-56789abcdef0",
  "event_type": "form_submission",
  "site_id": "8f3c2a1b-5d47-4c9e-b820-1f8a3e7d9c4f",
  "submitted_at": "2026-05-17T15:42:11Z",
  "name": "Jane Doe",
  "email": "jane@example.com",
  "phone": "+15125550199",
  "message": "Hi, I need a leaky pipe fixed in my kitchen. Available Tuesday afternoon.",
  "raw_fields": {
    "full_name": "Jane Doe",
    "email_address": "jane@example.com",
    "phone_number": "+15125550199",
    "message": "Hi, I need a leaky pipe fixed in my kitchen. Available Tuesday afternoon.",
    "preferred_contact": "phone"
  }
}

Top-level fields

FieldTypeDescription
event_idstringUUID. Idempotency key.
event_typestringAlways "form_submission".
site_idstringThe site that received the submission.
submitted_atstringISO 8601 UTC.
namestring | nullBest-effort extraction from the raw form. See matching rules below.
emailstring | nullSame.
phonestring | nullSame.
messagestring | nullSame.
raw_fieldsobjectAll form fields as the visitor submitted them, lossless. Matched fields are duplicated here for safety.

Field extraction

Warpweb-generated forms use varied field names depending on the site’s design. We extract the four common fields case-insensitively:

Top-level fieldMatches
namename, fullname, full_name, your_name, contact_name
emailemail, email_address, e-mail, mail
phonephone, phone_number, tel, mobile, cell
messagemessage, comments, inquiry, details, notes. If multiple match, the longest text field wins.

If extraction can’t find a match, the top-level field is null — but the original is still in raw_fields. Treat raw_fields as the source of truth if you need the visitor’s exact input.

Signature

HMAC-SHA256 of the raw body bytes (before any JSON parsing), keyed by your stored webhook secret, hex-encoded, prefixed with sha256=. Identical to Stripe’s signing scheme.

Important: Verify against the raw body, not the re-serialized JSON. JSON re-serialization changes whitespace and field ordering and breaks the signature.

See Verifying Signatures for Node, Python, and curl examples.

Retry behavior

OutcomeRetry?
HTTP 2xxsuccess — done.
HTTP 4xx (including 400, 401, 403, 404, 410, 422)NO retry. Logged and dropped.
HTTP 5xxretry.
Network error / connection refused / DNS failureretry.
Receiver timeout > 10streated as 5xx — retry.

Retry schedule: immediate, +30s, +5min, then dead-letter. Dead-letter rows appear in the dashboard with a manual replay button.

If you return a 4xx by mistake, the submission is gone (manual replay aside) — don’t return 4xx to “ignore” events. Return 200 and ignore on your end.

Idempotency

Use event_id as the dedupe key. If the same event_id arrives twice (your 200 got lost in transit, our retry fired), respond 200 immediately without re-processing — don’t create duplicate leads in your CRM.

A common pattern:

const seen = await db.webhookEvents.findUnique({ where: { event_id } })
if (seen) return res.status(200).send('ok')  // already processed
await db.webhookEvents.create({ data: { event_id } })
// ...process the lead

Dead-letter recovery

If all retries fail, the submission lands in your dashboard’s dead-letter queue with the full payload, your endpoint’s last response, and a “Replay” button. Replays generate a new event_id — your receiver will treat it as a fresh delivery, not a retry.