Anything a human can do, the API can do.
REST under /api/v1/*, zod-validated, session-authenticated, rate-limited. The same surface the dashboard uses — and the same surface the Claude agent calls through.
Overview
The Admaxxer public API is a session-authenticated, zod-validated REST surface mounted at /api/v1/*. Every route is workspace-scoped, encrypted at rest, and rate-limited. Payloads are JSON; errors return structured bodies with codes and next-step hints.
The same routes serve three consumers: the React dashboard, the Claude agent's tool layer, and (in v1.5) external API key holders. There is no separate "internal" or "agent-only" surface — anything the human dashboard can read or write, the agent can read or write under the same authorization.
Authentication today is the session cookie. Bearer-key auth lands with the API-key milestone in v1.5; the handler layer is already key-aware, so the public-API migration will be transparent.
Authentication
Session cookie (v1)
HTTP-only, Secure in production, SameSite=Lax so cross-site CTAs from marketing pages still attach the cookie while CSRF on POST routes is blocked. Issued by the magic-link, Google OAuth, and Apple OAuth flows. Default lifetime is 30 days, controlled by SESSION_MAX_AGE_DAYS.
Every /api/v1/* handler reads the session, looks up workspaceId and member role, and rejects with 401 when missing. The session table is the single source of truth — there is no separate JWT or stateless token in v1.
API keys (v1.5 roadmap)
Bearer-token authentication via Authorization: Bearer <key>. Workspace-scoped, rotateable from Settings, audited per call. The apiKeys table is already in the schema (key hash, prefix, name, expiresAt, lastUsedAt). Migration path will be transparent — same /api/v1/* routes, just an additional auth layer in the middleware stack.
Endpoints
All routes are session-auth, zod-validated, and rate-limited. All payloads are JSON. All responses use snake_case for keys that round-trip to platform APIs (campaign_id, daily_budget) and camelCase for Admaxxer-internal keys (workspaceId, createdAt).
Connections
CRUD over encrypted Meta + Google credentials. Tokens are sealed with AES-256-GCM at rest and never logged. All routes require session auth and resolve to the active workspace.
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/connections |
Create a new ad-platform connection. Body accepts the long-lived Meta user token or a Google OAuth refresh token + developer token. Response returns connection ID and a status probe. |
GET |
/api/v1/connections |
List connections in the active workspace. Returns id, platform, status, expiry, last-sync timestamp, and decrypt-readable boolean. |
DELETE |
/api/v1/connections/:id |
Soft-delete a connection. Triggers a meta-purge job for the 7-day data deletion SLA on Meta-side data. |
POST |
/api/v1/connections/:id/test |
Force a credential probe — calls a single read endpoint on the platform and returns status. Useful when the user has just rotated a token. |
Campaigns
Read and mutate campaigns on the active workspace's connections. Mutations are gated by the platform-side rate limit and audited to ad_sync_logs.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/ads/campaigns |
List campaigns. Query params: platform, connectionId, status. Uses the 15-minute insights cache where possible. |
POST |
/api/v1/ads/campaigns |
Create a new campaign. Body validates objective, daily_budget, name, and status (default PAUSED). Same surface the agent uses. |
PATCH |
/api/v1/ads/campaigns/:id |
Partial update — name, status, daily_budget. Whatever you don't send stays untouched. |
DELETE |
/api/v1/ads/campaigns/:id |
Pause or archive a campaign depending on platform semantics. Always reversible from the dashboard. |
Insights
The metrics layer. Backed by the 15-minute ad_sync_logs cache and platform APIs as fallback.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/ads/insights |
Aggregated metrics (spend, impressions, clicks, conversions, ROAS, CTR, CPC). Params: platform, since (ISO date), until (ISO date), campaignId (optional), breakdown ('day' | 'campaign' | 'platform' | omitted). |
Chat
Streaming SSE endpoints that wrap the Claude agent. Same routes the in-product chat panel uses.
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/ads/chat |
Send a user turn to the ad-ops agent. SSE response with text deltas, tool_use events, confirm_required envelopes, and a final usage block. |
POST |
/api/v1/analytics/chat |
Read-only analytics chat (the ⌘J surface). Restricted to PIPE_ALLOWLIST tools; cannot mutate ads. |
Rate limits
| Surface | Limit | Window | Notes |
|---|---|---|---|
| Connections (CRUD) | 30 req | per minute, per user | Burst-tolerant — protects against paste-loop bugs. |
| Campaign mutations (POST / PATCH / DELETE) | 60 req | per minute, per workspace | Co-shared across all members of a workspace. |
| Chat (POST /chat) | 20 req | per minute, per user | One request equals one user turn regardless of internal tool iterations. |
Underlying platform limits. Meta long-lived user tokens cap at roughly 200 calls per hour. Google Ads API sits at 15,000 ops per day on the default developer-token tier. Admaxxer's per-user limits sit well below these — but be aware of them when designing high-volume workflows. A 429 from /api/v1/* can come from either layer; the body's retryAfter field tells you how long to wait.
Webhooks
Inbound — Stripe
Endpoint: /api/stripe/webhook. Signature verified with STRIPE_WEBHOOK_SECRET. Drives the billing mirror — checkout, subscription, invoice, and charge events. Metadata keys are snake_case across the wire (user_id, plan_key, ga4_client_id, utm_source, etc.) — see CLAUDE.md GL#41 for the canonical key list.
Outbound — Roadmap (v1.5)
Customer-facing outbound webhooks (campaign-state-changed, daily-summary, anomaly-detected) are scoped for v1.5. The webhooks table is in the schema. The delivery loop and HMAC signing land alongside the API-key milestone — both target the same v1.5 release.
Architecture
Server
TypeScript strict + Express. Drizzle ORM over Postgres on Neon (single canonical URL — see CLAUDE.md). Tinybird hosts 33+ analytics pipes (visitors, revenue, MER, LTV, cohorts, MMM, forecast, incrementality). Anthropic SDK (@anthropic-ai/sdk) powers the Claude agent. Stripe SDK handles billing. BullMQ workers run on Upstash Redis with ioredis (maxRetriesPerRequest:null, handled retryStrategy, shared connection across queues — see GL#67).
Client
React 18 + Vite. TailwindCSS + shadcn/ui (radix primitives) + framer-motion + Recharts. wouter for routing. react-helmet-async for per-route meta. TanStack Query for server state. The motion vocabulary on landing.tsx is the canonical reference for premium UI elsewhere in the product.
Storage
Postgres on Neon (one canonical URL, see CLAUDE.md). Migrations are idempotent raw SQL (CREATE TABLE IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, INSERT ... ON CONFLICT DO UPDATE) executed via a pg Pool, or via drizzle-kit push. Token storage uses AES-256-GCM via server/crypto.ts; ENCRYPTION_KEY rotates without reissuing connections.
Edge
SSR_MODE='crawlers-only' middleware. The crawler-detector sniffs the User-Agent; SSR templates render full semantic HTML for AI ingesters; humans get the SPA. sitemap.xml + robots.txt + llms.txt are published from server/.
Background jobs
BullMQ workers backed by Upstash Redis. All jobs are idempotent — re-queue is safe.
meta-token-expiry— runs daily at 09:00 UTC. Scans connections whose Meta long-lived token expires inside the 7-day window and emails the owner with a re-paste link. Cron window deliberately wide so a single bad day doesn't silently expire a token.meta-purge— on-demand with a 7-day SLA. Deletes data for users who exercise the Meta data-deletion right. Triggered by inbound Meta webhook or byDELETE /api/v1/connections. Runs idempotently across runs.trialExpirySweep— runs hourly. Closes trial sessions and downgrades workspaces back to Starter when the trial window elapses. Idempotent — safe to run on a backlog without double-charging anyone.
Caching
- Ad insights — 15-minute TTL.
ad_sync_logs(cacheKey)keyed by (workspace, platform, query-shape, date range). Hits return immediately; misses fan out to Meta or Google. - Google access tokens — 50-minute in-memory TTL. Just under the 60-minute platform TTL; avoids a refresh round-trip on every campaign call. Refresh kicks in on miss.
- Anthropic prompt cache — ephemeral (~5 minutes). System prompt and
tools[]sent withcache_control: { type: 'ephemeral' }. Target hit rate >80% across an active session.
SSR & crawlability
Admaxxer ships SSR templates for every public page so AI crawlers see full semantic HTML instead of an empty SPA shell. Detection is User-Agent based; rendering is handled by server/ssr/middleware.ts with SSR_MODE = 'crawlers-only'. Never change to 'all' — that breaks the entire site UI for human visitors.
Allowed crawlers
GPTBot, ClaudeBot, PerplexityBot, DeepSeek, Googlebot, Bingbot, FacebookBot, and the long tail. See server/robots.ts for the canonical Allow list.
Discovery surfaces
/sitemap.xml — every public page with priority and hreflang. /llms.txt — structured content index for AI ingesters. /robots.txt — explicit crawler policy.
Adding a new public page requires updating the route-matcher, the SSR template, the robots Allow list, the sitemap, and the llms.txt index. The crawler detector itself does not need changes for new routes — only for new bots.
Errors & status codes
| Code | Meaning | Body / next step |
|---|---|---|
401 |
Unauthorized | Session cookie missing or expired, or bearer API key invalid. Re-auth via /login or rotate the key. |
402 |
Quota exceeded | Plan-level monthly cap hit. Body includes upsellUrl pointing at /pricing for the in-product upgrade flow. |
422 |
credentials_unreadable | AES-GCM decrypt failed — token bytes are corrupted or the encryption key has rotated. Prompt the user to reconnect via /integrations. |
429 |
Rate-limited | Either a local Admaxxer limit or an upstream Meta/Google limit. Body includes retryAfter in seconds. |
Every error body includes code and message at minimum. Quota errors include upsellUrl. Rate-limit errors include retryAfter in seconds. Decrypt errors include connectionId so the UI can deep-link the user to the reconnect modal.
Observability
- Token usage.
inputTokens,outputTokens, andcacheReadTokenspersist perchat_messagesrow. Aggregates roll up to the workspace billing dashboard so users can see exactly what their AI usage costs. - Audit trail. Every
executeToolinvocation writes one row toad_sync_logs(action, status, errorMessage, payload). Source of truth for "what did the agent actually do". - Error tracking. Sentry receives redacted exceptions. Token bytes are stripped before send; PII is filtered at the SDK layer.
- Postbuild canaries.
scripts/verify-pipes-pushed.tsbanner-warns when a Tinybird SQL change ships withoutnpm run tb:push. - Health endpoints.
/healthzreturns 200 when DB + Redis + Tinybird are reachable; failures fall through to 503 so the load balancer drains the instance.
Frequently asked
- Is there a public API key flow?
- Not in v1. All /api/v1/* calls are authenticated via the session cookie that the dashboard uses. A bearer-key flow (workspace-scoped, rotateable, audited) is on the v1.5 roadmap. The handler layer is already key-aware so the migration will be transparent.
- What's the underlying rate limit on Meta and Google?
- Meta long-lived user tokens: roughly 200 calls per hour. Google Ads API: 15,000 ops per day on the default developer-token tier. Admaxxer's per-user rate limits sit well below these so a single user almost never hits a platform-side throttle.
- How long do session cookies last?
- Default 30 days, controlled by SESSION_MAX_AGE_DAYS. Cookies are HTTP-only, Secure (in production), and SameSite=Lax to permit cross-site CTAs from marketing pages while blocking CSRF.
- Are insights cached?
- Yes. Each (workspace, platform, query-shape, date range) tuple is cached for 15 minutes via ad_sync_logs(cacheKey). Google access tokens are cached in-memory for 50 minutes — just under their 60-minute TTL — to avoid a refresh round-trip on every campaign call.
- How do I subscribe to outbound webhooks?
- Outbound webhooks are not in v1. Stripe webhooks are inbound (we receive payment events at /api/stripe/webhook). Customer-facing outbound subscriptions (campaign-state-changed, daily-summary, anomaly-detected) are scoped for v1.5.
- Where does observability data go?
- Token usage persists per chat message. Tool side-effects audit to ad_sync_logs. Error tracking goes to Sentry with redacted payloads. Raw token bytes never leave the encryption boundary.
- What's the SSR mode?
- crawlers-only. The middleware sniffs the User-Agent — if it's a known crawler (GPTBot, ClaudeBot, PerplexityBot, Googlebot, etc.) we render the SSR template; otherwise the SPA hydrates normally. SSR_MODE = 'crawlers-only' — never change to 'all', it breaks the entire site UI.
Build something
Read the agent reference next — same routes, different consumer. The Claude agent calls the same /api/v1/* endpoints you do; its tools wrap them in a tool-use loop with per-step server-side authorization.