# Ticket Fairy auth.md

You are an agent that wants to act on Ticket Fairy for an event organiser or a ticket buyer. This document tells you how to get a credential and how to use it. Follow the steps in order.

Ticket Fairy does not let an agent create an account or register itself. A person owns every account. That person approves you, either by entering a short code you give them or by approving you in their browser, or they create a token and give it to you. Never ask the person for their password, a sign-in code or a two-factor code, and never send those to Ticket Fairy for them.

There are two API hosts. `https://www.ticketfairy.com` is the sign-in page and the public ticket-buyer API under `/api/v1/`. `https://www.theticketfairy.com` is the organiser API under `/api/`. The organiser dashboard a person uses is `https://manage.ticketfairy.com`. A token from either token endpoint works on both APIs. What you can do with it is exactly what the person can do in the dashboard: their team roles decide which brands and events you can reach.

## Step 1: Discover

Each host publishes its own metadata. Read the pair for the API you will call.

```http
GET https://www.ticketfairy.com/.well-known/oauth-protected-resource
GET https://www.ticketfairy.com/.well-known/oauth-authorization-server
GET https://www.theticketfairy.com/.well-known/oauth-protected-resource
GET https://www.theticketfairy.com/.well-known/oauth-authorization-server
```

The protected resource metadata names the API, its authorization server and the one scope, `profile`. That scope is not a narrow read: a token with it acts with the person's full dashboard permissions, and their team roles decide which brands and events it reaches. Tell the person that before they approve you.

```json
{
    "resource": "https://www.ticketfairy.com",
    "resource_name": "Ticket Fairy",
    "resource_logo_uri": "https://www.ticketfairy.com/resources/images/home/fav/apple-touch-icon.png",
    "authorization_servers": [
        "https://www.ticketfairy.com"
    ],
    "scopes_supported": [
        "profile"
    ],
    "bearer_methods_supported": [
        "header"
    ],
    "resource_documentation": "https://www.ticketfairy.com/auth.md"
}
```

The authorization server metadata for the organiser API looks like this. The consumer host publishes the same shape with itself as `issuer` and `https://www.ticketfairy.com/api/v1/oauth/access_token` as `token_endpoint`.

```json
{
    "issuer": "https://www.theticketfairy.com",
    "authorization_endpoint": "https://www.ticketfairy.com/oauth/authorize",
    "token_endpoint": "https://www.theticketfairy.com/api/oauth/access_token",
    "scopes_supported": [
        "profile"
    ],
    "response_types_supported": [
        "code"
    ],
    "response_modes_supported": [
        "query"
    ],
    "grant_types_supported": [
        "authorization_code",
        "refresh_token",
        "urn:workos:agent-auth:grant-type:claim"
    ],
    "token_endpoint_auth_methods_supported": [
        "client_secret_post",
        "none"
    ],
    "code_challenge_methods_supported": [
        "S256"
    ],
    "service_documentation": "https://www.ticketfairy.com/auth.md",
    "agent_auth": {
        "skill": "https://www.ticketfairy.com/auth.md",
        "identity_endpoint": "https://www.theticketfairy.com/api/agent/identity",
        "register_uri": "https://www.theticketfairy.com/api/agent/identity",
        "identity_types_supported": [
            "service_auth",
            "identity_assertion"
        ],
        "service_auth": {
            "token_endpoint": "https://www.theticketfairy.com/api/oauth/access_token",
            "grant_types_supported": [
                "urn:workos:agent-auth:grant-type:claim"
            ],
            "credential_types_supported": [
                "access_token",
                "refresh_token"
            ]
        },
        "identity_assertion": {
            "assertion_types_supported": [
                "verified_email"
            ]
        },
        "oauth_authorization_code": {
            "authorization_endpoint": "https://www.ticketfairy.com/oauth/authorize",
            "token_endpoint": "https://www.theticketfairy.com/api/oauth/access_token",
            "code_challenge_methods_supported": [
                "S256"
            ],
            "credential_types_supported": [
                "access_token",
                "refresh_token"
            ]
        },
        "personal_access_token": {
            "issue_uri": "https://manage.ticketfairy.com/account",
            "credential_types_supported": [
                "access_token"
            ]
        },
        "revocation_uri": "https://www.theticketfairy.com/api/oauth/sessions"
    }
}
```

