Customer-Level Lifecycle Webhook
The customer-level lifecycle webhook is one URL per account that receives every async event from every site you build — site.complete, site.failed, site.revision_complete, and friends. It’s the recommended path for production integrations and replaces polling GET /v1/sites/:id in a loop.
Five endpoints manage it:
| Method | Path | Purpose |
|---|---|---|
GET | /v1/customer/webhook | Read the current configuration. |
PUT | /v1/customer/webhook | Set (or change) the destination URL. |
POST | /v1/customer/webhook/regenerate-secret | Rotate the signing secret. |
POST | /v1/customer/webhook/test-ping | Fire a synthetic event at your receiver. |
PATCH | /v1/customer/webhook/subscriptions | Choose which event types to receive. |
For the event catalog and per-type payload shapes, see Webhooks → Lifecycle Events. For signature verification, see Verifying Signatures.
Cost: Free. All five endpoints are unmetered.
GET /v1/customer/webhook
Read the current lifecycle-webhook configuration.
Request
curl https://api.warpweb.ai/v1/customer/webhook \
-H "Authorization: Bearer $WARPWEB_KEY"Response
{
"url": "https://api.yourapp.com/webhooks/warpweb-lifecycle",
"secret_preview": "whsec_a1…cd",
"has_secret": true,
"subscribed_events": [
"site.research_ready",
"site.complete",
"site.failed",
"site.revision_complete",
"site.revision_failed",
"site.revision_clarification_needed",
"form.submit"
],
"available_events": [
"site.research_ready",
"site.complete",
"site.failed",
"site.revision_complete",
"site.revision_failed",
"site.revision_clarification_needed",
"form.submit"
],
"auth_mode": "dual"
}| Field | Description |
|---|---|
url | The configured destination URL, or null if not set. |
secret_preview | First and last 2 characters of the signing secret (e.g. whsec_a1…cd). The full secret is only ever returned once at creation/rotation — store it in your secret manager when you receive it. |
has_secret | Boolean. true once a secret has been generated. |
subscribed_events | The event types you’ll receive. Defaults to the full list at first PUT; narrow with PATCH /v1/customer/webhook/subscriptions. |
available_events | All event types Warpweb can deliver. Use this to check for new event types between releases. |
auth_mode | Internal — informational only. |
PUT /v1/customer/webhook
Set or change the destination URL. The first call with a non-null URL auto-generates the signing secret and returns its plaintext once in secret_plaintext — capture it immediately. Subsequent calls only update the URL; rotate the secret via the dedicated endpoint (below).
Request
curl -X PUT https://api.warpweb.ai/v1/customer/webhook \
-H "Authorization: Bearer $WARPWEB_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.yourapp.com/webhooks/warpweb-lifecycle"
}'Body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | null | yes | HTTPS endpoint that will receive events. Pass null to disable delivery (subscriptions and secret are preserved). |
Response — first configure (secret auto-generated)
{
"url": "https://api.yourapp.com/webhooks/warpweb-lifecycle",
"secret_preview": "whsec_a1…cd",
"secret_generated_now": true,
"secret_plaintext": "whsec_a1b2c3…64-char-hex…"
}secret_plaintext is returned ONCE — on the first PUT with a non-null URL. Capture it into your secret manager before processing the response any further. If you lose it, rotate via POST /v1/customer/webhook/regenerate-secret.
Response — subsequent updates
{
"url": "https://api.yourapp.com/webhooks/warpweb-lifecycle-v2",
"secret_preview": "whsec_a1…cd",
"secret_generated_now": false
}secret_plaintext is omitted. The existing secret stays valid.
Errors
| Status | Body | Cause |
|---|---|---|
| 400 | { "error": "url must be http(s):// or null" } | URL doesn’t match http(s):// shape. |
| 404 | { "error": "no_customer_for_caller" } | Bearer token doesn’t resolve to a customer. |
| 500 | { "error": "persist_failed", "detail": "..." } | Database write failed. Safe to retry. |
POST /v1/customer/webhook/regenerate-secret
Explicitly rotate the signing secret. Use this immediately if you suspect a leak. The new secret takes effect on the next event delivery; the old secret is invalidated.
Request
curl -X POST https://api.warpweb.ai/v1/customer/webhook/regenerate-secret \
-H "Authorization: Bearer $WARPWEB_KEY"No body.
Response
{
"secret": "whsec_a1b2c3…new-64-char-hex…",
"secret_preview": "whsec_a1…cd",
"rotated_at": "2026-05-20T14:30:00.000Z"
}The new plaintext lives in secret and is returned once. Update your receiver’s secret store immediately. In-flight retries already in the queue are re-signed with the new secret on the next attempt.
For zero-downtime rotation, accept both the old and new secrets on your receiver for a few minutes while the change propagates.
Errors
| Status | Body | Cause |
|---|---|---|
| 404 | { "error": "no_customer_for_caller" } | Bearer token doesn’t resolve to a customer. |
| 500 | { "error": "audit_insert_failed" } or { "error": "persist_failed" } | Database write failed. Safe to retry. |
POST /v1/customer/webhook/test-ping
Fire a synthetic event at your configured URL — useful for exercising your receiver before any real traffic. Synchronous, does not enqueue, 5-second timeout.
By default, sends a webhook.test_ping envelope (a benign “is your receiver up?” payload). Pass event_type in the body to simulate any real lifecycle event with its canonical sample payload — handy for testing your event-type router on more than just test_ping.
Request — default (test ping)
curl -X POST https://api.warpweb.ai/v1/customer/webhook/test-ping \
-H "Authorization: Bearer $WARPWEB_KEY"Request — sample a specific event type
curl -X POST https://api.warpweb.ai/v1/customer/webhook/test-ping \
-H "Authorization: Bearer $WARPWEB_KEY" \
-H "Content-Type: application/json" \
-d '{ "event_type": "site.complete" }'Body
| Field | Type | Required | Description |
|---|---|---|---|
event_type | string | no | One of the canonical event types from Lifecycle Events. Omit (or pass null) to send the default webhook.test_ping envelope. |
Each ping uses a fresh event_id and occurred_at, so receivers can dedupe by event_id across multiple test pings. For customer-level pings, site_id is always null — a test ping isn’t about a specific site.
Response — success
{
"ok": true,
"status": 200,
"response_body_preview": "ok",
"duration_ms": 142,
"event_type": "site.complete"
}| Field | Description |
|---|---|
ok | true if your receiver returned 2xx within the 5-second timeout. |
status | HTTP status from your receiver (null on network failure or timeout). |
response_body_preview | Your receiver’s response body, truncated to 500 characters. |
duration_ms | Wall-clock time to first byte. |
event_type | Echo of the event type that was fired. |
Response — failure
{
"ok": false,
"status": null,
"response_body_preview": null,
"duration_ms": 5012,
"error": "timeout after 5s",
"event_type": "webhook.test_ping"
}No retries — a transient failure surfaces here as ok: false and you decide whether to call again.
Errors
| Status | Body | Cause |
|---|---|---|
| 400 | { "ok": false, "error": "no_webhook_url_configured" } | URL hasn’t been set yet — PUT /v1/customer/webhook first. |
| 400 | { "ok": false, "error": "no_signing_secret" } | Secret missing (rare — implies a partially-configured row). Rotate via POST /v1/customer/webhook/regenerate-secret. |
| 400 | { "error": "unknown_event_type", "detail": "...", "available_events": [...] } | event_type doesn’t match a known type. |
| 404 | { "error": "no_customer_for_caller" } | Bearer token doesn’t resolve to a customer. |
PATCH /v1/customer/webhook/subscriptions
Pick which event types you want to receive. Unsubscribed event types are still generated server-side; they just don’t get delivered to your URL.
By default (after first PUT), all subscribable event types are enabled.
Request
curl -X PATCH https://api.warpweb.ai/v1/customer/webhook/subscriptions \
-H "Authorization: Bearer $WARPWEB_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": ["site.complete", "site.failed", "site.revision_complete", "site.revision_failed"]
}'Body
| Field | Type | Required | Description |
|---|---|---|---|
events | string[] | yes | The event types you want to receive. Use the full list from GET /v1/customer/webhook’s available_events. Duplicates are de-duped. Empty array [] disables delivery (all events stop). |
Note: webhook.test_ping is not subscribable — it’s a synthetic event that bypasses the subscription filter by design.
Response
{
"subscribed_events": [
"site.complete",
"site.failed",
"site.revision_complete",
"site.revision_failed"
]
}The full saved subscription list. The order matches what you sent; duplicates have been removed.
Errors
| Status | Body | Cause |
|---|---|---|
| 400 | { "error": "events must be an array of strings" } | Body missing events, not an array, or array contained a non-string. |
| 400 | { "error": "unknown_event_type", "detail": "...", "available_events": [...] } | One of the requested types isn’t a known event. Catches typos. |
| 404 | { "error": "no_customer_for_caller" } | Bearer token doesn’t resolve to a customer. |
End-to-end setup
The standard production setup:
# 1. Point the webhook at your receiver.
curl -X PUT https://api.warpweb.ai/v1/customer/webhook \
-H "Authorization: Bearer $WARPWEB_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://api.yourapp.com/webhooks/warpweb-lifecycle" }'
# → capture the returned secret_plaintext into your secret manager
# 2. Optional: narrow the subscription to what you actually handle.
curl -X PATCH https://api.warpweb.ai/v1/customer/webhook/subscriptions \
-H "Authorization: Bearer $WARPWEB_KEY" \
-H "Content-Type: application/json" \
-d '{ "events": ["site.complete", "site.failed", "site.revision_complete", "site.revision_failed"] }'
# 3. Exercise the receiver before real traffic.
curl -X POST https://api.warpweb.ai/v1/customer/webhook/test-ping \
-H "Authorization: Bearer $WARPWEB_KEY" \
-H "Content-Type: application/json" \
-d '{ "event_type": "site.complete" }'
# 4. Build a site — your receiver gets site.complete within minutes.
curl -X POST https://api.warpweb.ai/v1/sites \
-H "Authorization: Bearer $WARPWEB_KEY" \
-H "Content-Type: application/json" \
-d '{ "businessName": "Acme Plumbing", "contactEmail": "owner@example.com" }'For ongoing operations:
- Use
GET /v1/customer/webhookto confirm the current URL and subscription set. - Use
POST /v1/customer/webhook/regenerate-secretimmediately if you suspect a secret leak. - Use
POST /v1/customer/webhook/test-pingwhen debugging a delivery problem to confirm your receiver is reachable and signing correctly.