API ReferenceDELETE /v1/sites/:id/domains/:domain

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

ParameterDescription
idThe site UUID returned from POST /v1/sites.
domainThe 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

  1. 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.
  2. Hosting tier flip. The site row’s hosting_tier flips from paid_custom_domain back to free_subdomain. The 7-day refresh cadence applies again — call POST /v1/sites/:id/refresh (or just hit the URL) to keep the site live.
  3. 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.
  4. Domain ownership preserved. The domains table row stays put. If you registered the domain through Warpweb via POST /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."
}
FieldDescription
site_idThe site you detached the domain from.
domainThe lowercased domain that was un-bound.
hosting_tierAlways free_subdomain after a successful detach.
already_freetrue 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_urlThe free subdomain URL the site now serves at.
subscription_sync.actionOne 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_idThe Stripe subscription that was synced, or null if no sub existed.
subscription_sync.paid_site_countThe new count of paid sites under your account after the detach.
messageHuman-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

StatusBodyCause
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.