Tenants API
All endpoints below use the same docs format and reference Zod/TS shapes from server code.
GET /api/tenants
- Purpose: List tenants for the authenticated user.
- Auth: Clerk session.
- Request shape: none.
- Response shape:
{ total, tenants[] }. - Key errors:
401unauthorized. - Example:
GET /api/tenants.
POST /api/tenants
- Purpose: Create a new tenant.
- Auth: Clerk session.
- Request shape: Slug + CMS configuration + optional cache/domain fields.
- Response shape:
{ ok, key, proxyBase }. - Key errors:
403plan limit,409slug already used,400validation. - Example: Body with
slug,cms, and provider-specific fields.
GET /api/tenants/slug-availability
- Purpose: Check whether a slug is available.
- Auth: Clerk session.
- Request shape: Query
slug. - Response shape:
{ slug, available }. - Key errors:
400invalid slug. - Example:
GET /api/tenants/slug-availability?slug=my-project.
GET /api/tenants/usage
- Purpose: Aggregate usage across all user tenants.
- Auth: Clerk session.
- Request shape: Optional period query.
- Response shape: Per-tenant list/aggregates.
- Key errors:
401. - Example:
GET /api/tenants/usage.
GET /api/tenants/{slug}
- Purpose: Read configuration for a single tenant.
- Auth: Clerk session + tenant access.
- Request shape: Path
{slug}. - Response shape:
{ key, value }. - Key errors:
404tenant not found. - Example:
GET /api/tenants/acme.
PUT /api/tenants/{slug}
- Purpose: Update tenant configuration (partial update).
- Auth: Clerk session + tenant access.
- Request shape: Body with changed fields (Zod validation).
- Response shape: Updated
{ key, value }. - Key errors:
400invalid body,404tenant not found. - Example: Body
{ cacheTTL, websiteDomain }.
DELETE /api/tenants/{slug}
- Purpose: Delete a tenant and related configuration.
- Auth: Clerk session + tenant access.
- Request shape: Path
{slug}. - Response shape:
{ ok, key }. - Key errors:
404tenant not found. - Example:
DELETE /api/tenants/acme.
GET /api/tenants/{slug}/analytics
- Purpose: Return tenant analytics overview.
- Auth: Clerk session + tenant access.
- Request shape: Path
{slug}+ optional period query. - Response shape: Analytics object for dashboards.
- Key errors:
404,400invalid period. - Example:
GET /api/tenants/acme/analytics.
GET /api/tenants/{slug}/bandwidth-daily
- Purpose: Daily bandwidth breakdown for a tenant.
- Auth: Clerk session + tenant access.
- Request shape: Path
{slug}+ optional range query. - Response shape: Daily usage points.
- Key errors:
400invalid range,404. - Example:
GET /api/tenants/acme/bandwidth-daily.
GET /api/tenants/{slug}/domains
- Purpose: List custom domain records for a tenant.
- Auth: Clerk session + tenant access.
- Request shape: Path
{slug}. - Response shape: Domain list with statuses.
- Key errors:
404. - Example:
GET /api/tenants/acme/domains.
POST /api/tenants/{slug}/domains
- Purpose: Add a custom domain to a tenant.
- Auth: Clerk session + tenant access.
- Request shape: Body with hostname field.
- Response shape: Created domain record.
- Key errors:
400invalid hostname,409already exists. - Example: Body
{ hostname: \"cdn.acme.com\" }.
DELETE /api/tenants/{slug}/domains
- Purpose: Remove a custom domain from a tenant.
- Auth: Clerk session + tenant access.
- Request shape: Path
{slug}+ body/query identifying the domain to remove. - Response shape:
{ ok }. - Key errors:
404domain not found. - Example:
DELETE /api/tenants/acme/domains.
POST /api/tenants/{slug}/domains/verify
- Purpose: Trigger/check custom domain DNS verification.
- Auth: Clerk session + tenant access.
- Request shape: Path
{slug}+ domain identifier. - Response shape: Domain verification status.
- Key errors:
400,404. - Example:
POST /api/tenants/acme/domains/verify.
GET /api/tenants/{slug}/logs
- Purpose: Tenant-level request log view.
- Auth: Clerk session + tenant access.
- Request shape: Path
{slug}+ pagination/filter query. - Response shape: Log entries + pagination metadata.
- Key errors:
400invalid query,404. - Example:
GET /api/tenants/acme/logs.
GET /api/tenants/{slug}/audit-events
- Purpose: Tenant audit history (membership, role, configuration, and other sensitive changes). Master admins are also granted access via the standard tenant access override.
- Auth: Clerk session + tenant membership (viewer+).
- Request shape: Path
{slug}+ optional querylimit(1..200, default 50) andbeforeId(keyset pagination — fetch entries withid < beforeId). - Response shape:
{ events[], hasMore }. - Key errors:
401,403,404tenant not found. - Example:
GET /api/tenants/acme/audit-events?limit=50&beforeId=1234.
POST /api/tenants/{slug}/refresh-cache
- Purpose: Increment cache versions and force new cache keys.
- Auth: Clerk session + tenant access (editor+).
- Request shape: Body with
type(api,assets,html,all). - Response shape:
{ ok, versions: { apiCacheVersion, assetCacheVersion, htmlCacheVersion } }. - Key errors:
400invalid type,403insufficient role. - Example: Body
{ type: \"html\" }.
POST /api/tenants/{slug}/edge-config
- Purpose: Manually rebuild and re-write the tenant's
proxy:{slug}KV snapshot from the current D1 state. Idempotent — no DB mutation, no cache purge. Useful when the dashboard shows stale derived runtime fields (e.g.publicOrigin, security gate) and the operator wants an explicit edge re-sync. - Auth: Clerk session + tenant access (editor+).
- Request shape: Path
{slug}. No body. - Response shape:
{ ok, summary, kv: { exists, publicOrigin } }.summaryechoes the derived runtime fields that were written;kvis a read-back from KV after the write so the caller has end-to-end confirmation of the synced blob. - Key errors:
403insufficient role,404tenant not found. - Example:
POST /api/tenants/acme/edge-config.
POST /api/tenants/{slug}/cachely-check
- Purpose: Cachely implementation checker. Surface-aware: it reports on the project's website surface (the URL a visitor hits). Probes the active custom domain (production) when present, else the preview
<slug>.cachely.ioURL when the site proxy is on, twice (plain sequential GETs), and combines the observed Cachely edge headers with stored config into an honest verdict. API-only / asset-only projects have no website surface and return a neutralnot_enabledverdict (not a failure). Read-only — no DB mutation, no cache purge. - Auth: Clerk session + tenant access.
- Request shape: Path
{slug}. No body. - Response shape:
{ state, headline, probeUrl, surfaceLabel, items: [{ id, label, state, detail }], probes }.stateis one ofimplemented,active_needs_attention,not_detected,not_enabled.surfaceLabelisproduction|preview|null;probeUrlisnullwhen no website surface was probed. Served-through-Cachely is proven only by Cachely-ownedx-cachely-*headers (a genericx-cacheis never sufficient); API proxy / custom-domain rows report configuration status only and point to the API Proxy test for live verification. - Key errors:
404tenant not found. - Example:
POST /api/tenants/acme/cachely-check.
POST /api/tenants/{slug}/website-implementation-check
- Purpose: Website code/runtime implementation check. Distinct from
cachely-check(which inspects configuration + the proxy surface): this fetches the project's real public website (SSRF-guarded normalise + fetch, reusing the Site Audit primitives), detects the platform from the actual HTML, and inspects it for evidence the site's code routes through Cachely (references to<slug>.cachely.io/cmsassets.com//~api/,@cachely-io/sdk) versus calling the CMS directly (e.g.cdn.contentful.com/images.ctfassets.net). Read-only. - Auth: Clerk session + tenant access.
- Request shape: Body
{ url }— the public website URL (not the Cachely proxy URL; product domains are rejected). - Response shape:
{ ok, finalUrl, platform, analysis }on success;{ ok: false, finalUrl, error, message }on validation/fetch failure.analysis={ cachelyUsage: detected|not_detected|inconclusive, cachelySignals, directCmsUsage: detected|potential|not_detected, directCmsSignals, cmsLabel, verdictState: implemented|mixed|not_using|inconclusive, verdict }. Direct-CMS is onlydetectedwhen a CMS host appears in a real rendered resource context (<img>/<link>attribute, CSSurl(), orLinkheader); a host found only in serialized data / inline script / CSP ispotential(matched URL surfaced for verification). SSR sites whose CMS/API calls happen server-side returninconclusive(honest — not a failure). - Key errors:
400invalid body. Validation/fetch issues are returned as{ ok: false }bodies, not thrown. - Example: Body
{ "url": "https://aletagency.com" }.
POST /api/tenants/{slug}/webhook
- Purpose: External webhook endpoint for CMS publish notifications (e.g. Webflow site_publish). Bumps htmlCacheVersion so the edge Worker fetches fresh HTML on the next request.
- Auth: Per-tenant URL-embedded secret (
?secret=). No Clerk session required. - Request shape: Query
secret. Body is ignored (compatible with any CMS webhook payload). - Response shape:
{ ok, htmlCacheVersion }. - Key errors:
401missing or invalid secret,403webhook not configured,404unknown project. - Example:
POST /api/tenants/acme/webhook?secret=abc123....
POST /api/tenants/{slug}/webhook-secret
- Purpose: Generate a webhook secret for the tenant (idempotent — returns existing secret if already generated).
- Auth: Clerk session + tenant access (editor+).
- Request shape: none.
- Response shape:
{ secret }. - Key errors:
403insufficient role,404tenant not found. - Example:
POST /api/tenants/acme/webhook-secret.
PUT /api/tenants/{slug}/webhook-secret
- Purpose: Rotate the webhook secret. The old secret becomes invalid immediately.
- Auth: Clerk session + tenant access (admin+).
- Request shape: none.
- Response shape:
{ secret }. - Key errors:
403insufficient role,404tenant not found. - Example:
PUT /api/tenants/acme/webhook-secret.
GET /api/tenants/{slug}/cache-refresh-webhooks
- Purpose: List provider cache-refresh webhooks for the tenant. Metadata only — never the secret hash or raw secret.
- Auth: Clerk session + tenant access (editor+).
- Request shape: none.
- Response shape:
{ webhooks: Array<{ id, provider, refreshApi, refreshAssets, enabled, createdAt, updatedAt, lastTriggeredAt, lastStatus }> }. - Key errors:
403insufficient role,404tenant not found. - Example:
GET /api/tenants/acme/cache-refresh-webhooks.
GET /api/tenants/{slug}/cache-refresh-webhooks/{id}/events
- Purpose: Recent fire history for one webhook — the received → synced/failed lifecycle with the
api_cache_versiondelta, duration, and KV-sync time. Metadata only; never the secret. Bounded to the most recent events. - Auth: Clerk session + tenant access (editor+).
- Request shape: none.
- Response shape:
{ events: Array<{ id, status, receivedAt, startedAt, finishedAt, previousApiCacheVersion, nextApiCacheVersion, refreshType, durationMs, kvSyncedAt, message, eventType }> }. - Key errors:
400missing webhook id,403insufficient role,404webhook not found. - Example:
GET /api/tenants/acme/cache-refresh-webhooks/whk_abc123/events.
POST /api/tenants/{slug}/cache-refresh-webhooks
- Purpose: Create a provider cache-refresh webhook (Contentful, Prismic, Storyblok, generic). Returns the full URL + raw secret ONCE; only the SHA-256 hash is persisted. Default refresh mode is API cache only.
- Auth: Clerk session + tenant access (admin+).
- Request shape: Body
{ provider: 'contentful' | 'prismic' | 'storyblok' | 'generic', refreshAssets?: boolean }. - Response shape:
{ webhook, secret, url }—urlis the full receiver URL with the raw secret embedded; show it once and discard. - Key errors:
400invalid body,403insufficient role,404tenant not found. - Example: Body
{ provider: "contentful" }→{ url: "https://app.cachely.io/api/webhooks/cache-refresh/whk_…/whsec_…", ... }.
POST /api/tenants/{slug}/cache-refresh-webhooks/{id}/regenerate
- Purpose: Rotate the secret on an existing cache-refresh webhook. The old URL stops working immediately — the user must paste the new URL into their CMS.
- Auth: Clerk session + tenant access (admin+).
- Request shape: none.
- Response shape:
{ webhook, secret, url }— same shape as create;urlshown once. - Key errors:
400missing webhook id,403insufficient role,404webhook not found. - Example:
POST /api/tenants/acme/cache-refresh-webhooks/whk_abc123/regenerate.
DELETE /api/tenants/{slug}/cache-refresh-webhooks/{id}
- Purpose: Permanently remove a cache-refresh webhook. The hash on disk is dropped; the URL stops working immediately. Historical
tenant_webhook_eventsrows are retained. - Auth: Clerk session + tenant access (admin+).
- Request shape: none.
- Response shape:
{ ok: true }. - Key errors:
400missing webhook id,403insufficient role,404webhook not found. - Example:
DELETE /api/tenants/acme/cache-refresh-webhooks/whk_abc123.
POST /api/webhooks/cache-refresh/{webhookId}/{secret}
- Purpose: Public, unauthenticated receiver for provider cache-refresh webhooks. On a valid secret, increments
api_cache_version(andasset_cache_versionif the webhook opts in), re-syncs the proxy config to KV, and records an event intenant_webhook_events. - Auth: URL-embedded bearer secret (compared in constant time against the stored SHA-256 hash). No Clerk session.
- Request shape: Body is best-effort parsed for provider event-type extraction (Contentful:
x-contentful-topicheader; Prismic:body.type; Storyblok:body.actionorbody.event; generic: ignored). Bounded to 64 KB. - Response shape:
{ ok: true, refreshed: true, versions: { apiCacheVersion, assetCacheVersion, htmlCacheVersion } }on success;{ ok: true, refreshed: false }for no-op configs. - Key errors:
404for any auth failure (unknown id, invalid secret, disabled webhook) — intentionally generic to avoid leaking existence;413if the body exceeds 64 KB;500on internal refresh failure. - Example:
POST /api/webhooks/cache-refresh/whk_abc123/whsec_def456(configured in the CMS provider's webhook settings).
PUT /api/tenants/{slug}/api-config
- Purpose: Save API proxy configuration without token.
- Auth: Clerk session + tenant access.
- Request shape: Body with
apiOrigin, auth mode, TTL, and preview/cache options. - Response shape: Saved API config.
- Key errors:
400validation,404. - Example: Body with
apiOriginandapiAuthMode.
PUT /api/tenants/{slug}/api-token
- Purpose: Write-only save of encrypted API token.
- Auth: Clerk session + tenant access.
- Request shape: Body
{ token }. - Response shape:
{ ok, updatedAt }. - Key errors:
400token missing,404. - Example: Body
{ token: \"***\" }.
GET /api/tenants/{slug}/usage
- Purpose: Usage overview for a single tenant.
- Auth: Clerk session + tenant access.
- Request shape: Path
{slug}+ optional period. - Response shape:
{ current, previous, period }. - Key errors:
404,400invalid period. - Example:
GET /api/tenants/acme/usage.
GET /api/tenants/{slug}/members
- Purpose: List all members for a tenant. Pending invites (with PII) are only included for admins and owners.
- Auth: Clerk session + tenant membership (viewer+).
- Request shape: Path
{slug}. - Response shape:
{ members[], invites[] }.invitesis empty for non-admin callers. - Key errors:
401,403,404tenant not found. - Example:
GET /api/tenants/acme/members.
POST /api/tenants/{slug}/members/invite
- Purpose: Create or refresh an invite for a new member. Requires admin role. Admins cannot assign the owner role.
- Auth: Clerk session + tenant admin+.
- Request shape: Path
{slug}+ body{ email: string, role: "viewer"|"editor"|"admin"|"owner" }. - Response shape:
{ id, email, role, token, expiresAt, createdAt, emailSent }. - Key errors:
400validation,403insufficient privileges or role escalation,404tenant not found. - Example: Body
{ email: "user@example.com", role: "editor" }.
POST /api/tenants/{slug}/members/remove
- Purpose: Remove a member from the tenant. Cannot remove yourself, the last owner, or someone with equal/higher privileges (unless owner).
- Auth: Clerk session + tenant admin+.
- Request shape: Path
{slug}+ body{ userId: string }. - Response shape:
{ ok, userId }. - Key errors:
400validation,403self-removal or privilege check,404member not found. - Example: Body
{ userId: "user_123" }.
POST /api/tenants/{slug}/members/revoke-invite
- Purpose: Revoke a pending invite by its ID.
- Auth: Clerk session + tenant admin+.
- Request shape: Path
{slug}+ body{ inviteId: number }. - Response shape:
{ ok, inviteId }. - Key errors:
400validation,403,404invite not found or already revoked. - Example: Body
{ inviteId: 42 }.
POST /api/tenants/{slug}/members/update-role
- Purpose: Change a member's role. Cannot change your own role, assign owner (unless caller is owner), or act on members with equal/higher privileges.
- Auth: Clerk session + tenant admin+.
- Request shape: Path
{slug}+ body{ userId: string, role: "viewer"|"editor"|"admin"|"owner" }. - Response shape:
{ userId, role, updatedAt }. - Key errors:
400validation,403privilege checks,404member not found. - Example: Body
{ userId: "user_123", role: "admin" }.
✦
Need help understanding this?Ask Cachely Copilot about features, setup, or integrations.
Ask Copilot →