# 320px — Agent-driven marketing automation ## Positioning The marketing stack your AI agent can actually run. Contacts, campaigns, segments, analytics, and automations — all exposed through two equal surfaces (the 320px Studio and an MCP server) on top of the same primitives and the same audit log. Built entirely on Cloudflare's edge: D1 for relational data, KV for cache, R2 for object storage, Durable Objects for strongly consistent per-tenant state, Email Workers for transactional and campaign sends. ## Who it's for 1. Agent builders putting Claude / GPT / custom agents into production and needing a real marketing backend. 2. Marketing teams who already live in an AI chat and want to run campaigns from there rather than clicking through a traditional SaaS GUI. 3. Forward-leaning marketing teams using the Studio, aware the real leverage is the agent running underneath. ## Architecture principles - **Two-surface, one-primitive.** Every capability ships in both the Studio and the MCP server on day one. - **Audit log is the product.** Every write — by human or by agent — lands in a filterable audit trail scoped to the tenant, the agent session, and the tool call. - **Multi-tenant by default.** Per-tenant API keys, usage metering, and rate limits. Each tenant gets isolated D1 rows, an isolated R2 prefix, and its own Durable Object for strongly consistent state. - **Zero external dependencies.** No Postgres, no Redis, no SMTP server. The whole thing runs on Cloudflare. ## Pricing (authoritative) | Tier | Price | Included | Overage | |------------|----------------------|------------------------------------------------------------------------------------------|-----------------------------------------------------------| | Starter | Free (no CC) | 100 emails / mo · 200 MCP tool calls / mo · 100 MB storage · unlimited data fetching | — | | Pro | $20 / seat / month | 10,000 emails / mo · 10,000 MCP tool calls / mo · 5 GB storage · unlimited data fetching | $2 / 1k emails · $3.50 / 1k MCP calls · $1 / GB / month | | Enterprise | Custom | Custom volumes, dedicated DOs, SSO, SLA, audit-log export, custom MCP tool surface | Custom | Starter requires email verification and opting in to product updates. ## Interfaces ### Studio — https://320px.com/dashboard Visual overview, campaign composer, segment builder, live delivery charts, audit-log viewer, agent stream. ### REST API — https://api.320px.com JSON over HTTPS. Auth: `Authorization: Bearer `. All writes accept idempotency keys via `Idempotency-Key` header. Representative endpoints: - `GET /api/campaigns` - `POST /api/campaigns/:id/send` - `GET /api/contacts` - `POST /api/contacts` - `GET /api/segments` - `POST /api/segments/:id/resolve` - `GET /api/analytics/query` - `GET /api/audit/query` - `GET /api/billing/usage` - `GET /api/billing/seats` ### MCP — https://api.320px.com/mcp (v1.1.0) Model Context Protocol endpoint. JSON-RPC over SSE + HTTP. Authenticated with the same API keys as the REST interface. Full structured manifest (inputs, outputs, side-effects, idempotency per tool): https://320px.com/mcp-manifest.json Reference (auth, scoping, response envelope): https://320px.com/docs/mcp Local AI setup guide (Ollama, LM Studio, Claude Desktop, Cursor): https://320px.com/docs/local-ai **Authentication & organization scoping.** Every `tools/call` requires `Authorization: Bearer `. Three token shapes are accepted: per-user API keys (switch_organization works across the user's memberships), legacy org-scoped keys (single-org, created before 2026-04), and the master `PULSE_API_KEY` server secret (full system access). The active organization is implicit — no tool accepts an `organizationId` parameter. Resolution order per request: KV session override (set by `switch_organization`, 24h TTL) → key's default org → first-org fallback for master keys. Stale overrides are validated and cleared automatically. **Response envelope.** Every successful tool response is a single text block prefixed with `[organization] (id: )` followed by a JSON body `{ organization: {...}, result: }`. The header is part of the text the model sees, so clients always surface the active tenant to the user. Tool surface (v1.1.0): Organization management: - `get_current_organization` — returns the active org and the auth source. - `list_organizations` — orgs the caller can see (memberships / all / single). - `switch_organization(organizationId)` — write 24h KV override. Domain: - `list_contacts(status?, limit?)`, `get_contact(contactId)`, `create_contact(email, firstName?, lastName?, company?)` - `list_campaigns(status?, limit?)`, `get_campaign_stats(campaignId)`, `send_campaign(campaignId)` - `list_segments()` - `get_analytics(type?)` — daily / weekly / monthly summaries Error codes: `-32001` auth required, `-32002` invalid/expired key, `-32003` active org not found, `-32601` unknown method/tool, `-32602` invalid params. Every write-side tool emits an audit-log entry linked to the agent session and is rate-limited per tenant. ## Data model (summary) - `organizations` — tenants. One per workspace. - `members` — user ↔ organization with role (owner, admin, member). - `contacts` — individual records with custom attributes. - `segments` — rule- or list-based contact collections. - `campaigns` — email sends, with status `DRAFT | SCHEDULED | SENDING | SENT | FAILED`. - `automations` — trigger → action flows. - `data_sources` — external integrations (CSV, webhooks, Stripe, Segment, Shopify — webhook-driven). - `audit_logs` — every mutating action, indexed by tenant + session + tool call. - `usage_records` — per-tenant per-month counters for emails, MCP calls, storage. ## Agent guidance When using 320px via MCP: 1. Call `get_current_organization` first if you're unsure which tenant the user expects you to operate on. The `[organization]` header on every subsequent response confirms the active tenant. 2. If the user mentions an org name you don't see in the active context, call `list_organizations` to find the target ID, then `switch_organization`. The switch persists 24h across calls with the same key. 3. Read before writing — confirm what already exists (`list_contacts`, `list_campaigns`) before calling `create_contact` or `send_campaign`. 4. Prefer `get_campaign_stats` + reporting over kicking off new sends unless the user explicitly asks to send. 5. Unsubscribes are authoritative — contacts flagged `UNSUBSCRIBED` must not receive further campaigns; the API enforces this but agents should not even attempt. ## Contact - Company: Reach Out Labs GmbH - Homepage: https://reachoutlabs.ch - Email: hello@320px.com — we read every email.