Read `agent_auth` in full. `skill` is this document. `identity_endpoint` is where you ask for access; `register_uri` is the same endpoint under the name an earlier draft of the profile used. `identity_types_supported` lists `service_auth`: you name the person by email address, and the person approves you in the claim ceremony in Step 3. It also lists `identity_assertion` with the one assertion type `verified_email`, which is the same claim ceremony under the name an earlier draft of the profile used. This server does not accept an ID-JAG, so `identity_assertion.assertion_types_supported` does not list it. The `oauth_authorization_code` and `personal_access_token` blocks describe the two other ways a person can give you a credential, in Steps 4 and 5. This server does not accept an `anonymous` registration, so it publishes no `claim_endpoint` and no `events_endpoint`. There is no dynamic client registration and no RFC 7009 revocation endpoint. Do not send a request to a path this document does not name.

## Step 2: Pick a method

1. **You know the person's email address, or can ask them for it.** Use the claim ceremony in Step 3. It works from anywhere, including a hosted service with no browser of its own, and it is the `service_auth` method `agent_auth.identity_types_supported` advertises.
2. **You run on the person's own computer and can open their browser.** Use the Ticket Fairy command-line tool. It is the registered public client for the browser flow. Ask the person before you install it with `npm install -g ticketfairy`, then run `ticketfairy login`. It completes Step 4 for you, keeps the credential in the system keychain and gives you organiser tools through its MCP server. Run `ticketfairy --help` for the commands.
3. **The person would rather hand you a token.** Ask them for a Personal Access Token and follow Step 5.
4. **You are an OAuth application registered with Ticket Fairy and hold your own `client_id`.** Complete Step 4 yourself. Ticket Fairy registers an agent platform's application by agreement, not by self-service; there is no dynamic client registration. Application-level scopes for agent ticket purchasing and organiser provisioning are not issued yet, and this document and the metadata will list them only when they are.

The person needs a Ticket Fairy organiser account before any of these work. If they have none, follow `https://www.ticketfairy.com/.well-known/agent-skills/start-organising-events/SKILL.md` first: signup, legal terms and email verification happen in their browser. Then return here. An agent cannot create the account, accept terms, choose a payout destination or purchase a plan for the person.

Before you start, tell the person what you will do with the access and confirm that they want to continue. That confirmation is the person's consent gate.

## Step 3: Ask for access and let the person approve it

This is the claim ceremony. You ask for access as a named person, they approve it in their dashboard by entering a six-digit code you give them, and you then collect a token. Nothing is granted until they enter the code.

### 3a. Ask for access

```http
POST https://www.theticketfairy.com/api/agent/identity
Content-Type: application/json

{
  "type": "service_auth",
  "login_hint": "person@example.com",
  "agent_name": "Acme Assistant"
}
```

`login_hint` is the email address of the Ticket Fairy account you want to act for. `agent_name` is what the person sees on the approval page, so use the name they know you by. The consumer host serves the same endpoint at `https://www.ticketfairy.com/api/v1/agent/identity`. An agent built against an earlier draft of the profile can still send `"type": "identity_assertion"` with `"assertion_type": "verified_email"`; the response then names that type and has status 201.

Response (200):

```json
{
  "registration_id": "reg_...",
  "registration_type": "service_auth",
  "client_id": "ticketfairy-agent-auth",
  "claim_url": "https://manage.ticketfairy.com/claim",
  "claim_token": "clm_...",
  "claim_token_expires": "2026-09-07 02:30:00",
  "post_claim_scopes": ["profile"],
  "claim": {
    "user_code": "123456",
    "expires_in": 1800,
    "verification_uri": "https://manage.ticketfairy.com/claim?claim_attempt_token=cla_...",
    "interval": 30
  }
}
```

Keep `claim_token` in memory for this ceremony only. It is returned once, it is the only thing that collects your token in Step 3c, and it is never part of a URL. The whole ceremony expires at `claim_token_expires`, which is 30 minutes; after that, start again here.

Keep `client_id`. It is the application your session belongs to, and refreshing the access token in Step 6 needs it.

### 3b. Hand the link and the code to the person

Give the person `claim.verification_uri` and `claim.user_code` in one message. Suggested wording:

