KrexelDocsHome

API reference

Every endpoint, in one place.

Hand-curated from the worker source. When routes change in apps/worker/src/index.ts, update this file. The CLI and MCP server both sit on top of these endpoints — there is nothing here that is internal-only.

Base URL & authentication

Base URL: https://api.krexel.com. The marketing site is a separate Vercel deployment.

API key auth: most endpoints require a Krexel API key. Send it in the Authorization: Bearer krx_live_… header. Keys are minted via POST /api/v1/keys and the plaintext is returned exactly once.

Session auth: the dashboard uses Supabase session cookies, not API keys. From the browser you just need to be signed in — the site proxies the request to the worker with the right auth.

Errors: every error is { "error": "<code>", "message": "<human readable>" }. HTTP status carries the category: 4xx for client problems, 5xx for upstream failures. 401 = bad/expired key, 403 = wrong owner (or not admin), 404 = no such resource, 413 = deploy too large, 429 = rate limited, 503 = upstream not configured.

Rate limits: the free tier signs in at most 5 times per minute from the same network; the deploy and key endpoints are not currently throttled but will be when abuse starts. Auth failures increment a per-email counter that locks sign-in for 10 minutes after 10 failures.

Health

Liveness and readiness probes. Public — no auth.

  • GET/healthzPublic

    Minimal liveness check. Returns 200 with { ok, version } as long as the worker is running. No upstream checks.

  • GET/readyzPublic

    Full readiness probe — exercises KV, CF Pages API. Returns 200 with per-check status if all healthy, 503 if anything is broken. Powers the public /status page.

Auth & session

Signup and the calling customer. /me is the source of truth for the dashboard chrome.

  • POST/api/v1/signupPublic

    Create an account. Body: { email, password?, plan? }. Returns { email, plan }. On the free tier no payment is required.

    Show request & response

    Request

    {
      "email": "you@example.com",
      "password": "correct-horse-battery-staple",
      "plan": "builder"
    }

    Response (200)

    {
      "email": "you@example.com",
      "plan": "builder",
      "created_at": "2026-07-14T10:00:00.000Z",
      "api_key_id": "krx_k_8f4a2b9d..."
    }
  • GET/api/v1/meAPI key

    Returns the calling customer: { email, plan, isAdmin, deployCount, tourDismissed, … }. Used by the dashboard to render the chrome.

  • PATCH/api/v1/meAPI key

    Update notification prefs, dismiss the onboarding tour, or rename the account. Body: any subset of the writable fields.

  • DELETE/api/v1/meAPI key

    Permanently delete the account and every associated deploy, domain, key, and team membership. Idempotent on a non-existent customer.

API keys

Mint, list, and revoke API keys. Plaintext is returned exactly once on POST.

  • POST/api/v1/keysAPI key

    Mint a new API key. Body: { name }. Returns the plaintext key exactly once — store it. The server retains only a bcrypt hash.

    Show request & response

    Request

    {
      "name": "Claude Code (macbook)"
    }

    Response (200)

    {
      "id": "krx_k_8f4a2b9d1c6e7f02",
      "name": "Claude Code (macbook)",
      "key": "krx_sk_live_3aF8sL2bQ9kM0vW5zE7uX1c4dR8tY6pH4jK9mN2qT3sV8wA5zD2hF7g",
      "created_at": "2026-07-14T10:00:00.000Z"
    }
  • GET/api/v1/keysAPI key

    List every key the caller owns. Returns id, name, created_at, last_used_at — never the plaintext or the hash.

  • DELETE/api/v1/keys/:idAPI key

    Revoke a key. The deleted key fails on the next request with 401. Idempotent on an already-revoked id.

Deploys

