DELETE /v1/sites/:id/domains/:domain
Detach a custom domain from a site and stop paying for the paid hosting tier on that site. The site keeps serving traffic at its free <slug>.warpweb.app subdomain — the detach is the inverse of POST /v1/sites/:id/domains.
This is the canonical “stop paying for one domain” operation. It is per-site — it does not touch any other sites under your account, and it does not cancel your Warpweb subscription. Stripe billing quantity decrements on success; if this was the last paid site, the subscription is flagged for cancellation at period end.
Cost: Free.
Request
curl -X DELETE https://api.warpweb.ai/v1/sites/8f3c2a1b-5d47-4c9e-b820-1f8a3e7d9c4f/domains/brooksideplumbing.com \
-H "Authorization: Bearer wwk_<your-key>"Path parameters
| Parameter | Description |
|---|---|
id | The site UUID returned from POST /v1/sites. |
domain | The apex domain to detach (e.g. brooksideplumbing.com). Pass the same value you used at attach time. Case-insensitive — Warpweb lowercases before lookup. The www. variant is un-bound automatically when present. |
Body
No body. Send an empty request.
What happens
- Cloudflare un-bind. The apex and
www.variants of the custom domain are removed from the site’s Cloudflare Pages project. The free subdomain (<slug>.warpweb.app) keeps serving. - Hosting tier flip. The site row’s
hosting_tierflips frompaid_custom_domainback tofree_subdomain. The 7-day refresh cadence applies again — callPOST /v1/sites/:id/refresh(or just hit the URL) to keep the site live. - Stripe quantity sync. Your active-sites subscription quantity decrements by one. If this was your last paid site, the subscription is flagged
cancel_at_period_end— you keep paid hosting on any sites that were still attached until the current period ends, then the line item drops off entirely. No mid-cycle proration; Stripe handles the cadence. - Domain ownership preserved. The
domainstable row stays put. If you registered the domain through Warpweb viaPOST /v1/domains/register, you keep ownership of the registration — re-attach to any site later without re-buying.
Response
{
"success": true,
"site_id": "8f3c2a1b-5d47-4c9e-b820-1f8a3e7d9c4f",
"domain": "brooksideplumbing.com",
"hosting_tier": "free_subdomain",
"already_free": false,
"fallback_url": "https://brookside-plumbing-a1b2c3.warpweb.app",
"subscription_sync": {
"action": "decremented",
"subscription_id": "sub_1Q3xYz...",
"paid_site_count": 2
},
"message": "brooksideplumbing.com detached. Site now serves at https://brookside-plumbing-a1b2c3.warpweb.app. Re-attach anytime from the Sites page."
}| Field | Description |
|---|---|
site_id | The site you detached the domain from. |
domain | The lowercased domain that was un-bound. |
hosting_tier | Always free_subdomain after a successful detach. |
already_free | true when the site was already on the free tier at call time. The endpoint still re-runs the Cloudflare un-bind + Stripe sync as a self-heal, but no tier flip happens. See Idempotency below. |
fallback_url | The free subdomain URL the site now serves at. |
subscription_sync.action | One of decremented, cancel_at_period_end_set, no_active_subscription, or noop. Surfaces what the Stripe sync did so you can reconcile in your billing UI. |
subscription_sync.subscription_id | The Stripe subscription that was synced, or null if no sub existed. |
subscription_sync.paid_site_count | The new count of paid sites under your account after the detach. |
message | Human-readable summary safe to surface in your UI. |
Idempotency
Safe to call any number of times. Calling DELETE on a site that’s already on free_subdomain returns 200 with already_free: true and the same response shape — the endpoint still re-runs the Cloudflare un-bind (which is itself 404-idempotent) and the Stripe quantity sync (which is state-machine-idempotent) to self-heal any drift from a previous partial run. No credits are charged on either path.
Stripe sync is best-effort: a transient Stripe outage does NOT block a successful detach (the Cloudflare un-bind has already happened). A weekly reconcile cron picks up any drift between Cloudflare bindings, the sites table, and Stripe subscription quantity.
Errors
| Status | Body | Cause |
|---|---|---|
| 400 | { "error": "domain path parameter is required" } | Missing domain path segment. |
| 404 | { "error": "Site not found" } | No site with that ID belongs to your account. Same 404 for not-exists vs. not-owned (we don’t leak which sites exist). |
| 422 | { "error": "Site has no pages_project_name — nothing to detach" } | Site never deployed (no Cloudflare Pages project was created). Nothing to un-bind. |
| 500 | { "error": "Domain was removed from Cloudflare but our records did not update. Contact support.", "internal_error": "..." } | Cloudflare un-bind succeeded but the DB tier-flip failed. Support can reconcile manually. |
| 502 | { "error": "Failed to remove <domain> from Cloudflare — your subscription was NOT changed. Try again in a moment.", "internal_error": "..." } | Cloudflare un-bind failed (transient). Subscription was NOT decremented. Safe to retry. |
See Error shapes for the canonical error envelope across endpoints.
Related
POST /v1/sites/:id/domains— attach a custom domain (inverse of this call).POST /v1/sites/:id/refresh— keep the free-subdomain fallback alive after detach.GET /v1/sites/:id— confirmhosting_tierflipped tofree_subdomain.