API ReferenceEmail resolution

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 body

First 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 as websiteUri. If tied, prefer non-generic local-parts (mike@, dispatch@ rank above hello@, 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:

ValueWhenRecommended UX
missingsites.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.
placeholdersites.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.
confirmedsites.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

SurfaceField path
GET /v1/sites/:idemail_status
GET /v1/sites/:id/researchemail_status (sibling of contact_email)
site.research_readypayload.email_status
site.completepayload.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 inputemail_status after cascadeBehavior
review: "auto" (default), no allowMissingEmailconfirmed or placeholderAuto-advance through generating → reviewing → deploying → complete.
review: "auto" (default), no allowMissingEmailmissingForced pause at research_review. site.research_ready fires with email_status: 'missing' so receivers can render an “add an email” prompt.
review: "auto" + allowMissingEmail: trueanyAuto-advance regardless.
review: "manual"anyAlways 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:

  1. 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.
  2. Portfolio site / no lead capture — you want the site built but don’t care about form submissions.
  3. 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:

ChannelFires whenConfigured via
DB capture (form_submissions table)alwaysautomatic
Webhook POSTsites.form_webhook_url is setPOST /v1/sites/:id/webhooks/forms
Emailsites.contact_email is setcontactEmail 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.