Ship, patch, inspect, rollback. Patches weigh 0.1 against the quota; full deploys weigh 1.0.

  • POST/api/v1/deployAPI key

    Ship a new site. Multipart: `manifest` (JSON) + `file` (tar of the site) OR multiple `files` parts with paths. Quota-checked before parsing. Returns 201 with { deploy_id, preview_url, status: 'queued' }. The actual build runs asynchronously — poll GET /api/v1/deploys/:id or stream /api/v1/deploys/:id/stream.

  • POST/api/v1/deploy/patchAPI key

    Apply a list of patch ops to an existing site: {create | replace | replace_all | delete}. Body: { parent_deploy_id, ops: [{ op, path, content_base64? }] }. Patches weigh 0.1 against the monthly quota; full deploys weigh 1.0.

  • GET/api/v1/deploysAPI key

    List every deploy owned by the caller, newest first. Returns DeployRecord[] (see GET /:id for shape).

  • GET/api/v1/deploys/:idAPI key

    Read a single deploy record. 404 if no such id, 403 if it belongs to a different customer. The response is the full DeployRecord: status, preview_url, logs, cf_deployment_id, custom_domain, parent_deploy_id, …

    Show response body

    Response (200)

    {
      "id": "krx_d_a3f1b8c92e4d",
      "email": "you@example.com",
      "domain": "my-site.pages.dev",
      "project_name": "my-site",
      "status": "ready",
      "framework": "next",
      "preview_url": "https://my-site.pages.dev",
      "cf_deployment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "custom_domain": null,
      "parent_deploy_id": null,
      "created_at": "2026-07-14T10:00:00.000Z",
      "ready_at": "2026-07-14T10:00:34.000Z",
      "duration_ms": 34000,
      "logs": []
    }
  • GET/api/v1/deploys/:id/streamAPI key

    SSE stream of deploy status transitions. The server polls the KV record every ~1s and emits an `event: status` with the latest snapshot. Closes on terminal status (ready | error). Useful for CLI live tail.

  • GET/api/v1/deploys/:id/filesAPI key

    Return the full file tree + per-file contents for a deploy. Used by `krexel rollback` to materialize the previous version. Body is large for non-trivial sites — prefer GET /:id/versions + targeted reads.

  • GET/api/v1/deploys/:id/logsAPI key

    Return the persisted `logs` field for a deploy. Idempotent — the build log never changes after a deploy reaches a terminal status.

  • GET/api/v1/deploys/:id/versionsAPI key

    Walk the parent_deploy_id chain for a file, returning the list of deploys that touched it. Used by `krexel history <path>`.

  • POST/api/v1/deploys/:id/rollbackAPI key

    Create a NEW deploy that ships the same file tree as the target. The new deploy gets a fresh deploy_id and a `parent_deploy_id` pointing at the source. Atomic — the original deploy is never modified.

  • POST/api/v1/deploys/:id/domainAPI key

    Attach a custom domain to a specific deploy. Body: { domain }. Returns the domain record. The customer's registrar must point a CNAME at the platform Pages project before SSL will provision.

  • POST/api/v1/deploys/:id/domain/refreshAPI key

    Re-poll Cloudflare for the SSL status of the domain attached to this deploy. Triggers the 'your domain is live' email exactly once when the status transitions pending → active.

Usage

Per-customer quota summary for the current calendar month.

  • GET/api/v1/usageAPI key

    Per-customer usage breakdown for the current calendar month: { deploys_this_month, deploys_patch, deploy_quota, over_deploy_quota }. Patch deploys weigh 0.1; full deploys weigh 1.0.

Custom domains

Attach a custom domain to a deploy or to the platform Pages project. SSL is automatic.

  • POST/api/v1/deploys/:id/domainAPI key

    Attach a custom domain to a specific deploy. Body: { domain }. Returns the domain record. The customer's registrar must point a CNAME at the platform Pages project before SSL will provision.

  • POST/api/v1/deploys/:id/domain/refreshAPI key

    Re-poll Cloudflare for the SSL status of the domain attached to this deploy. Triggers the 'your domain is live' email exactly once when the status transitions pending → active.

  • GET/api/v1/domainsAPI key

    List every custom domain attached to the platform Pages project for the calling customer. Includes live status (pending | active | error).

  • POST/api/v1/domains/attachAPI key

    Attach a domain without binding it to a specific deploy (the platform decides routing). Body: { domain }. Useful when the same domain should follow the customer's latest deploy automatically.

    Show request & response

    Request

    {
      "domain": "alex.dev"
    }

    Response (200)

    {
      "domain": "alex.dev",
      "status": "pending",
      "cname_target": "krexel-prod.pages.dev",
      "verification_token": "krexel-verify-a3f1b8c92e4d",
      "attached_at": "2026-07-14T10:00:00.000Z"
    }
  • POST/api/v1/domains/:domain/refreshAPI key

    Re-poll Cloudflare for the SSL status of a platform-attached domain. Same as /deploys/:id/domain/refresh but addressed by the domain itself.

  • DELETE/api/v1/domains/:domainAPI key

    Detach a domain from the platform Pages project. The customer's CNAME record is not touched — they decide whether to remove it.

