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: 401 unauthorized.
  • 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: 403 plan limit, 409 slug already used, 400 validation.
  • 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: 400 invalid 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: 404 tenant 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: 400 invalid body, 404 tenant 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: 404 tenant 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, 400 invalid 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: 400 invalid 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: 400 invalid hostname, 409 already 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: 404 domain 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: 400 invalid 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 query limit (1..200, default 50) and beforeId (keyset pagination — fetch entries with id < beforeId).
  • Response shape: { events[], hasMore }.
  • Key errors: 401, 403, 404 tenant 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: 400 invalid type, 403 insufficient 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 } }. summary echoes the derived runtime fields that were written; kv is a read-back from KV after the write so the caller has end-to-end confirmation of the synced blob.
  • Key errors: 403 insufficient role, 404 tenant 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.io URL 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 neutral not_enabled verdict (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 }. state is one of implemented, active_needs_attention, not_detected, not_enabled. surfaceLabel is production | preview | null; probeUrl is null when no website surface was probed. Served-through-Cachely is proven only by Cachely-owned x-cachely-* headers (a generic x-cache is never sufficient); API proxy / custom-domain rows report configuration status only and point to the API Proxy test for live verification.
  • Key errors: 404 tenant 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 only detected when a CMS host appears in a real rendered resource context (<img>/<link> attribute, CSS url(), or Link header); a host found only in serialized data / inline script / CSP is potential (matched URL surfaced for verification). SSR sites whose CMS/API calls happen server-side return inconclusive (honest — not a failure).
  • Key errors: 400 invalid 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: 401 missing or invalid secret, 403 webhook not configured, 404 unknown 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: 403 insufficient role, 404 tenant 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: 403 insufficient role, 404 tenant 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: 403 insufficient role, 404 tenant 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_version delta, 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: 400 missing webhook id, 403 insufficient role, 404 webhook 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 }url is the full receiver URL with the raw secret embedded; show it once and discard.
  • Key errors: 400 invalid body, 403 insufficient role, 404 tenant 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; url shown once.
  • Key errors: 400 missing webhook id, 403 insufficient role, 404 webhook 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_events rows are retained.
  • Auth: Clerk session + tenant access (admin+).
  • Request shape: none.
  • Response shape: { ok: true }.
  • Key errors: 400 missing webhook id, 403 insufficient role, 404 webhook 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 (and asset_cache_version if the webhook opts in), re-syncs the proxy config to KV, and records an event in tenant_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-topic header; Prismic: body.type; Storyblok: body.action or body.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: 404 for any auth failure (unknown id, invalid secret, disabled webhook) — intentionally generic to avoid leaking existence; 413 if the body exceeds 64 KB; 500 on 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: 400 validation, 404.
  • Example: Body with apiOrigin and apiAuthMode.

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: 400 token 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, 400 invalid 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[] }. invites is empty for non-admin callers.
  • Key errors: 401, 403, 404 tenant 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: 400 validation, 403 insufficient privileges or role escalation, 404 tenant 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: 400 validation, 403 self-removal or privilege check, 404 member 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: 400 validation, 403, 404 invite 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: 400 validation, 403 privilege checks, 404 member not found.
  • Example: Body { userId: "user_123", role: "admin" }.
Need help understanding this?Ask Cachely Copilot about features, setup, or integrations.
Ask Copilot →