# auth.md: authenticating an agent against Billabex

You are an agent. This document is the whole credential path for the Billabex public REST API and
the Billabex MCP server, in the order you should walk it: discover -> pick a method -> register ->
claim -> exchange -> use the access_token -> errors -> revocation. Do not skip ahead; each step
names what the next one needs.

Two origins appear below. `https://next.billabex.com` is both the resource server that hosts the API and the
authorization server that issues tokens: Billabex runs one origin for both, so the `issuer` and
the `resource` are the same URL. `https://developer.billabex.com` is this documentation portal, which serves no
credential and needs none.

## What this service does and does not implement

Read this before matching Billabex against a profile you already support.

| Mechanism | Supported | Where |
| --- | --- | --- |
| RFC 9728 protected resource metadata (PRM) | Yes | `https://next.billabex.com/.well-known/oauth-protected-resource` |
| RFC 8414 authorization server metadata | Yes | `https://next.billabex.com/.well-known/oauth-authorization-server` |
| OpenID Connect discovery | Yes | `https://next.billabex.com/.well-known/openid-configuration` |
| RFC 7591 Dynamic Client Registration | Yes, open, no human approval | `https://next.billabex.com/api/oauth/register` |
| OAuth 2.1 authorization code + PKCE (S256) | Yes, the only user-delegation flow | `https://next.billabex.com/api/oauth/authorize` |
| Refresh tokens | Yes | `https://next.billabex.com/api/oauth/token` |
| RFC 7009 token revocation | Yes | `https://next.billabex.com/api/oauth/revoke` |
| RFC 7662 token introspection | Yes | `https://next.billabex.com/api/oauth/introspect` |
| `agent_auth` identity_endpoint, `identity_assertion`, `service_auth`, `anonymous` | **No** | not implemented |
| ID-JAG (`urn:ietf:params:oauth:token-type:id-jag`) assertions | **No** | not implemented |
| Claim ceremony (`user_code`, `claim_endpoint`) | **No** | not implemented |
| Client credentials grant | **No** | a token is always bound to a user |

The four rows marked **No** are the agentic-registration profile. Billabex publishes no
`identity_endpoint`, `claim_endpoint` or `events_endpoint`, so do not probe for them: the
`agent_auth` block of the authorization server metadata carries a `skill` pointing back at this
document and nothing else, on purpose. If your client library has an agentic-registration path,
fall back to the plain OAuth path described below, which reaches the same place with one browser
round trip.

The one thing that profile exists to remove, a human approving your client, is already removed
here: registration is dynamic and open.

## Step 1 - Discover

Discovery is two hops, and you may already have done the first one.

Any unauthenticated call to a protected endpoint answers 401 with a `WWW-Authenticate` header
naming the protected resource metadata:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://next.billabex.com/.well-known/oauth-protected-resource"
```

### 1a. Fetch the protected resource metadata

```bash
curl -s https://next.billabex.com/.well-known/oauth-protected-resource
```

```json
{
  "resource": "https://next.billabex.com",
  "authorization_servers": ["https://next.billabex.com"],
  "scopes_supported": ["accounts:read", "invoices:read", "mcp:read", "..."],
  "bearer_methods_supported": ["header"],
  "resource_documentation": "https://developer.billabex.com/en/guides/oauth/"
}
```

- `resource` is the canonical URL of the API you are calling.
- `authorization_servers` names the origin that issues tokens for it. Here it is the same origin.
- `scopes_supported` narrows by path: append the resource path to the metadata URL and you get the
  scopes for that path alone. `https://next.billabex.com/.well-known/oauth-protected-resource/mcp` answers
  `["mcp:read", "mcp:write"]`, which is what an MCP client should ask for.
- `bearer_methods_supported` is `["header"]`: send the token in `Authorization`, never in a query
  string or a form body.

### 1b. Fetch the authorization server metadata

```bash
curl -s https://next.billabex.com/.well-known/oauth-authorization-server
```

Read `issuer`, `authorization_endpoint`, `token_endpoint`, `registration_endpoint`,
`revocation_endpoint`, `scopes_supported`, `grant_types_supported` and
`code_challenge_methods_supported` from it rather than hardcoding the paths printed in this
document. The `agent_auth` block is present and carries only `skill`, the URL of this file.