Billing

Stripe Checkout, plan changes, webhooks, and the usage meter.

  • POST/api/v1/billing/checkoutAPI key

    Start a Stripe Checkout flow for a plan upgrade. Body: { plan: 'builder' | 'pro' | 'studio' }. Returns { url } — the customer is redirected there.

  • POST/api/v1/billing/webhookStripe webhook

    Stripe webhook receiver. Authenticated via the Stripe-Signature header, not a Krexel API key. Verifies signature, then updates the customer's plan in KV.

  • POST/api/v1/billing/change-planAPI key

    Change plan without going through Checkout (for in-app switches between paid tiers). Body: { plan }. Rejects paid plans unless BILLING_ENABLED is set on the worker.

  • GET/api/v1/billing/usageAPI key

    Per-customer usage + quota summary for the current calendar month. The dashboard's 'X / Y deploys used' meter reads from this.

Team

Workspace members and pending invites. Studio plan only.

  • GET/api/v1/teamAPI key

    List active members + pending invites for the caller's workspace. Returns { members: [...], invites: [...] }. Only available on the Studio plan.

  • POST/api/v1/team/inviteAPI key

    Create a pending invite + send a one-time accept email. Body: { email, role: 'admin' | 'member' }. Returns the invite record. Expires in 7 days.

  • POST/api/v1/team/acceptPublic

    Accept a workspace invite. Body: { token }. The token is single-use; the invitee must already have a Krexel account (or signup happens in the same flow).

  • DELETE/api/v1/team/invites/:idAPI key

    Revoke a pending invite. The accept URL becomes invalid. Idempotent on an already-revoked id.

  • DELETE/api/v1/team/members/:idAPI key

    Remove a member from the workspace. The removed customer keeps their own account but loses access to the shared deploys/domains. The owner cannot be removed.

Webhooks

Customer-owned subscriptions. The worker POSTs to a registered URL on every deploy status transition, signed with HMAC-SHA256.

  • GET/api/v1/webhooksAPI key

    List every webhook subscription owned by the caller. The full secret is never returned — only a `secret_hint` with the last 4 characters. Plan-gated quota (see Webhooks section below).

  • POST/api/v1/webhooksAPI key

    Create a subscription. Body: { url, events? } — `url` must be HTTPS and pass the SSRF guard; `events` defaults to ["deploy.*"], the only pattern supported today (plus the catch-all "*"). Returns 201 with the full WebhookRecord including the plaintext `secret` exactly once — store it, it cannot be retrieved later. 402 when the plan's quota is exhausted, 400 when the URL is invalid or in a reserved range.

    Show request & response

    Request

    {
      "url": "https://hooks.example.com/krexel",
      "events": ["deploy.ready", "deploy.error"]
    }

    Response (200)

    {
      "id": "krx_wh_b7c1e9d4f8a2",
      "email": "you@example.com",
      "url": "https://hooks.example.com/krexel",
      "events": ["deploy.ready", "deploy.error"],
      "secret": "whsec_3aF8sL2bQ9kM0vW5zE7uX1c4dR8tY6pH4",
      "secret_hint": "...6pH4",
      "created_at": "2026-07-14T10:00:00.000Z",
      "consecutive_failures": 0,
      "disabled": false
    }
  • DELETE/api/v1/webhooks/:idAPI key

    Remove a subscription and stop all future deliveries to it. Idempotent on an already-deleted id. Does NOT revoke in-flight retries that are already queued.

  • POST/api/v1/webhooks/:id/rotate-secretAPI key

    Generate a new signing secret for the subscription. Returns the full new secret exactly once. The old secret stops working immediately. Also resets `consecutive_failures` to 0 — a common recovery path after the old secret leaked.

    Show response body

    Response (200)

    {
      "id": "krx_wh_b7c1e9d4f8a2",
      "email": "you@example.com",
      "url": "https://hooks.example.com/krexel",
      "events": ["deploy.ready", "deploy.error"],
      "secret": "whsec_k1E9sT3bV7qM0wX4zA2hC6dF8tY5pJ2r",
      "secret_hint": "...5pJ2r",
      "created_at": "2026-07-14T10:00:00.000Z",
      "rotated_at": "2026-07-14T11:30:00.000Z",
      "consecutive_failures": 0,
      "disabled": false
    }
  • GET/api/v1/webhooks/:id/deliveriesAPI key

    Read the last 50 delivery attempts for a subscription. Each entry has { attempted_at, event, deploy_id, response_status, duration_ms, error?, attempt }. Records expire after 7 days. The Test endpoint does NOT write here — it returns its result inline.

  • POST/api/v1/webhooks/:id/testAPI key

    Send a synthetic `deploy.test` event to the subscription's URL with the same HMAC-SHA256 signature format as a real delivery. Single attempt, 5s timeout, no retry, does NOT increment the failure counter. Returns { ok, status, duration_ms, event: "deploy.test", timestamp } so the dashboard can show "✓ 200 in 342ms" inline. 409 if the subscription is disabled.