> Open this link, sign in to Ticket Fairy, and enter this 6-digit code: **123456**
> https://manage.ticketfairy.com/claim?claim_attempt_token=...

Say that the code goes into the page they land on, not back to you. They will sign in, see which agent asked and which address it named, type the code and approve. Only the person whose email you supplied can approve it, so a code that reaches somebody else is useless to them.

### 3c. Poll for the token

Poll the `token_endpoint` from Step 1 with the claim grant. Wait `claim.interval` seconds between polls.

```http
POST https://www.theticketfairy.com/api/oauth/access_token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_...
```

While the person has not finished, the response is 400 with:

```json
{"error": "authorization_pending", "error_description": "..."}
```

Once they approve, the response is the same token payload as every other grant:

```json
{
  "access_token": "<access_token>",
  "refresh_token": "<refresh_token>",
  "token_type": "bearer",
  "scope": "profile"
}
```

Keep both tokens in secure storage and go to Step 6. If the window closes first, the response is `expired_token`; go back to Step 3a and ask again.

## Step 4: Authorize in the browser

This is the OAuth 2.0 authorization code grant with PKCE (RFC 7636). A public client, which includes the command-line tool and any client that cannot keep a secret, must use PKCE with `S256`; for a public client the server rejects `plain`. A confidential client sends its `client_secret` at the token endpoint as well. Any client that sent a `code_challenge` must send the matching `code_verifier` at the token endpoint: the server checks every challenge it bound to a code.

### 4a. Open the authorization page

Generate a `code_verifier`, derive `code_challenge` with S256, and create a random `state`. Then open this URL in the person's browser:

```http
GET https://www.ticketfairy.com/oauth/authorize?response_type=code&client_id=<client_id>&redirect_uri=<redirect_uri>&scope=profile&state=<state>&code_challenge=<code_challenge>&code_challenge_method=S256
```

`redirect_uri` must match the client's registered redirect list exactly. The command-line tool is registered for the loopback address only: `http://127.0.0.1:<port>/callback` or `http://localhost:<port>/callback`, with an explicit port from 1024 to 65535.

### 4b. Let the person sign in and approve

The page asks the person to sign in, or to create an account, and then to allow the access you asked for. Keep sign-in, email verification and two-factor checks in the browser. If the person's account uses two-factor authentication, the browser can land on their account page after that check instead of returning to your `redirect_uri`; ask the person to open the Step 4a URL again, and the now signed-in session completes the approval. If the page shows an error, read its message: it names the invalid `client_id`, `redirect_uri` or scope.

### 4c. Receive the code

The browser is redirected to your `redirect_uri` with `code`, `expires` (seconds until the code expires; codes live for 10 minutes) and your `state`. Check that `state` matches the value you sent.

```http
GET <redirect_uri>?code=<code>&expires=598&state=<state>
```

### 4d. Exchange the code for tokens

Send a form-encoded POST to the `token_endpoint` from Step 1. Send `code_verifier` whenever you sent a `code_challenge`; a confidential client adds `client_secret`.

```http
POST https://www.theticketfairy.com/api/oauth/access_token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&client_id=<client_id>&code=<code>&redirect_uri=<redirect_uri>&code_verifier=<code_verifier>
```

Response (200):

```json
{
  "access_token": "<access_token>",
  "refresh_token": "<refresh_token>",
  "token_type": "bearer",
  "scope": "profile"
}
```

The response carries no `expires_in`. An access token issued this way lasts about 180 days. Keep both tokens in secure storage. Go to Step 6.

## Step 5: Personal Access Token

A Personal Access Token is a bearer token the person creates for you. It authenticates identically to an access token from Step 3 or Step 4, has no refresh token, and lasts for the period the person chose, up to one year.

Ask the person to open the organiser dashboard at `https://manage.ticketfairy.com/account`, go to the Personal Access Tokens section, choose **New Personal Access Token**, enter a name that identifies you, and copy the token. The token is shown once. A person who already uses the command-line tool can run `ticketfairy auth tokens create "<name>"` instead. The person then gives you the token through a secure channel, never through a URL.

A person who is signed in can also create one with the API, and so can you with an access token from Step 3 or Step 4. A Personal Access Token cannot create another Personal Access Token.

