Edgeweir
Reference

API and endpoints

Console HTTP endpoints, public API authentication and error format, and a summary of the node channel.

Endpoints

These paths are served on the web port (PORT, default 3000) by ROLE=app and all only.

PathAuthenticationPurpose
/api/v1/*x-api-keyPublic API (OpenAPI); cookies are stripped from requests
/api/v1/openapi.jsonNoneOpenAPI document
/rpc/*Session cookie + x-csrf-token: orpcWeb UI only (oRPC); x-api-key is stripped from requests
/api/auth/*Per endpointAllow-listed better-auth endpoints; everything else returns 404
/healthzNoneHealth check
/install.shNoneNode installer
/downloads/*NoneRelease file mirror; 404 unless EDGEWEIR_DOWNLOADS_DIR is set
Any other pathNoneWeb UI

Unmatched requests under /api/*, /rpc/*, /downloads/*, /install.sh, and /healthz return 404 with {"error":"not found"} and never fall back to the web UI.

Public API

/api/v1 and /rpc are generated from the same oRPC contract in packages/contract. The OpenAPI document is at /api/v1/openapi.json, with servers set to <EDGEWEIR_PUBLIC_URL>/api/v1; "OpenAPI" in System settings links to it.

curl -fsS https://cdn-admin.example.com/api/v1/openapi.json

Authentication

  • Header x-api-key: <key>: an AccessKey (starts with ewk_) or a service account key (starts with ews_).
  • An AccessKey acts as the operator (the only account, created by the setup wizard); its scope decides which procedures it can call, see AccessKey. A service account key can call only the procedures listed under Service accounts.
  • Invalid, revoked, or missing key: 401.
  • Each AccessKey allows up to 600 requests in a row; the count restarts when more than 60 seconds pass since the previous request. Beyond that the answer is 429 API_KEY_RATE_LIMITED with data.retryAfterSeconds. Use a service account key for continuous polling; service account keys are not counted.
  • Procedures that need no key (security: [] in OpenAPI): GET /system/status and POST /system/setup.
  • GET /me returns the caller: { user: { id, name, email, twoFactorEnabled }, serviceAccount }; with an AccessKey, serviceAccount is null.

AccessKey

Managed in Personal settings → Access keys (user menu), or through accessKeys.* on /rpc with a signed-in session.

ActionWhereNotes
CreateEnter "Name" (up to 64 characters), choose "Scope", click "Create"Default "Read and write". The key is shown once. Keys are created only from a signed-in console session; POST /access-keys on /api/v1 with a key returns 403 ACCESS_KEY_SESSION_REQUIRED.
ViewKey list; GET /api/v1/access-keysPrefix, scope, state, last use
Revoke"Revoke key"; DELETE /api/v1/access-keys/{id}Requests with the key return 401 afterwards; the key stays in the list marked "Revoked"
ScopeCallable procedures
Read onlyGET procedures and POST /rules/validate; other methods return 403 ACCESS_KEY_READ_ONLY
Read and writeEvery procedure except creating AccessKeys

Keys without a scope count as read and write. Creation and revocation are written to the audit log (api_key.create, api_key.revoke); actions performed with an AccessKey appear in the audit log with actor type api_key. better-auth's /api/auth/api-key/* endpoints are closed and return 404.

Service accounts

A service account is a machine identity for integrations calling /api/v1. It cannot sign in: it has no password, passkey or session, only keys. Service accounts are managed in System settings → Service accounts.

ActionNotes
Create, editName (up to 64 characters, unique), scopes, enabled. A disabled account's keys return 401
Create keyKeys start with ews_ and are shown once; only their SHA-256 is stored. The list shows the prefix and the last use (1-minute resolution)
Revoke keyRequests with the key return 401 afterwards
DeleteDeletes every key too

These changes are written to the audit log (service_account.create, service_account.update, service_account.delete, service_account.key_create, service_account.key_revoke); actions of a service account appear with actor type service_account. Service account keys work on /api/v1 only.

A service account can call only the procedures below, each with its scope:

ProcedureEndpointScope
system.statusGET /system/status—
account.meGET /me—
dns.catalogGET /dns/catalog—
settings.getGET /settingssystem:read
clusters.list, clusters.getGET /clusters, GET /clusters/{id}clusters:read
sites.list, sites.getGET /sites, GET /sites/{id}sites:read
sites.launchGET /sites/{id}/launchsites:read
sites.setEnabledPUT /sites/{id}/enabledsites:write
dns.siteTargetGET /sites/{siteId}/cnamesites:read
usage.list, usage.changesGET /usage, GET /usage/changesusage:read
CaseResponse
Missing scope403 SCOPE_REQUIRED; data.scope names the scope
Procedure not in the table403 SERVICE_ACCOUNT_FORBIDDEN
Invalid or revoked key, disabled account401
The change needs a capability an active node of the cluster lacks409 NODE_CAPABILITY_REQUIRED, see Node capabilities

For a service account, GET /me returns serviceAccount: { id, name, scopes }; user carries the service account's id and name (empty email, twoFactorEnabled false). Scopes apply to service accounts only; AccessKeys use the read-only / read-and-write scopes.

Idempotency keys

POST, PUT and PATCH on /api/v1 accept the Idempotency-Key header: 1–255 printable ASCII characters, as an RFC 8941 string ("key") or bare.

CaseResponse
First requestRuns normally; method, path (with query), request body SHA-256 and the final response are kept
Same caller, same key, same requestThe stored response (status and body) with Idempotent-Replayed: true
Same key, other method, path or body422 IDEMPOTENCY_KEY_MISMATCH
The first request is still running409 IDEMPOTENCY_IN_PROGRESS
Invalid key400 IDEMPOTENCY_KEY_INVALID
The response carries a credential shown once: POST /access-keys, POST /service-accounts/{id}/keys, POST /enrollment-tokens, POST /probe-tokens400 IDEMPOTENCY_KEY_UNSUPPORTED, nothing runs; send it again without the header
  • Keys are per caller: all AccessKeys share one set, each service account has its own.
  • Records are kept 24 hours; expired ones are deleted hourly.
  • 5xx responses are not kept, so the caller can retry with the same key; neither are 401 and 429 (the procedure did not run). 4xx responses are kept and replayed.
  • A record still running after 10 minutes counts as interrupted (a crashed console instance); the next request takes it over and runs again.
  • GET and DELETE ignore the header.

Optimistic concurrency

These writes accept an optional expectedUpdatedAt (ISO 8601, the updatedAt the caller read). A different value returns 409 UPDATED_AT_MISMATCH with the current value in data.updatedAt.

ProcedureupdatedAt of
sites.setEnabledThe site
clusters.setRolloutPolicypolicyUpdatedAt of the cluster's canary policy (publications and canary progress leave it alone; the updatedAt read then passes too while nothing has changed since)
l4Apps.update, l4Apps.setEnabledThe L4 app

When a site or L4 app is already enabled or disabled as requested, sites.setEnabled and l4Apps.setEnabled return the current state without comparing expectedUpdatedAt.

Enabling and disabling sites

PUT /sites/{id}/enabled (procedure sites.setEnabled) with the body {"enabled":false}. The operator (session or read-and-write AccessKey) and sites:write service accounts can call it.

  • A disabled site is not shipped to nodes; nodes answer HTTP requests for its domains with 503 (X-Edgeweir-Error: site-disabled) and fail the TLS handshake of HTTPS requests; its DNS records stay; certificate renewal continues and HTTP-01 challenges are answered.
  • A change publishes a configuration revision (reason codes site_enabled, site_disabled) and writes an audit entry (site.enable, site.disable); an unchanged state returns the current state without a revision or audit entry.
  • Purging or prefetching a disabled site: 409 SITE_DISABLED.
  • The response is { site, revision }; site.enabled holds the current state.

Site delivery and launch check

Sites (sites.list, sites.get and the site that writes return) carry delivery:

FieldDescription
statepending: no online node runs the site; partial: some online nodes run an older version or have an unhealthy data plane; live: every online node runs the latest version; disabled
totalNodesOnline active nodes of the site's cluster
servingNodesOf those, nodes whose applied configuration has the site (any version); for a disabled site, the nodes that still run it
currentNodesOf those, nodes running the site's latest version (canary candidates included) with a healthy data plane
canary{ endsAt, autoPromote } while the cluster's configuration canary keeps the nodes outside the canary on the site's previous version (when the window ends; autoPromote: false waits for a manual promotion), otherwise null

name may be left out when creating a site (POST /sites); it defaults to the first domain (cut at 100 characters).

GET /sites/{id}/launch (procedure sites.launch) resolves each of the site's domains when called, which can take seconds:

FieldDescription
addressesThe cluster's edge addresses (values for A / AAAA records): the primary scheduling addresses of its online active nodes, the configured ones of a node that has any, otherwise the public addresses it reports; IPv4 first
domains[]name (as on the site, *.example.com for a wildcard), probe (the name resolved; a wildcard resolves the fixed name edgeweir-check.example.com under it), pointing
domains[].pointingok: every address it resolves to belongs to an active node of the cluster (configured or reported public, backup addresses and offline nodes included); elsewhere: some address does not; unresolved: no A or AAAA record; unknown: the lookup failed (a timeout, for example) or the cluster's nodes have no known address
certificatestate: none (the site has no certificate), covered (the chain covers every domain), uncovered (it misses the domains in uncovered), issuing (an ACME issuance is queued or running), failed (the last issuance failed, error holds its code), expired; plus id, name, uncovered, error
deliveryAs above

Node capabilities

When a change makes the configuration need a capability that an active node of the cluster lacks (nodes report theirs in supportedFeatures, e.g. challenge-v1, modsecurity-v1):

CallerResult
The operator (session or AccessKey)Saved and published; nodes lacking the capability keep their configuration and show "Upgrade required" in Clusters & nodes
Service account409 NODE_CAPABILITY_REQUIRED; data.features lists the missing capabilities (comma separated) and data.nodes the nodes lacking them (the first 5, comma separated, ending in +N when there are more); nothing is saved

Automatic console jobs that publish configurations are held to the same rule as service accounts. Upgrading nodes: Node upgrades.

Bans

ProcedureEndpoint
bans.listGET /bans
bans.createPOST /bans
bans.deleteDELETE /bans/{id}
settings.bans, settings.setBansGET, PUT /settings/bans

Service accounts cannot call these procedures (403 SERVICE_ACCOUNT_FORBIDDEN); read-only AccessKeys call GET only. scope: site covers one site, platform covers every site (shown as "Global" in the UI).

RequestFields
POST /bansscope (site / platform), siteId (required with site, not allowed with platform), cidr (IP address or CIDR), reason (abuse, attack, scanner, spam, other), durationSeconds (60–604800)
GET /bansQuery parameters scope, siteId, source (manual / auto), address (an IP or CIDR: the bans that cover it or lie inside it; 400 BAN_INVALID_CIDR when invalid), page, pageSize (1–100, default 50)
PUT /settings/bansmaxTotal (100–100000, default 10000), shareAutoBans (default true)

Lists answer { items, total } with active bans only (neither expired nor lifted), newest first. Ban fields:

FieldDescription
id, scope, cidrcidr is canonical, e.g. 203.0.113.7/32
reason, sourcesource is manual or auto; automatic bans have the reason cc_ip_rate
siteId, siteNamenull for platform bans
node, triggerThe node { id, name } and trigger { metric, observed, threshold, windowSeconds } of an automatic ban; null for manual bans
createdByWho created a manual ban { type, id, name }
createdAt, expiresAtISO 8601
seqBan change sequence (decimal string)
distributedWhether nodes receive it; false for automatic bans that are not shared
unappliedNodesOnline nodes that report they could not hold the ban
  • When an active manual ban of the same scope, site and address exists, create sets the new reason and expiry and returns the same id (audit ban.update); otherwise the audit entry is ban.create. delete lifts any active ban, manual or automatic, and writes ban.delete.
  • maxTotal caps the active manual bans, site and global bans together; a renewal does not count as a new ban. shareAutoBans decides whether automatic bans reach the other nodes of the cluster. setBans is audited as system.bans_update.
  • A node's ban state: banStatus of GET /nodes/{id} (appliedSequence, entries, capacity, unappliedIds, unapplied, kernelEntries, autoEvicted, reportedAt), null when the node reports none; capabilities are in supportedFeatures (bans-v1, kernel-ban-v1).
Error codeStatusWhen
BAN_INVALID_CIDR400Not an IP address or CIDR
BAN_PREFIX_TOO_SHORT400Prefix shorter than /16 (IPv4) or /48 (IPv6); data.min is the minimum
BAN_EXPIRY_OUT_OF_RANGE400durationSeconds outside 60–604800
BAN_PROTECTED_ADDRESS400Covers a node address, loopback or an unspecified address, or overlaps an "Allow" IP list; data.address is the conflicting address
BAN_PLATFORM_LIMIT409Active manual bans reached maxTotal; data.limit
BAN_NOT_FOUND404The ban does not exist, expired, or was lifted
SITE_NOT_FOUND404No site has the given siteId
curl -fsS -X POST -H "x-api-key: $EDGEWEIR_API_KEY" -H 'content-type: application/json' \
  -d '{"scope":"platform","cidr":"203.0.113.0/24","reason":"attack","durationSeconds":86400}' \
  https://cdn-admin.example.com/api/v1/bans

Behavior: Bans.

Challenges and CC mitigation

ProcedureEndpoint
protection.getGET /sites/{id}/protection
protection.updatePATCH /sites/{id}/protection
security.stateGET /sites/{id}/security
security.eventsGET /sites/{id}/security/events
settings.protection, settings.setProtectionGET, PUT /settings/protection
settings.ccTemplate, settings.setCcTemplateGET, PUT /settings/cc-template

Service accounts cannot call these procedures (403 SERVICE_ACCOUNT_FORBIDDEN); read-only AccessKeys call GET only. Challenge types and CC levels: cookie302, js, pow, captcha (levels also normal).

RequestFields
PATCH /sites/{id}/protectionChanges only the fields given: underAttack, underAttackChallenge, passTtlSeconds (300–86400), powDifficulty (8–24), powHighDifficulty (8–26, at least powDifficulty), logJa4, cc (any of its fields, merged into the saved policy)
ccenabled, followTemplate, maxLevel, highPowInsteadOfCaptcha, windowSeconds (5–60), siteQps, urlQps, ipQps (0–1000000, 0 turns the trigger off), ipBanSeconds (60–86400), originErrorPercent (0–100), originErrorMinRequests, escalateAfterSeconds (1–3600), cooldownSeconds (1–86400)
GET /sites/{id}/securityQuery parameter hours (1–168, default 24)
GET /sites/{id}/security/eventsQuery parameters kind (site_level / path_level / ip_banned), page, pageSize (1–100, default 50)
PUT /settings/protectionunderAttack (global Under Attack), underAttackChallenge, eventRetentionDays (7–365, default 30)
PUT /settings/cc-templateEvery field of cc except enabled and followTemplate

Responses:

ProcedureContent
protection.get, protection.updateThe fields above plus siteId, cc (template thresholds while it follows the template), ccTemplate (the current CC template), effectiveCc (thresholds the nodes use, null while the policy is off), platformUnderAttack (whether global Under Attack is on), updatedAt
security.statenodes: { id, name, online, level, escalatedPaths, reportedAt } for every active node of the cluster; topIps, topPaths: { value, count } from the events of the last hours hours (up to 10 each, approximate); hours
security.events{ items, total }, newest first; event fields id, node ({ id, name }, null once the node is deleted), occurredAt, kind, level, previousLevel, path, address, metric, observed, threshold, topIps, topPaths
  • A change publishes the site's cluster (reason site_protection_updated) and is audited as site.protection_update; a change of global Under Attack publishes every cluster (platform_protection_updated), audited as system.protection_update; a template change publishes clusters with sites that follow it (cc_template_updated), audited as system.cc_template_update. The daily key rotation publishes challenge_keys_rotated.
  • Challenges, Under Attack, and CC need the node capability challenge-v1; logJa4 also needs ja4-v1. When an active node of the cluster lacks one, see Node capabilities.
Error codeStatusWhen
PROTECTION_POW_DIFFICULTY400powHighDifficulty is below powDifficulty; data.min is the lowest allowed value
SITE_NOT_FOUND404The site does not exist
curl -fsS -X PATCH -H "x-api-key: $EDGEWEIR_API_KEY" -H 'content-type: application/json' \
  -d '{"underAttack":true,"underAttackChallenge":"pow","cc":{"enabled":true,"followTemplate":true}}' \
  https://cdn-admin.example.com/api/v1/sites/<site ID>/protection

Behavior: Challenges and CC mitigation.

Compression and OWASP CRS

ProcedureEndpoint
https.get, https.updateGET, PUT /sites/{id}/https
https.checkGET /sites/{id}/https/check
sites.featuresGET /sites/{id}/features
waf.getGET /sites/{id}/waf
waf.updatePATCH /sites/{id}/waf
waf.topRulesGET /sites/{id}/waf/rules

Service accounts cannot call these procedures (403 SERVICE_ACCOUNT_FORBIDDEN); read-only AccessKeys can call GET only.

RequestFields
PUT /sites/{id}/httpssettings replaces all HTTPS settings of the site; missing fields take their defaults, so GET first and change what you need. Compression fields: brotli, brotliLevel (1–11, default 6), brotliMinLength, brotliTypes; zstd, zstdLevel (1–19, default 3), zstdMinLength, zstdTypes; gzip, gzipMinLength, gzipTypes; minimum lengths 1–1048576 (default 256), types are arrays of MIME types (up to 32)
PATCH /sites/{id}/wafChanges only the fields given: mode (off / detect / block), paranoiaLevel (1–4), anomalyThreshold (1–1000), excludedRuleIds (900000–999999, unique, up to 200; initialization and evaluation rules 901xxx, 949xxx, 959xxx, 980xxx return 400 WAF_RULE_NOT_EXCLUDABLE with them in data.ids), requestBodyLimit (0–134217728 bytes)
GET /sites/{id}/waf/rulesQuery parameters range (1h / 6h / 24h / 7d / 30d, default 24h), limit (1–50, default 10)
GET /sites/{id}/https/checkQuery parameter ca (letsencrypt / zerossl, default letsencrypt): the CA whose CAA permission is checked

Responses:

ProcedureContent
sites.featuresbrotli, zstd, crs, each { available, reason }; when an active node of the cluster lacks brotli-v1 / zstd-v1 / modsecurity-v1, available is false and reason is nodes; otherwise reason is null
waf.get, waf.updatesiteId, the fields above (excludedRuleIds ascending), updatedAt (null until first saved, with the defaults off, 1, 5, [], 131072)
waf.topRules{ approximate: true, items: [{ ruleId, requests }] }, most matched first; detection rules only, without 901xxx, 949xxx, 959xxx, 980xxx
https.checkrequest: the request one-click HTTPS sends (name, names, email, challenge, dnsCredentialId); blockers: everything in the way, each a code with parameters: nodes_offline (cluster), nodes_lack_http01 (nodes), dns_not_pointing (name, pointing: unresolved / elsewhere), dns_credential_missing (names), dns_credential_failed (credential, error: an API error code), caa_forbidden (name); certificates: issued, unexpired certificates covering every domain of the site, { id, name }
  • https.update publishes the site's cluster (reason certificate_updated), audited as site.https_update; waf.update publishes (site_waf_updated), audited as site.waf_update.
  • bindSiteId of POST /certificates/request: once issued, the certificate is bound to that site (forceHttps and the other settings unchanged), the site's cluster is published, audited as site.https_update (actor system); names must cover every domain of the site (400 CERTIFICATE_DOMAIN_MISMATCH), and a site has at most one such request at a time (409 CERTIFICATE_BUSY). A certificate's bindSiteId is the site's ID until it is issued, then null.
  • A feature with available false can still be turned on through the API; see Node capabilities.
  • Unknown site: 404 SITE_NOT_FOUND.
curl -fsS -X PATCH -H "x-api-key: $EDGEWEIR_API_KEY" -H 'content-type: application/json' \
  -d '{"mode":"block","paranoiaLevel":1,"excludedRuleIds":[920350]}' \
  https://cdn-admin.example.com/api/v1/sites/<site ID>/waf
curl -fsS -H "x-api-key: $EDGEWEIR_API_KEY" \
  'https://cdn-admin.example.com/api/v1/sites/<site ID>/waf/rules?range=1h'

Behavior: HTTPS and certificates and OWASP CRS managed rules.

Purge, prefetch, origins and error pages

ProcedureEndpoint
cacheTasks.create, cacheTasks.get, cacheTasks.listPOST /cache-tasks, GET /cache-tasks/{id}, GET /cache-tasks
sites.purgeAllPOST /sites/{id}/purge: a whole-site purge, the same as POST /cache-tasks with {"type":"site","siteIds":["<site ID>"]}; returns the task (it no longer publishes a revision or changes cacheGeneration)
sites.updatePATCH /sites/{id} (originSettings, cacheSettings)
sites.originHealthGET /sites/{id}/origin-health
errorPages.getGET /sites/{id}/error-pages
errorPages.updatePUT /sites/{id}/error-pages
settings.errorPages, settings.setErrorPagesGET, PUT /settings/error-pages

Service accounts cannot call these procedures (403 SERVICE_ACCOUNT_FORBIDDEN); read-only AccessKeys can call only GET.

RequestFields
POST /cache-taskstype: url, prefix, site, prefetch, host, tag, sitemap. host: hosts (up to 500 host names, without port or wildcard); tag: siteIds (1–100) and tags (1–500, trimmed and stored in lowercase, each 1–128 bytes of printable ASCII without commas); sitemap: urls with exactly one sitemap URL, maxUrls (1–10000, default 1000); prefetch and sitemap: variants (desktop / mobile, default ["desktop"])
PATCH /sites/{id}originSettings.activeHealthCheck: enabled, path, method (GET / HEAD), expectedStatusMin, expectedStatusMax, host, intervalSeconds (5–300), timeoutSeconds (1–60, not above the interval), healthyThreshold, unhealthyThreshold (1–10); originSettings.sessionAffinity: enabled, ttlSeconds (60–604800); originSettings.protocol (http1 / http2), originSettings.grpc (true only with http2, otherwise 400 ORIGIN_GRPC_REQUIRES_HTTP2); cacheSettings.keepCacheTag. Omitted, these five keep their values; the other fields of originSettings and cacheSettings are still replaced as a whole, so GET first
POST /sites, PATCH /sites/{id}origins[].hostHeader: empty (the request's), or a host name or IP, optionally with a port: IPv6 with a port as [2001:db8::1]:8443, without a port without brackets; at most 259 bytes, no whitespace, quotes, /, or \. Values nodes refuse get 400 ORIGIN_HOST_HEADER_INVALID; saved values still read back
PUT /sites/{id}/error-pagespages: [{ status, template }], status one of 403, 429, 502, 503, 504, at most once each, template 1–65536 bytes (UTF-8); interceptOriginErrors; optional expectedUpdatedAt. Replaces everything. Templates with {{time}} or {{path}} make the configuration need the node capability rules-v3
PUT /settings/error-pagesunknownHost, siteDisabled: templates, an empty string meaning the built-in page, at most 65536 bytes each; with {{time}} or {{path}} the configurations of every cluster need the node capability rules-v3

Responses:

ProcedureContent
cacheTasks.*Tasks add variants ([] for purges) and maxUrls (null except for sitemap tasks); targets holds the hosts, the normalized tags, or the sitemap URL; node results add the error codes sitemap_failed and sitemap_empty
sites.originHealthEach origin's nodes has one entry per node and source, with source (passive / active); downNodes counts online nodes with an unhealthy entry of either source, each node once
sites.featuresAdds activeHealthCheck, sessionAffinity, originHttp2, errorPages, purgeByTag, prefetchVariants
errorPages.get, errorPages.updatesiteId, pages (sorted by status), interceptOriginErrors, updatedAt (null until first saved)
logs.query, logs.exportQuery parameter requestId (exact, up to 128 characters); entries add requestId, the CSV a requestId column
  • errorPages.update publishes the site's cluster (reason site_error_pages_updated) and is audited as site.error_pages_update; settings.setErrorPages publishes every cluster (error_pages_updated) and is audited as system.error_pages_update.
Error codeStatusCase
ORIGIN_HOST_HEADER_INVALID400An origin's hostHeader is not a host name or IP that nodes accept (see above); data.hostHeader
CACHE_TASK_HOST_INVALID400A host has a port or wildcard or is not a valid host name; data.hosts
CACHE_TASK_TAG_INVALID400A tag breaks the rules; data.tags
CACHE_TASK_HOST_UNKNOWN400A host, or the sitemap's host, belongs to no site; data.hosts
NODE_CAPABILITY_REQUIRED409Tasks: an active node of the cluster lacks purge-tag-v1 (hosts, tags) or prefetch-v2 (mobile, sitemaps); data.features, data.nodes
ERROR_PAGE_TOO_LARGE400A template exceeds 65536 bytes; data.status, data.limit (status 404 or 503 for platform templates)
curl -fsS -X POST -H "x-api-key: $EDGEWEIR_API_KEY" -H 'content-type: application/json' \
  -d '{"type":"tag","siteIds":["<site ID>"],"tags":["product-42"]}' \
  https://cdn-admin.example.com/api/v1/cache-tasks
curl -fsS -X POST -H "x-api-key: $EDGEWEIR_API_KEY" -H 'content-type: application/json' \
  -d '{"type":"sitemap","urls":["https://www.example.com/sitemap.xml"],"maxUrls":2000,"variants":["desktop","mobile"]}' \
  https://cdn-admin.example.com/api/v1/cache-tasks

Behavior: Origins and cache and Error pages.

Cache zone, PURGE method, content settings and maintenance

ProcedureEndpoint
clusters.setCachePUT /clusters/{id}/cache
nodes.setCachePUT /nodes/{id}/cache
maintenance.get, maintenance.updateGET, PUT /sites/{id}/maintenance

Service accounts cannot call these procedures (403 SERVICE_ACCOUNT_FORBIDDEN); read-only AccessKeys can call only GET.

RequestFields
PUT /clusters/{id}/cachemaxSizeGb (1–65536), inactiveDays (1–90); publishes the cluster (reason cluster_cache_updated), audited as cluster.cache_update
PUT /nodes/{id}/cachemaxSizeGb (1–65536, null follows the cluster); publishes the node's cluster (node_cache_updated), audited as node.cache_update
PUT /sites/{id}/maintenanceenabled, template (0–65536 bytes, empty for the built-in maintenance page), retryAfterSeconds (0–86400), allowedCidrs (up to 64, normalized like IP list entries: IPv4-mapped prefixes are saved as IPv4, mapped prefixes under /96 are refused), allowedPathPrefixes (starting with /, without ?, #, or control characters, at most 1024 bytes of UTF-8 each, up to 32), optional expectedUpdatedAt (409 UPDATED_AT_MISMATCH when it differs); publishes (site_maintenance_updated), audited as site.maintenance_update
POST /sites, PATCH /sites/{id}originSettings.tries (1–5, default 3), originSettings.statusRetry (default true); cacheSettings.cacheKey.query adds exclude, with queryParams the parameters left out, a name may end in *; cacheSettings.xCache (default true); cacheSettings.purgeMethod: { enabled, key? }, key 16–256 printable characters, write-only, omitted keeps the saved key, enabling without a key gets 400 PURGE_KEY_REQUIRED; cacheRules[].cacheSetCookie (default false); contentSettings: charset ({ name, force, uppercase }, name one of off, utf-8, gbk, gb18030, gb2312, big5, iso-8859-1, shift_jis, euc-kr), requestBodyLimit (bytes, 0–10737418240, default 104857600, 0 for no limit). PATCH keeps xCache, purgeMethod, originSettings.tries and originSettings.statusRetry when omitted
PUT /sites/{id}/httpsAdds gzipLevel (0–9, 0 for the node default) and compressMaxLength (bytes, 0 for no limit)
PUT /sites/{id}/error-pagesstatus adds 400, 401, 404, 405, 410, 500, "4xx", and "5xx"; pages add redirectUrl (instead of template, see Redirect pages) and responseStatus (200–599, 0 keeps the status; only 0 for redirect pages)
PUT /sites/{id}/rulesConfiguration actions add requestBodyLimit (bytes, 0–10737418240)

Responses:

ProcedureContent
clusters.list, clusters.getAdd cache: { maxSizeGb, inactiveDays }
nodes.list, nodes.getAdd cache: { maxSizeGb, usage }: maxSizeGb is the node's own size (null follows the cluster), usage the last report { usedBytes, maxBytes, measuredAt } (null until reported)
sites.getcacheSettings.purgeMethod is { enabled, keySet }, never the key; adds contentSettings
sites.featuresAdds siteContent
maintenance.get, maintenance.updatesiteId, the fields above, updatedAt (null until first saved)
cacheTasks.*source adds purge_method (created by a PURGE request; createdByName is the node's name)

Configurations that use the new fields need the node capability site-content-v1 (a node's own cache size needs cache-zone-v1), see Node capabilities. Behavior: Origins and cache and Error pages.

Rules and bulk redirects

ProcedureEndpoint
rules.get, rules.saveGET, PUT /sites/{id}/rules
platformRules.get, platformRules.save (global rules)GET, PUT /platform-rules
rules.validatePOST /rules/validate
rules.topLogged (log rule matches)GET /sites/{id}/rules/logged
bulkRedirects.getGET /sites/{id}/bulk-redirects
bulkRedirects.savePUT /sites/{id}/bulk-redirects

Service accounts cannot call these procedures (403 SERVICE_ACCOUNT_FORBIDDEN); read-only AccessKeys can call only GET and POST /rules/validate.

RequestFields
PUT /sites/{id}/rules, PUT /platform-rulesrules: replaces everything, up to 64 per site and 32 for the platform; each with id (optional; an id that is not one of this site's or the platform's rules gets a new one, so rules read elsewhere can be saved as they are), name (1–100 characters), phase, expression (up to 4096 characters), enabled (default false: a rule sent without it is saved disabled), action. phase: request-transform, redirect, config, waf-custom, ratelimit, cache, origin, response-transform, compression
action (kind: "redirect")Exactly one of value (static target) and target (value expression); statusCode (301, 302, 303, 307, 308, default 301); preserveQuery (default false); setQuery ([{ name, value, expression }], up to 16, unique names); removeQuery (parameter names, up to 16, none also in setQuery). Names [A-Za-z0-9._~-]{1,64}, value printable ASCII up to 256 characters; with an expression (a value expression, default "") the value is empty, and nodes percent-encode what it computes per request
action (kind: "rewrite")As redirect without statusCode; preserveQuery defaults to true
action (kind: "request_header", "response_header")header (1–64 token characters, lowercased, not a protected header), value (static value, up to 4096 characters without control characters), expression (a value expression, default ""; with one the value is empty), remove (default false; with it value and expression are empty); response_header also has append (default false: adds a line next to the response's lines of the same header; not together with remove). Nodes skip the action when the expression's value exceeds 4096 bytes or has a control character
action (kind: "config")At least one field. cacheBypass, forceHttps, gzip (booleans); in the config phase only: brotli, zstd, websocket, underAttack, ccEnabled (booleans), ccMaxLevel (cookie302, js, pow, captcha), originConnectTimeoutMs (100–120000), originSendTimeoutMs, originReadTimeoutMs (100–3600000), logSampleRate (0–10000, in 1/10,000). Omitted fields override nothing
action (kind: "origin")origin phase. originGroup (an origin group of the site, empty for the default group; always empty in global rules), hostHeader (as an origin's hostHeader, empty overrides nothing), sni (a host name, empty overrides nothing), port (0–65535, 0 overrides nothing); at least one change
action (kind: "compression")compression phase. algorithms: unique entries of zstd, br, gzip in preference order; [] turns compression off
POST /sites, PATCH /sites/{id}cacheRules[] adds expression (a condition of the cache phase, up to 16384 characters; when empty, pathPrefixes, paths, and extensions build it; when set, those are empty or equal its builder form) and browserTtlSeconds (0–31536000, 0 keeps the origin's Cache-Control); origins[] adds group ([a-z0-9_-]{0,32}, empty for the default group, at least one origin in the default group)
PUT /sites/{id}/bulk-redirectsredirects: replaces everything, up to 5000, unique source; each with source (/path or host/path, 2–512 bytes without whitespace, ?, or control characters, lowercase host), target (static redirect target, up to 1024 bytes), statusCode (default 301), preserveQuery (default false)
POST /rules/validateexpression (up to 16384 characters), phase, kind: condition (default, a rule condition), value (a value expression of phase: a redirect target, rewrite path, header value, or query parameter value), cacheRule (a cache rule condition; phase is ignored)
GET /sites/{id}/rules/loggedrange (1h, 6h, 24h (default), 7d, 30d), limit (1–50, default 10)

Responses:

ProcedureContent
rules.get, rules.save, platformRules.*The rules in saved order, with id
rules.topLogged{ approximate: true, items: [{ ruleId, name, platform, requests }], unsupportedNodes }: requests that matched the site's Log rules and global Log rules, most first; name is the rule's current name, null once it is deleted; platform marks global rules; unsupportedNodes counts the active nodes of the site's cluster that do not report matches (no node capability rule-log-v1)
bulkRedirects.*[{ source, target, statusCode, preserveQuery }] in saved order
sites.get; site of sites.create and sites.updatecacheRules[] always carry expression ("true" matches every request); pathPrefixes, paths, and extensions hold its structured form when the condition has the builder's shape and are empty otherwise; browserTtlSeconds is added. origins[] carry group
rules.validate{ valid, position, message, code?, params? }; when invalid, position is the character where it fails (from 0), message the reason in English, code a stable reason code (such as unknown_field, ordered_comparison, expected_token) and params the values the reason names (such as token of expected_token)
sites.featuresAdds rulesV2 and rulesV3; reason nodes means an active node of the cluster lacks rules-v2 or rules-v3
  • rules.save publishes the site's cluster (reason rules_updated) and is audited as site.rules_update; platformRules.save publishes every cluster and is audited as platform.rules_update; bulkRedirects.save publishes the site's cluster (rules_updated) and is audited as site.bulk_redirects_update (with the entry count).
  • Configurations that use functions, the new fields, value expressions, query parameter edits, origin or compression actions, the new config phase fields, gzip: true, cache rule conditions not in the builder's shape, browserTtlSeconds, bulk redirects, or origin groups other than the default need the node capability rules-v2.
  • Configurations that use the rules-v3 fields (http.request.cookies[…], http.request.uri.args[…], http.referer, http.user_agent, http.request.version, http.request.scheme, http.request.id, http.request.timestamp.sec, edge.server_port, ip.geoip.as_name, http.response.cache_status), functions (url_encode, base64_encode, base64_decode, md5, sha1, sha256, substring, to_string), wildcard / strict wildcard, an expression of a header or query parameter, append, statusCode: 303, or error page templates with {{time}} or {{path}} need the node capability rules-v3. Bulk redirects keep the statuses 301, 302, 307, and 308.
  • The code of rules.validate adds cookie_name, argument_name, and integer_argument (params min and max).
Error codeStatusCase
ORIGIN_HOST_HEADER_INVALID400The hostHeader of an origin action is not a host name or IP that nodes accept; data.hostHeader
RULE_INVALID400An origin action picks an origin group the site does not have, or a global rule picks one; sites.update removes an origin group a rule still picks; a saved rule or cache rule condition no longer compiles
BULK_REDIRECT_HOST_UNKNOWN400The host of a host/path source is not a domain of the site (one label under a wildcard domain of the site is fine); data.hosts (comma-separated, up to 5)
IP_LIST_REFERENCE_UNKNOWN404A rule or cache rule condition references IP lists that do not exist; data.lists names them (the first 5)
NODE_CAPABILITY_REQUIRED409An active node of the cluster lacks rules-v2 or rules-v3 (changes by service accounts and background jobs); data.features, data.nodes
SITE_NOT_FOUND404The site does not exist or is outside the caller's scope
curl -fsS -X PUT -H "x-api-key: $EDGEWEIR_API_KEY" -H 'content-type: application/json' \
  -d '{"redirects":[{"source":"/old","target":"/new","statusCode":301,"preserveQuery":true}]}' \
  https://cdn-admin.example.com/api/v1/sites/<site ID>/bulk-redirects
curl -fsS -X POST -H "x-api-key: $EDGEWEIR_API_KEY" -H 'content-type: application/json' \
  -d '{"expression":"regex_replace(http.request.uri.path, \"^/old/\", \"/new/\")","phase":"redirect","kind":"value"}' \
  https://cdn-admin.example.com/api/v1/rules/validate

Behavior: Rules, IP lists, and GeoIP and Origins and cache.

Usage

One record per site and UTC 5-minute window [windowStart, windowEnd), summing the minute statistics every node reported for the window.

ProcedureEndpointQuery parameters
usage.listGET /usagefrom, to (5-minute aligned, UTC, half-open), siteId, cursor, limit (1–5000, default 1000)
usage.changesGET /usage/changesafterSeq (default "0"), limit (1–5000, default 1000)

Record fields:

FieldNotes
id<siteId>.<Unix seconds of windowStart>; always the same for a site and window
siteIdRecords remain after the site is deleted
windowStart, windowEndISO 8601
requests, bytesSent, bytesReceivedDecimal integer strings (bytes out and in), exact beyond 2^53
revisionStarts at 1; +1 when a recomputation changes a value
seqGlobally increasing (decimal string, gaps allowed); assigned on creation and revision
updatedAtLast write
  • usage.list is ordered by (window, site) and returns { items, nextCursor, completeUntil }; nextCursor null means the last page. Unaligned from / to, or to not after from: 400 USAGE_RANGE_INVALID; an invalid cursor: 400 USAGE_CURSOR_INVALID.
  • usage.changes returns records created or revised after afterSeq, in seq order, as { items, lastSeq, completeUntil }; pass lastSeq as the next afterSeq. Revised records appear again.
  • Windows without traffic have no record.
  • Closed windows are recomputed every minute; late data that changes a value increments revision and assigns a new seq; otherwise neither changes. A statistics batch reported twice does not change the result.
  • Kept 100 days by default, adjustable in System settings → Usage (35–400 days).

completeUntil (ISO 8601 or null): windows that end at or before it contain the data of every node that was active then.

RuleNotes
Node watermarkOnce every statistics batch is acknowledged, a node reports complete_until: the start of the minute of its last successful statistics drain; every earlier minute has been uploaded. While the console is unreachable the node keeps draining statistics into its local spool, and before it stops it saves them, the current minute included; after the spool limit made it drop statistics, the watermark stays at the first dropped minute for 24 hours
Nodes taken into accountEnabled nodes with a heartbeat within the offline threshold: 60 minutes by default, adjustable in System settings → Usage (5–1440 minutes)
ComputationThe lowest watermark of those nodes; a node that never reported one (older node versions) counts from its enrollment; a node more than the offline threshold behind counts as now minus the threshold (like an offline node, what it sends later is a revision); windows still waiting to be recomputed hold it back; rounded down to 5 minutes
MonotonicIt only moves forward. Data a node sends after being offline longer than the threshold is a revision (revision + 1)
Disabled or deleted nodesNot taken into account

Clusters and overview

ProcedureEndpointNotes
clusters.list, clusters.getGET /clusters, GET /clusters/{id}Clusters and their summary
clusters.rolloutGET /clusters/{id}/rolloutConfiguration canary: policy, current rollout, and canary nodes
clusters.rollbackPreviewGET /clusters/{id}/rollback-previewWhat a rollback to the query parameter revision would publish; refuses like the rollback, writes nothing
overview.getGET /overviewOverview: cluster, node, and site counts, recent revisions, what needs attention
settings.nodeChannelCheckGET /settings/node-channel-checkThe console's TLS handshake with its own node channel URL

Service accounts with clusters:read call clusters.list and clusters.get; they cannot call the others (403 SERVICE_ACCOUNT_FORBIDDEN). All are GET, so read-only AccessKeys call them.

ProcedureResponse fields
clusters.list, clusters.getliveNodeCount: online enabled nodes; appliedNodeCount: those of them that run their target revision (while a canary runs, the canary nodes' target is the candidate and the other nodes' the stable revision)
clusters.rolloutcandidateChanges: while a rollout runs, { sites: { added, changed, removed }, reasons }; sites holds the sites the candidate adds, changes, and removes against the stable revision ([{ id, name }]), reasons the revisions published after the stable one (elements as in GET /clusters/{id}/revisions, without repeated reasons); null without a running rollout
clusters.rollbackPreview{ revision, currentRevision, unchanged, sites: { added, changed, removed } }: currentRevision is the cluster's latest revision (null without one); unchanged is true when the content equals the latest revision; sites are the changes against it
overview.getattention: [{ kind, clusterId, clusterName, revision, at, count, version }] in the order of the kind list below, [] when nothing needs attention. kind: nodes_unhealthy, dns_failed, dns_blocked, upgrade_failed, canary_rolled_back, canary_awaiting_promotion, canary_running, nodes_lagging, nodes_no_address. revision: the DNS revision or the candidate; at: the window end of canary_running, the rollback time of canary_rolled_back; count: the number of nodes; version: the target version of upgrade_failed. Fields that do not apply are null, 0, or empty
settings.nodeChannelCheck{ url, result, checkedAt }: url is the node channel URL in effect; result is ok (the chain includes the node channel CA), unreachable (no handshake within 3 seconds), mismatch (another chain answered, or the URL is not https), or refused (a URL saved in system settings resolves to a special-purpose address the outbound policy does not allow; no connection is made). A result is reused for 30 seconds; advisory only
Error codeStatusWhen
ROLLBACK_RESOURCE_UNAVAILABLE409rollbackPreview and rollback: a site, domain, certificate, or IP list the chosen revision references was deleted or is unavailable, or the certificate has expired
REVISION_NOT_FOUND404The cluster has no such revision
CLUSTER_NOT_FOUND404The cluster does not exist

Behavior: Clusters and system and Adding nodes.

Node channel URL

ProcedureEndpointNotes
settings.nodeChannelGET /settings/node-channel{ url, effectiveUrl, source }: url is the URL saved in system settings (an empty string when none is); effectiveUrl the URL install commands carry; source is setting, environment (EDGEWEIR_NODE_API_URL), or default
settings.setNodeChannelPUT /settings/node-channelBody { url }: https://host[:port] without a path, query, fragment, or credentials, otherwise 400; saved as its origin. An empty string clears the saved URL. Responds like settings.nodeChannel. Applies at once, the node channel certificate adds the new URL's name; audited as system.node_channel_update

Service accounts cannot call them (403 SERVICE_ACCOUNT_FORBIDDEN); read-only AccessKeys call settings.nodeChannel only. Enrolled nodes keep the URL they enrolled with; see Node channel URL and certificate.

Node upgrades

ProcedureEndpointNotes
upgrades.releaseGET /node-releases/{version}The version's release files per architecture in the release source
upgrades.latestVersionGET /node-upgrades/latest-version{ version }: the latest version of the release source, null when it cannot be told; cached for 10 minutes
upgrades.listGET /node-upgradesUpgrades; query parameter clusterId
upgrades.createPOST /node-upgrades{ version, nodeGroupId }: a leading v of version is dropped, nodeGroupId is the canary node group
upgrades.promotePOST /node-upgrades/{id}/promotePromotes the remaining nodes
upgrades.cancelPOST /node-upgrades/{id}/cancelCancels node tasks that are "Waiting for canary" or "Pending"

Service accounts cannot call these procedures (403 SERVICE_ACCOUNT_FORBIDDEN); read-only AccessKeys call GET only.

Error codeStatusWhen
UPGRADE_RELEASE_UNAVAILABLE502The release source has no readable manifest for the version, or the manifest lists no supported archive
UPGRADE_NODES_UNAVAILABLE409Active nodes of the cluster do not meet the upgrade requirements; data.nodes lists them (at most 10, the rest as "+N")
UPGRADE_CANARY_EMPTY409The chosen node group has no active node
UPGRADE_TOO_MANY_NODES409The cluster has more active nodes than data.limit (1000)
UPGRADE_BUSY409Nodes already have an unfinished upgrade, or are upgrading during cancellation; data.nodes lists them
UPGRADE_FINISHED409Cancelling an upgrade that has ended
UPGRADE_NOT_READY409The canary group does not meet the promotion condition yet
UPGRADE_NOT_FOUND404The upgrade does not exist

Requirements, states, and node-side verification: Node upgrades.

Regional probes, scheduling addresses, and scheduling

ProcedureEndpointNotes
probes.listGET /probesEvery probe
probes.createTokenPOST /probe-tokensA one-time probe enrollment token
probes.updatePATCH /probes/{id}Renames, disables, or enables a probe
probes.deleteDELETE /probes/{id}Deletes a probe and revokes its certificate
probes.resultsGET /probe-resultsLatest probe results
settings.probes, settings.setProbesGET, PUT /settings/probesProbe settings
nodes.setAddressesPUT /nodes/{id}/addressesA node's scheduling addresses and levels
nodes.setProbePUT /nodes/{id}/probeLets a node also probe
scheduling.listGET /scheduling/rulesScheduling rules; query parameter clusterId (optional)
scheduling.createPOST /scheduling/rulesCreates a rule, returns 201
scheduling.updatePATCH /scheduling/rules/{id}Changes only the given fields
scheduling.deleteDELETE /scheduling/rules/{id}Deletes a rule; its actions in effect end
scheduling.previewGET /clusters/{clusterId}/scheduling/previewEvery rule under the current metrics; writes nothing

Service accounts cannot call these procedures (403 SERVICE_ACCOUNT_FORBIDDEN); read-only AccessKeys call GET only. POST /probe-tokens refuses an Idempotency-Key (see Idempotency keys).

RequestFields
POST /probe-tokensname (1–64 characters), regionId, ttlMinutes (5–10080, default 60)
PATCH /probes/{id}name (1–64 characters), enabled, both optional. Disabling deletes the probe's results
GET /probe-resultsQuery parameters probeId (a probe ID, or the ID of a node that also probes) and nodeId (the measured node), both optional
PUT /settings/probesReplaces the settings: intervalSeconds (5–60), timeoutMs (500–10000, at most intervalSeconds × 1000), attempts (1–10), lossPercent (1–100), ipDownSeconds, ipUpSeconds (5–3600)
PUT /nodes/{id}/addressesaddresses: replaces the list, at most 8 { address, level }; address is a single unicast IP (private allowed), no duplicates; level is 0 (primary), 1 (backup 1), or 2 (backup 2), and a non-empty list needs a level 0; [] goes back to the reported addresses. Publishes the cluster's DNS revision (reason manual)
PUT /nodes/{id}/probeenabled; turning it off deletes the node's probe results
POST /scheduling/rulesclusterId, lineName (a line name of the cluster's DNS binding, null for every line, default null; required by backup_group), name (1–100 characters), enabled (default true), match (all by default / any), conditions, action (remove_node, backup_group, backup_ip), holdSeconds, recoverSeconds (0–86400, default 300)
conditions[]1–8 of: metric (cpu_percent, load1, memory_percent, egress_mbps, connections, probe_loss_percent, probe_latency_ms), aggregate (avg by default / max / min), comparator (gt, ge, lt, le), threshold (0–10¹²), durationSeconds (0–3600, default 0), regionId (only for probe_loss_percent and probe_latency_ms, default null)
PATCH /scheduling/rules/{id}The fields of POST except clusterId, all optional. enabled: false, or a different lineName, match, action, or conditions, ends the actions in effect first and the states start over; changing only name, holdSeconds, or recoverSeconds does not. lineName is checked only when lineName or action changes
ProcedureResponse
probes.list, probes.updateProbe: id, name, regionId, regionName, regionCode, enabled, online, lastSeenAt, enrolledAt, hostname, agentVersion, os, arch, certNotAfter, targets, lastRound ({ checkedAt, results, failed, lossPercent, avgRttMs }, null without results), createdAt
probes.createTokentokenId, token (ewp_…, returned once), expiresAt, serverUrl (node channel), caSha256, command (the docker run start command)
probes.delete, scheduling.delete{ ok: true }
probes.resultsSorted by node, address, port, and prober, at most 5000: proberKind (probe / node), proberId, proberName, regionId, regionName, nodeId, nodeName, address, port, method (tcp / http / https), sent, lost, lossPercent, rttMs (median of successful attempts, 0 when all were lost), error (timeout, refused, reset, tls, status, unreachable), checkedAt
settings.probes, settings.setProbesThe probe settings; the defaults 10, 3000, 3, 50, 30, 60 until saved
Nodes returned by nodes.*Add probeEnabled; metrics ({ cpuPercent, load1, load5, load15, memoryUsedBytes, memoryTotalBytes, egressBps, activeConnections, reportedAt }, null when the latest heartbeat carried none, as from nodes without metrics-v1); schedulingAddresses ([{ address, level, source, reachable }], source is reported or configured); schedulingLevel (the level DNS uses now); remoteAddress (source address of the node's enrollment and latest heartbeat connection; a proxy's when it connects through one); dnsIssue (no_public_address without scheduling addresses, else null); authError (why the node channel refused the node's own certificate since its last heartbeat, e.g. CERT_HAS_EXPIRED, else null)
scheduling.list, scheduling.create, scheduling.updateRule: id, clusterId, the request fields (conditions with defaults filled in), activeNodes ([{ nodeId, nodeName, since }], nodes in active or recovering), createdAt, updatedAt
scheduling.preview{ clusterId, evaluatedAt, rules }. Per rule: ruleId, ruleName, enabled, lineName, match, action, nodes. Per node: nodeId, nodeName, state (idle, pending, active, recovering), conditions (the condition fields plus value (null without data), holds, heldSeconds, satisfied), matches, inEffect, wouldActivate, wouldRecover, activeSince, recoveringSince, recoversAt

New fields of DNS bindings and records:

Request or responseFields
binding.lines[] of PUT /clusters/{clusterId}/dnsAdd resolutionLine (default by default, telecom, unicom, mobile, edu, overseas), backupNodeGroupIds (node groups of this cluster, at most 4, no duplicates, not the line's own group, default []), minHealthyIps (1–64, default 1). GET returns the defaults for lines saved before
records[] of GET /clusters/{clusterId}/dns and GET /clusters/{clusterId}/dns/exportAdd line: the record's resolution line; absent on default-line records
DNS revisions (revision, blocked, GET /clusters/{clusterId}/dns/revisions, …)reason is manual, health, rollback, force, or scheduling; add reasonParams: for scheduling, ruleId, rule, nodeId, node, action, and event (activated / recovered); {} for the other reasons
GET /dns/catalogcapabilities.lines changes from a boolean to the array of resolution lines the provider supports
Error codeStatusWhen
DNS_LINE_UNSUPPORTED400A binding line's resolutionLine is not among the lines of the account's provider; data.line
REGION_IN_USE409Deleting a region that still has probes (regions.delete); data.probes is the probe count
PROBE_NOT_FOUND404The probe does not exist; probeId of probes.results is neither a probe nor a node
NODE_REGION_REQUIRED409nodes.setProbe turns probing on while the node's node group has no region
NODE_ADDRESS_INVALID400A scheduling address is not a single unicast IP (a CIDR, a host name, loopback, link-local, multicast, …) or is a duplicate; data.address
SCHEDULING_RULE_NOT_FOUND404The rule does not exist
SCHEDULING_RULE_INVALID400backup_group without lineName, or a lineName missing from the cluster's DNS binding
REGION_NOT_FOUND404The regionId of probes.createToken or of a condition does not exist
NODE_NOT_FOUND404The node does not exist, including nodeId of probes.results
CLUSTER_NOT_FOUND404The cluster of a rule or preview does not exist
BAD_REQUEST400Input validation, for example timeoutMs longer than the interval, scheduling addresses without a level 0, or regionId on a node metric

Behavior: Regional probes and scheduling and DNS steering and alerts.

Listener ports and client IP

ProcedureEndpointNotes
clusters.listenPortsGET /clusters/{clusterId}/listen-portsThe cluster's extra HTTP / HTTPS ports and nodes without edge-ports-v1
clusters.setListenPortsPUT /clusters/{clusterId}/listen-portsReplaces the extra ports and publishes a revision
clusters.clientIpGET /clusters/{clusterId}/client-ipThe client IP setting and nodes without client-ip-v1
clusters.setClientIpPUT /clusters/{clusterId}/client-ipReplaces the client IP setting and publishes a revision

Service accounts cannot call these procedures (403 SERVICE_ACCOUNT_FORBIDDEN); read-only AccessKeys only the GET ones.

RequestFields
PUT /clusters/{clusterId}/listen-portshttpPorts, httpsPorts: at most 16 ports 1–65535 each, without 80 and 443, deduplicated and sorted; a port in both is refused (LISTEN_PORT_CONFLICT), as is one inside a port pool (LISTEN_PORT_IN_POOL); removing a port sites use returns LISTEN_PORT_IN_USE
PUT /clusters/{clusterId}/client-ipsettings: mode (direct / proxy_protocol / header, default direct); the header mode requires trustedCidrs (1–64 IPs or CIDRs, normalized, deduplicated and sorted; no IPv4-mapped IPv6) and header (a lowercase header name, [a-z0-9-], 1–64 characters, never a hop-by-hop header, Host, Cookie, Authorization, X-Request-Id or X-Edgeweir-*); direct takes an optional dropForwardedFor

A site's ports change with ports ({ http, https }) in PATCH /sites/{id}, and POST /sites takes the same optional field (omitted: 80 and 443); sites carry ports. Error codes: SITE_PORT_UNAVAILABLE, SITE_PORTS_EMPTY, SITE_HTTPS_PORT_NEEDS_CERTIFICATE. The settings of PUT /sites/{id}/https add redirectStatus (301, 302, 303, 307, 308, default 301), redirectPort (443 or an HTTPS port of the site, default 443, HTTPS_REDIRECT_PORT_INVALID) and redirectExcludedDomains (domains of the site, at most 50, HTTPS_REDIRECT_DOMAIN_INVALID). GET /sites/{id}/features adds edgePorts and clientIp; clusters of GET /clusters add clientIpMode.

Port pools and L4 apps

ProcedureEndpointNotes
clusters.portPoolsGET /clusters/{clusterId}/port-poolsThe cluster's port pools, reserved ports, and nodes without l4-v1 or l4-v2
clusters.setPortPoolsPUT /clusters/{clusterId}/port-poolsReplaces the port pools; publishes no configuration revision
l4Apps.listGET /l4-appsL4 apps sorted by port and protocol; query parameter clusterId (optional)
l4Apps.getGET /l4-apps/{id}One app
l4Apps.createPOST /l4-appsCreates an app, returns 201
l4Apps.updatePATCH /l4-apps/{id}Changes only the given fields
l4Apps.setEnabledPUT /l4-apps/{id}/enabledDisables or enables an app
l4Apps.deleteDELETE /l4-apps/{id}Deletes an app with its origins and statistics
l4Apps.statsGET /l4-apps/{id}/statsPer-minute statistics

Service accounts cannot call these procedures (403 SERVICE_ACCOUNT_FORBIDDEN); read-only AccessKeys call GET only.

RequestFields
PUT /clusters/{clusterId}/port-poolspools: replaces the list, at most 64 { protocol, from, to }; protocol is tcp, udp, or both, from and to are 1024–65535, from not above to
POST /l4-appsclusterId, name (1–100 characters, trimmed), protocol (tcp / udp), port (1024–65535), origins; optional: portEnd (last port of a range, above port, at most 1000 ports, default null), originPortMode (fixed / same, default fixed), certificateId (TCP only, the node terminates TLS, default null), tlsMinimumVersion (1.2 / 1.3, default 1.2), enabled (default true), acceptProxyProtocol (default false), proxyProtocolVersion (0–2, 0 sends none, default 0), maxFails (1–100, default 3), failTimeoutSeconds (1–3600, default 30), connectTimeoutMs (100–60000, default 5000), idleTimeoutSeconds (1–86400; omitted: 600 for TCP, 30 for UDP), allowListIds, blockListIds (IP list IDs, at most 16 each, deduplicated, default []), maxConnections (0–10000000), newConnectionsPerSecond (0–1000000); 0 means no limit for the last two, default 0
origins[]1–32 { address, port, weight, backup }: address is a host name or IP under the rules for site origins; port 1–65535, omitted with originPortMode same (stored as 0); weight 1–100, default 1; backup default false. At least one origin has backup false
PATCH /l4-apps/{id}The fields of POST except clusterId and enabled, all optional; origins replaces the list, and origins whose address and port stay keep their ID (and with it the nodes' passive health state); changing protocol leaves idleTimeoutSeconds as it is; optional expectedUpdatedAt
PUT /l4-apps/{id}/enabledenabled; optional expectedUpdatedAt
GET /l4-apps/{id}/statsQuery parameters from and to (ISO 8601), from before to, at most 7 days
ProcedureResponse
clusters.portPools, clusters.setPortPoolsclusterId; pools (sorted by first port, then protocol); reservedPorts (the ports of the cluster's HTTP / HTTPS listeners, extra ports included, never part of a pool); nodesWithoutL4 ([{ id, name }], active nodes of the cluster that do not report l4-v1); nodesWithoutL4V2 (active nodes that do not report l4-v2)
The app returned by l4Apps.list, l4Apps.get, and the other proceduresid, clusterId, clusterName, name, protocol, port, enabled, acceptProxyProtocol, proxyProtocolVersion, portEnd, originPortMode, certificateId, certificateName, tlsMinimumVersion, origins ([{ id, address, port, weight, backup }], in the saved order), maxFails, failTimeoutSeconds, connectTimeoutMs, idleTimeoutSeconds, allowListIds, blockListIds, maxConnections, newConnectionsPerSecond, dnsTarget, dnsLines, createdAt, updatedAt
dnsTargetThe CNAME clients connect to, <app ID>.<cluster domain>, published only while the app is enabled; null while the cluster's DNS is Not managed
dnsLinesPer binding line { name, target }: target is <line>.<app ID>.<cluster domain> with line aliases, else <line>.<cluster domain>
l4Apps.create, l4Apps.update, l4Apps.setEnabled{ app, revision }
l4Apps.delete{ revision }
l4Apps.statsappId, from, to; bucketSeconds: 60 for ranges up to a day, 300 up to five days, else 3600; points: one per bucket from the bucket of from, oldest first, empty buckets zero; totals; nodes: every reporting node { nodeId, nodeName, … }, busiest first
Statistics countersconnections (connections or UDP sessions accepted), refused (refused by the IP lists or limits), peakConcurrent, bytesReceived (from clients), bytesSent (to clients). In points and totals, peakConcurrent is the highest per-minute sum of the nodes' peaks in the bucket or range; in nodes it is the node's own highest value
  • create and update publish the cluster's configuration revision (reasons l4_app_created, l4_app_updated) and audit l4_app.create, l4_app.update; setEnabled publishes when the state changes (l4_app_updated) and audits l4_app.enable or l4_app.disable, and returns the latest revision without publishing or auditing when it does not; delete publishes (l4_app_deleted) and audits l4_app.delete. setPortPools audits cluster.port_pools_update.
  • A configuration with an enabled app needs the node capability l4-v1, see Node capabilities.
Error codeStatusWhen
L4_APP_NOT_FOUND404The app does not exist
L4_APP_LIMIT409The cluster has 256 apps (disabled ones included); data.limit
L4_PORT_OUTSIDE_POOL400The port is outside the cluster's port pools for the protocol; data.port
L4_PORT_IN_USE409Another app of the cluster (disabled ones included) uses the port and protocol, or the new pools would leave an app's port outside; data.apps (name (port/protocol), comma-separated)
L4_PORT_RESERVED400A pool or an app port is a port of the cluster's HTTP / HTTPS listeners; data.port
L4_PORT_POOL_OVERLAP400Pools of one protocol overlap, and both overlaps tcp and udp; data.pools (from-to/protocol, comma-separated)
L4_PROXY_PROTOCOL_UNSUPPORTED400A UDP app with acceptProxyProtocol or a non-zero proxyProtocolVersion
IP_LIST_NOT_FOUND404A list of allowListIds or blockListIds does not exist
IP_LIST_IN_USE409DELETE /ip-lists/{id} on a list a rule, a cache rule condition, or an L4 app still references; data.users names the first 5: rule names (site rules with the site, such as block (shop)), sites whose cache rules use it, and L4 app names
ORIGIN_ADDRESS_FORBIDDEN400An origin is a special-purpose address outside the origin allow list; data.address, data.range
UPDATED_AT_MISMATCH409expectedUpdatedAt is not the current value
CLUSTER_NOT_FOUND404The cluster does not exist
BAD_REQUEST400Input validation, for example a port below 1024, from above to, only backup origins, or a statistics range over 7 days
curl -fsS -X PUT -H "x-api-key: $EDGEWEIR_API_KEY" -H 'content-type: application/json' \
  -d '{"pools":[{"protocol":"both","from":20000,"to":20100}]}' \
  https://cdn-admin.example.com/api/v1/clusters/<cluster ID>/port-pools
curl -fsS -X POST -H "x-api-key: $EDGEWEIR_API_KEY" -H 'content-type: application/json' \
  -d '{"clusterId":"<cluster ID>","name":"game","protocol":"tcp","port":20000,"origins":[{"address":"game-origin.example.com","port":7000}],"proxyProtocolVersion":2}' \
  https://cdn-admin.example.com/api/v1/l4-apps

Behavior: Layer-4 forwarding.

Example

List sites (procedure sites.list, GET /api/v1/sites):

curl -fsS -H "x-api-key: $EDGEWEIR_API_KEY" \
  "https://cdn-admin.example.com/api/v1/sites?page=1&pageSize=20"
Query parameterDescription
searchMatches the site name or any of its domains; up to 100 characters
clusterIdCluster UUID
pagePage number, default 1
pageSizeItems per page, 1–100, default 20

Response: {"items":[…],"total":<count>}.

Error format

{
  "defined": false,
  "code": "ACCESS_KEY_READ_ONLY",
  "status": 403,
  "message": "access key is read only",
  "data": {}
}
FieldDescription
codeStable error code. Full list of codes and HTTP statuses: packages/contract/src/errors.ts
statusHTTP status code
messageEnglish text for clients that do not know the code
dataMessage parameters

Authentication and input validation failures use the generic oRPC codes UNAUTHORIZED (401) and BAD_REQUEST (400).

Web UI RPC

/rpc/* uses the oRPC RPC protocol for the web UI; the path is the procedure path, e.g. POST /rpc/sites/list.

RequirementDescription
Session cookieIssued by the sign-in endpoints under /api/auth
x-csrf-token: orpc403 when missing
x-api-keyStripped; cannot replace the session cookie

Third-party integrations use /api/v1.

Authentication endpoints

better-auth handles /api/auth/*, limited to the paths and methods below. Matching is exact (no prefixes, encoded variants, or trailing slashes); every other request returns 404. x-api-key is stripped from requests; the client IP is resolved according to EDGEWEIR_TRUSTED_PROXIES before it reaches better-auth. There is no public sign-up: the setup wizard creates the only account. AccessKeys are managed through accessKeys.*. With NODE_ENV=production, rate limiting is on, with counters in PostgreSQL.

PathMethod
/api/auth/get-sessionGET
/api/auth/sign-in/emailPOST
/api/auth/sign-outPOST
/api/auth/change-passwordPOST
/api/auth/two-factor/enablePOST
/api/auth/two-factor/disablePOST
/api/auth/two-factor/verify-totpPOST
/api/auth/two-factor/verify-backup-codePOST
/api/auth/passkey/generate-register-optionsGET
/api/auth/passkey/verify-registrationPOST
/api/auth/passkey/generate-authenticate-optionsGET
/api/auth/passkey/verify-authenticationPOST
/api/auth/passkey/list-user-passkeysGET
/api/auth/passkey/delete-passkeyPOST

Health check

GET /healthz returns 200:

{ "status": "ok", "version": "20260929-a1b2c3d" }
FieldDescription
statusAlways ok
versionRunning version (EDGEWEIR_VERSION); dev from source

The response comes from the listening HTTP server; the database is not checked. Container health check: Command line.

Installer and release mirror

GET /install.sh returns the node installer (text/x-shellscript, cache-control: no-store) with the console URL replaced by EDGEWEIR_PUBLIC_URL. Options: Command line.

GET and HEAD on /downloads/* serve files from EDGEWEIR_DOWNLOADS_DIR; the URL path equals the relative path in the directory:

PathContentcache-control
/downloads/<project>/latestLatest version number, textno-cache
/downloads/<project>/v<semver>/<file>Release filepublic, max-age=86400, immutable

<project> is edgeweir-node or cosign; the edgeweir-openresty and edgeweir-openresty-modsecurity packages go into the same version directory as edgeweir-node. Other paths, missing files, and symbolic links leading out of the directory return 404. Preparing the directory: Adding nodes.

Node channel

ItemValue
ProtocolConnect-RPC over HTTPS (HTTP/2, HTTP/1.1 accepted), TLS 1.2 or later
ListenerNODE_API_HOST:NODE_API_PORT, default 8443
Servicesedgeweir.node.v1.NodeService (nodes) and edgeweir.node.v1.ProbeService (regional probes and nodes that also probe), defined in proto/edgeweir/node/v1/node.proto and probe.proto
Server certificateIssued by the console's internal CA at every start; names: Environment variables
AuthenticationEnroll, EnrollProbe: a one-time enrollment token (ewt_, ewp_); the node or probe pins the internal CA's SHA-256 fingerprint beforehand. Every other RPC: a client certificate issued by the internal CA (mTLS), with the node ID (O=Edgeweir Node) or probe ID (O=Edgeweir Probe) as CN; probe certificates call ProbeService only, node certificates call GetProbeTargets and ReportProbeResults only while the node also probes
Other paths404

The node channel must be reached directly or through TCP passthrough; a proxy that terminates TLS breaks node mTLS. See Ports, reverse proxy, and trusted proxies.

Edit on GitHub

On this page