Stackbilder Platform API Reference
Base URL: https://stackbilder.com
Last updated: 2026-05-02
Worker: stackbilt-web (Astro SSR on Cloudflare, 47 API route files / 60 HTTP method handlers bundled in one worker)
All API routes live at /api/* paths on stackbilder.com. They are Astro server-side endpoints compiled into the stackbilt-web Cloudflare Worker at deploy time. There is no separate API worker. Cross-worker concerns (authentication, TarotScript execution, img-forge rendering, CodeBeast ledger) are handled via Cloudflare service bindings from inside this worker.
The API is the canonical contract for all four consumer types: the web UI at /app/*, the MCP gateway, the Charter CLI, and direct API consumers. No consumer is privileged. For platform architecture and plan tiers, see Stackbilder Platform. For the full ecosystem context, see Ecosystem.
Table of contents
Authentication
Two auth modes:
| Mode | How | Use |
|---|---|---|
| Session cookie | better-auth.session_token (dev) or __Secure-better-auth.session_token (prod) |
Web UI from a logged-in browser |
| API key | Authorization: Bearer ea_* |
CLI, MCP, server-to-server, CI |
Routes typically accept either unless noted. A handful (admin endpoints, form-handling helpers) restrict to one mode.
For API keys, resolve your identity with GET /api/account/me — useful for CLIs, MCP callers, and for determining the orgId used in entitlement checks.
Mint an API key from the web UI at /settings or via POST /api/keys.
Common patterns
Identity scope — split-by-policy contract
Per edge-auth’s split-by-policy contract (receipt cbfcacecda58e33be77f9cd2b6afb142ecd18a2b30a68a31906658a723d5c16e):
- Org-level scope — SKU, tier, plan, features, Trust Page ownership. Every entitlement check uses
user.orgId. - Per-resource scope — quota attribution, D1 row tenant_id, rate-limit keys. Uses
user.tenantId || user.orgId || user.id.
This is why API-key auth (where tenantId is typically null) and session auth (where tenantId is the current tenant) resolve to the same org-level answer for entitlements but different values for per-resource scopes.
Error response shape
Most routes return JSON errors in this shape:
{ "error": "human-readable message", "code": "machine_code_optional", "upgrade": "/pricing_optional" }
Codes you’ll see in practice:
already_subscribed— 409 on checkout when the tenant already has a paid subscriptioncross_tenant_conflict— 409 on trust/slug or bundle write when another tenant owns itcross_tenant_forbidden— 403 on delete when another tenant owns itlibrary_limit— 402 on Evidence library when at tier capno_stripe_customer— 422 on billing portal when no customer existsevidence_gap_fill_requires_pro— 402 when tier isn’t pro/teamevidence_gap_fill_rate_limited— 429 on 30/hr defense rate limitevidence_gap_fill_quota_exceeded— 429 on edge-auth quotaempty_library— 400 on gap-fill when no assets exist for tenant
Rate limits
| Route family | Limit | Source |
|---|---|---|
/api/billing/checkout |
10/hr per user | stackbilt-web |
/api/billing/downgrade |
5/hr per user | stackbilt-web |
/api/v1/evidence/gap-fill |
30/hr per tenant | stackbilt-web (defense layer) |
/api/v1/evidence/gap-fill |
evidence_gap_fills quota |
edge-auth |
/api/agents/* |
scaffolds quota |
edge-auth |
/api/flows/* |
scaffolds quota |
edge-auth |
/api/images/generate |
images quota |
edge-auth |
/api/contact |
3/hr per IP | stackbilt-web |
/api/subscribe |
5/hr per IP | stackbilt-web |
/api/support/tickets |
5/hr per user | stackbilt-web |
/api/scaffold/preview |
10/min per IP | stackbilt-web |
/api/mcp tools |
5–10/min per isolate | stackbilt-web |
Service bindings
The worker uses these Cloudflare service bindings to reach sibling workers (RPC / Fetcher over internal network):
| Binding | Target | Purpose |
|---|---|---|
AUTH_SERVICE |
edge-auth | Sessions, API keys, entitlements, quotas, Stripe integration |
TAROTSCRIPT |
tarotscript-worker | Agent consultations, scaffold harness, receipt signing |
IMG_FORGE |
img-forge-gateway | Image generation |
CODEBEAST |
codebeast | /decide receipts ledger (Trust Page governance timeline) |
Plus direct bindings: OBSERVE_DB, TRUST_DB, EVIDENCE_DB (D1), TRUST_BUNDLES (R2).
Reference
Health & meta
GET /api/health
Purpose: Public liveness and dependency health check for external uptime monitors.
Auth: None
Success responses:
// 200 OK (healthy) or 503 (degraded)
{
status: "healthy" | "degraded",
checks: {
auth_service: { status: "pass" | "fail", latency_ms: number },
tarotscript: { status: "pass" | "fail", latency_ms: number },
img_forge: { status: "pass" | "fail", latency_ms: number }
},
latency_ms: number,
timestamp: string
}
Example:
curl https://stackbilder.com/api/health
Source: src/pages/api/health.ts
Notes: Probes AUTH_SERVICE, TAROTSCRIPT, IMG_FORGE bindings.
Authentication & account
GET /api/auth/relay
Purpose: OAuth session relay — receives session token from auth.stackbilt.dev after OAuth callback, sets secure cookie, redirects to target page.
Auth: None
Query params:
token(required) — Session token from auth serviceredirect(optional) — Target path after login; default/dashboard. Validated as safe relative path.
Response: 302 redirect with Set-Cookie: better-auth.session_token=...
Redirects to /login if no token, or /dashboard if redirect is unsafe.
Notes: Called by edge-auth during SSO flow. Cookie is HttpOnly, Secure, SameSite=Lax, 7-day Max-Age.
Source: src/pages/api/auth/relay.ts
GET /api/account/me
Purpose: Introspect the authenticated caller’s identity — { userId, orgId, tenantId, email }.
Auth: Session cookie or Bearer ea_*
Success responses:
// 200 OK
{
userId: string,
orgId: string | null,
tenantId: string | null,
email: string | null // empty when using API key auth
}
Error responses:
401— no valid session or API key
Example:
curl -H "Authorization: Bearer ea_YOUR_KEY" https://stackbilder.com/api/account/me
Source: src/pages/api/account/me.ts
Notes: Used by Charter CLI (charter whoami), MCP tools for caller identity, and for determining the orgId used in CONSULT_DOGFOOD_ORGS or entitlement checks.
POST /api/account/name
Purpose: Update the authenticated user’s display name.
Auth: Session cookie only
Request body:
{ name: string } // 0–100 chars; empty string clears
Success responses:
// 200 OK
{ ok: true, name: string | null }
Error responses:
400— invalid JSON or name exceeds 100 chars401— not authenticated403— session token invalid or insufficient permissions (forwarded from AUTH_SERVICE)502— AUTH_SERVICE unavailable
Notes: Forwards session token to AUTH_SERVICE.updateUser() for server-side ownership verification.
Source: src/pages/api/account/name.ts
API keys
GET /api/keys
Purpose: List the authenticated user’s API keys (masked).
Auth: Session cookie only
Success responses:
// 200 OK
[
{
id: string,
name: string,
maskedKey: string, // last 4 chars shown
createdAt: string,
lastUsed: string | null
}
]
Source: src/pages/api/keys/index.ts
POST /api/keys
Purpose: Generate a new API key.
Auth: Session cookie only
Request body:
{ name: string } // required, non-empty
Success responses:
// 201 Created
{ id: string, name: string, key: string, createdAt: string }
The full key (format ea_*) is returned once; save it now — it cannot be retrieved later.
Error responses:
400— name missing or empty401— not authenticated
Source: src/pages/api/keys/index.ts
DELETE /api/keys/:id
Purpose: Revoke an API key.
Auth: Session cookie only
Success responses: 204 No Content
Source: src/pages/api/keys/[id].ts
Usage
GET /api/usage
Purpose: Current quota usage and tier for the authenticated user.
Auth: Session cookie or Bearer ea_*
Success responses:
// 200 OK
{
scaffolds: { used: number, limit: number },
images: { used: number, limit: number },
tier: "free" | "pro" | "team",
cycle_ends_at: string // ISO 8601
}
Tier limits: free = 3 scaffolds / 5 images, pro/team = 50 scaffolds / 100 images.
Notes: SKU/tier is org-level per edge-auth split-by-policy contract. Defaults to free tier if AUTH_SERVICE lookup fails or tenant not provisioned.
Source: src/pages/api/usage.ts
Agent consultations
Structured C-level agent consultations powered by TarotScript. Both CTO and CISO receive signed receipts independently verifiable at https://verify.stackbilt.dev/<hash>.
POST /api/agents/run
Purpose: Execute an agent consultation — returns structured analysis + guidance + receipt.
Auth: Session cookie or Bearer ea_*
Request body:
{
role: string, // cto, ciso, cfo, cmo, cpo, architect
intention: string, // what decision or question
context?: Record<string, unknown>, // optional structured context
painSignals?: string[], // optional pain points
responseMode?: "symbolic" | "natural" | "structured-only",
seed?: number // optional for deterministic results
}
Success responses:
200 OK—AgentRunResultTypefrom@stackbilt/contracts/agent-response(or its stackbilt-web mirror atsrc/contracts/agent-response.ts):{ schema_version, success, role, response, symbolicResponse, analysis, guidance?, receipt, responseMode, metadata, envelope? }
Error responses:
400— “role and intention are required”401— Unauthorized429—{ error, upgrade: "/pricing" }— scaffolds quota exceeded502— upstream TarotScript error
Example:
curl -X POST https://stackbilder.com/api/agents/run \
-H "Authorization: Bearer ea_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"role": "cto",
"intention": "Evaluate Cloudflare Workers vs AWS Lambda for a multi-region edge API",
"context": { "stage": "seed", "teamSize": "2_5" }
}'
Source: src/pages/api/agents/run.ts
Notes: Forwards to tarotscript /agents/run with inscribe: true so receipts persist to D1 for verification. Consumes one scaffolds quota credit. CTO runs keep claims private (publish_claims not set); CISO intake flow opts in via /ciso-intake route below.
POST /api/agents/ciso-intake
Purpose: Structured CISO posture intake — returns a signed Trust Bundle receipt or a refusal with remediation guidance.
Auth: Session cookie or Bearer ea_*
Request body: CisoIntakePayload (see src/contracts/ciso-intake.ts)
{
schemaVersion: 1,
companyProfile: {
name: string,
employeeCount: number,
foundingDate: string, // ISO date YYYY-MM-DD
fundingStage: "pre-seed" | "seed" | "series-a" | "series-b" | "series-c" | "growth" | "bootstrap" | "public",
customerTypes: ("smb" | "mid-market" | "enterprise" | "consumer" | "government" | "nonprofit" | "other")[],
dataResidency: ("us" | "eu" | "uk" | "ca" | "apac" | "latam" | "global")[]
},
techStack: {
cloudProvider: CloudProvider[], // aws | gcp | azure | cloudflare | vercel | fly | digitalocean | on-prem | hybrid | other
compute: Compute[], // containers | serverless | vm | kubernetes | workers | lambda | cloud-run | ec2 | edge-functions | other
database: Database[], // postgres | mysql | mongodb | dynamodb | firestore | d1 | cloudflare-kv | redis | sqlite | spanner | other
monitoring: Monitoring[], // datadog | new-relic | sentry | honeycomb | grafana | cloudflare-analytics | workers-analytics | splunk | none | other
cicd: CiCd[] // github-actions | gitlab-ci | circleci | jenkins | cloudflare-pages | vercel | manual | wrangler | other
},
dataHandled: ("pii" | "phi" | "pci" | "financial" | "biometric" | "credentials" | "health" | "none")[],
controlBaseline: {
mfa: { scope: "universal" | "privileged-only" | "partial" | "none", owner: string, notes?: string },
encryptionAtRest: { algo: "aes-256" | "aes-128" | "customer-managed-keys" | "cloud-provider-default" | "none", owner: string, notes?: string },
encryptionInTransit: { tlsVersion: "tls-1.3" | "tls-1.2" | "tls-1.1" | "tls-1.0" | "legacy" | "none", owner: string, notes?: string },
iam: { model: "least-privilege" | "role-based" | "attribute-based" | "flat" | "ad-hoc" | "none", owner: string, notes?: string },
logging: { retentionDays: number, centralized: boolean, owner: string, notes?: string },
workstationSecurity: { screenLock: boolean, diskEncryption: boolean, edr: boolean, owner: string, notes?: string },
passwordManager: { tool: string, coverage: "universal" | "partial" | "none", owner: string, notes?: string },
peerReview: { model: "peer" | "single-maintainer" | "multi-agent" | "automated-only" | "none", owner: string, notes?: string }
},
subprocessors: Array<{
name: string,
purpose: string,
dataAccess: SubprocessorDataAccess[], // pii | phi | pci | financial | biometric | credentials | health | logs | telemetry | metadata | billing | support | none
jurisdiction: string,
dpaSigned: boolean
}>,
incidentHistory: Array<{
date: string,
severity: "low" | "medium" | "high" | "critical",
scope: string,
resolution: string
}>,
certifications: {
soc2: { status: CertStatus, lastAudit?: string },
iso27001: { status: CertStatus, lastAudit?: string },
penTest: { status: CertStatus, lastAudit?: string, scope?: string }
}, // CertStatus = "none" | "in-progress" | "scheduled" | "type-i" | "type-ii" | "certified" | "completed"
compensatingControls: Array<{ gap: string, mitigation: string, owner: string }>
}
Success responses:
// 200 OK — accepted
{
accepted: true,
refusal: {
refused: false,
findings: Array<{
id: string, path: string,
description: string, remediation: string,
recoveredBy?: { gap: string, owner: string }
}>,
summary: string
},
receipt: {
hash: string,
verifyUrl: string, // path — full URL: https://verify.stackbilt.dev<verifyUrl>
claimsSchemaVersion: number
},
seed: number,
latencyMs: number
}
Error responses:
400— Invalid JSON or Zod validation error from TarotScript401— Unauthorized422— refused on unrecovered instant-disqualifier:{ refused: true, summary: string, findings: Array<{ id, path, description, remediation }> }429— scaffolds quota exceeded withupgrade: "/pricing"502— upstream TarotScript error
Source: src/pages/api/agents/ciso-intake.ts
Notes: Zod-validated on the TarotScript side (tarotscript#203). Consumes one scaffolds quota credit. Refusal runs server-side and is authoritative — client-side disqualifier detection in CisoConsultation.tsx is UX-only. Successful receipts can be registered to a Trust Page slug via POST /api/trust/slugs for public publication at trust.stackbilder.com/<slug>.
POST /api/agents/bootstrap
Purpose: Deck research — analyze domain gaps and assign research methodology tier.
Auth: Session cookie or Bearer ea_*
Request body:
{
role: string, // cto | ciso | cfo | cmo | cpo | architect
domain?: string, // optional domain or business context
internalSignals?: string[] // optional internal signal / pain data
}
Success responses:
200 OK—BootstrapResultTypewith methodology metadata (signalType, signalClass, gapType, gapClass, researchStrategy, researchType, evidenceBar, tierAssignment, methodologyConfidence)
Error responses:
400— “role is required”401— Unauthorized429— scaffolds quota exceeded502— upstream TarotScript error
Source: src/pages/api/agents/bootstrap.ts
Notes: Consumes one scaffolds quota credit.
GET /api/agents/:role/memory
Purpose: Query agent memory state (zone counts, entropy, recent cards) for a specific role.
Auth: Session cookie or Bearer ea_*
Path params:
role— cto | ciso | cfo | cmo | cpo | architect
Success responses:
200 OK—MemoryQueryResultType:{ role, exists, tick?, zones: { discard, deck, metaInsight, identity }, entropy: { shannonDiversity, elementCounts, depletionRatio, transCapacity, totalCards }, recentMemories?, updatedAt? }
Error responses:
400— “role is required”401— Unauthorized502— upstream TarotScript error
Source: src/pages/api/agents/[role]/memory.ts
Notes: Read-only; does NOT consume quota. Use to monitor agent memory saturation.
Evidence Engine
E-E-A-T content quality tooling backed by @stackbilt/evidence-core (Apache-2.0, usable standalone). See AEGIS wiki evidence-engine-gap-fill-architecture for full architecture.
POST /api/v1/evidence/validate
Purpose: Run E-E-A-T validation against content. Deterministic (no LLM, no library access).
Auth: Session cookie or Bearer ea_*
Request body:
{
content: ContentInput | string, // required; bare string wraps to { body: string }
policyVersion?: "google_november_2024_reputation" // default
}
Max JSON payload: 128 KiB.
Success responses:
// 200 OK
{
validationId: string,
cached: boolean, // true if returned from 1-hour dedup cache
score: number, // 0-100, equal-weight active policy requirements satisfied
hasGaps: boolean,
gapCount: number,
policyVersion: string,
policyName: string,
gaps: Array<{
pillar: "Experience" | "Expertise" | "Authoritativeness" | "Trustworthiness",
type?: string,
description: string,
severity?: string
}>,
suggestions: string[]
}
Error responses:
400—{ error: "content is required" | "invalid_json" | "Unknown evidence policy version ..." }401— session invalid / tenant resolution failed413—{ error: "payload_too_large", limit: 131072 }429—{ error: "evidence_validation_quota_exceeded", upgrade: "/pricing" }
Notes: Dedup cache: identical (tenant, content, policy) within 1 hour returns cached validationId without consuming quota or writing a new audit row. Input hash is SHA-256 of normalized payload.
Source: src/pages/api/v1/evidence/validate.ts
POST /api/v1/evidence/gap-fill
Purpose: Iterative evidence gap-fill loop — validate → match library assets → merge → LLM revise → re-validate. Pro-gated.
Auth: Session cookie or Bearer ea_*; Pro/Team only (402 otherwise)
Request body:
{
content: ContentInput | string,
policyVersion?: string, // default "google_november_2024_reputation"
maxIterations?: number, // clamped 1–5, default 3
targetPillars?: EvidencePillar[],
assetPreferences?: {
domains?: EvidenceDomain[],
types?: EvidenceAssetType[]
}
}
Max JSON payload: 128 KiB.
Success responses:
// 200 OK
{
gapFillId: string,
iterations: number,
initialGaps: number,
finalGaps: number,
converged: boolean, // true iff finalGaps === 0
revised: ContentInput,
iterationLog: Array<{
iteration: number,
gapsBefore: number, gapsAfter: number,
assetsInjected: string[],
llmCallCostEstimate: number,
note?: string
}>,
creditsUsed: number, // USD, accrued from canonical llm-providers TokenUsage.inputTokens/outputTokens
gapFillHash: string, // SHA-256 of canonical projection bound into v2.2 receipts (#115)
bailReason?: "max_iterations" | "cost_ceiling" | "llm_bail" | "schema_drift" // omitted if converged
}
Error responses:
400—{ error: "content is required" | "invalid_json" | "empty_library" | ... }401— session invalid / tenant resolution failed402—{ error: "evidence_gap_fill_requires_pro", upgrade: "/pricing" }413—{ error: "payload_too_large", limit: 131072 }429— rate-limit (30/hr) ORevidence_gap_fill_quota_exceeded502— LLM provider misconfiguration, LLM failure, asset match DB failure, orgap_fill_persist_failed
Source: src/pages/api/v1/evidence/gap-fill.ts. Loop + persistence helpers live in src/lib/evidence-gap-fill.ts so /attest can run the same path with runGapFill: true.
Notes:
- Pro/team only (plan lookup via
user.orgId). - 30/hr per-tenant rate limit +
evidence_gap_fillsquota from edge-auth. - $0.25 cost ceiling per request; LLM model = Cerebras GLM-4.7.
- Fast-path: if initial validation finds 0 gaps, returns immediately with no LLM spend.
- Asset matching: pillar→type heuristic, ordered by
usage_count ASC, created_at DESC; up to 6 assets per iteration. - Persisted (#115): every successful run writes an
evidence_gap_fillsrow with the canonical iteration log +gap_fill_hash. The hash is the binding the v2.2 receipt commits to, and the row is whatgetGapFillForPublicRenderre-hashes for the trust-page render. - Calibration telemetry (#95 item 1): every run writes a summary row to
OBSERVE_DB.traceswithworker_name = "internal:evidence-gap-fill"andtrace_id = gapFillId.root_attrsJSON captures iterations, gap counts, bail reason, and credits used. Hidden from user-facing Observe endpoints viaINTERNAL_WORKER_SQL_FILTER. Fail-soft — telemetry errors are logged and swallowed. Per-iteration spans are deferred to item 6.
POST /api/v1/evidence/attest
Purpose: Composite “survived adversarial review” pipeline. One call runs initial validate → critique + revise → optional gap-fill polish → final validate → signed publish receipt. Pro/Team only.
Auth: Session cookie or Bearer ea_*; Pro/Team only (402 otherwise)
Request body:
{
content: ContentInput | string,
publicPayload: Record<string, unknown>, // tenant-authored projection returned by /verify
policyVersion?: string, // default "google_november_2024_reputation"
approvedPlan?: string[], // optional Collaborative Planning v2.1
runGapFill?: boolean, // #115 — opt into v2.2 polish + binding
gapFillOptions?: {
maxIterations?: number,
targetPillars?: EvidencePillar[],
assetPreferences?: { domains?: string[], types?: string[] }
}
}
Success response (201):
{
receiptHash: string, // 64-hex HMAC; lookup key on trust.stackbilder.com/evidence/:hash
verifyUrl: string, verifyJsonUrl: string,
receiptVersion: "2" | "2.1" | "2.2", // 2.2 when runGapFill bound; 2.1 with approvedPlan; 2 critique-only
policyVersion: string,
contentHash: string,
publishedAt: number,
revised: ContentInput, // post-pipeline content the receipt's HMAC binds
planHash?: string, // present on v2.1+
gapFillHash?: string, // present on v2.2 only
gapFillSkipped?: "empty_library" | "loop_failed", // runGapFill requested but stage skipped → falls through to v2.1/v2
pipelineSummary: {
initialGaps: number, finalGaps: number,
attacksLogged: number, revisedDiffersFromInput: boolean,
bailReasons: string[],
gapFill?: { iterations, initialGaps, finalGaps, assetsInjected, bailReason? }
},
creditsUsed: number // sum of critique + (optional) gap-fill USD
}
Error responses:
400—{ error: "content is required" | "invalid_public_payload" | "invalid_approved_plan" | "Unknown evidence policy version ..." }401— session invalid / tenant resolution failed402—{ error: "evidence_attest_pro_required", upgrade: "/pricing" }413—{ error: "payload_too_large" | "public_payload_too_large", limit: ... }429—{ error: "evidence_attest_rate_limited" }(20/hr) |evidence_validations_quota_exceeded|evidence_gap_fills_quota_exceeded409—{ error: "receipt_conflict" }(HMAC collision — vanishingly rare)502—{ error: "attest_failed", at: "initial_validate" | "critique" | "critique_persist" | "final_validate" | "validation_persist" | "publish_insert" }|attest_critique_bailed
Quota cost: 1 evidence_validations per call. With runGapFill: true, +1 evidence_gap_fills. Defense rate-limit is 20/hr per tenant; the LLM cost-multiplier of the polish pass is bundled into that allowance — no separate cap.
Receipt versions:
v2(DBreceipt_version=2) — critique onlyv2.1(DBreceipt_version=3) — addsplanHashfromapprovedPlanv2.2(DBreceipt_version=4, #115) — addsgapFillHash. The matchingevidence_gap_fillsrow stores the canonical iteration log;getGapFillForPublicRenderre-hashes and compares before the trust page renders the section.
Source: src/pages/api/v1/evidence/attest.ts. Receipt primitives in src/lib/evidence-receipts.ts; gap-fill loop in src/lib/evidence-gap-fill.ts.
GET /api/v1/evidence/library
Purpose: List tenant’s evidence assets with optional filtering.
Auth: Session cookie or Bearer ea_*
Query params:
type(optional) — one of:case_study|customer_quote|expert_quote|original_visual|proprietary_data|first_hand_experience|technical_diagram|research_findingdomain(optional) — one of:medical|legal|financial|technology|marketing|business|education|health_wellness|food|food_science|food_regulation|ecommerce|real_estate|automotive|generallimit(optional) — 1–100, default 20cursor(optional) — opaque pagination cursor from priornextCursorresponse
Success responses:
// 200 OK
{
assets: AssetResponse[], // id, type, title, content, domains[], tags[], qualityScore, verificationStatus, authorName, authorBio, sourceUrl, usageCount, createdAt, updatedAt
nextCursor?: string // present if more results
}
Error responses:
400— invalid type/domain enum or malformed cursor401— session invalid / tenant resolution failed
Source: src/pages/api/v1/evidence/library/index.ts
Notes: Tenant-scoped (per-resource scoping, user.tenantId || user.orgId || user.id). Cursor is base64-encoded (createdAt, id) tuple, DESC order.
POST /api/v1/evidence/library
Purpose: Create a new evidence asset.
Auth: Session cookie or Bearer ea_*; tier-gated (Free: 0 cap, Pro/Team: 50 cap)
Request body:
{
type: EvidenceAssetType, // required
title: string, // required, 10–200 chars
content: Record<string, unknown>, // required, opaque JSON
domains: EvidenceDomain[], // required, non-empty
tags?: string[],
qualityScore?: number | null, // 0–100 or null
verificationStatus?: "verified" | "unverified" | "pending" | "disputed",
authorName?: string,
authorBio?: string,
sourceUrl?: string
}
Max JSON payload: 128 KiB.
Success responses:
201 Created—{ asset: AssetResponse }
Error responses:
400—{ error: "invalid_json" | "invalid_input", errors?: [{ field, message }] }401— session invalid / tenant resolution failed402—{ error: "library_limit", tier, limit, current, upgrade: "/pricing" }413—{ error: "payload_too_large", limit: 131072 }502— D1 batch insert failure
Notes: Post-insert capacity enforcement via atomic D1 batch — INSERT + COUNT + optional compensating DELETE. Safe for concurrent writers at cap boundary.
Source: src/pages/api/v1/evidence/library/index.ts
GET /api/v1/evidence/library/:id
Purpose: Retrieve a single evidence asset.
Auth: Session cookie or Bearer ea_*
Success responses: 200 OK — { asset: AssetResponse }
Error responses: 400 invalid id | 401 | 404 (or cross-tenant — same response to avoid leaking existence)
Source: src/pages/api/v1/evidence/library/[id].ts
PUT /api/v1/evidence/library/:id
Purpose: Partial or full update of an evidence asset.
Auth: Session cookie or Bearer ea_*
Request body: Same shape as POST, all fields optional. Max JSON payload: 128 KiB.
Success responses: 200 OK — { asset: AssetResponse }
Error responses: 400 invalid input | 401 | 404 | 413 payload too large | 502 batch update failure
Notes: If domains change, join table is re-synced atomically. updated_at always refreshes.
Source: src/pages/api/v1/evidence/library/[id].ts
DELETE /api/v1/evidence/library/:id
Purpose: Remove an evidence asset and its domain join rows.
Auth: Session cookie or Bearer ea_*
Success responses: 200 OK — { id, deleted: true }
Error responses: 400 | 401 | 404 | 502
Source: src/pages/api/v1/evidence/library/[id].ts
Trust Page
Slug→hash admin API that backs trust.stackbilder.com/<slug>. Ownership is org-level per split-by-policy contract. See AEGIS wiki trust-page-governance-timeline-architecture and trust-bundle-storage-architecture.
GET /api/trust/slugs
Purpose: List all trust slugs owned by the caller’s org.
Auth: Session cookie or Bearer ea_*
Success responses:
// 200 OK
{
tenantId: string, // historical name; semantically the orgId
slugs: Array<{
slug: string,
current_hash: string, // 64-char lowercase hex
tenant_id: string,
created_at: string,
updated_at: string
}>
}
Notes: Cross-tenant listing not supported.
Source: src/pages/api/trust/slugs/index.ts
POST /api/trust/slugs
Purpose: Create or update a slug → receipt-hash mapping.
Auth: Session cookie or Bearer ea_*
Request body:
{
slug: string, // 1–64 lowercase alphanumeric chars, optional interior hyphens
hash: string // 64-char lowercase hex (SHA-256 receipt hash)
}
Success responses:
200 OK— updated existing slug; returns row201 Created— created new slug; returns row
Error responses:
400— malformed body, invalid slug format, or invalid hash format401— no auth409—{ error: "...", code: "cross_tenant_conflict" }— slug owned by a different org
Example:
curl -X POST https://stackbilder.com/api/trust/slugs \
-H "Authorization: Bearer ea_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"slug":"acme-corp","hash":"<64-char hex>"}'
Source: src/pages/api/trust/slugs/index.ts
Notes: Slug and hash normalized to lowercase. First-come, first-owned at the org level. Idempotent: same org re-POSTing the same slug updates current_hash + updated_at.
GET /api/trust/slugs/:slug
Purpose: Fetch a single slug row if the caller’s org owns it.
Auth: Session cookie or Bearer ea_*
Success responses: 200 OK — single slug row
Error responses: 400 invalid slug | 401 | 404 (not found OR owned by another org — no existence-leak)
Source: src/pages/api/trust/slugs/[slug].ts
DELETE /api/trust/slugs/:slug
Purpose: Remove a slug mapping.
Auth: Session cookie or Bearer ea_*
Success responses: 204 No Content
Error responses:
400invalid slug401no auth403{ error, code: "cross_tenant_forbidden" }— exists but owned elsewhere (explicit permission signal)404slug doesn’t exist at all
Source: src/pages/api/trust/slugs/[slug].ts
GET /api/trust/bundle/:hash
Purpose: List Trust Bundle artifact metadata for a receipt hash (bytes not included).
Auth: Session cookie or Bearer ea_*
Path params: hash — 64-char lowercase hex
Success responses:
// 200 OK
{
receipt_hash: string,
artifacts: Array<{
receipt_hash, artifact_slug, tenant_id,
content_hash, // sha256 of the artifact bytes
mime_type, byte_size,
display_name, excerpt: string | null,
signed: 0 | 1,
created_at: string
}>,
count: number
}
Notes: Tenant-scoped. Cross-tenant receipts return an empty list (not 403).
Source: src/pages/api/trust/bundle/[hash]/index.ts
GET /api/trust/bundle/:hash/:slug
Purpose: Fetch a single artifact’s manifest row (metadata only).
Auth: Session cookie or Bearer ea_*
Path params: hash + artifact slug
Success responses: 200 OK — single BundleArtifactRow
Error responses: 400 invalid | 401 | 404 (not found or cross-tenant)
Notes: Artifact bytes are served from the public route /trust/:slug/bundle/:artifact_slug (not the admin API).
Source: src/pages/api/trust/bundle/[hash]/[slug].ts
POST /api/trust/bundle/:hash/:slug
Purpose: Upload or overwrite an artifact for a Trust Bundle.
Auth: Session cookie or Bearer ea_*
Path params: hash + artifact slug
Request headers (required):
Content-Type— real MIME type of the artifact. Allowed:application/pdf, JSON (application/json,application/ld+json,application/schema+json), text (text/plain,text/markdown,text/csv), zip/xlsx, or legacyapplication/vnd.ms-excel.X-Display-Name— human-readable artifact name, max 120 chars.
Request headers (optional):
X-Excerpt— one-paragraph card summary, max 500 chars.X-Signed: true— artifact embeds its own signature (e.g. signed PDF).
Request body: Raw artifact bytes, max 10 MiB. The API validates file signatures for PDFs, zip/xlsx, legacy XLS, JSON, and text-like formats before writing to R2.
Success responses:
200 OK— updated —{ row, content_hash, created: false }201 Created— created —{ row, content_hash, created: true }
Error responses:
400— invalid hash, invalid slug, missing/empty body, missing/oversizedX-Display-Name, oversizedX-Excerpt, or invalidContent-Type401— no auth413— artifact exceeds 10 MiB409—{ error, code: "cross_tenant_conflict" }415— unsupported MIME type or declared type does not match artifact bytes
Known artifact slugs (informational; not enforced): posture-overview, stride-threat-model, data-flow, ir-plan, shared-responsibility, subprocessors, caiq-lite, oscal-json.
Notes: R2 write occurs first (keyed <hash>/<slug>), then D1. Safe to retry — R2 puts are idempotent per key. content_hash is threaded into tarotscript receipt signing to enable tamper detection.
Source: src/pages/api/trust/bundle/[hash]/[slug].ts
DELETE /api/trust/bundle/:hash/:slug
Purpose: Remove an artifact — deletes R2 object, then D1 manifest row.
Auth: Session cookie or Bearer ea_*
Success responses: 204 No Content
Error responses:
403— artifact exists, owned by another tenant404— not found (or invalid hash/slug format)
Source: src/pages/api/trust/bundle/[hash]/[slug].ts
Scaffolder (Flows)
Stackbilder’s original product — deterministic governed scaffold output optionally polished with LLM Oracle.
POST /api/flows
Purpose: Create a new scaffold — classify intent, run scaffold-cast spread, optionally run Oracle polish.
Auth: Session cookie or Bearer ea_*
Request body:
{
intention: string, // required, non-empty
project_type?: string, // default "api"
oracle?: boolean // true to run Oracle LLM polish (Pro/Team only)
}
Success responses:
// 201 Created
{
id: string, // receipt hash
classification: { pattern: string, confidence: string },
traits: Record<string, string>,
tier2_recommended: boolean,
output: string,
facts: Record<string, unknown>
}
Error responses: 400 | 401 | 429 quota exceeded with upgrade: "/pricing" | 502
Notes: Consumes one scaffolds quota credit. Oracle runs only if body.oracle === true AND tier is pro/team. Oracle result persists to SESSION KV with 90-day TTL under key oracle:<id>.
Source: src/pages/api/flows/index.ts
GET /api/flows
Purpose: List the user’s scaffolds from the TarotScript grimoire.
Auth: Session cookie or Bearer ea_*
Success responses:
// 200 OK
{
flows: Array<{
id: string, intention: string,
project_type: string,
status: "completed",
created_at: string
}>
}
Notes: Filters grimoire by spreadType: "ScaffoldCast" | "scaffold-cast". Gracefully returns empty array on upstream error.
Source: src/pages/api/flows/index.ts
GET /api/flows/:id
Purpose: Full scaffold detail — governance, files, next steps, Oracle status.
Auth: Session cookie or Bearer ea_*
Path params: id — receipt hash
Success responses:
// 200 OK
{
id, intention, project_type, status, created_at, seed,
governance: { threat_model, adr, test_plan },
scaffold: { files: Array<{ path, content, role }> },
next_steps: string[],
output,
oracle_enhanced: boolean,
oracle_summary: string
}
File role values: scaffold | config | governance | test | doc.
Error responses: 401 | 404 | 502
Source: src/pages/api/flows/[id].ts
GET /api/flows/:id/oracle
Purpose: Retrieve persisted Oracle enhancement status/result for an existing scaffold.
Auth: Session cookie or Bearer ea_*
Path params: id — receipt hash
Success responses:
// 200 OK, not enhanced yet
{ enhanced: false }
// 200 OK, enhanced result persisted
{ enhanced: true, files, summary }
Error responses:
401— Unauthorized
Source: src/pages/api/flows/[id]/oracle.ts
Notes: Reads SESSION KV key oracle:<id>. Does not enforce Pro/Team; it only reports whether an already-persisted result exists.
POST /api/flows/:id/oracle
Purpose: Trigger LLM Oracle polish on an existing scaffold; persist enhanced files to KV.
Auth: Session cookie or Bearer ea_*; Pro/Team only
Path params: id — receipt hash
Success responses:
200 OK—{ enhanced: true, files, summary }
Error responses:
401— Unauthorized403—{ error: "Oracle requires a Pro subscription", upgrade_url: "/pricing" }404— flow not found422— flow has no prompt context (predates Oracle support)502— TarotScript or Oracle service error
Source: src/pages/api/flows/[id]/oracle.ts
Notes: Persists result to SESSION KV at oracle:<id> with 90-day TTL. Idempotent — returns cached result if already enhanced.
Images
GET /api/images
Purpose: List user’s generated image jobs.
Auth: Session cookie or Bearer ea_*
Query params:
limit(default 20),offset(default 0),state(optional filter, e.g.completed,pending)
Success responses:
// 200 OK
{ images: Array<{ id, prompt, quality_tier, status, image_url, created_at }> }
Source: src/pages/api/images/index.ts
POST /api/images/generate
Purpose: Create a new image generation job.
Auth: Session cookie or Bearer ea_*
Request body:
{
prompt: string, // required
quality_tier?: string, // default "standard"
negative_prompt?: string
}
Success responses:
// 201 Created
{ id, job_id, state, original_prompt, final_prompt, asset_url, created_at }
Error responses: 400 | 401 | 429 images quota exceeded with upgrade: "/pricing" | 502
Notes: Consumes one images quota credit.
Source: src/pages/api/images/generate.ts
GET /api/images/:id
Purpose: Retrieve single image job status.
Auth: Session cookie or Bearer ea_*
Success responses: 200 OK — { id, prompt, quality_tier, status, image_url, error, created_at }
Source: src/pages/api/images/[id].ts
DELETE /api/images/:id
Purpose: Delete an image job (cascades to img-forge asset cleanup).
Auth: Session cookie or Bearer ea_*
Success responses: 204 No Content
Error responses: 401 | 404 | 409 conflict (e.g. still processing) | 502
Source: src/pages/api/images/[id].ts
Worker observability
Pro feature backed by @stackbilt/worker-observability (Apache-2.0, installable standalone). See the worker-observability docs for the OSS library and Stackbilder Platform for the hosted dashboard.
Internal telemetry filter. All user-facing read endpoints in this section (/summary, /traces, /traces/:id) apply INTERNAL_WORKER_SQL_FILTER from src/lib/observe.ts — worker_name NOT LIKE 'internal:%'. stackbilt-web writes internal calibration telemetry (e.g. evidence gap-fill runs per #95) directly to OBSERVE_DB using the internal:* prefix so those rows never surface as phantom workers in a tenant’s Observe UI. If you add a new user-facing query that reads traces/spans/logs by worker_name, apply this filter.
POST /api/observe/ingest
Purpose: Unified batch telemetry ingest — metrics, spans, logs, alerts — from @stackbilt/worker-observability consumers.
Auth: Session cookie or Bearer ea_*; Pro-gated
Request body:
{
service: string, // worker name (required)
metrics?: MetricPoint[], // { name, value, unit, timestamp, tags, type: "counter" | "gauge" | "histogram" | "summary" }
spans?: TraceSpan[], // { traceId, spanId, operationName, service, startTime, endTime, duration, status, attributes, events }
logs?: LogEntry[], // { timestamp, level: "debug" | "info" | "warn" | "error" | "fatal", message, context, error, metadata }
alerts?: AlertIncident[] // { id, rule: { name, severity }, status: "firing" | "resolved", startTime, endTime, value, message, metadata }
}
Success responses: 202 Accepted — { ok: true }
Error responses:
400— missingservicefield, empty payload, or invalid JSON401— no auth413— payload exceeds 64 KB403—{ error, tier, limit, upgrade }— worker count cap reached for tier (Free 1 / Pro 5 / Team 20)429—{ error, tier, dailyLimit, used, upgrade, retryable: false }— daily event budget exhausted (Free 10K / Pro 500K / Team 2M per worker per day)502— DB failure
Notes: Retention: Free 24h / Pro 30d / Team 30d. Writes to OBSERVE_DB D1. Root spans (no parentSpanId) upserted to traces; all spans inserted to spans. All records include expires_at for TTL cleanup (hourly cron).
Source: src/pages/api/observe/ingest.ts
GET /api/observe/summary
Purpose: Per-worker summary stats (error rate, p95 latency, request count) over a time range.
Auth: Session cookie or Bearer ea_*
Query params:
range—1h|6h|24h(default) |7d|30dworker(optional) — filter to specific worker name
Success responses:
// 200 OK
{
workers: Array<{
worker_name, total_traces, error_count, error_rate,
avg_duration_ms, p95_duration_ms, last_seen
}>,
range: string
}
Notes: p95 computed client-side (D1 lacks PERCENTILE_CONT).
Source: src/pages/api/observe/summary.ts
GET /api/observe/traces
Purpose: Paginated trace list with filters.
Auth: Session cookie or Bearer ea_*
Query params:
range— time window (default24h)worker(optional) — filter by workerstatus(optional) —ok|errorlimit(default 50, max 200),offset(default 0)
Success responses:
// 200 OK
{
traces: Array<{ trace_id, worker_name, status, started_at, finished_at, duration_ms, error_msg }>,
total: number
}
Source: src/pages/api/observe/traces/index.ts
GET /api/observe/traces/:id
Purpose: Full trace detail with correlated spans + logs.
Auth: Session cookie or Bearer ea_*
Success responses:
// 200 OK
{
trace: { ... },
spans: Array<{ span_id, trace_id, parent_span_id, operation, service, start_time, end_time, duration_ms, status, attributes, events }>,
logs: Array<{ id, level, message, ts, error }>
}
Error responses: 400 missing id | 401 | 404
Source: src/pages/api/observe/traces/[id].ts
GET /api/observe/alerts
Purpose: List fired and resolved alert incidents.
Auth: Session cookie or Bearer ea_*
Query params:
status(optional) —firing|resolved; omit for all
Success responses:
// 200 OK
{
alerts: Array<{
id, worker_name, rule_name, severity, status,
message, value, started_at, ended_at
}>
}
Notes: ended_at is null for active alerts. Returns up to 100 rows ordered by started_at DESC.
Source: src/pages/api/observe/alerts.ts
Billing
⚠️ Live-mode Stripe (account acct_1T8cxHL8cDQ0gdtT) since 2026-04-11. All billing routes hit real customers.
POST /api/billing/checkout
Purpose: Create a Stripe Checkout session. Two modes: legacy Pro tier, or SKU (CISO Trust Bundle / Hosting, CTO Consultation).
Auth: Session cookie or Bearer ea_*
Request body:
// Empty body → legacy Pro flow (uses STRIPE_PRO_PRICE_ID)
{}
// OR SKU flow:
{ productKey: "ciso-trust-bundle" | "ciso-trust-page-hosting" | "cto-consultation" }
Success responses: 200 OK — { url: string } (Stripe Checkout session URL)
Error responses:
400— invalid productKey, returnsaccepted: [...]401— Unauthorized409—{ error, code: "already_subscribed", currentTier }(legacy Pro pre-flight only)429—{ error: "Too many checkout attempts" }(10/hr)502— upstream Stripe/edge-auth error
SKU catalog (as of 2026-04-19):
| productKey | Price | Mode |
|---|---|---|
ciso-trust-bundle |
$499 one-time | payment |
ciso-trust-page-hosting |
$149/mo | subscription |
cto-consultation |
gated / not priced | — |
CISO SKUs are invite-gated at the edge-auth layer (server-side CISO_INVITE_ALLOWLIST_ORGS secret).
Source: src/pages/api/billing/checkout.ts
POST /api/billing/portal
Purpose: Create a Stripe Billing Portal session for subscription management.
Auth: Session cookie or Bearer ea_*
Success responses: 200 OK — { url: string }
Error responses:
401— Unauthorized422—{ error, code: "no_stripe_customer" }— no Stripe customer (admin override, comp account, or lapsed sub)502— upstream error
Source: src/pages/api/billing/portal.ts
POST /api/billing/downgrade
Purpose: Downgrade tenant to free tier. Handles active subscription (cancel at period end), admin override, or past-due.
Auth: Session cookie or Bearer ea_*
Success responses:
// 200 OK
{
ok: boolean,
previousTier: "free" | "pro" | "team",
stripeAction: "canceled" | "no_subscription" | "already_canceled",
effectiveAt: string,
noop: boolean
}
canceled— active Stripe sub scheduled for cancellation; tier stays untileffectiveAtno_subscription— admin override or comp account; immediate tier flipalready_canceled— reconciling a danglingstripe_subscription_id; immediate flip
Error responses:
400—"You're already on the free plan."(short-circuit when tier is already free)401— Unauthorized403—{ error: "Forbidden: ..." }— ownership/role denial from edge-auth429— 5/hr rate limit502— upstream error
Source: src/pages/api/billing/downgrade.ts
Notes: Idempotent. Safe to call repeatedly.
GET /api/billing/promos
Purpose: List all promotion codes (admin endpoint).
Auth: Session cookie
Success responses: 200 OK — { codes: Array<{ id, code, percentOff, duration, maxRedemptions, timesRedeemed, active }> }
Source: src/pages/api/billing/promos.ts
POST /api/billing/promos
Purpose: Create a new promotion code.
Auth: Session cookie
Request body:
{
code: string,
percentOff: number, // 1–100
duration: "once" | "repeating" | "forever",
durationInMonths?: number,
maxRedemptions?: number
}
Success responses: 201 Created — { id, code, couponId }
Error responses: 400 invalid | 401 | 502
Notes: Code is uppercased on submission.
Source: src/pages/api/billing/promos.ts
DELETE /api/billing/promos/:id
Purpose: Revoke a promo code.
Auth: Session cookie
Success responses: 204 No Content
Error responses: 401 | 502
Source: src/pages/api/billing/promos/[id].ts
Marketing & support
POST /api/scaffold/preview
Purpose: Anonymous scaffold preview (Phase 1 only — no LLM, deterministic). Used by the public homepage hero.
Auth: None (rate-limited by IP)
Request body:
{ intention: string }
Success responses:
// 200 OK
{
classification: { pattern: string, confidence: string },
traits: Record<string, string>,
tier2_recommended: boolean,
governance: { threat_model, adr, test_plan },
files: string[]
}
Error responses: 400 missing/empty intention | 429 10/min per IP | 502 upstream TarotScript
Source: src/pages/api/scaffold/preview.ts
POST /api/contact
Purpose: Public contact form → Resend email to admin@stackbilt.dev.
Auth: None (rate-limited)
Request body: { email: string, message: string (10+ chars) }
Success responses: 200 OK — { ok: true }
Error responses: 400 invalid | 429 3/hr per IP | 502 Resend error | 503 Resend not configured
Source: src/pages/api/contact.ts
POST /api/subscribe
Purpose: Newsletter signup → Resend audience.
Auth: None (rate-limited)
Request body: { email: string }
Success responses: 200 OK — { ok: true }
Error responses: 400 invalid email | 429 5/hr per IP | 502 Resend error
Source: src/pages/api/subscribe.ts
Notes: Gracefully accepts signup even if Resend not configured (returns 200). Sends welcome email to subscriber + admin notification.
POST /api/support/tickets
Purpose: In-app bug/support ticket. Auto-triaged via TarotScript triage-cast spread.
Auth: Session cookie
Request body:
{
message: string, // 10–5000 chars
url?: string,
userAgent?: string
}
Success responses: 200 OK — { ok: true, triage: { category?, urgency? } }
Error responses: 400 invalid | 401 | 429 5/hr per user | 502 Resend delivery failed
Source: src/pages/api/support/tickets.ts
Notes: TarotScript classifies category, urgency, sentiment, complexity, and escalation flag. Email sent to admin@stackbilt.dev with full classification. Best-effort — delivers ticket even if classification fails.
Telemetry
POST /api/csp-report
Purpose: Public CSP violation report endpoint. Logs to console, persists to OBSERVE_DB with 30-day retention.
Auth: None
Request body: Browser-standard CSP report shape (document-uri, violated-directive, etc.)
Success responses: Always 204 No Content (swallows all errors)
Notes: Visible via wrangler tail. Filters reports missing document-uri or violated-directive (ignores scanner garbage).
Source: src/pages/api/csp-report.ts
POST /api/errors
Purpose: Client-side error telemetry (from React ErrorBoundary components) → Worker console.
Auth: None
Request body: { type?, component?, message?, url?, timestamp? }
Success responses: 204 No Content
Error responses: 413 payload exceeds 8 KB
Notes: Truncates message and url to 500 chars.
Source: src/pages/api/errors.ts
MCP server
POST /api/mcp / GET /api/mcp
Purpose: JSON-RPC 2.0 MCP server exposing admin Cloudflare tools (analytics, inventory, cost forecasting).
Auth:
POST— Session cookie orBearer ea_*, admin-onlyGET— Public (tool discovery)
POST request body:
{
jsonrpc: "2.0",
id: number | string | null,
method: "initialize" | "tools/list" | "tools/call" | "ping" | "notifications/initialized",
params?: {
name?: string, // for tools/call
arguments?: Record<string, unknown> // for tools/call
}
}
POST success:
{
jsonrpc: "2.0",
id: ...,
result: { /* method-dependent; tools/call returns { content: [{ type: "text", text }], isError? } */ }
}
JSON-RPC error codes:
-32700parse error |-32600invalid request |-32601method not found |-32602invalid params |-32000unauthorized
Tools available: cf_worker_analytics, cf_resource_inventory, cf_cost_estimate, cf_cost_forecast
Rate limits: 10/min (analytics, inventory, estimate); 5/min (forecast)
Source: src/pages/api/mcp/index.ts
Notes: Server info — name=stackbilt-cf-admin, version=0.1.0, protocolVersion=2024-11-05. Each tool backed by 5-min KV cache to bound CF API cost.
Admin
All admin routes require a session with an admin user (admin@stackbilt.dev or equivalent). Admin check lives in src/lib/auth.ts::isAdmin.
GET /api/admin/cf/inventory
Purpose: List all Cloudflare resources (Workers, D1, R2, KV) owned by the configured account.
Auth: Admin-only
Success responses:
// 200 OK
{
configured: true,
inventory: {
workers: string[],
d1Databases: Array<{ name, uuid }>,
r2Buckets: string[],
kvNamespaces: Array<{ title, id }>,
counts: { workers, d1, r2, kv }
}
}
Graceful error: 200 OK { configured: false, message, inventory: null } when CF integration not configured.
Error responses: 401 | 403 not admin | 502 CF API error
Notes: Env vars: CF_ANALYTICS_TOKEN, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN (falls back to CF_ANALYTICS_TOKEN). Uses 4 parallel CF REST calls. KV-backed cache, 5-min TTL.
Source: src/pages/api/admin/cf/inventory.ts
GET /api/admin/cf/cost-estimate
Purpose: Estimate monthly Cloudflare cost from a 1–30 day worker-request sample.
Auth: Admin-only
Query params: days (optional, 1–30, default 7)
Success responses:
// 200 OK
{
configured: true,
estimate: {
period: string,
dailyRequests, monthlyRequests,
freeTier: Array<{ resource, used, limit, unit, pctUsed, overage, estimatedCost }>,
totalEstimatedMonthlyCost: number,
note: string
}
}
Source: src/pages/api/admin/cf/cost-estimate.ts
Notes: Only Worker Requests wired up; D1/R2/KV cost constants exist but CF billing API returns 403. 5-min KV cache per (accountId, days).
GET /api/admin/cf/forecast
Purpose: Cost forecast over 7/14/30-day rolling windows.
Auth: Admin-only
Success responses: 200 OK — { configured: true, forecast: { window7d, window14d, window30d } }
Notes: Fan-out to 3 GraphQL calls — 5-min internal KV cache prevents repeat work. MCP-layer rate limit is 5/min (not 10) due to expense.
Source: src/pages/api/admin/cf/forecast.ts
GET /api/admin/cf/worker-analytics
Purpose: Top 50 Workers by request volume over 1–30 days, with error counts and CPU time.
Auth: Admin-only
Query params: days (optional, 1–30, default 7)
Success responses:
// 200 OK
{
configured: true,
days: number,
workers: Array<{ scriptName, requests, errors, cpuTimeMs }>
}
Source: src/pages/api/admin/cf/worker-analytics.ts
Notes: GraphQL workersInvocationsAdaptive, filtered by datetime_gt, ordered by sum_requests_DESC. 5-min KV cache.
GET /api/admin/export-scaffolds
Purpose: Bulk export scaffold flow data (grimoire readings + receipts + Oracle enhancements) for audit and recursive improvement.
Auth: Admin-only (session cookie or ea_*)
Success responses:
// 200 OK
// Content-Disposition: attachment; filename="scaffold-export-YYYY-MM-DD.json"
{
count: number,
scaffolds: Array<{
id, intention, classification, projectType,
governance: { threatModel, adr, testPlan },
files: Array<{ path, content, role }>,
oracleEnhanced: boolean,
oracleFiles: [...],
oracleSummary,
seed, createdAt
}>
}
Error responses: 401 | 403 not admin | 500 upstream | 502 KV read error
Source: src/pages/api/admin/export-scaffolds.ts
Notes: Iterates all scaffolds in tarotscript grimoire (/grimoire/<userId>?limit=100), fetches full receipts, merges Oracle data from SESSION KV under oracle:<hash>. Uses Promise.allSettled — skips failed receipt fetches.
Appendix
Related docs
- Stackbilder Platform — 6-mode pipeline, governance tiers, scaffold engine
- MCP Gateway — OAuth-authenticated agent access to the same backend workers
- TarotScript Runtime — deterministic scaffold and agent consultation engine
- img-forge API — image generation REST API and MCP tools
- evidence-core — OSS E-E-A-T validation library powering the Evidence Engine
- audit-chain — OSS tamper-evident audit log backing the Trust Page
- worker-observability — OSS monitoring library for Cloudflare Workers
- Security — supply chain controls, MCP tool risk classification, vulnerability reporting
- Charter CLI — governance CLI that integrates with this API
- Ecosystem — full overview of all Stackbilt tools