```http
POST https://www.theticketfairy.com/api/user/personal-access-tokens
Authorization: Bearer <an access token from Step 3 or Step 4>
Content-Type: application/json

{"data": {"attributes": {"name": "<name>", "expires_in_seconds": 2592000}}}
```

The 201 response carries `data.attributes.access_token` once, with `expires_at`. Go to Step 6.

## Step 6: Use the access token

Present the token in the `Authorization` header on every API request:

```http
GET https://www.theticketfairy.com/api/events
Authorization: Bearer <access_token>
```

The organiser API entry points are listed at `https://www.ticketfairy.com/.well-known/api-catalog`. Public event search needs no token; see `https://www.ticketfairy.com/.well-known/agent-skills/find-event-tickets/SKILL.md`. A token does not let you buy tickets for a person or pay for anything: checkout, payment, bank and tax details and plan purchase stay in the person's browser.

When a request returns 401, the token has expired or was revoked. If you hold a `refresh_token`, send this form-encoded POST to the same `token_endpoint`. Confidential clients add `client_secret`.

```http
POST https://www.theticketfairy.com/api/oauth/access_token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&client_id=<client_id>&refresh_token=<refresh_token>
```

The response has the same shape as Step 4d and replaces both tokens. If the refresh fails, or you hold a Personal Access Token, go back to Step 2 and ask the person again.

## Errors

The token endpoint answers with `application/json`.

| Code | Where | What to do |
| --- | --- | --- |
| `invalid_request` (400) | token endpoint | The `error_description` names the missing or invalid parameter: grant type, client id, client secret, code, code verifier, redirect URI, refresh token or scope. Correct it. A code that was used, has expired or was issued to another client is reported here too; start again at Step 4a. |
| `server_error` (500) | code exchange | Wait, then retry the same request once. |
| 404 | token endpoint | You did not use POST; the endpoint only matches POST requests. |
| `authorization_pending` (400) | token endpoint (claim grant) | The person has not approved you yet. Wait `claim.interval` seconds and poll again. |
| `expired_token` (400) | token endpoint (claim grant) | The 30-minute window closed. Start again at Step 3a. |
| `invalid_grant` (400) | token endpoint (claim grant) | The claim token is not one this server issued. Start again at Step 3a. |
| `issuer_not_enabled` (400) | `/agent/identity` | This server does not accept an ID-JAG or another assertion from an identity provider. Send `type` `service_auth` with the person's email address as `login_hint`. |
| `unsupported_identity_type` (400) | `/agent/identity` | The `type` is not one this server accepts. Send `type` `service_auth`. |
| `anonymous_not_enabled` (400) | `/agent/identity` | This service does not register an agent without a person. Ask for their email address and use Step 3. |
| 429 | any | You sent too many requests. Honour `Retry-After` when present; otherwise wait five minutes. Do not retry in parallel. The organiser token endpoint allows 20 requests per five minutes for one address. |
| 401 | any API request | The token expired or was revoked. Refresh it (Step 6) or ask the person again (Step 2). |
| 403 | any API request | The token is valid but the person's role does not allow the action. Tell the person; do not retry. |
| 422 | any API request | Read the message. Most often the request body was rejected, for example a missing token name or an `expires_in_seconds` outside 300 seconds to one year; correct it and send again. The code `no_dashboard_access` means the account has no organiser access; tell the person. |

## Revocation

The person can end your access at any time from the dashboard. A signed-in person, or you with their token, can also do it through the API. Remove only your own session; the list includes the person's other applications.

Sessions from Step 3 and Step 4, including the command-line tool's, are listed and removed here. An agent approved through the claim ceremony appears under the name it gave:

```http
GET https://www.theticketfairy.com/api/oauth/sessions
DELETE https://www.theticketfairy.com/api/oauth/sessions/<session_id>
```

Personal Access Tokens are listed, rotated and revoked here. Rotation issues a new token and keeps the old one working for up to seven days.

```http
GET https://www.theticketfairy.com/api/user/personal-access-tokens
POST https://www.theticketfairy.com/api/user/personal-access-tokens/<token_id>/rotate
DELETE https://www.theticketfairy.com/api/user/personal-access-tokens/<token_id>
```

There is no RFC 7009 revocation endpoint. After a revocation, the next API request returns 401; go back to Step 2.
