# Connecting

Read this when you have no access token, or when the refresh chain has ended and
the user has to approve again.

Nothing here asks the user to paste a key. You start the flow, they approve it in
their own browser, and the tokens reach you on a channel they never see. The one
thing they carry by hand is eight digits.

**The invitation is not an API credential.** It is a claim check: it names the
buyer who has to be asked, and it can be spent once to ask them. It cannot run a
call, cannot read a balance or an identity, cannot be exchanged for a token, and
cannot create a second intent. Do not try to call anything with it, and do not
show it back to the user — they gave it to you, and an agent reciting a secret
is how a person learns that reciting secrets is normal.

## Ask them one thing

> What should this connection be called?

Suggest an answer so they can agree rather than compose one — this device, this
workspace, or your own name.

**That is the only question**, and the other three fields are yours to fill in:

| Field | Where it comes from |
| --- | --- |
| `runtime` | you already know what you are: `` |
| `purpose` | one line you write from what they have already told you |
| the link | sending it *is* the question "are you ready?" — asking first spends a turn to earn the right to send another |

They see all three on the approval screen, can edit the name there, and can
decline. Reading them back in chat first buys nothing.

## 1. Make a PKCE pair

43–128 characters of unreserved ASCII from a secure random source, and its
SHA-256 as base64url with no padding. Keep the verifier — **the eight-digit code
is worthless without it**, which is what makes eight digits safe for a person to
carry.

S256 only. `plain` is refused.

## 2. Create the intent

```bash
curl -s "https://api.apiosk.com/functions/v1/agent-gateway/v1/connections/intents" \
  -H "Authorization: Bearer <the apk_inv_… invitation they gave you>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "<their answer>",
    "runtime": "",
    "purpose": "<one line you write>",
    "client_id": "",
    "redirect_uri": "apiosk:oob",
    "code_challenge": "<S256 of your verifier>",
    "code_challenge_method": "S256"
  }'
```

`redirect_uri` is `apiosk:oob` unless you are genuinely listening somewhere: a
local port (`http://127.0.0.1:<port>/…`) or a real `https://` endpoint. Saying
`apiosk:oob` is better than improvising an address nothing will answer.

**Send no spending limit.** There is no field for one; the intent row has no
column to hold it. A request carrying `per_call_limit`, `daily_limit`,
`budget` or anything shaped like them is refused with a sentence saying so.

You get back a `verification_url`, good for **one hour**.

**If your request timed out and you never saw the answer, send exactly the same
request again.** An identical retry returns the same intent; a changed one is
refused, because the invitation is spent.

## 3. Give them the link

> **Open this and approve the connection:** \<verification_url\>
>
> You'll see what I'm asking for and set what I'm allowed to spend. Paste me the
> eight digits it shows you.

Nothing else. No paragraph in front of it, no explanation of OAuth or PKCE
unless you are asked, and no guess at what the limits will be — you do not know,
and they are about to choose.

## 4. Exchange the eight digits

Five minutes from the moment they saw them.

```bash
curl -s "https://api.apiosk.com/functions/v1/agent-gateway/v1/oauth/token" \
  -d grant_type=authorization_code \
  -d code=12345678 \
  -d code_verifier=<the verifier from step 1> \
  -d client_id= \
  -d redirect_uri=apiosk:oob
```

Spaces do not matter — `1234 5678` and `12345678` are the same code. Nothing
else about it is flexible: exactly eight, all digits, and a leading zero is a
digit like any other.

```json
{
  "access_token": "apk_access_…",
  "token_type": "Bearer",
  "expires_in": 86400,
  "refresh_token": "apk_refresh_…",
  "refresh_expires_in": 7776000
}
```

On `400 {"error": "invalid_grant"}`, read the description before doing anything:

- **`expired`** or **`already been used`** — spent, and no retry changes that.
  Start again from step 1 with a new verifier and a new invitation, and tell them
  plainly that it timed out. Never ask them to paste the same code again, and
  never ask them to hurry.
- **`too many incorrect codes`** — five wrong attempts ended this request. Start
  again from step 1.
- **anything else** — the request around the code was wrong, not the code. Check
  that `code_verifier` is the string this `code_challenge` was built from, and
  that `redirect_uri` is byte-for-byte what step 2 sent. Ask them to read the
  digits once more before assuming they mistyped.

## 5. Staying connected

The access token lasts **24 hours**; the refresh token lasts **90 days** and
rotates on every use.

Refresh on `401` with
`WWW-Authenticate: Bearer error="invalid_token", error_description="expired"`,
or shortly before `expires_in` runs out:

```bash
curl -s "https://api.apiosk.com/functions/v1/agent-gateway/v1/oauth/token" \
  -d grant_type=refresh_token \
  -d refresh_token=<your refresh token>
```

**Store the new refresh token before using the new access token, and keep no
second copy.** Presenting a rotated refresh token is treated as a stolen
credential: every token on the connection is revoked and the user has to approve
again. A client that keeps a backup of its last refresh token is the client that
trips this.

Ask a person to approve again only when the refresh token has expired, has been
revoked, or they disconnected Apiosk. Everything else is your own bookkeeping.

## 6. Then ask for data

The access token is also the credential for every data request, which goes to
Gateway v2 at `https://gateway.apiosk.com` — not to `https://api.apiosk.com/functions/v1/agent-gateway`, which only
connects you. `SKILL.md` has the workflow.
