Manage your connected domains programmatically: list, add, remove, test the connection, and update per-domain settings. Base path: /api/domains.
Endpoints that accept a JSON body validate it against a schema; a malformed body is rejected with a default 400 validation response whose shape differs from the { "error": ... } bodies documented below.
Available on Business and Enterprise plans. Other plans receive 403 { "error": "plan_required", "requiredPlan": "business" }. Authenticate with an API key via x-lovablehtml-api-key: <API_KEY> or Authorization: Bearer <API_KEY>.
Key Scopes
API keys can be account-wide (unscoped) or scoped to a single domain. Which keys an endpoint accepts:
| Endpoint | Account-wide key | Domain-scoped key |
|---|---|---|
| List, add, remove domains | Yes | No — 403 { "error": "requires_unscoped_key" } |
| Get, check, settings, sitemap | Yes | Yes, for the key's own domain only |
Using a key scoped to a different domain returns 403 { "error": "api_key_domain_scope_mismatch" } (or domain_not_owned). An account-wide key targeting a domain not on your account returns 404 { "error": "not_found" }.
List Domains
/api/domainsList all domains on your account, ordered by creation date. Requires an account-wide key.
Response Body
{"domains": [{"id": "a1b2c3d4-e5f6-...","domain": "your-app.com","originHost": "my-app.lovable.app","status": "connected","sitemapUrl": "https://your-app.com/sitemap.xml","createdAt": "2026-01-15T09:30:00.000Z","updatedAt": "2026-02-01T12:00:00.000Z"}]}
status is one of pending, connected, or via api; it can also be suspended for operator-locked domains. The same values apply to the detail object below.
Example
const response = await fetch("https://encited.com/api/domains", {headers: {"x-lovablehtml-api-key": "<API_KEY>",},});const data = await response.json();console.log(data.domains);
curl -X GET \"https://encited.com/api/domains" \-H "x-lovablehtml-api-key: <API_KEY>"
Add a Domain
/api/domainsConnect a new domain to your account. Returns the DNS records to configure. Requires an account-wide key.
Request Body
| Parameter | Type | Description |
|---|---|---|
domainrequired |
string | Domain to connect (e.g., "your-app.com" or "app.your-app.com"). |
originHost |
string | Host where your SPA is published (e.g., "my-app.lovable.app"). Must differ from the domain and must not be an editor URL. |
Response Body
{"id": "a1b2c3d4-e5f6-...","domain": "your-app.com","status": "pending","records": [{ "type": "A", "name": "@", "value": "203.0.113.10", "ttl": 300 },{ "type": "A", "name": "www", "value": "203.0.113.10", "ttl": 300 }],"isSubdomain": false}
Subdomains get a single A record named after the subdomain part. Create the returned records at your DNS provider, then call the connection check endpoint.
Response Codes
Domain added in pending status. Returns DNS record instructions.
invalid_domain, origin_host_same_as_domain, or
origin_host_is_editor_url.
domain_suspended, or domain_limit_reached when your plan's domain limit is
used up — the API never purchases extra domain slots; add slots from the
dashboard or upgrade your plan.
domain_already_connected. The domain is already connected to an account.
insert_failed. The domain could not be saved; retry the request.
service_unavailable. Configuration could not be applied; retry the request.
Example
const response = await fetch("https://encited.com/api/domains", {method: "POST",headers: {"Content-Type": "application/json","x-lovablehtml-api-key": "<API_KEY>",},body: JSON.stringify({domain: "your-app.com",originHost: "my-app.lovable.app",}),});const result = await response.json();console.log(result.records);
curl -X POST \"https://encited.com/api/domains" \-H "Content-Type: application/json" \-H "x-lovablehtml-api-key: <API_KEY>" \-d '{"domain": "your-app.com","originHost": "my-app.lovable.app"}'
Get a Domain
/api/domains/:domainGet the full configuration of a single domain, including settings and sitemap state.
Response Body
{"id": "a1b2c3d4-e5f6-...","domain": "your-app.com","status": "connected","originHost": "my-app.lovable.app","ignorePaths": "/admin/*","preferredHost": "apex","redirectRules": [{"source": "/old-page","destination": "/new-page","status": 301,"preserveQuery": true}],"customResponseHeaders": [{"pathPattern": "/docs/*","headers": [{ "name": "X-Robots-Tag", "value": "noarchive" }]}],"onDemandRenderEnabled": true,"sitemapUrl": "https://your-app.com/sitemap.xml","discoveredUrlCount": 42,"createdAt": "2026-01-15T09:30:00.000Z","updatedAt": "2026-02-01T12:00:00.000Z"}
Response Codes
Returns the domain detail object.
api_key_domain_scope_mismatch or domain_not_owned. The key is scoped to a
different domain.
not_found. The domain is not connected to your account.
Example
curl -X GET \"https://encited.com/api/domains/your-app.com" \-H "x-lovablehtml-api-key: <API_KEY>"
Remove a Domain
/api/domains/:domainDisconnect a domain and remove its configuration. Requires an account-wide key.
Response Body
{"deleted": true}
Response Codes
Domain removed.
requires_unscoped_key. Domain-scoped keys cannot remove domains.
not_found. The domain is not connected to your account.
service_unavailable. Configuration could not be applied; retry the request.
Example
curl -X DELETE \"https://encited.com/api/domains/your-app.com" \-H "x-lovablehtml-api-key: <API_KEY>"
Check Connection
/api/domains/:domain/checkRun a DNS lookup and a rendering probe against the domain, and promote it to connected when either succeeds.
The check is promote-only: a pending domain becomes connected when its DNS records match, or via api when the rendering probe succeeds. A failed check never demotes an already-connected domain — a transient DNS failure on your side won't disconnect a live site.
Response Body
{"dns": {"match": true,"cloudflareProxy": false,"lookupFailed": false},"probe": { "ok": true },"status": "connected","promoted": true}
| Parameter | Type | Description |
|---|---|---|
dns.match |
boolean | DNS records point at Encited with no conflicting records. |
dns.cloudflareProxy |
boolean | All resolved IPs belong to Cloudflare — usually means the orange-cloud proxy is hiding your A records. |
dns.lookupFailed |
boolean | The DNS lookup itself failed (transient resolver error). |
probe.ok |
boolean | A crawler-style request through the domain reached Encited. |
status |
string | Stored status after the check, post-promotion when it happened. |
promoted |
boolean | True when this check moved the domain out of pending. |
Response Codes
Check completed. Inspect dns, probe, and promoted.
api_key_domain_scope_mismatch or domain_not_owned.
not_found. The domain is not connected to your account.
rate_limited. Connection checks are limited to ~6 per minute per key.
Example
curl -X POST \"https://encited.com/api/domains/your-app.com/check" \-H "x-lovablehtml-api-key: <API_KEY>"
Update Settings
/api/domains/:domain/settingsPartially update a domain's settings. Omitted fields are left untouched.
Request Body
All fields are optional; send only what you want to change.
| Parameter | Type | Description |
|---|---|---|
originHost |
string | Host where your SPA is published. Must differ from the domain and must not be an editor URL (400 otherwise, same as adding a domain). Reachability is a soft check: an unreachable value is flagged in invalidFields but still saved and synced. A value that normalizes to an empty host returns 400 invalid_origin_host. |
ignorePaths |
string | Path patterns to exclude from prerendering, one per line. Send an empty string to clear. |
preferredHost |
"apex" | "www" | null | Primary host to canonicalize to. Send null to clear the preference. |
redirectRules |
array | Full replacement list of { source, destination, status?, preserveQuery? } rules. status is 301 (default) or 307; preserveQuery defaults to true. |
customResponseHeaders |
array | Full replacement list of { pathPattern, headers } rules (max 10 rules, 5 headers each). Plan-gated. |
onDemandRenderEnabled |
boolean | Enable or disable on-demand rendering for the domain. |
Response Body
Returns the updated domain detail object (same shape as Get a Domain) plus invalidFields. The update is applied per field: valid fields are saved even when others fail validation, and every failing field name is listed in invalidFields. Entries can be per-rule keys like redirectRules.0.source, not just top-level field names. A field listed in invalidFields is not necessarily discarded — an originHost that fails the reachability check is still saved and synced to the proxy config, so verify the stored value with a follow-up Get a Domain request.
{"id": "a1b2c3d4-e5f6-...","domain": "your-app.com","status": "connected","originHost": "my-app.lovable.app","ignorePaths": "/admin/*","preferredHost": "apex","redirectRules": [],"customResponseHeaders": [],"onDemandRenderEnabled": true,"sitemapUrl": "https://your-app.com/sitemap.xml","discoveredUrlCount": 42,"createdAt": "2026-01-15T09:30:00.000Z","updatedAt": "2026-02-01T12:00:00.000Z","invalidFields": []}
Check invalidFields even on 200. A response like "invalidFields": ["originHost"] means that field failed a check (e.g., the origin host was
unreachable) — but the value may still have been saved and synced. Confirm the
stored value with Get a Domain.
Response Codes
Settings saved. Rejected fields, if any, are listed in invalidFields.
invalid_origin_host (the originHost value does not normalize to a valid
host), origin_host_same_as_domain, origin_host_is_editor_url,
redirect_limit_reached (includes limit and current), or
invalid_custom_headers (includes details).
upgrade_required_for_custom_headers, api_key_domain_scope_mismatch, or
domain_not_owned.
not_found. The domain is not connected to your account.
rate_limited. Settings updates count against the ~60/minute limit.
service_unavailable. Configuration could not be applied; retry the request.
Example
const response = await fetch("https://encited.com/api/domains/your-app.com/settings",{method: "PATCH",headers: {"Content-Type": "application/json","x-lovablehtml-api-key": "<API_KEY>",},body: JSON.stringify({ignorePaths: "/admin/*",redirectRules: [{ source: "/old-page", destination: "/new-page", status: 301 },],}),},);const result = await response.json();console.log(result.invalidFields); // [] when everything applied
curl -X PATCH \"https://encited.com/api/domains/your-app.com/settings" \-H "Content-Type: application/json" \-H "x-lovablehtml-api-key: <API_KEY>" \-d '{"ignorePaths": "/admin/*","redirectRules": [{ "source": "/old-page", "destination": "/new-page", "status": 301 }]}'
Update Sitemap
/api/domains/:domain/sitemapSet, clear, or refresh the domain's sitemap. Fetches and validates the sitemap, then reports the discovered URL count.
Request Body
The JSON body is optional. An empty request, {}, and { "refresh": true } are all equivalent: they re-fetch and re-validate the currently stored sitemap and re-run URL discovery, so they count against the tight rate limit and can fail with fetch_failed. To read the current state without fetching, use Get a Domain instead.
| Parameter | Type | Description |
|---|---|---|
sitemapUrl |
string | Sitemap URL to set. Sending "" clears the stored sitemap. Omitting the field keeps the existing URL. |
refresh |
boolean | Force a re-fetch and re-discovery even when the URL is unchanged. Without it, resubmitting the stored URL is a no-op that returns the cached count. |
Response Body
Returns the domain detail object (same shape as Get a Domain). discoveredUrlCount reflects the number of URLs found in the sitemap after the fetch.
{"id": "a1b2c3d4-e5f6-...","domain": "your-app.com","status": "connected","originHost": "my-app.lovable.app","ignorePaths": "","preferredHost": null,"redirectRules": [],"customResponseHeaders": [],"onDemandRenderEnabled": true,"sitemapUrl": "https://your-app.com/sitemap.xml","discoveredUrlCount": 42,"createdAt": "2026-01-15T09:30:00.000Z","updatedAt": "2026-02-01T12:00:00.000Z"}
Response Codes
Sitemap saved (or cleared) and the detail object returned.
fetch_failed (sitemap URL unreachable) or parse_failed (not valid XML
sitemap content).
api_key_domain_scope_mismatch or domain_not_owned.
not_found. The domain is not connected to your account.
rate_limited. Calls that fetch the sitemap count against the ~6/minute
limit; only clears ("") and resubmitting the same stored URL without
refresh skip the fetch.
service_unavailable. Configuration could not be applied; retry the request.
Example
# Set (or change) the sitemap URLcurl -X PATCH \"https://encited.com/api/domains/your-app.com/sitemap" \-H "Content-Type: application/json" \-H "x-lovablehtml-api-key: <API_KEY>" \-d '{ "sitemapUrl": "https://your-app.com/sitemap.xml" }'# Force a re-fetch of the stored sitemapcurl -X PATCH \"https://encited.com/api/domains/your-app.com/sitemap" \-H "Content-Type: application/json" \-H "x-lovablehtml-api-key: <API_KEY>" \-d '{ "refresh": true }'# Clear the sitemapcurl -X PATCH \"https://encited.com/api/domains/your-app.com/sitemap" \-H "Content-Type: application/json" \-H "x-lovablehtml-api-key: <API_KEY>" \-d '{ "sitemapUrl": "" }'
Rate Limits
Limits are applied per API key:
| Operations | Limit |
|---|---|
| Connection check, sitemap updates that fetch | ~6 requests/minute |
| Everything else | ~60 requests/minute |
Over the limit, requests receive 429 { "error": "rate_limited" }. Back off and retry after a minute.
