# Pager API

Base URL: `https://a-gnt.com/page/api`

Everything is JSON. Authenticated endpoints take `Authorization: Bearer pgr_…`. The CLI, MCP server, and native apps are all thin clients over this surface — there is no privileged path.

## Connecting the iOS app

The complete app flow, in order:

### 0. Sign in with a-gnt (webview)

The simplest app login is a webview at `https://a-gnt.com/page/login?app=1`. The user signs in with their a-gnt account (or email/phone under "More sign-in options"); the webview finishes by navigating to `/page/login/success#token=pgr_…`. Intercept that navigation, read the token from the URL fragment (it never reaches server logs), store it in the Keychain, and dismiss the webview. Pager accounts are linked to a-gnt accounts (`agnt_user_id`) whenever a session or matching email exists.

Server-to-server equivalent: `POST /auth/agnt` with the a-gnt session cookie returns `{ token, account }`. `GET /auth/agnt/status` (cookie only) returns `{ session, linked, plan }` without minting anything — the a-gnt dashboard uses it for the dynamic Pager card.

### 1. Sign in with phone number

```
POST /auth/sms/start     { "phone": "+12025550123" }
→ 200 { "ok": true }                    # SMS code sent (6 digits, 10 min TTL)
→ 429 { "error": "rate_limited" }       # 3 sends per 15 min per number
→ 503                                   # Twilio not configured on the server

POST /auth/sms/verify    { "phone": "+12025550123", "code": "123456", "device_name": "Joseph's iPhone" }
→ 200 { "token": "pgr_…", "account": { "id": "acc_…", "phone": "+1…", "email": null, "plan": "free" } }
→ 401 { "error": "bad_code" | "expired" | "too_many_attempts" | "no_code" }
```

Store the token in the Keychain. It never expires; the account can mint and list tokens per device. Alternatively, a token minted on a laptop with `page token new ios` can be pasted into the app — same thing.

### 2. Register for push

Get the APNs device token from `didRegisterForRemoteNotificationsWithDeviceToken`, then:

```
POST /devices   { "platform": "ios", "token": "<hex device token>", "env": "prod", "name": "Joseph's iPhone" }
→ 201 { "device": { "id": "dev_…", … } }
```

Use `"env": "sandbox"` for debug builds — the server routes to `api.sandbox.push.apple.com` for that device. Re-registering the same token is an upsert, safe to do on every launch. `GET /devices` lists, `DELETE /devices/:id` removes.

**Server-side APNs setup (one-time, on the box):** put the `.p8` key from developer.apple.com in `~/Sites/pager/` and set in `.env`: `APNS_KEY_P8=/home/joseph/Sites/pager/AuthKey_XXXX.p8`, `APNS_KEY_ID`, `APNS_TEAM_ID`, `APNS_TOPIC=<bundle id>`, then `pm2 restart pager`. Delivery is skipped (not failed) until these exist.

### 3. Receive pages

The push payload:

```json
{
  "aps": {
    "alert": { "title": "The migration is ready. Deploy?", "body": "Tests passed. 14 files changed." },
    "sound": "default",
    "thread-id": "pager",
    "category": "PAGE_ACTIONS",
    "interruption-level": "time-sensitive"
  },
  "page": {
    "id": "pg_…",
    "type": "question",
    "url": null,
    "actions": [ { "id": "deploy", "label": "Deploy" }, { "id": "review", "label": "Review" } ],
    "priority": "high",
    "created_at": "2026-08-03T12:21:00Z"
  }
}
```

`category` is only present when the page has actions — register a `PAGE_ACTIONS` notification category and build `UNNotificationAction`s from `page.actions` (or use `content-available` + a Notification Service Extension to attach them dynamically). `interruption-level` is `time-sensitive` for high priority, `active` otherwise.

### 4. The inbox

```
GET  /pages?limit=25&status=open      → { "pages": [ … ] }
GET  /pages/:id                       → { "page": { … } }
POST /pages/:id/ack                   { "status": "seen" | "done" | "open" }
```

A page object:

```json
{
  "id": "pg_…", "type": "note|reminder|update|question",
  "title": "…", "body": "…", "url": null,
  "source": { "type": "cli", "name": "monday-draft-a1-nyc3", "user": "joseph" },
  "priority": "normal", "actions": null, "deliver_at": null,
  "delivered": { "at": "…", "push": { "ok": true } },
  "response": null, "responded_at": null,
  "status": "open", "created_at": "…"
}
```

### 5. Answer questions

```
POST /pages/:id/respond   { "action": "deploy", "note": "optional free text" }
→ 200 { "page": { …, "response": { "action": "deploy", "label": "Deploy", "note": null, "at": "…" } } }
```

