Domains API
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#
GET /api/domains#
List 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#
POST /api/domains#
Connect a new domain to your account. Returns the DNS records to configure. Requires an account-wide key.
Request Body#
| Parameter | Type | Description |
|---|---|---|
domain (required) |
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#
- 201 Created — Domain added in
pendingstatus. Returns DNS record instructions. - 400 Bad Request —
invalid_domain,origin_host_same_as_domain, ororigin_host_is_editor_url. - 403 Forbidden —
domain_suspended, ordomain_limit_reachedwhen your plan's domain limit is used up — the API never purchases extra domain slots; add slots from the dashboard or upgrade your plan. - 409 Conflict —
domain_already_connected. The domain is already connected to an account. - 500 Server Error —
insert_failed. The domain could not be saved; retry the request. - 503 Service Unavailable —
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#
GET /api/domains/:domain#
Get 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#
- 200 Success — Returns the domain detail object.
- 403 Forbidden —
api_key_domain_scope_mismatchordomain_not_owned. The key is scoped to a different domain. - 404 Not Found —
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#
DELETE /api/domains/:domain#
Disconnect a domain and remove its configuration. Requires an account-wide key.
Response Body#
{
"deleted": true
}
Response Codes#
- 200 Success — Domain removed.
- 403 Forbidden —
requires_unscoped_key. Domain-scoped keys cannot remove domains. - 404 Not Found —
not_found. The domain is not connected to your account. - 503 Service Unavailable —
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#
POST /api/domains/:domain/check#
Run 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#
- 200 Success — Check completed. Inspect
dns,probe, andpromoted. - 403 Forbidden —
api_key_domain_scope_mismatchordomain_not_owned. - 404 Not Found —
not_found. The domain is not connected to your account. - 429 Rate Limited —
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#
PATCH /api/domains/:domain/settings#
Partially 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#
- 200 Success — Settings saved. Rejected fields, if any, are listed in
invalidFields. - 400 Bad Request —
invalid_origin_host(theoriginHostvalue does not normalize to a valid host),origin_host_same_as_domain,origin_host_is_editor_url,redirect_limit_reached(includeslimitandcurrent), orinvalid_custom_headers(includesdetails). - 403 Forbidden —
upgrade_required_for_custom_headers,api_key_domain_scope_mismatch, ordomain_not_owned. - 404 Not Found —
not_found. The domain is not connected to your account. - 429 Rate Limited —
rate_limited. Settings updates count against the ~60/minute limit. - 503 Service Unavailable —
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#
PATCH /api/domains/:domain/sitemap#
Set, 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#
- 200 Success — Sitemap saved (or cleared) and the detail object returned.
- 400 Bad Request —
fetch_failed(sitemap URL unreachable) orparse_failed(not valid XML sitemap content). - 403 Forbidden —
api_key_domain_scope_mismatchordomain_not_owned. - 404 Not Found —
not_found. The domain is not connected to your account. - 429 Rate Limited —
rate_limited. Calls that fetch the sitemap count against the ~6/minute limit; only clears ("") and resubmitting the same stored URL withoutrefreshskip the fetch. - 503 Service Unavailable —
service_unavailable. Configuration could not be applied; retry the request.
Example#
# Set (or change) the sitemap URL
curl -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 sitemap
curl -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 sitemap
curl -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.
