# auth.md — Spoken (spoken.md)

> How an autonomous agent obtains and uses credentials for the Spoken API.
> Human-readable version: https://spoken.md/
> Last updated: 2026-08-05

## Who this is for

Agents that need podcast transcripts and must acquire their own credential without a human operator configuring one by hand. If you already hold a `pt_` key, skip to [Using the credential](#using-the-credential).

## At a glance

| | |
|---|---|
| Resource | `https://spoken.md` |
| Identity model | Anonymous — no account, no user identity required |
| Credential | Opaque API key, `pt_` + 32 hex characters |
| Registration | `POST https://spoken.md/buy` |
| Claim | `GET https://spoken.md/claim?session_id={id}` |
| Free trial | Key `pt_demo`, no registration |
| Payment | Required before the key carries credits |
| Programmatic revocation | Not supported — see [Revocation](#revocation) |

Spoken is **not** an OAuth service. There is no authorization endpoint, no token
endpoint, no refresh token, and no consent screen. The credential is a static
key you send on every request. Everything below is the whole protocol.

Spoken deliberately does **not** publish `/.well-known/oauth-authorization-server`
or `/.well-known/openid-configuration`. Those documents would advertise an
authorization server that does not exist, and a client that chained to them
would fail on a missing token endpoint. Their absence is the accurate signal.
The machine-readable registration details live in the `agent_auth` block of the
Protected Resource Metadata instead.

## Try before registering

The demo key needs no registration and costs nothing:

~~~sh
curl -s 'https://spoken.md/transcripts/1000651996090' \
  -H 'x-api-key: pt_demo'
~~~

`pt_demo` fetches only the demo episode. Any other episode ID returns `402`.
Use it to verify request shape and output format before you spend money.

## Registration

Spoken uses the **anonymous** identity type: registration mints a key with no
identity attached, and payment is what activates it. No email, no OAuth
provider, no identity assertion is required to begin.

### Step 1 — request a key

~~~sh
curl -s -i -X POST 'https://spoken.md/buy?pack=100'
~~~

This mints a fresh `pt_` key with a zero credit balance and responds `302` with
a `Location` header pointing at a hosted Stripe Checkout page. `pack` accepts
`100`, `500`, or `2000` (defaults to `100`). See https://spoken.md/pricing.md
for current prices.

The key is **not returned in this response**. It is bound to the checkout
session and released only after payment.

### Step 2 — complete payment

Payment happens on Stripe's hosted page and needs a human, or an agent with a
funded payment method. Follow the `Location` URL. On success Stripe redirects to
`https://spoken.md/success.html?session_id={CHECKOUT_SESSION_ID}`.

Capture the `session_id` query parameter from that redirect — it is the only
handle to your key.

### Step 3 — claim the credential

~~~sh
curl -s 'https://spoken.md/claim?session_id=cs_live_...'
~~~

Returns the provisioned key once the session is paid:

~~~json
{
  "apiKey": "pt_...",
  "credits": 100,
  "amountPaid": 15,
  "transactionId": "cs_live_...",
  "purchasedCredits": 100
}
~~~

Store `apiKey` durably — it is the credential for every subsequent request, and
this endpoint is the only way to read it back. The same key is also emailed to
the address entered at checkout.

Before payment settles `/claim` returns `402`. Retry; do not re-register, or you
will mint a second key and pay twice.

## Using the credential

Send the key on every request, either way. Both are equivalent — same key, same
privileges — so use whichever your HTTP client makes easy:

~~~sh
# Preferred, and what the docs use elsewhere
curl -s 'https://spoken.md/transcripts/1000651996090' \
  -H 'x-api-key: pt_live_key_here'

# RFC 6750 bearer, for clients that only speak Authorization
curl -s 'https://spoken.md/transcripts/1000651996090' \
  -H 'Authorization: Bearer pt_live_key_here'
~~~

Never put the key in a query string or a URL you log. It does not expire and
does not rotate, so a leaked key is valid until it is manually disabled.

### Scopes

Keys are **unscoped**. Every key carries every capability the resource exposes —
there is no mechanism to request a subset, and no scope parameter anywhere. The
`scopes_supported` list published in the Protected Resource Metadata names the
capabilities that exist, not a menu you choose from:

- `transcripts:read` — fetch a transcript
- `search:read` — search episodes, list a show's back catalogue
- `balance:read` — read the credit balance on your own key

## Credits and billing

Access is metered, not time-boxed. One credit is one successful transcript
fetch. Repeat fetches of the same episode are free, and errors are never
charged. `/search` and `/balance` need a key but never deduct credits. Credits
do not expire.

Check the balance at any time:

~~~sh
curl -s 'https://spoken.md/balance' -H 'x-api-key: pt_live_key_here'
~~~

Every `200` also carries `X-Credits-Remaining` and `X-Credits-Charged` headers,
so a well-behaved agent can track its balance without polling.

When credits run out the API returns `402` with a `top_up_url`. Buy more against
the **existing** key — do not register a second one:

~~~sh
curl -s -i -X POST 'https://spoken.md/top-up?key=pt_live_key_here&pack=100'
~~~

## Handling auth failures

| Status | Meaning | What to do |
|---|---|---|
| `401` | Missing or invalid key | Register, or check the header spelling. Response carries `demo_key` and `purchase_url`. |
| `402` | Out of credits, or demo key used on a non-demo episode | `POST` the `top_up_url` from the response body. |
| `429` | Service-level throttle | Back off and retry. No credit charged. |

`401` responses carry a `WWW-Authenticate` challenge advertising both accepted
schemes and a `resource_metadata` pointer to
`https://spoken.md/.well-known/oauth-protected-resource`.

## Revocation

There is **no programmatic revocation endpoint**. Spoken publishes no
`revocation_uri` because none exists — do not synthesise one.

To disable a compromised key, email info@spoken.md from the address used at
checkout. Any unspent credits are moved to a replacement key.

Spoken does not push revocation events to agents. A key that stops working
returns `401`; treat that as terminal and stop retrying.

## Machine-readable discovery

- Protected Resource Metadata, including the `agent_auth` block: https://spoken.md/.well-known/oauth-protected-resource
- OpenAPI: https://spoken.md/.well-known/openapi.json
- API catalog: https://spoken.md/.well-known/api-catalog
- Agent instructions: https://spoken.md/agents.md
- Pricing: https://spoken.md/pricing.md

## Note for scanners

`POST /buy` mints a key and opens a Stripe Checkout session. Do not probe it
during passive scans. Everything a scanner needs is in this file and the
Protected Resource Metadata document.
