Encited

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 pending status. Returns DNS record instructions.
  • 400 Bad Requestinvalid_domain, origin_host_same_as_domain, or origin_host_is_editor_url.
  • 403 Forbiddendomain_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.
  • 409 Conflictdomain_already_connected. The domain is already connected to an account.
  • 500 Server Errorinsert_failed. The domain could not be saved; retry the request.
  • 503 Service Unavailableservice_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 Forbiddenapi_key_domain_scope_mismatch or domain_not_owned. The key is scoped to a different domain.
  • 404 Not Foundnot_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 Forbiddenrequires_unscoped_key. Domain-scoped keys cannot remove domains.
  • 404 Not Foundnot_found. The domain is not connected to your account.
  • 503 Service Unavailableservice_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, and promoted.
  • 403 Forbiddenapi_key_domain_scope_mismatch or domain_not_owned.
  • 404 Not Foundnot_found. The domain is not connected to your account.
  • 429 Rate Limitedrate_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 Requestinvalid_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).
  • 403 Forbiddenupgrade_required_for_custom_headers, api_key_domain_scope_mismatch, or domain_not_owned.
  • 404 Not Foundnot_found. The domain is not connected to your account.
  • 429 Rate Limitedrate_limited. Settings updates count against the ~60/minute limit.
  • 503 Service Unavailableservice_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 Requestfetch_failed (sitemap URL unreachable) or parse_failed (not valid XML sitemap content).
  • 403 Forbiddenapi_key_domain_scope_mismatch or domain_not_owned.
  • 404 Not Foundnot_found. The domain is not connected to your account.
  • 429 Rate Limitedrate_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.
  • 503 Service Unavailableservice_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.