Admin

Internal — requires is_admin: true on the customer record. Every action writes to the audit log.

  • GET/api/v1/admin/meAdmin only

    Confirm admin status + list the admin's permissions. Safe to call from the dashboard sidebar on every page to show/hide the /admin nav. Requires is_admin: true on the customer record.

  • GET/api/v1/admin/customers/:emailAdmin only

    Read-only view of any customer record. Returns plan, created_at, notification_prefs, team member count, is_admin, etc. Never returns the API key hash or the encrypted CF token.

  • GET/api/v1/admin/deploys/:emailAdmin only

    List every deploy for any customer. Used when a customer says 'I lost my site' and you need to find the last good deploy to rollback.

  • GET/api/v1/admin/audit-logAdmin only

    Read the audit log (last 50 admin actions, newest first). Every admin action writes a log entry: { ts, admin_email, permission, target, details? }. Entries are kept for 1 year in KV.

  • POST/api/v1/admin/audit-logAdmin only

    Record a manual admin action (e.g. 'refunded $25 via Stripe dashboard'). Body: { permission, target, note }. The entry appears in the next GET. Useful for things that don't have a structured endpoint.

Cron

Internal jobs. Auth via the KREXEL_CRON_SECRET worker secret.

  • POST/api/v1/cron/retentionCron secret

    Daily retention sweep. Deletes R2 file trees older than the configured threshold (default 90d R2 Standard + 90d-1yr IA → 1yr+ deleted). Auth via KREXEL_CRON_SECRET. Returns { deleted, kept }.

Webhook events

Subscribe to deploy.* events today — deploy.ready when a deploy finishes successfully and deploy.failed when it hits a terminal error. The deploy.test event is fired only by the Test endpoint, never by a real deploy.

Signed payload

Every delivery carries a Stripe-compatible HMAC-SHA256 signature:

Krexel-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>

The signed string is <unix>.<rawBody> — same format Stripe uses. To verify on the customer side:

  1. Split the header on , and read t= and v1=.
  2. Compute HMAC-SHA256(secret, ${t}.${rawBody}) and constant-time-compare to v1.
  3. Reject if now - t > 5 minutes — that is the 5-minute replay window.

The Test endpoint signs the synthetic payload with the same key, so your verifier code is identical for real deliveries and for tests.

Retry, timeout, and auto-disable

  • Real deliveries make up to 3 attempts: immediate, +5s, +30s. Each attempt has a 5s timeout. Any 2xx response (on any attempt) is treated as success and resets the failure counter.
  • Test endpoint is single-attempt. No retry chain, no exponential backoff, and it does NOT increment the failure counter — a failed test cannot disable a healthy webhook.
  • Auto-disable: 10 consecutive failed real deliveries flip disabled: true on the subscription and the worker emails the customer (idempotent via disabled_notified_at). Rotate the secret and re-enable from the dashboard to recover.

SSRF guard

Subscription URLs are validated on create/update. The guard rejects:

  • Non-HTTPS URLs.
  • Hostnames: localhost, *.localhost, *.local, *.internal.
  • IPv4 RFC1918 (10/8, 172.16/12, 192.168/16), loopback (127/8), link-local / AWS metadata (169.254/16), 0/8.
  • IPv6 loopback (::1), ULA (fc00::/7), link-local (fe80::/10), and IPv4-mapped (::ffff:0:0/96) — the last blocks an IPv6 bypass of the IPv4 check.

Plan quota

The number of subscriptions a customer can own is hardcoded in worker/src/lib/plans.ts:

  • Free: 0 (no webhooks — upgrade to subscribe).
  • Starter: 1.
  • Builder: 3.
  • Pro: 5.
  • Studio: unlimited (-1).

Exceeding the quota returns 402 with { error: "quota_exceeded", limit, current }.