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\" }.

GET /api/tenants/{slug}/asset-budget

  • Purpose: R2 storage budget for the project — the override stored on the tenant plus, read live from the edge, the effective policy (deployment defaults merged with the override) and the budget ledger (reservedUsd, stopAtUsd, spentUsd, storageReserveUsd, stored bytes/objects, billing period). Read-only; inspecting never charges the ledger.
  • Auth: Clerk session + master admin (tenant access alone is not enough).
  • Request shape: Path {slug}. No body.
  • Response shape: { override: { monthlyUsd?, reservePercent? } | null, edge: { ok: true, policy: { monthlyUsd, reservePercent, stopUsd, billingDay, maxStoredBytes, maxObjects }, ledger | null } | { ok: false, error } }. edge.ok: false means the worker could not be reached; the override is still returned.
  • Key errors: 403 not a master admin, 404 tenant not found.
  • Example: GET /api/tenants/acme/asset-budget.

PUT /api/tenants/{slug}/asset-budget

  • Purpose: Set or clear the project's R2 storage budget override. The worker stops new R2 work when its reservations reach monthlyUsd × (1 − reservePercent / 100); the billing day and capacity ceilings stay deployment-level. Updates the tenant row and republishes the project's KV config, so the new stop applies on the worker's next budget lease (about a minute).
  • Auth: Clerk session + master admin.
  • Request shape: Body { monthlyUsd?: number (0..100000), reservePercent?: number (0..99) }, or null to restore the deployment defaults. Omitted fields keep the deployment default for that value.
  • Response shape: { ok, override }.
  • Key errors: 400 invalid body, 403 not a master admin, 404 tenant not found.
  • Example: Body { "monthlyUsd": 25 }.

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 →