API ReferencePOST /v1/sites

POST /v1/sites

Create a new site. Warpweb researches the business, generates copy and design, and deploys to a free *.warpweb.app subdomain. Returns immediately with a siteId; the build runs asynchronously and the final result is delivered via the site.complete (or site.failed) lifecycle webhook.

Cost: ~200–500 credits, billed against actual usage at the end of the build. Most builds land near 300; the high end (500) hits when the business has 200+ Google reviews and a lot of photos to process. Failed builds don’t charge. The exact cost lands in usage.cost_usd on the site.complete webhook.

Request

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"
  }'

Resolution priority. The handler picks the business identifier in this order: placeId > mapsUrl > businessName + businessLocation. Pass whichever you have — extra fields are ignored when a higher-priority one is present.

Retry safety: Idempotency-Key header

Builds are expensive (~$1–5 of LLM spend). To make retries safe — your client times out, your job runner re-fires, etc. — send an Idempotency-Key header on the request:

curl -X POST https://api.warpweb.ai/v1/sites \
  -H "Authorization: Bearer wwk_<your-key>" \
  -H "Idempotency-Key: 8e7b1c2d-...-your-uuid" \
  -H "Content-Type: application/json" \
  -d '{ ... }'

The server hashes (customer_id, route, body, idempotency_key) and caches the response for 24h. A second request with the same key + same body returns the exact same response without re-running the build. Different body with the same key returns 409 conflict so you can’t accidentally swap content under a key you’ve used.

Recommended client pattern: generate a UUID once when the operator-facing action starts, send it on every retry. Keys are scoped per customer + route, so safe to use the same UUID across endpoints.

Body

