Email resolution
Google Places does not return business emails, so Warpweb resolves contact_email through a sequential cascade and surfaces an explicit email_status enum so callers can tell whether the value came from them, was scraped, or is still missing. This page documents the cascade, the email_status signal, the allowMissingEmail escape hatch, and how email delivery relates to (and is independent of) form-submission webhooks.
The email value is one of three independent contact-form delivery channels on every site — DB capture (form_submissions), webhook POST, and email. They don’t interact. A site can run on webhook-only, email-only, both, or neither.
The cascade
caller's contactEmail → use it (whatever you pass on POST wins)
│ null
▼
scrape of websiteUri → mailto: + contact-page sweep on the resolved Place's website
│ null
▼
operator entry at review → contactEmail in POST /v1/sites/:id/approve-research bodyFirst non-null value wins for downstream use — contact-form email delivery, operator notifications, etc. The cascade runs once during research and the result is written to sites.contact_email.
What counts as “found” during the scrape
- A single
<a href="mailto:...">on the homepage or/contact,/contact-us,/about→ that address. - Multiple
mailto:links → prefer addresses on the same domain aswebsiteUri. If tied, prefer non-generic local-parts (mike@,dispatch@rank abovehello@,info@,contact@). - Skipped signals: addresses containing
wordpress,wix.com,squarespace.com,noreply,example.com. - Cap: one address selected per site.
If the resolved Place has no websiteUri at all, the scrape step is skipped and the cascade falls through to operator entry.
email_status — the three-state signal
Every endpoint and webhook that touches the contact-email situation reports a three-state enum derived from sites.contact_email plus the originating operator’s account email:
| Value | When | Recommended UX |
|---|---|---|
missing | sites.contact_email is null. No email anywhere. | Render a prominent “add an email” prompt. Form submissions still capture to the DB and (if configured) fire webhooks, but no email channel is wired. |
placeholder | sites.contact_email equals the originating operator’s auth email. Common when an upstream AI (e.g. Prickl) defaulted to the operator’s own inbox while it didn’t yet know the customer’s. | Render a soft nudge (“we used your email — want the customer’s instead?”). Don’t block; the site still works. |
confirmed | sites.contact_email is set AND differs from the operator’s account email. Came from the operator typing it deliberately OR from the extractor’s email harvest. | No nudge needed. |
The signal is derived at read time on every response and webhook payload — POST /v1/sites/:id/contact updates the underlying column without rewriting research history, so consumers always see live state.
Where it surfaces
| Surface | Field path |
|---|---|
GET /v1/sites/:id | email_status |
GET /v1/sites/:id/research | email_status (sibling of contact_email) |
site.research_ready | payload.email_status |
site.complete | payload.email_status |
The review sheet should always render an editable email field — email_status only changes how loud the affordance is, not whether it’s there. Operators frequently want to override even a successfully-scraped address.
When the operator confirms or edits the value during the build-plan review, send the final email on POST /v1/sites/:id/approve-research as contactEmail. After the site is built, use POST /v1/sites/:id/contact instead.
Auto-advance vs forced pause
For warpweb_public callers (sites created via this API), the cascade outcome decides whether the build auto-advances out of research_review or pauses for review.
| Caller-side input | email_status after cascade | Behavior |
|---|---|---|
review: "auto" (default), no allowMissingEmail | confirmed or placeholder | Auto-advance through generating → reviewing → deploying → complete. |
review: "auto" (default), no allowMissingEmail | missing | Forced pause at research_review. site.research_ready fires with email_status: 'missing' so receivers can render an “add an email” prompt. |
review: "auto" + allowMissingEmail: true | any | Auto-advance regardless. |
review: "manual" | any | Always pauses at research_review (the explicit-pause path you opted into). email_status reflects the cascade result. |
Sites originated via prickl or proportal always pause at research_review regardless of the cascade — those flows surface the build-plan sheet to a human operator every time. allowMissingEmail is ignored for those callers.
The allowMissingEmail escape hatch
allowMissingEmail: true on the create body opts the caller out of the email-missing safety-net pause. It’s an informed opt-in to “no email channel on this site” — not “no delivery at all”.
What happens when allowMissingEmail: true AND the cascade returns email_status: 'missing':
- Build auto-advances and deploys.
- Contact form generates and renders on the live site as normal.
- Submissions still capture to
form_submissions(DB-side). - Webhook fires if you’ve configured one via
POST /v1/sites/:id/webhooks/forms. - No email fires (none configured — the opt-out).
- Dashboard surfaces a notice only when both email and webhook are missing: “This site is capturing leads to the dashboard but no delivery is configured.” Add either an email or a webhook to clear it.
Use cases:
- Platform builder — you create sites for your own end-clients, register your backend as the webhook destination, and email clients from your own platform. Zero need for warpweb to hold an email per site.
- Portfolio site / no lead capture — you want the site built but don’t care about form submissions.
- Webhook-only by design — same as #1 but for a single business.
allowMissingEmail is ignored for prickl and proportal originated sites — those flows always render the build-plan sheet where the operator decides per site whether to type an email or skip.
Email and webhook delivery are independent
Three separate contact-form delivery channels live on every site:
| Channel | Fires when | Configured via |
|---|---|---|
DB capture (form_submissions table) | always | automatic |
| Webhook POST | sites.form_webhook_url is set | POST /v1/sites/:id/webhooks/forms |
sites.contact_email is set | contactEmail on create / approve-research / POST /v1/sites/:id/contact, or via the cascade above |
They don’t interact. Setting allowMissingEmail: true only affects the email channel — webhook registration is a separate endpoint and unaffected. The platform-builder flow is a clean two-call sequence:
# 1. Create the site without collecting end-client email.
curl -X POST https://api.warpweb.ai/v1/sites \
-H "Authorization: Bearer wwk_<your-key>" \
-H "Content-Type: application/json" \
-d '{
"mapsUrl": "https://maps.app.goo.gl/abc123",
"allowMissingEmail": true
}'
# 2. Register your backend as the webhook destination.
curl -X POST https://api.warpweb.ai/v1/sites/$SITE_ID/webhooks/forms \
-H "Authorization: Bearer wwk_<your-key>" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://platform.example.com/webhooks/leads"
}'Same goes for the customer-level lifecycle webhook (PUT /v1/customer/webhook) — it lives on its own endpoint and is unaffected by anything on the email side.
Related
POST /v1/sites—contactEmailandallowMissingEmailare documented in the Body table.GET /v1/sites/:id/research— readcontact_emailandemail_statusfrom the response.POST /v1/sites/:id/approve-research— submit the operator-confirmed email at review time.POST /v1/sites/:id/contact— update the delivery email after the site is live.site.research_readywebhook — carriesemail_statusso receivers can branch on it.site.completewebhook — also carriesemail_statusfor post-build nudges.POST /v1/sites/:id/webhooks/forms— register a webhook destination for form submissions, independent of email.