# auth.md — Authenticating as an agent

How to call this platform's APIs programmatically. Everything below is standard OAuth 2.0 and its
discovery RFCs — there is no proprietary handshake, and no registration form to fill in by hand.

**The machine-readable documents are authoritative.** This page is for the person writing the
client; where the two disagree, the JSON is right.

## What needs authentication, and what does not

| Surface | Auth |
|---|---|
| `https://ticketing-staging.softjourn.if.ua/{organizer}/api/ucp/catalog/lookup` and `/search` | **None.** Anonymous, public catalogue |
| `https://ticketing-staging.softjourn.if.ua/{organizer}/.well-known/ucp` | **None** |
| `https://ticketing-staging.softjourn.if.ua/.well-known/acp.json` | **None** |
| `https://ticketing-staging.softjourn.if.ua/.well-known/api-catalog` | **None** |
| Storefront pages, including their `Accept: text/markdown` form | **None** |
| `https://ticketing-staging.softjourn.if.ua/mcp` — the public MCP connector | **None.** Read-only buyer-facing tools |
| `https://ticketing-staging.softjourn.if.ua/mcp/organizer` — the organizer MCP connector | **Bearer token**, obtained below |

If all you need is to find events, read prices and availability, and send a buyer to a pre-filled
basket, **you need no credentials at all**. Start at `/.well-known/api-catalog`.

## Getting a token, in five steps

### 1. Ask, and read the challenge

```http
POST https://ticketing-staging.softjourn.if.ua/mcp/organizer
```

Unauthenticated, this answers `401` with:

```http
WWW-Authenticate: Bearer realm="mcp",
  resource_metadata="https://ticketing-staging.softjourn.if.ua/.well-known/oauth-protected-resource/mcp/organizer"
```

### 2. Fetch the protected resource metadata (RFC 9728)

The URL the challenge named — or construct it yourself, as
`/.well-known/oauth-protected-resource` plus the resource's path. Both forms are served, along
with the document at the origin root.

```json
{
  "resource": "https://ticketing-staging.softjourn.if.ua/mcp/organizer",
  "authorization_servers": ["https://ticketing-staging.softjourn.if.ua"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["organizer:read","organizer:write"],
  "resource_documentation": "https://ticketing-staging.softjourn.if.ua/mcp"
}
```

### 3. Fetch the authorization server metadata (RFC 8414)

```http
GET https://ticketing-staging.softjourn.if.ua/.well-known/oauth-authorization-server
```

It names `authorization_endpoint`, `token_endpoint`, `registration_endpoint` and
`revocation_endpoint`, and declares `code` as the only response type,
`authorization_code`/`refresh_token` as the grants, and **`S256` as the only PKCE method**.

### 4. Register (RFC 7591 dynamic client registration)

```http
POST https://ticketing-staging.softjourn.if.ua/oauth/register
```

This is the agent registration step. No account, no contract and no manual review precede it —
that is what makes the whole flow automatable.

`token_endpoint_auth_methods_supported` is `["none"]`: clients are public, so there is no client
secret to store, and **PKCE is what protects the exchange**. Treat the `S256` challenge as
mandatory rather than optional.

### 5. Authorization code, then the token

Send the user through `authorization_endpoint` with your PKCE challenge, exchange the code at
`token_endpoint`, and present the result as `Authorization: Bearer …`. Refresh with the
`refresh_token` grant; revoke at `revocation_endpoint`.

## Two things worth knowing before you build

**Ask for the least you need.** `scope` takes any subset of the vocabulary above, space-separated:

| Scope | Grants |
|---|---|
| `organizer:read` | Read the organizer’s events, venues, seating and readiness — nothing changes. |
| `organizer:write` | Create and change the organizer’s events, venues, maps, prices and schedules. |

A request with no `scope` is granted both, and the token response always states what was granted.
An unknown scope is refused with `invalid_scope`. The scope is bound at `/authorize` — a refresh
keeps it and can never widen it — and it is enforced on every surface the token works on: a
read-only grant sees no write tool in `tools/list` and is refused every GraphQL mutation with
`WWW-Authenticate: Bearer error="insufficient_scope"`. Within a scope, a token still has at most
the permissions the authenticated person's role has; a scope narrows, it never widens.

**Nothing here reserves a seat.** No API on this platform holds inventory for an agent: a purchase
link pre-fills a basket, and the seat is held only when a buyer confirms it in checkout. If you are
building a flow that assumes a hold, it does not exist yet — on this platform or in any published
agentic-commerce protocol.

## Identifying your agent (optional)

If you sign requests with Web Bot Auth (RFC 9421 over Ed25519) and publish a key directory this
deployment allowlists, the edge verifies the signature and the storefront attributes the traffic to
you. It grants no extra access — it is identity, not authorization — and an unsigned request is
never refused for being unsigned.