FieldTypeRequiredDescription
mapsUrlstringone of (mapsUrl, placeId, businessName + businessLocation)Google Maps URL of the business. Accepts the long google.com/maps/place/Name/@lat,lng/... form, the short maps.app.goo.gl/... share link, and maps.google.com/?cid=.... You can also pass an explicit place_id:... query-param URL — Warpweb extracts the ID directly. share.google/* short URLs are NOT supported — Google’s bot detection blocks server-side resolution from datacenter IPs. Open the share link in a browser, wait for it to land on the canonical google.com/maps/place/... URL, and paste that. See share.google rejection below.
placeIdstringone of (mapsUrl, placeId, businessName + businessLocation)Google Places ID from POST /v1/businesses/search. When set, Warpweb pulls research data from this exact Place — no ambiguity. Best for LLM-agent flows that received a bare name and disambiguated via search.
businessNamestringrequired when using businessName + businessLocationThe business as customers know it. Also used for branding and copy when set alongside mapsUrl or placeId.
businessLocationstringrequired when using businessName + businessLocationCity + state/region. Drives local SEO + narrows the auto-Places-match when no placeId or mapsUrl is provided.
contactEmailstringno (cascade)Where contact-form submissions deliver if no webhook is configured. When omitted, Warpweb tries to scrape one from the business’s website (mailto: links + contact pages). If that returns nothing, the site pauses at research_review for operator entry (unless allowMissingEmail: true — see below). Cascade: caller → website scrape → review. See Email resolution for the full cascade.
allowMissingEmailbooleannoDefault false. When true, disables the email-missing safety-net pause for warpweb_public callers — the build auto-advances even if no email was found. Use when you’ll deliver leads via webhook only and don’t want an email channel on the site. Ignored for prickl / proportal originated sites (those flows always pause at research_review regardless). See Email resolution → allowMissingEmail.
businessDescriptionstringnoFree-text description of what the business does. The more specific, the better the generated copy.
ownerPromptstringnoVoice/tone direction or anything you want emphasized (e.g. “Emphasize 24/7 emergency service”).
facebookUrlstringnoPublic Facebook page URL — used as a signal during research.
existingSiteUrlstringnoURL of the business’s current website. When set, Warpweb extracts services, FAQs, testimonials, photos, and (per the cascade above) an email from this site to seed the build — measurably better defaults than a from-scratch generation when the business already has web copy. Best-effort: extraction failures don’t fail the build.
licensesstring[]noLicense or certification numbers (e.g. plumbing licenses, electrical permits, septic-installer TCEQ numbers). Used as trust signals — surfaced in the footer/header where the vertical earns it (HVAC, plumbing, septic, electrical, real estate, etc.).
uploadedPhotosstring[]noURLs of photos you want included on the site.
logoUrlstringnoURL to a business logo (used for branding and color extraction).
designStylestringnoPick a design style. Auto-selected if omitted. One of: Bold & Modern, Clean Minimal, Warm & Friendly, Corporate Professional, Dark & Sleek, Rustic & Natural, Vibrant & Energetic, Elegant & Refined. Pass exactly as shown (case-sensitive). Also editable in the build-plan sheet alongside services, email, etc., and on POST /v1/sites/:id/approve-research. Unrecognized values are silently dropped.
reviewstringno"auto" (default) — research runs, then the build advances straight through to deploy. "manual" — research runs, then the site pauses at generation_phase: "research_review" and fires site.research_ready. Caller must POST to /v1/sites/:id/approve-research to advance. Use manual for production builds where you want to verify services, hours, and photos before generation spends credits. Note: even in auto mode, the email cascade can force a pause — see Email resolution.
allowStockPhotosbooleannoDefault false. When true, the photo cascade falls back to generic Pexels stock photos for any section the operator’s uploaded + Google Places photos didn’t cover. When false (default), empty sections render with Lucide icons over the site’s brand colors — visually less dense, but every photo on the deployed site is the operator’s real photography. Trade-off: stock photos fill more visual real estate but don’t represent the actual business — set true only when generic coverage is genuinely better than icon-and-color sections. Also toggleable in the build-plan sheet and on POST /v1/sites/:id/approve-research.

share.google URLs are rejected

Warning. share.google/* short URLs are explicitly blocked at the handler with a 400. They require a JavaScript-driven redirect that Google’s bot detection consistently blocks from datacenter IPs (verified across stealth + text-search fallbacks). The canonical google.com/maps/place/<name>/@<lat>,<lng>/... URL always resolves; the maps.app.goo.gl/... short link does too.

Fix on the operator side: open the share.google/... link in any browser, wait for it to land on the canonical Maps URL, then paste that. Or skip the URL entirely and pass placeId (from POST /v1/businesses/search) or businessName + businessLocation.

The 400 response for this case has a hint field with the recovery path inline — surface it in your UI rather than your own copy:

{
  "error": "share.google/* short URLs are not supported. Open the link in a browser, wait for it to land on the canonical google.com/maps/place/... URL, then paste that URL instead. (Or pass placeId / businessName directly.)",
  "hint": "Google's bot detection blocks server-side resolution of share.google links from datacenter IPs. The canonical /maps/place/<name>/@<lat>,<lng>/... URL always works."
}

The same { error, hint } envelope is also returned when a non-share.google mapsUrl resolves to null AND the URL doesn’t carry a derivable business name in its /place/<name>/ segment. See Error shapes for the canonical envelope across endpoints.

Response

{
  "siteId": "8f3c2a1b-5d47-4c9e-b820-1f8a3e7d9c4f",
  "status": "generating",
  "slug": "brookside-plumbing-a1b2c3"
}
FieldDescription
siteIdOpaque UUID. Use this for all subsequent calls.
statusAlways generating on initial response.
slugThe URL-safe slug used in the deployed subdomain (<slug>.warpweb.app).

usage lands on the lifecycle webhook, not this response. The build runs asynchronously — token + cost data isn’t available at POST time. The final usage aggregate ({ credits_charged, tokens_in, tokens_out, cost_usd }) ships on the site.complete lifecycle webhook payload and is also queryable via GET /v1/sites/:id once the build lands. See Error shapes → defensive parsing for the recommended client handling.

Status lifecycle

StatusMeaning
generatingThe build is in progress.
completeThe site is deployed and reachable at deployment_url.
failedThe build failed. No credits charged. See failed_phase + last_error on the site row or site.failed webhook.

If you’ve configured a customer-level lifecycle webhook, you’ll receive a site.complete event when the site goes live — no polling required.

Errors

StatusBodyCause
400{ "error": "businessName is required (or provide placeId / mapsUrl so we can derive it from Google Places)" }No usable business identifier in the body.
400{ "error": "share.google/* short URLs are not supported...", "hint": "..." }The mapsUrl was a share.google/... short link. See share.google URLs are rejected above. Surface hint in your UI.
400{ "error": "Could not resolve mapsUrl...", "hint": "..." }mapsUrl resolved to null AND no business name could be derived from the URL’s /place/<name>/ segment. Surface hint for the recovery path.
400{ "error": "mapsUrl resolution failed. Try passing placeId directly." }Resolver threw an unexpected error. Use the Place ID directly as a workaround.
402{ "error": "PAYMENT_REQUIRED", ... }Account balance is 0 and auto-refill is off. Top up at app.warpweb.ai/billing.
429{ "error": "Daily site generation limit reached (...)", "quota": {...} }Per-day quota exceeded. The quota object has shape { used: number, limit: number, resetDescription: string } — surface resetDescription in your UI (“resets at midnight UTC”, etc.).
500{ "error": "Failed to create site record" }Unexpected server error. Safe to retry.