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
| Field | Type | Required | Description |
|---|---|---|---|
mapsUrl | string | one 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. |
placeId | string | one 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. |
businessName | string | required when using businessName + businessLocation | The business as customers know it. Also used for branding and copy when set alongside mapsUrl or placeId. |
businessLocation | string | required when using businessName + businessLocation | City + state/region. Drives local SEO + narrows the auto-Places-match when no placeId or mapsUrl is provided. |
contactEmail | string | no (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. |
allowMissingEmail | boolean | no | Default 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. |
businessDescription | string | no | Free-text description of what the business does. The more specific, the better the generated copy. |
ownerPrompt | string | no | Voice/tone direction or anything you want emphasized (e.g. “Emphasize 24/7 emergency service”). |
facebookUrl | string | no | Public Facebook page URL — used as a signal during research. |
existingSiteUrl | string | no | URL 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. |
licenses | string[] | no | License 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.). |
uploadedPhotos | string[] | no | URLs of photos you want included on the site. |
logoUrl | string | no | URL to a business logo (used for branding and color extraction). |
designStyle | string | no | Pick 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. |
review | string | no | "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. |
allowStockPhotos | boolean | no | Default 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 a400. They require a JavaScript-driven redirect that Google’s bot detection consistently blocks from datacenter IPs (verified across stealth + text-search fallbacks). The canonicalgoogle.com/maps/place/<name>/@<lat>,<lng>/...URL always resolves; themaps.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 passplaceId(fromPOST /v1/businesses/search) orbusinessName + 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"
}| Field | Description |
|---|---|
siteId | Opaque UUID. Use this for all subsequent calls. |
status | Always generating on initial response. |
slug | The URL-safe slug used in the deployed subdomain (<slug>.warpweb.app). |
usagelands on the lifecycle webhook, not this response. The build runs asynchronously — token + cost data isn’t available at POST time. The finalusageaggregate ({ credits_charged, tokens_in, tokens_out, cost_usd }) ships on thesite.completelifecycle webhook payload and is also queryable viaGET /v1/sites/:idonce the build lands. See Error shapes → defensive parsing for the recommended client handling.
Status lifecycle
| Status | Meaning |
|---|---|
generating | The build is in progress. |
complete | The site is deployed and reachable at deployment_url. |
failed | The 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
| Status | Body | Cause |
|---|---|---|
| 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. |