You can also read everything at once, with no credential and no 401 first, from the discovery
document of the API:

```bash
curl -i https://next.billabex.com/api/public/v1/status
```

It returns the OAuth endpoints, both rate-limit quotas and every documentation URL, and its
response already carries the `RateLimit` and `RateLimit-Policy` headers, so you can size your
throttling before your first authenticated call.

## Step 2 - Pick a method

There is one decision, and it is about who the token acts for.

1. **You are acting for a specific Billabex user, and a browser can be opened once.** Use
   [authorization code with PKCE](#step-3---register). This is the path for every agent: an
   assistant acting for a signed-in user, a CLI, a background sync set up once by a human.
2. **You are acting for no user.** Stop. Billabex issues no token that is not bound to a user, and
   has no client credentials grant. Every access token carries a `sub`, and authorization is
   evaluated against that user's membership of the organization whose data you are reading. An
   agent with no user behind it can still read the unauthenticated surface listed in
   [Step 6](#step-6---use-the-access_token), and nothing else.
3. **You hold a session with your own identity provider and want to exchange it.** Not supported.
   Billabex accepts no `identity_assertion`, and no assertion type, id-jag included. Fall back to
   option 1.

Before sending a user into the flow, surface what you are asking for: the name Billabex, the
organization the token will act in, and the scope list. The consent screen shows the same set, and
a user who sees the same thing twice trusts the second one.

## Step 3 - Register

A client registers itself. There is no form, no sales contact and no approval queue.

```bash
curl -s -X POST https://next.billabex.com/api/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My agent",
    "redirect_uris": ["http://127.0.0.1:8976/callback"],
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "token_endpoint_auth_method": "none",
    "scope": "openid accounts:read invoices:read"
  }'
```

The response carries `client_id`, and `client_secret` when you asked for a confidential client.
Store the `client_id`; a public client registered with `token_endpoint_auth_method: "none"` needs
nothing else, because PKCE, not a secret, is what binds the code to you.

Registration is the step the agentic-registration profile calls `identity_endpoint`. Billabex
answers it with RFC 7591 instead, at the `registration_endpoint` published in its metadata.

A human can do the same thing from the [OAuth Clients page](https://developer.billabex.com/en/oauth-clients/) of this
portal, signed in with a Billabex account. Use that when the client is long lived and someone wants
to see it in a list.

## Step 4 - Claim

Nothing to claim. Billabex runs no claim ceremony: there is no `claim_endpoint`, no
`claim_token` and no `user_code` to read out to a user.

The consent screen does the work the ceremony would: the user authenticates at Billabex, sees which
client is asking, for which organization and for which scopes, and approves or refuses. That
happens inside Step 5 rather than beside it, so go straight there.

## Step 5 - Exchange

Run OAuth 2.1 authorization code with PKCE. `S256` is the only challenge method you should use;
`plain` is advertised for legacy clients and you are not one.

Generate a `code_verifier` of 43 to 128 unreserved characters, and send its SHA-256 as the
challenge:

```
GET https://next.billabex.com/api/oauth/authorize
  ?response_type=code
  &client_id=<client_id>
  &redirect_uri=http://127.0.0.1:8976/callback
  &scope=openid%20accounts%3Aread%20invoices%3Aread
  &state=<random>
  &code_challenge=<BASE64URL(SHA256(code_verifier))>
  &code_challenge_method=S256
```

Open that URL in the user's browser. Ask only for the scopes the task needs: a request for
`invoices:all` when the task only reads is refused by users, and by reviewers.

The user signs in, consents, and is redirected back with `code` and your `state`. Check `state`
before doing anything else. Then exchange:

```bash
curl -s -X POST https://next.billabex.com/api/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d code=<code> \
  -d redirect_uri=http://127.0.0.1:8976/callback \
  -d client_id=<client_id> \
  -d code_verifier=<code_verifier>
```

```json
{
  "access_token": "<token>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "<token>",
  "scope": "openid accounts:read invoices:read"
}
```

**Refresh.** When the access token expires, POST `grant_type=refresh_token&refresh_token=...` to
the same endpoint. There is no assertion to re-exchange: the refresh token is the durable half of
the credential, so store it the way you would store a password, and never log it.

## Step 6 - Use the access_token

Send it as a bearer token in the header, which is the only method the resource advertises:

```bash
curl -s https://next.billabex.com/api/public/v1/organizations \
  -H "Authorization: Bearer $BILLABEX_ACCESS_TOKEN"
```

Almost every endpoint takes an `organizationId`. Read it once from `/api/public/v1/organizations`,
cache it, and pass it to the rest of the API. The MCP server takes the same token, at
`https://next.billabex.com/mcp`, with the `mcp:read` and `mcp:write` scopes.

Three surfaces need no token at all, and are the right place to look before you authenticate:

- `https://next.billabex.com/api/public/v1/status`: the discovery document, quotas included.
- `https://next.billabex.com/mcp/tools`: every MCP tool with its description, JSON Schema and required scope.
- `https://developer.billabex.com/openapi.json`: the full OpenAPI contract.

**Throttle yourself.** Every response carries `RateLimit` and `RateLimit-Policy`. Read them and
slow down before you are told to, rather than after.

## Errors

Every failure is JSON, never HTML, and carries `code`, `message` and `resolution` plus `path`,
`method`, `type` and `timestamp`. Read `resolution` before retrying: it says whether retrying
can work at all.

| Status or code | Where | What to do |
| --- | --- | --- |
| `invalid_client` | `/api/oauth/token` | The `client_id` is unknown or was deleted. Register again at Step 3. |
| `invalid_grant` | `/api/oauth/token` | The code was used, expired, or the `code_verifier` does not match the challenge. Restart at Step 5 with a fresh verifier. |
| `invalid_request` | `/api/oauth/authorize` | A missing or malformed parameter, usually `redirect_uri` not matching the one registered. Fix the request; do not retry it unchanged. |
| `unsupported_grant_type` | `/api/oauth/token` | Only `authorization_code` and `refresh_token` are accepted. Client credentials and JWT bearer are not. |
| `invalid_scope` | `/api/oauth/authorize` | A scope that is not in `scopes_supported`. Re-read the metadata from Step 1b. |
| 401 | any API or MCP call | The token is missing, expired or revoked. Refresh once; if that fails, restart at Step 5. |
| 403 | any API or MCP call | The scope was never granted, or the user is not a member of that organization. Retrying the same call fails the same way: ask for a new authorization with the scope you need. |
| 400 | any API call | A rejected field. Every public endpoint refuses unknown fields rather than dropping them, so check spelling against the OpenAPI specification. |
| 429 | any API or MCP call | Wait for `Retry-After`. Both a per-token and a per-IP quota exist; `RateLimit-Policy` names them. |
| 5xx | any | Exponential backoff, retry the same request. |

## Revocation

Two layers can end what you hold, and they fail differently.

- **Credential layer (RFC 7009).** POST to the `revocation_endpoint` to kill one token. This is the
  call you make yourself when a task finishes or a user disconnects your integration:

  ```bash
  curl -s -X POST https://next.billabex.com/api/oauth/revoke \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d token=<access_or_refresh_token> \
    -d token_type_hint=access_token \
    -d client_id=<client_id>
  ```

  It is idempotent and answers 200 whether or not the token existed.

- **Consent layer.** A user can revoke your client's consent from their Billabex account at any
  time, and an administrator can delete the OAuth client outright. Both invalidate the access token
  and the refresh token together. You discover it as a 401 that a refresh does not fix, followed by
  `invalid_grant` on the refresh: restart at Step 5 and expect the user to consent again, or at
  Step 3 if the client itself is gone.

There is no push channel for either: Billabex publishes no `events_endpoint` and sends no security
event token, so treat a 401 that survives one refresh as revocation and stop retrying.

## Reference

- Developer hub: https://developer.billabex.com/developers/
- OAuth guide: https://developer.billabex.com/en/guides/oauth/
- Scopes, one by one: https://developer.billabex.com/en/guides/scopes/
- Errors: https://developer.billabex.com/en/guides/errors/
- Rate limiting: https://developer.billabex.com/en/guides/rate-limiting/
- Agent instructions: https://developer.billabex.com/llms.txt
- OpenAPI specification: https://developer.billabex.com/openapi.json
