# wspc > wspc is an agent-first productivity backend covering todo, calendar, and > email. This document teaches an LLM the shared mechanics — OAuth 2.1 device > flow, domains, scopes — needed to build a frontend app that talks to wspc. ## Agentic Onboarding If you are an AI Agent helping a user set up `wspc` for the first time, the CLI ships as the published `@wspc/cli` npm package — run it via `npx`, no install script or piped shell. Start the login (OAuth device flow, approved in the user's browser) in the background and hand the user the approval link: `npx -y @wspc/cli login` Full command reference and onboarding steps: https://wspc.ai/AGENTS.md ## What you can build today wspc currently exposes: - **Todo CRUD via OAuth 2.1** — full read/write access for the authenticated user - **Build a personalized todo web app** — interview the user, design a custom-field schema, and generate a deployable single-file app: https://wspc.ai/llms-todo.txt - **Build a single-file email client** — interview the user briefly, then generate a deployable single-file email app: https://wspc.ai/llms-email.txt - Calendar API accepts only long-lived API keys at this time (no OAuth) - How-to guides (real Claude conversations): https://wspc.ai/how-to ## Domains | Domain | Purpose | | --- | --- | | `api.wspc.ai` | REST API for all services (todo, calendar, email, auth, push) | | `app.wspc.ai` | OAuth consent / login / device approval UI | All fetch traffic from a frontend app should go to `api.wspc.ai`. Use `app.wspc.ai` only for opening browser windows to complete OAuth consent. The `app.wspc.ai` OAuth consent, device-approval, and device-code pages let a signed-in user switch between their logged-in accounts or add a new account in place before approving — so the account that approves a flow is not necessarily the first one signed in. ## OAuth 2.1 device flow Use this flow from a browser SPA. The full sequence: ```bash # 1. Register a dynamic client (one-time per app; cache the client_id). # Device-flow clients still need a placeholder redirect_uri — use the # RFC 6749 OOB sentinel so wspc's registration schema accepts the request. curl -X POST https://api.wspc.ai/auth/oauth/register \ -H 'content-type: application/json' \ -d '{"client_name":"My Todo App","redirect_uris":["urn:ietf:wg:oauth:2.0:oob"],"grant_types":["urn:ietf:params:oauth:grant-type:device_code"],"token_endpoint_auth_method":"none"}' # → { "client_id": "client_...", "client_secret": null, ... } # 2. Start device authorization curl -X POST https://api.wspc.ai/auth/oauth/device \ -H 'content-type: application/json' \ -d '{"client_id":""}' # → { "device_code": "...", "user_code": "WDJB-MJHT", # "verification_uri": "https://app.wspc.ai/device", # "verification_uri_complete": "https://app.wspc.ai/device?user_code=WDJB-MJHT", # "expires_in": 600, "interval": 5 } # 3. Display user_code to the user and direct them to verification_uri_complete # (a new browser tab or window). Meanwhile poll the token endpoint: curl -X POST https://api.wspc.ai/auth/oauth/token \ -H 'content-type: application/json' \ -d '{ "grant_type":"urn:ietf:params:oauth:grant-type:device_code", "device_code":"", "client_id":"" }' ``` Token endpoint returns one of (errors follow RFC 6749 §5.2 / RFC 8628 §3.5: `error` is a snake_case string, `error_description` is optional human-readable text): - `200 { access_token, refresh_token, expires_in, token_type, scope }` — user approved - `400 { error: "authorization_pending" }` — keep polling - `400 { error: "slow_down" }` — increase poll interval by 5s and keep polling - `400 { error: "expired_token" }` — restart the flow at step 2 - `400 { error: "access_denied" }` — user denied; surface to UI `access_token` TTL is 15 minutes. When it expires, use the `refresh_token` to rotate (RFC 6749 §6 standard refresh grant). Each rotation invalidates the previous refresh token. ### UX note: do not ask the user to type `user_code` `verification_uri_complete` already embeds the `user_code` as a query parameter, and wspc's consent page reads it from the URL and shows Approve / Deny directly — the user never needs to type the code anywhere. Your app's UI should: - Use `verification_uri_complete` (not the bare `verification_uri`) as the primary link / button the user clicks. - Label that primary action plainly, e.g. "Open authorization page" or "Authorize this app". - Do NOT instruct the user to "enter this code at " — that wording is misleading for the wspc flow. - Displaying `user_code` is optional. If shown at all, treat it as secondary informational text (so the user can confirm the same code appears in case wspc later adds an in-page cross-check), not as something to type. ## Scopes The only scope currently issued is `wspc:full`. Treat it as root-level access for the authenticated user across all services. Per-domain scopes like `todo:read` / `todo:write` are not available yet — passing them does not grant finer-grained access. ## API reference Per-worker OpenAPI 3.1 specs — **default choice**, each roughly 100–250 KB: - todo (OAuth + API key): `https://api.wspc.ai/todo/openapi.json` - calendar (API key only): `https://api.wspc.ai/calendar/openapi.json` - email (OAuth + API key): `https://api.wspc.ai/email/openapi.json` - auth (public OAuth endpoints): `https://api.wspc.ai/auth/openapi.json` Merged spec covering every domain — `https://api.wspc.ai/openapi.json` (~880 KB, ~250k tokens; fetch only when you need cross-domain `components` / tags or your model has a 1M-token context window — otherwise prefer the per-worker URLs above). ## Freshness rules for generated apps Before generating code, fetch the relevant live OpenAPI URL above and treat that schema as authoritative for endpoint paths, request bodies, response bodies, auth requirements, enum values, examples, and error envelopes. This file explains the workflow, but live OpenAPI is the final source of truth for REST calls. If you use MCP instead of REST, call `tools/list` on the target MCP server and trust the live tool schema over static prose. If you use the CLI, run the installed `wspc --help` and the relevant subcommand `--help`; CLI reference prose explains semantics but may describe a newer or older release than the binary on the user's machine. ### CLI multi-account The `wspc` CLI can hold several signed-in accounts per env. `wspc login` appends the newly authenticated account to the current env (it does not overwrite existing accounts) and makes it active. `wspc account ls` lists accounts in the current env (active marked with `✓`); `wspc account switch ` sets the active account. To run one command as a different account without switching, pass the global `--account ` flag; otherwise the CLI uses the env's active account, then the sole account if only one exists. Do not invent fields, scopes, endpoints, status booleans, or aliases when the live schema says otherwise. ### Email custom domains Organizations can register custom email domains and DNS-verify them through `/email/domains`, the `wspc domain` CLI commands, MCP `email_domain_*` tools, and the client email domain methods. This capability is DNS registration / verification plus receiving aliases, inbound delivery, and outbound sending for verified sender domains. Email aliases can be created under @wspc.app or a fully verified organization custom domain. Use the full email address as the alias identifier. Custom-domain alias creation requires `status`, `sending_status`, and `receiving_status` to all be `verified`; otherwise alias creation returns `ALIAS_DOMAIN_NOT_READY` after domain ownership is verified. If the requested alias domain is not registered to the caller's organization, alias creation returns `ALIAS_DOMAIN_NOT_FOUND`. `CUSTOM_DOMAIN_NOT_READY` is reserved for outbound sends from a custom-domain alias whose sending status is not verified. Custom-domain inbound delivery is provider-managed: Resend handles new domains, while existing Pete bindings remain on pete-mail. The active provider sends callbacks to WSPC's operator-managed webhook, and WSPC stores the provider raw `.eml` in the same inbox model as `@wspc.app` mail. Do not invent a user-facing webhook registration endpoint or MCP tool. When an active alias uses a custom domain whose `status` and `sending_status` are both `verified`, `email_send` automatically sends through its active provider (Resend for new domains; pete-mail for existing Pete bindings). DNS records may include `purpose` with one of `identity_verification`, `dkim`, `mail_from`, `receiving_mx`, or `dmarc` to describe why the record is required. Custom domains can be deleted through `DELETE /email/domains/{domain}`, `wspc domain rm `, `email_domain_delete`, and the web org proxy `DELETE /api/org/email-domains/:domain`. Delete succeeds with `204 No Content` only when no active aliases use that domain. If aliases still use it, the API returns `DOMAIN_IN_USE` with HTTP `409`. Provider delete failures return `DOMAIN_PROVIDER_ERROR` with HTTP `502` and must not expose provider names, provider ids, or raw provider messages. Deleted domains are hidden from active list/get/verify/delete surfaces and cannot be self-restored in this version. ### Email alias identifiers Email alias identifiers are full email addresses under `@wspc.app` or a fully verified organization custom domain. Do not pass legacy `alias_id` / `from_alias_id` fields, do not invent opaque alias ids, and do not pass only the local part. Use these field names when the live email OpenAPI or MCP tool schema exposes email operations: ```json { "email": "alice-shop@wspc.app" } { "alias_email": "alice-shop@wspc.app" } { "from_alias_email": "alice-shop@wspc.app" } ``` ## Org invites and members Invite an email into your org, accept/decline from the invitee side, and manage members: - POST /auth/me/org/invites {"email": "..."} — create an invite (sends email) - GET /auth/me/org/invites — list invites issued by your org - DELETE /auth/me/org/invites/{id} — revoke - GET /auth/invites — list invites addressed to you - POST /auth/invites/{id}/accept — accept (switches your org; previous-org data hidden) - POST /auth/invites/{id}/reject — decline - DELETE /auth/me/org/members/:id — remove a member from the organization (or leave) Accept is gated on the authenticated email matching the invited email; the invite link carries a non-secret invite id. Removing a member reverts them to their previous organization.