The moment this lands, any terminal blocked on `page ask` (or an agent blocked on `pager_ask`) unblocks with the answer. Call it from the notification action handler so a long-press answer works without opening the app.

## Creating pages (agents, CLI, CI)

```
POST /pages
{
  "title": "required, ≤200 chars",
  "body": "≤8 KB", "url": "…",
  "type": "note|reminder|update|question",     // optional, inferred otherwise
  "priority": "low|normal|high",
  "choices": ["Ship", "Review"],               // shorthand → actions
  "actions": [{ "id": "ship", "label": "Ship" }],
  "deliver_at": "ISO 8601",                    // omit = deliver now
  "channels": ["push", "email", "sms"],        // omit = sensible default
  "source": { "type": "ssh", "name": "host", "session": "tmux:x" }
}
→ 201 { "page": { … }, "delivery": { … } | null }
```

Long-poll for an answer (25 s max per request, loop it):

```
GET /pages/:id/response?wait=25
→ 200 { "response": null } | { "response": { "action": "…", … }, "responded_at": "…" }
```

## Account and tokens

```
GET   /me                        → account, device count, token list (names only)
PATCH /me                        { "email": …, "name": …, "phone": … }
POST  /tokens                    { "name": "ios" } → { "token": "pgr_…" }   # shown once
```

## Billing (Pro, $19.99/mo)

```
GET /billing/checkout?account=acc_…   → 302 to Stripe Checkout
```

In-app purchase, for the iOS binary (App Store rules make linking out to Stripe
from inside the app a violation — the app sells through StoreKit instead):

```
POST /billing/apple   { "signed_transaction": "<jws>", "product_id": "com.agnt.page.pro.monthly" }
→ 200 { "account": { …, "plan": "pro" } }
→ 400 { "error": "could not verify that purchase" }
→ 409 { "error": "that subscription is already linked to another account" }
→ 503 { "error": "apple billing not configured" }      # no root cert / bundle id
```

`signed_transaction` is StoreKit 2's `VerificationResult.jwsRepresentation`. The
server verifies the x5c chain to Apple's root, checks the leaf signature over the
payload, and enforces `bundleId`, `productId`, revocation, and expiry before the
plan moves. Verification is offline — no shared secret, no App Store round trip.
Set `APPLE_ROOT_CA`, `APPLE_BUNDLE_ID`, and `APPLE_PRODUCT_ID`; without them the
endpoint returns 503 rather than trusting anything.

Web checkout (Stripe) is unchanged:

Pass the account id so the webhook can attribute the subscription (`client_reference_id`). On `checkout.session.completed` the plan flips to `pro` (SMS page delivery unlocks); on subscription cancellation it flips back. Webhook: `POST /stripe/webhook`, signature-verified.

## Delivery semantics

- No `deliver_at` → delivered synchronously on create; the response includes per-channel results.
- With `deliver_at` → a scheduler sweeps every 15 s for due pages.
- **Push is a Pro feature.** Default channel: push to registered devices for Pro accounts; email otherwise (or when there are no devices). SMS is never a default — request it via `channels`, requires Pro + a phone on the account. Free accounts always get email delivery; the in-app inbox works on every plan.
- Unconfigured channels are recorded as `skipped`, never `failed`. A page is always stored regardless — the inbox is the source of truth, delivery is best-effort on top.
- APNs `410 Unregistered` prunes the device automatically.

## Sync (Apple / Google)

Pager owns the message; Reminders/Tasks/Calendar are one-way destinations, never the data model.

- **Android push (server-side, live):** `POST /devices` with `platform: "android"` and an
  FCM registration token. Delivery goes through the FCM HTTP v1 API; set
  `FCM_SERVICE_ACCOUNT_JSON` to the service-account key path. The `data` block
  carries the same page fields the iOS payload does (FCM requires string values,
  so `actions` arrives JSON-encoded). Questions route to a high-importance
  `questions` notification channel, everything else to `pages`. Unconfigured =
  skipped, never failed, exactly like APNs.

- **Apple (iOS app, on-device):** implement "Save to Reminders" and "Add to Calendar" page actions with EventKit. No server involvement, no OAuth — map `title` → title, `body` → notes, `deliver_at` → due date/event start, and write the `pg_` id into the Reminder notes so the app can show "saved" state.
- **Google (server-side, roadmap):** per-account OAuth connection; reminders mirror to Google Tasks, scheduled pages to Calendar. Not yet implemented — no endpoints exist for it today, so don't build against it.

## Errors

Flat and boring: `4xx/5xx` with `{ "error": "human-readable reason" }`. No error codes to memorize yet.
