WebhooksOverview

Webhooks

Warpweb sends two kinds of outbound webhooks:

  • Lifecycle Events — fired when an async build or revision changes state. Delivered to the customer-level URL you configure with PUT /v1/customer/webhook. Recommended over polling.
  • Form Submissions — fired when someone fills out a form on a deployed site. Delivered to the per-site URL you configure with POST /v1/sites/:id/webhooks/forms.

All webhooks are signed with HMAC-SHA256. Always verify signatures — see Verifying Signatures for copy-paste snippets in multiple languages.

Envelope shape

Every webhook POST carries this envelope:

{
  "event_id": "00000000-0000-0000-0000-000000000001",
  "type": "site.complete",
  "site_id": "11111111-1111-1111-1111-111111111111",
  "occurred_at": "2026-05-18T12:05:00.000Z",
  "payload": {
    "...": "type-specific fields"
  }
}

The per-site form-submission webhook uses the same envelope with type: "form.submit". See the per-type pages for payload shapes.

Endpoint requirements

Your webhook receiver should:

  1. Verify X-Warpweb-Signature against the raw request body using your stored secret. See Verifying Signatures.
  2. Parse JSON and inspect type (envelope field) or X-Warpweb-Event-Type header.
  3. Dedupe on event_id if you’ve seen it before — return 200 immediately for duplicates.
  4. Process the event, or enqueue it for async processing.
  5. Return 2xx within 10 seconds.

If your processing is slow, enqueue and return 200 immediately. Don’t make Warpweb wait — it’s better to acknowledge fast and retry on your own queue than to time out and trigger our retry loop. (Any queue works — AWS SQS, GCP Pub/Sub, BullMQ, Sidekiq, or an INSERT into your own DB and a background worker.)

Common conventions

  • All deliveries are POST with Content-Type: application/json.
  • All payloads include an event_id (UUID). Use it as your idempotency key — duplicates can happen.
  • Signature lives in the X-Warpweb-Signature header (sha256=<hex>).
  • Timestamp lives in the X-Warpweb-Timestamp header (Unix seconds) — used for replay protection. Receivers should reject requests whose timestamp is more than 300 seconds (5 minutes) from local wall clock.
  • Event type echoed in X-Warpweb-Event-Type so a router can dispatch without parsing the body.
  • User-Agent is Warpweb-Webhook/2.0.
  • Receiver timeout: 10 seconds. Return a 2xx response within that window.
  • Retry schedule on 5xx / network errors: immediate, +30s, +5min, +30min. After that, dead-letter.
  • 4xx responses are not retried — they’re treated as permanent rejections.

Lifecycle event types

The customer-level webhook can deliver these event types. Subscribe to the ones you care about via PATCH /v1/customer/webhook/subscriptions:

typeFires when
site.research_readyInitial research complete (mid-build status).
site.completeAn initial POST /v1/sites build deployed successfully.
site.failedAn initial build failed. No credits were charged.
site.revision_completeA POST /v1/sites/:id/revisions revision deployed successfully.
site.revision_failedA revision failed (or was ruled out of scope). No credits charged.
site.revision_clarification_neededThe agent needs more info before proceeding. Reply with another revision call.
form.submitSame payload as the per-site form-submission webhook. Useful if you’d rather receive everything on one URL.

In addition, the test-ping endpoints (POST /v1/customer/webhook/test-ping and POST /v1/sites/:id/webhook/test-ping) send one synthetic event with type: "webhook.test_ping". It’s not subscribable — receivers should accept it and no-op. If your router rejects unknown event types, special-case webhook.test_ping so synthetic pings pass through.

See Lifecycle Events for full per-type payload shapes.