POST /v1/sites/:id/contact
Update the contact email for a live site. Affects two things:
- Form-submission delivery routing —
sites.contact_emailis the inbox where contact-form submissions get delivered. This endpoint always updates the column. The next form submission delivers to the new address regardless of what’s painted on the HTML. - The visible email on the page — when the site has shipped AND there was a prior email in the rendered HTML, Warpweb walks the site’s HTML files and replaces every occurrence of the old email with the new one (
mailto:hrefs, plain-text displays, schema.org structured-data fields). A fresh Cloudflare Pages deploy follows so the change goes live.
This is the post-build counterpart to contactEmail on POST /v1/sites/:id/approve-research — once the site is shipped, use this endpoint instead.
Cost: Free. Deterministic find-replace + redeploy; no LLM.
When to use it
- Operator originally typed their own email (or Prickl filled it in with theirs) and now has the customer’s real address.
- Operator wants to change which inbox receives form submissions for any reason.
- An automated flow detected
email_status: "missing"or"placeholder"on thesite.research_readyorsite.completewebhook and wants to fill the gap.
Request
curl -X POST https://api.warpweb.ai/v1/sites/<site_id>/contact \
-H "Authorization: Bearer wwk_<your-key>" \
-H "Content-Type: application/json" \
-d '{
"contactEmail": "jjseptic@hotmail.com"
}'Body
| Field | Type | Required | Description |
|---|---|---|---|
contactEmail | string | yes | The new contact email. Must look like name@example.com (trimmed, contains @, has a . after @). Whitespace-only / missing @ / no domain → 400. |
syncHtml | boolean | no, default true | When true and the site has shipped and there’s a prior email in HTML, Warpweb walks the staged HTML files and replaces every occurrence before re-deploying. Pass false to skip — only sites.contact_email (the form-submission delivery routing) is updated, the rendered HTML is left alone. |
Response
Success — full sync
{
"ok": true,
"contactEmail": "jjseptic@hotmail.com",
"emailStatus": "confirmed",
"syncedHtml": true,
"filesUpdated": 2,
"deploymentUrl": "https://jj-septic-llc-dea19a.pages.dev/"
}emailStatus is the three-state signal — see Email resolution. filesUpdated is the count of HTML files actually rewritten.
Success — DB updated, HTML sync skipped
When something prevents the deterministic find-replace, the DB still updates and the response carries htmlSyncSkipped + an actionable hint:
htmlSyncSkipped | When | What to do |
|---|---|---|
opted_out | Caller passed syncHtml: false. | Nothing — you opted out. |
not_yet_shipped | Site is still in researching / generating / reviewing / deploying. The generator reads sites.contact_email when it runs, so the new value is picked up automatically. | Nothing — the in-flight build will use the new value. |
no_prior_email_in_html | The site shipped with no email rendered in HTML — there’s no anchor for find-replace. Common when the site uses a contact form without displaying a mailto: link. | Form submissions now route to the new address. To also show the email on the page, fire POST /v1/sites/:id/revisions with a prompt like "Add a contact email to the page footer pointing to <new>". |
{
"ok": true,
"contactEmail": "jjseptic@hotmail.com",
"emailStatus": "confirmed",
"syncedHtml": false,
"filesUpdated": 0,
"htmlSyncSkipped": "no_prior_email_in_html",
"hint": "Form submissions now deliver to jjseptic@hotmail.com. To also show the address on the page, fire POST /v1/sites/<site_id>/revisions with prompt: \"Add a contact email to the contact page and footer pointing to jjseptic@hotmail.com.\""
}Success — no-op
When the new email matches the existing value exactly (case-insensitive on the domain), the endpoint short-circuits:
{
"ok": true,
"contactEmail": "jjseptic@hotmail.com",
"emailStatus": "confirmed",
"syncedHtml": false,
"filesUpdated": 0,
"noop": true
}Safe to call repeatedly — idempotent.
Partial success — DB updated, HTML sync failed
If the find-replace + re-deploy throws after the DB update succeeded, you get a 502:
{
"error": "html_sync_failed",
"detail": "<underlying error message>",
"contactEmailUpdated": true,
"hint": "Form submissions now deliver to <new> (DB updated). HTML sync failed; retry POST /v1/sites/<id>/contact with the same body to retry the visible-display sync."
}The form-submission delivery routing change did land — the next submission goes to the new address. The visible HTML still shows the old email. Retry the same POST to retry the find-replace + deploy.
Errors
| Status | error | When |
|---|---|---|
400 | contactEmail is required (string) | Body missing or non-string contactEmail. |
400 | contactEmail cannot be empty | Body had contactEmail but it was whitespace-only. |
400 | contactEmail must look like name@example.com | Doesn’t contain @, or no domain after @, or no . in the domain. |
400 | Request body must be valid JSON | Body not parseable as JSON. |
404 | site_not_found | Site doesn’t exist OR belongs to a different caller. (Identical shape for both — we don’t leak existence of other callers’ sites.) |
500 | db_update_failed | The UPDATE sites SET contact_email = … query itself failed. Retry. |
502 | html_sync_failed | DB update landed; HTML find-replace or Cloudflare deploy errored. contactEmailUpdated: true confirms the routing change took effect. Retry to retry the HTML sync. |
Limitations
Substring replacement, not RFC 5322 boundary matching. The endpoint replaces every occurrence of the old email string in HTML files. If one of your pages has both old@example.com and very-old@example.com, the substring old@example.com inside the longer address ALSO gets replaced. In practice contact pages rarely list multiple similar addresses, but if you hit this, fire a follow-up revision to clean up.
Same-domain rewriting depends on the find-replace anchor. The endpoint doesn’t know the structure of your page — it only finds-and-replaces the exact old-email substring. If the HTML displayed the email in some unexpected encoding (e.g. JavaScript-obfuscated info AT example DOT com), the find-replace won’t catch it. In that case filesUpdated: 0 is returned with htmlSyncSkipped: 'no_prior_email_in_html', and a revision is the right tool.
No file-extension scan beyond HTML. CSS, JS bundles, and JSON manifests are not walked. The deployed site is the only file-shape that displays the email to end-users; if your generator emits the email into a JS bundle (unusual), that bundle won’t be rewritten — fire a revision for that case too.
Also available via revisions
The same atomic “fix both routing and visible HTML” logic is exposed to the revision-AI loop as a tool called set_contact_email. If the operator says “change the email to X” in a chat that’s wired up to fire revisions, the AI uses this tool to keep the DB column and visible HTML in sync — the same outcome as calling this endpoint directly. No special endpoint call needed from the chat layer; just send the operator’s prompt to POST /v1/sites/:id/revisions and let the AI route it.
The endpoint and the tool share the same underlying logic (applyContactEmailChange in SiteForge’s services/contact-email-update.ts), so behavior — no-anchor handling, case-insensitive matching, idempotency — is identical between the two entry points. Use the endpoint for direct API calls from your own backend; let the AI use the tool when the operator’s intent comes through chat.
Related
- Email resolution — the cascade + the
email_statusenum. GET /v1/sites/:id/research— readcontact_emailandemail_statusbefore deciding whether to call this endpoint.GET /v1/sites/:id—email_statusalso surfaces here for post-build state polling.POST /v1/sites/:id/approve-research— the pre-build counterpart for setting the email at research-review time.POST /v1/sites/:id/revisions— fire a revision when the visible HTML needs structural changes (e.g. adding a contact email to a site that never displayed one). The AI’sset_contact_emailtool handles the simple swap case automatically.site.research_readyandsite.complete— both carryemail_statusso you can drive an automated “the email needs attention” flow.