POST /v1/sites/:id/approve-research
Approve a site that’s paused in research_review and advance it into generation. Optionally merge final edits — services, hours, FAQs, photos, brand colors — into the research payload before the build runs. Only relevant for sites created with "review": "manual"; auto-mode sites advance themselves and never sit in research_review.
Cost: Free (the generation itself still costs the usual ~200–500 credits — see POST /v1/sites).
Request
curl -X POST https://api.warpweb.ai/v1/sites/8f3c2a1b/approve-research \
-H "Authorization: Bearer wwk_<your-key>" \
-H "Content-Type: application/json" \
-d '{
"services": [
{ "name": "Drain cleaning", "description": "Hydro-jetting and cabling.", "price": "from $189" },
{ "name": "Water-heater install" }
],
"hours": {
"Monday": "8:00 AM - 6:00 PM",
"Sunday": "Closed"
},
"excludeImages": ["research/photo-3.jpg"],
"ownerPrompt": "Emphasize 24/7 emergency service"
}'All body fields are optional. An empty {} is valid — it accepts the research payload as-is and starts the build.
Body
| Field | Type | Description |
|---|---|---|
services | array | Replace the services list. { name, description?, price? }. Empty name entries are dropped. |
testimonials | array | Replace the testimonials list. { quote, author?, rating?, source? }. Empty quote entries are dropped. |
faqs | array | Replace the FAQ list. { question, answer }. Empty question entries are dropped. |
hours | object | Weekday → hour string (e.g. "Monday": "9:00 AM - 5:00 PM" or "Closed"). Replaces the hours map wholesale. |
brandColors | object | { primary?, secondary?, accent?, personality? }. Hex strings. Omitted slots preserve research-extracted values. |
existingSiteUrl | string | Override the existing-site URL recorded at create time. |
ownerPrompt | string | Voice / tone direction merged into the build prompt. Same field as on create. |
businessDescription | string | One-sentence pitch the generator uses for the hero / meta-description. |
designStyle | string | Override the auto-selected design style. Unknown values are silently ignored — the existing style stays. |
allowStockPhotos | boolean | Toggle the stock-photo (Pexels) layer of the photo cascade. Default false as of 2026-05-24 — sites with empty upload + Google Places slots render Lucide icons over brand colors instead of generic stock imagery. Set true to opt in to stock fallback when you’d rather have generic photo coverage than icon-and-color sections. Also accepted at create time on POST /v1/sites. |
excludeImages | string[] | Drop photos from generation. Each entry can be either the localPath (for existing_site / upload / facebook photos: e.g. "images/existing-3.jpg") or the full storage_url (for places / stock photos, which only live in the photo catalog and not on disk: e.g. "https://...supabase.../places-XYZ.jpg"). Use whatever the photos[] entry from GET /v1/sites/:id/research exposed — both forms are accepted. Excluded photos are tombstoned in the photo catalog (metadata.excluded=true), filtered out of the build picker, AND removed from the on-disk staging dir when local. Surfaces on subsequent GET /research calls with exclude: true so your UI can pre-check the operator’s prior exclusions. |
uploadedPhotos | string[] | URLs to download and add to the photo library. Same field as on create. |
contentPlan | object | Partial overrides to the AI-generated content plan (e.g. servicesToHighlight, competitiveAdvantages). Merged shallow over the existing plan. |
contactEmail | string | Operator-confirmed contact email. Written to sites.contact_email before the build kicks off. Use this when the email cascade returned null at research time (email_status: "missing") or when the operator wants to swap the placeholder for the customer’s real address. After the site is built, use POST /v1/sites/:id/contact instead. See Email resolution. |
services, testimonials, faqs, and hours write into the priority-1 content source — they beat anything the extractor finds during research.
Response
{
"success": true,
"phase": "generating"
}The build runs asynchronously from here. Listen for site.complete (or site.failed) on the customer lifecycle webhook, or poll GET /v1/sites/:id until status flips to complete.
Errors
| Status | Body | Cause |
|---|---|---|
| 400 | { "error": "Site is not in research review phase" } | The site has already advanced (or never entered) research_review. Check generation_phase on GET /v1/sites/:id. Idempotent — repeated calls after a successful approve also land here. |
| 400 | { "error": "No research data found" } | The site exists but research_data is empty. Should not happen for sites that reached research_review; usually indicates a corrupted row. |
| 404 | { "error": "Site not found" } | No site with that ID belongs to your account. |
Related
GET /v1/sites/:id/research— fetch the payload you’re approving.PATCH /v1/sites/:id/research-draft— save in-progress edits without advancing the phase.POST /v1/sites— set"review": "manual"to land here.