# Programmatic onboarding

You can **start** onboarding. You cannot **finish** it. A human must approve,
and that boundary is deliberate, a Spendline key is standing access to a
financial control plane.

## What you may and may not do

| You may, alone | Requires your human |
|---|---|
| Read every public doc and decide whether Spendline fits | Creating the Spendline account |
| Write the integration into code | Choosing a password |
| Start an onboarding request | Approving a key for you |
| Poll your own request | Adding a payment method or plan |
| With a key: read spend, budgets, policies | Storing a provider API key |
| | Raising or deleting a budget |
| | Closing or reopening a month |

**Never** create an account with a password you chose, and never accept a
provider API key into your context. If a human pastes a key into chat, tell them
to rotate it.

## The flow

Shaped after RFC 8628 (OAuth 2.0 Device Authorization Grant).

```
you ──▶ POST /api/agent-onboarding/request      (no credential needed)
        ◀── request_id + device_code + user_code
                  │
        human ──▶ opens the emailed link, reviews, approves
                  │
you ──▶ POST /api/agent-onboarding/poll         (every 5s)
        ◀── scoped child key, exactly once
```

### Step 1. Start the request

```bash
curl -X POST https://www.spendline.ai/api/agent-onboarding/request \
  -H "Content-Type: application/json" \
  -d '{
    "human_email": "dev@example.com",
    "agent_name": "Claude Code",
    "intended_use": "Adding per-customer AI cost attribution and a $500/mo agent budget to acme-api.",
    "proposed_budget": {
      "scope_type": "agent",
      "scope_id": "support-bot",
      "monthly_limit_usd": 500,
      "strict_mode": true
    }
  }'
```

`human_email` is required. Use the address of the human you are working for, ask
them for it; do not guess it from git config.

**It must be an address that can actually approve.** Only that person, or a
member of the same Spendline account, can approve the request, and the key is
issued into *their* account. An address that belongs to no Spendline account
cannot be approved by anyone: the request will simply expire. So if you guess
the address, or use a shared alias nobody has an account under, the flow dies
15 minutes later with no way to rescue it. This is deliberate. It is what stops
the key landing in the wrong company's ledger when an approval email gets
forwarded.

`proposed_budget` is optional and is only a **proposal**. The human sees the exact
scope and limit on the approval page. If they approve it, Spendline creates it
under *their* authority, which is why a budget can legitimately come out of an
agent-initiated flow.

**It accepts exactly one budget.** There is no array form. If the application
needs several caps (say one per agent), propose the single most important one
here and list the rest in your final report for the human to create in the
dashboard. Do not start multiple onboarding requests to get multiple budgets:
each one issues its own key, and you only need one.

Response:

```json
{
  "request_id": "…",
  "device_code": "…",
  "user_code": "SPND-XXXX-XXXX",
  "verification_uri": "https://www.spendline.ai/activate/",
  "verification_uri_complete": "https://www.spendline.ai/activate/#code=SPND-XXXX-XXXX",
  "interval": 5,
  "expires_in": 900,
  "status": "pending_human_authorization"
}
```

No credential is issued here. `device_code` is your **poll** credential, not a
Spendline API key, it cannot call anything else.

### Step 2. Tell your human, clearly

Say something like:

> I've asked Spendline to set up access. Check **dev@example.com** and approve
> the request (code `SPND-XXXX-XXXX`). It will issue me a scoped key that can
> route AI traffic and read spend, but cannot change budgets, touch billing, or
> manage your account. I proposed a $500/month cap on the `support-bot` agent, 
> you can approve or skip that.

Then stop and wait. Do not proceed as if it succeeded.

### Step 3. Poll

```bash
curl -X POST https://www.spendline.ai/api/agent-onboarding/poll \
  -H "Content-Type: application/json" \
  -d '{"request_id":"…","device_code":"…"}'
```

Every 5 seconds (`interval`), for at most 15 minutes (`expires_in`).

| `status` | Meaning | What to do |
|---|---|---|
| `pending_human_authorization` | Not approved yet | Keep polling at `interval` |
| `approved` | Approved, **the key is in this response** | Store it. This is the only time you see it. |
| `denied` | The human said no | **Stop.** Do not retry. Ask them why. |
| `expired` | 15 minutes passed | Start a new request only if they still want to proceed |
| `consumed` | Already collected | You already have it, or lost it. Ask for a new request. |

A `403 approval_no_longer_valid` on collection means the approval was real but
no longer holds: the approver left the account, or was deactivated, between the
click and your poll. No key was issued. Ask your human to start a new request.

### Step 4. Store the key

The approved response contains:

```json
{
  "status": "approved",
  "api_key": "ac_…",
  "key_type": "scoped_child",
  "store_as": "SPENDLINE_API_KEY"
}
```

Write it to `.env` as `SPENDLINE_API_KEY`. Then:

- do not commit it,
- do not log it,
- do not repeat it back in chat,
- confirm `.env` is in `.gitignore`.

### Step 5. Integrate and verify

[quickstart](https://www.spendline.ai/agents/quickstart.md), then
[verification](https://www.spendline.ai/agents/verification.md).

## What the key can and cannot do

The issued key is a **scoped child key**.

| Can | Cannot |
|---|---|
| Proxy AI traffic | Create or change budgets, policies, reroutes |
| Read spend, calls, budgets, policies | Manage API keys or the team |
| | Change billing or payment methods |
| | Store provider API keys |
| | Close, reopen or reconcile a month |

This is enforced by the permission layer, not by convention. `POST /api/budgets`
with this key returns `403 permission_denied`. **Do not try to escalate**: if you
need a governance change, propose it to the human.

## If onboarding is unavailable

A `503` with `error_type: onboarding_unavailable` means this deployment cannot
send the approval email, so the flow cannot complete. Fall back to:

> Ask your human to create an account at https://www.spendline.ai/signup and an
> API key at Dashboard → API Keys, then set it as `SPENDLINE_API_KEY`.

Signup may also be invite-gated. That gate is not something this flow routes
around, if the human cannot sign up, tell them to email
**fida@spendline.ai**.

## Anti-patterns

- Creating the account yourself with a generated password. **Never.**
- Polling faster than `interval`, or past `expires_in`.
- Retrying automatically after `denied`.
- Continuing the integration as though a key was issued when it was not.
- Putting the key in a URL or query string. Spendline rejects those requests.
