# clip — API & Agent Reference

Ephemeral URL service. Upload content, get a short-lived link. Both
humans and agents are first-class clients; this document is the
single source of truth for the HTTP surface and is reachable at:

- `https://clip.rac.so/api` (canonical)
- `https://clip.rac.so/AGENTS.md` (alias, case-insensitive)
- `https://clip.rac.so/llms.txt` (curated index — points back here)

Base URL: `https://clip.rac.so`

## Quick orientation

- **Humans**: visit the website, sign in with GitHub or Google, mint
  an API key from the dashboard, and use it as a Bearer token.
- **Agents**: walk the [device flow](#device-flow) below to provision
  yourself an API key on the operator's behalf — no human credential
  ever passes through you.
- **MCP-speaking clients (Claude Desktop, Cursor, Zed, …)**: install
  the [`@rac.so/clip-mcp`](https://www.npmjs.com/package/@rac.so/clip-mcp)
  stdio server and clip becomes a native tool. Auth is still via a
  bearer token you mint once with the device flow.
- **Either**: every authenticated request needs
  `Authorization: Bearer clip_<key>`. Keys never expire on their own;
  revoke them from the dashboard or via API.

---

## Device flow

RFC 8628 OAuth Device Authorization Grant. Designed for the case
where an agent is acting on behalf of a human who has a browser
nearby. The agent never sees the operator's IdP credentials; the
operator never sees the agent's machine. Three actors, four steps:

### 1. Agent: request a code

```
POST /auth/device
Content-Type: application/json

{"agent_label": "claude-code"}
```

`agent_label` is optional; if provided it ends up in the issued key's
label so the operator can recognise it later.

Response (200):
```json
{
  "device_code":               "<opaque, ~43 chars, base64url>",
  "user_code":                 "ABCD-EFGH",
  "verification_uri":          "https://clip.rac.so/device",
  "verification_uri_complete": "https://clip.rac.so/device?user_code=ABCD-EFGH",
  "expires_in":                600,
  "interval":                  5
}
```

Save the `device_code`. Print the `verification_uri` and `user_code`
to the operator. The `verification_uri_complete` form is convenient
when you can render a clickable link (or a QR code).

`POST /auth/device` also accepts `application/x-www-form-urlencoded`
for off-the-shelf OAuth clients.

### 2. Operator: visit the URL and authorise

The operator visits `verification_uri`, types the `user_code`, picks a
sign-in provider, completes its flow, and lands on `/device/success`.
clip resolves the operator to an account with the same email-anchored
find-or-create logic as a regular login — an existing account is reused,
a brand-new operator gets one auto-provisioned — and binds the agent's
key to it. (The operator is authorising an agent, not signing in.)

### 3. Agent: poll the token endpoint

```
POST /auth/token
Content-Type: application/json

{
  "grant_type":  "urn:ietf:params:oauth:grant-type:device_code",
  "device_code": "<the one from step 1>"
}
```

Poll every `interval` seconds (5s minimum). Possible responses:

| HTTP | Body                                  | Meaning                                                                 |
|------|---------------------------------------|-------------------------------------------------------------------------|
| 200  | `{"access_token":"clip_…","token_type":"Bearer"}` | Operator finished. The token IS your long-lived API key. |
| 400  | `{"error":"authorization_pending"}`   | Operator hasn't finished yet — keep polling.                            |
| 400  | `{"error":"slow_down"}`               | You polled before `interval` elapsed. Wait at least 5s before retrying. |
| 400  | `{"error":"access_denied"}`           | Operator clicked Cancel. Stop polling.                                  |
| 400  | `{"error":"expired_token"}`           | TTL exceeded (10 min), already consumed, or unknown `device_code`. Stop; start over. |

Like `/auth/device`, this endpoint accepts both JSON and form-urlencoded.

### 4. Agent: store and use the key

Treat the returned `access_token` exactly like any other clip API
key — long-lived, revocable, scoped to the operator's account. Future
runs of the same agent should reuse the stored key rather than
running the device flow again.

---

## MCP server (Claude Desktop / Cursor / Zed / …)

If your runtime speaks the [Model Context Protocol](https://modelcontextprotocol.io),
clip ships an official stdio server that turns the four most common
operations into native tools — no curl, no code generation, no
remembering the API shape.

```bash
npx -y @rac.so/clip-mcp     # served from npm
```

Wired into Claude Desktop:

```json
{
  "mcpServers": {
    "clip": {
      "command": "npx",
      "args":    ["-y", "@rac.so/clip-mcp"],
      "env":     { "CLIP_API_KEY": "clip_..." }
    }
  }
}
```

Tools exposed: `clip_upload`, `clip_list`, `clip_get`, `clip_delete`.
Auth is still your device-flow-minted API key — the server is a thin
passthrough. Source: [github.com/Racso/clip/tree/main/mcp](https://github.com/Racso/clip/tree/main/mcp).

---

## OAuth login (humans)

```
GET /auth/github
GET /auth/google
```

Top-level browser navigation only — these set a session cookie via
HttpOnly, scoped to clip.rac.so. The redirect_uri registered with
each provider is `https://clip.rac.so/auth/<provider>/callback`. After
a successful dance the browser lands on `/dashboard`; after a failure,
on `/login?auth_error=<reason>`.

OAuth is email-anchored: clip looks up accounts by the IdP-verified
email address, so signing in with GitHub and later signing in with
Google using the same email lands you in the same account.

---

## Upload

```
POST /api/
Authorization: Bearer clip_<key>
X-Extension: <ext>

<raw body>
```

clip is **extension-first**, and a type is **mandatory** — every upload must
declare its type explicitly. There is no silent fallback to binary; an upload
with no type signal is rejected with `400`. Send one of:

- **`X-Extension` header** (preferred), or **`?ext=`** query — the bare file
  extension that fits the content (e.g. `md`, `json`, `diff`, `html`, `png`).
  It drives both the stored MIME type and how the clip renders. For arbitrary
  bytes with no meaningful extension, use `bin`, `dat`, or `raw`.
- **`X-Content-Type` header**, or **`?contentType=`** query — an escape hatch
  for an exotic MIME no extension maps to (e.g. `application/vnd.foo+json`).

The ambient `Content-Type` request header is **ignored** for type resolution —
clients auto-set it, so it's unreliable. Use `X-Content-Type` when you want to
set a MIME explicitly.

Query parameters:
- `ttl` — Time-to-live in minutes. Default: 1440 (1 day). Maximum depends on tier.
- `ext` — Extension; same as the `X-Extension` header (the header wins if both are set).
- `contentType` — Explicit MIME escape hatch; same as the `X-Content-Type` header.

Response (201):
```json
{
  "id":          "1wR7Uu-d8T7KBdu",
  "url":         "https://clip.rac.so/view/1wR7Uu-d8T7KBdu",
  "contentType": "text/plain; charset=utf-8",
  "extension":   "txt",
  "size":        42,
  "createdAt":   "2026-05-24T12:00:00.000Z",
  "expiresAt":   "2026-05-25T12:00:00.000Z"
}
```

### Extensions with special rendering

| Extension | Resolved type      | Rendering                          |
|-----------|--------------------|------------------------------------|
| `diff`    | `text/x-diff`      | Syntax-highlighted unified diff    |
| `json`    | `application/json` | Collapsible tree view              |
| `md`      | `text/markdown`    | Rendered markdown                  |
| `html`    | `text/html`        | Served as-is (static page)         |
| other     | per extension      | Raw content with the resolved MIME |

Pass `?ext=diff`, `?ext=json`, `?ext=md` (or the `X-Extension` header) to select
rendering.

## Retrieve

Two paths with explicit intent:

```
GET /view/<id>     # render diff/markdown/JSON as HTML; otherwise raw
GET /raw/<id>      # always raw bytes with stored content-type
GET /c/<id>        # legacy alias for /raw/<id>
```

Use `/view/` when sharing with a human (browsers get the renderer where
we have one, native rendering where we don't). Use `/raw/` when you
want parseable bytes: a JSON clip comes back as `application/json`, a
diff as `text/x-diff`, an image as `image/png`, etc. `POST /` returns
a `/view/<id>` URL by default; swap the prefix to consume the bytes.

Both paths accept an `<id>.<ext>` suffix that overrides the stored
content-type for that response — useful when a clip was uploaded
without a precise type (e.g. quick-paste sends `text/plain`).

- `/view/abc.diff` — force the diff renderer regardless of stored type
- `/raw/abc.html` — force `Content-Type: text/html`
- `/raw/abc.txt`  — view HTML source as plain text in a browser

Unknown extensions fall back to the stored content-type.

- `404` — Clip not found.
- `410` — Clip expired (rendered as a friendly page).

---

## Account

### Who am I

```
GET /api/me
Authorization: Bearer clip_<key>
```

Response:
```json
{"id": "uuid", "email": "you@example.com", "tier": "free"}
```

Fast, cheap — useful as a credential sanity check.

### Account details

```
GET /api/accounts/<id>
Authorization: Bearer clip_<key>
```

Response:
```json
{
  "id":      "uuid",
  "email":   "you@example.com",
  "tier":    "free",
  "balance": 12,
  "keys": [
    {"id": "uuid", "label": "default", "createdAt": "...", "active": true}
  ]
}
```

Only readable for your own account.

### Mint an additional API key

```
POST /api/accounts/<id>/keys
Authorization: Bearer clip_<key>
Content-Type: application/json

{"label": "my-other-machine"}
```

Labels are 1–64 characters from `[A-Za-z0-9 ._-]` (letters, digits,
space, dot, underscore, dash). Leading and trailing whitespace is
trimmed before validation. 400 if the label is otherwise.

Response (201):
```json
{"id": "uuid", "apiKey": "clip_…", "preview": "clip_…", "label": "my-other-machine"}
```

Save the `apiKey` — it's shown only once. The `preview` is the
display-safe partial returned for every key in the keys list.

### Revoke an API key

```
DELETE /api/accounts/<id>/keys/<key_id>
Authorization: Bearer clip_<key>
```

### List your clips

```
GET /api/accounts/<id>/clips
Authorization: Bearer clip_<key>
```

Returns active (un-expired) clips ordered newest-first.

### Delete a clip

```
DELETE /api/clips/<clip_id>
Authorization: Bearer clip_<key>
```

Returns `{"ok": true}`.

### Sign out (browser session)

```
DELETE /api/sessions
```

Destroys the cookie session. Bearer-token clients don't need this —
revoke the key instead.

---

## Tiers and quotas

| Tier | Max body | Max TTL  | Quota                 |
|------|----------|----------|-----------------------|
| free | 5 MB     | 16 hours | 15 clips per week     |
| paid | 25 MB    | 30 days  | per prepaid balance   |

A `paid` account uploads against its `balance` (`clips_remaining`) —
one credit per upload, refunded automatically if storage fails.

| Package | Price | Credits |
|---------|-------|---------|
| starter | $1    | 500     |
| pro     | $5    | 5,000   |
| bulk    | $25   | 50,000  |

(Stripe checkout integration is not live yet.)

---

## Errors

Every error response is JSON:
```json
{"error": "human-readable description"}
```

| Status | Meaning                                              |
|--------|------------------------------------------------------|
| 400    | Empty body / malformed request                       |
| 401    | Missing or invalid Bearer token                      |
| 402    | Quota exhausted                                      |
| 403    | Forbidden (e.g. accessing another account's data)    |
| 404    | Resource not found                                   |
| 410    | Clip expired                                         |
| 413    | Body too large for tier                              |
| 429    | Rate limited — back off using `Retry-After`          |
| 503    | Provider misconfigured (OAuth endpoints only)        |

### Rate limits

Token-bucket per API key, sized by tier:

| Tier | Sustained | Burst |
|------|-----------|-------|
| free | 1 req/s   | 5     |
| paid | 5 req/s   | 30    |

Account creation (`POST /api/accounts`), email login (`POST /api/sessions`),
and OAuth start (`GET /auth/<provider>`) are rate-limited per source IP
at roughly 1 request per 10 seconds with a burst of 3.

A `429` response includes a `Retry-After` header (in seconds). Back
off and retry — automated retries with no backoff will get keep
hitting the same wall.

---

## Examples

### Device flow, end-to-end

```bash
# 1. Agent: get a code
RESP=$(curl -sX POST https://clip.rac.so/auth/device \
  -H 'Content-Type: application/json' \
  -d '{"agent_label":"my-agent"}')

DEVICE_CODE=$(echo "$RESP" | jq -r .device_code)
USER_CODE=$(echo "$RESP" | jq -r .user_code)

echo "Operator: visit https://clip.rac.so/device and enter $USER_CODE"

# 2. Operator finishes in browser

# 3. Agent: poll until success
while true; do
  RESP=$(curl -sX POST https://clip.rac.so/auth/token \
    -H 'Content-Type: application/json' \
    -d "{\"grant_type\":\"urn:ietf:params:oauth:grant-type:device_code\",\"device_code\":\"$DEVICE_CODE\"}")
  ERR=$(echo "$RESP" | jq -r '.error // empty')
  case "$ERR" in
    "")                       KEY=$(echo "$RESP" | jq -r .access_token); break ;;
    "authorization_pending")  sleep 5 ;;
    "slow_down")              sleep 10 ;;
    *)                        echo "Failed: $ERR"; exit 1 ;;
  esac
done

echo "Got key: $KEY"

# 4. Use it — a type is mandatory, so always send X-Extension (or ?ext)
curl -X POST https://clip.rac.so/api/ \
  -H "Authorization: Bearer $KEY" \
  -H "X-Extension: txt" \
  -d "hello from a freshly-onboarded agent"
```

### Upload examples

Every upload declares its type explicitly via `X-Extension` (or `?ext=`).
For arbitrary bytes use `bin`/`dat`/`raw`; for an unmapped MIME use
`X-Content-Type`.

```bash
# Plain text
curl -X POST https://clip.rac.so/api/ \
  -H "Authorization: Bearer clip_…" \
  -H "X-Extension: txt" \
  -d "Hello world"

# Diff (syntax-highlighted)
git diff | curl -X POST https://clip.rac.so/api/?ext=diff \
  -H "Authorization: Bearer clip_…" \
  --data-binary @-

# JSON (tree view)
curl -X POST https://clip.rac.so/api/ \
  -H "Authorization: Bearer clip_…" \
  -H "X-Extension: json" \
  -d '{"key":"value","nested":{"a":1}}'

# HTML (served as a static page)
curl -X POST 'https://clip.rac.so/api/?ext=html' \
  -H "Authorization: Bearer clip_…" \
  -d '<h1>Hello</h1><p>This is a page.</p>'

# Raw bytes (binary) — explicit opt-in
curl -X POST https://clip.rac.so/api/ \
  -H "Authorization: Bearer clip_…" \
  -H "X-Extension: bin" \
  --data-binary @photo.png

# Unmapped MIME via the escape hatch
curl -X POST https://clip.rac.so/api/ \
  -H "Authorization: Bearer clip_…" \
  -H "X-Content-Type: application/vnd.foo+json" \
  -d '{"foo":1}'

# With a custom TTL (in minutes)
curl -X POST 'https://clip.rac.so/api/?ttl=60' \
  -H "Authorization: Bearer clip_…" \
  -H "X-Extension: txt" \
  -d "expires in an hour"
```
