# GlobalFinReg auth.md

You are an agent. This document tells you how to register for a credential to the **Global FinReg (GFR) API**
on behalf of a GFR user, and how to use it. It follows the [auth.md protocol](https://github.com/workos/auth.md).
Follow the steps in order.

GFR supports one registration method: **service_auth**. You send the user's email, the user signs in to GFR
and confirms a 6-digit code you show them, and you receive an access token for that user's account.
GFR does not support `anonymous` or ID-JAG registration. Agents built for auth.md before v0.6.0 can register
with `identity_assertion` + `verified_email` instead: it is the same email-verified flow under its old name.

- API: https://api2.globalfinreg.com
- API reference for agents: https://api2.globalfinreg.com/agentapi
- OpenAPI document: https://api2.globalfinreg.com/swagger/agent-v1/swagger.json
- API catalog: https://api2.globalfinreg.com/.well-known/api-catalog
- MCP server: https://api2.globalfinreg.com/mcp

The user must already have a GFR account. They can sign up at https://www.globalfinreg.com, or you can
create one for them with the MCP server's `create_user` tool (see Step 6): GFR emails them a link to set a
password, and once they have, you register as below. Creating an account gives you no access to it.

## Step 1 — Discover

```http
GET https://api2.globalfinreg.com/.well-known/oauth-protected-resource
GET https://api2.globalfinreg.com/.well-known/oauth-authorization-server
```

The Authorization Server metadata carries the `agent_auth` block:

```json
{
  "issuer": "https://api2.globalfinreg.com",
  "token_endpoint": "https://api2.globalfinreg.com/oauth2/token",
  "revocation_endpoint": "https://api2.globalfinreg.com/oauth2/revoke",
  "grant_types_supported": ["urn:ietf:params:oauth:grant-type:jwt-bearer", "urn:workos:agent-auth:grant-type:claim"],
  "agent_auth": {
    "skill": "https://api2.globalfinreg.com/auth.md",
    "identity_endpoint": "https://api2.globalfinreg.com/agent/identity",
    "register_uri": "https://api2.globalfinreg.com/agent/identity",
    "claim_endpoint": "https://api2.globalfinreg.com/agent/identity/claim",
    "identity_types_supported": ["service_auth", "identity_assertion"],
    "service_auth": { "credential_types_supported": ["access_token"] },
    "identity_assertion": {
      "assertion_types_supported": ["verified_email"],
      "credential_types_supported": ["access_token"]
    }
  }
}
```

## Step 2 — Pick a method

Use **service_auth**. You need the email address of the user's GFR account. Before registering, tell the user
that you are asking for access to their Global FinReg account and confirm the email address with them.

## Step 3 — Register

```http
POST https://api2.globalfinreg.com/agent/identity
Content-Type: application/json

{
  "type": "service_auth",
  "login_hint": "user@example.com",
  "agent": "claude-code"
}
```

- `type` *(required)*: `"service_auth"`.
- `login_hint` *(required)*: the email address of the user's GFR account.
- `agent` *(optional)*: your name or runtime (for example `claude-code`). It is shown to the user when they approve.
- Before v0.6.0 this was `{"type": "identity_assertion", "assertion_type": "verified_email", "assertion": "user@example.com"}`;
  GFR accepts both bodies and runs the same ceremony.
- `concordium` *(optional)*: your entry in the [Concordium Agent Registry](https://docs.concordium.com/en/mainnet/technical-reference/agent-registry/index.html)
  (CIS-8004), shown to the user when they approve. Send `token_id` or `token_address`; the other fields are optional:

  ```json
  "concordium": {
    "network": "ccd:9dd9ca4d19e9393877d2c44b70f89acb",
    "registry_contract": "10082.0",
    "token_id": 42,
    "token_address": "<Base58Check CIS-2 token address>",
    "owner": "<Base58Check Concordium account address>",
    "agent_uri": "https://example.com/agent-card.json"
  }
  ```

  `network` is the CAIP-2 chain id (`ccd:9dd9ca4d19e9393877d2c44b70f89acb` mainnet, default, or
  `ccd:4221332d34e1694168c2a0c0b3fd0f27` testnet). GFR shows these details as provided by you and does not
  verify them on-chain yet.

Response (200):

```json
{
  "registration_id": "reg_...",
  "registration_type": "service_auth",
  "claim_url": "https://api2.globalfinreg.com/agent/identity/claim",
  "claim_token": "clm_...",
  "claim_token_expires": "2026-10-07T12:00:00.000Z",
  "post_claim_scopes": ["gfr.agent"],
  "claim": {
    "user_code": "123456",
    "expires_in": 600,
    "verification_uri": "https://www.globalfinreg.com/dashboard/agent-approve?attempt=att_...",
    "interval": 5
  }
}
```

The response is the same whether or not the email belongs to a GFR account. For an unknown email the user
can never approve, and polling ends with `expired_token`. `claim_token` is returned once; keep it in memory
until Step 4 is done.

## Step 4 — Claim ceremony

GFR also emails the user a link to the approval page. Show the user `verification_uri` and `user_code` in one
message, for example:

> Open this link, sign in to Global FinReg, and enter this 6-digit code: **123456**
> https://www.globalfinreg.com/dashboard/agent-approve?attempt=att_...

The code goes into the page the user lands on after signing in, not back to you. Only the signed-in owner of
the account can approve, and five wrong codes lock the attempt.

### Poll for completion

```http
POST https://api2.globalfinreg.com/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_...
```

Poll every `interval` seconds. While waiting you get `400 {"error": "authorization_pending"}`; on
`slow_down` add 5 seconds to your interval. When the user approves:

```json
{
  "access_token": "<bearer token>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "gfr.agent",
  "identity_assertion": "gfa_...",
  "assertion_expires": "2027-01-04T12:00:00.000Z"
}
```

Keep `identity_assertion` secret: it is your long-lived credential (90 days) for new access tokens. Treat it as
an opaque string.

If the 10-minute `user_code` window passes you get `expired_token`. Re-initiate with the same `claim_token` and
`login_hint` to get a new `user_code` (the user gets a new email), then show it to the user and resume polling:

```http
POST https://api2.globalfinreg.com/agent/identity/claim
Content-Type: application/json

{ "claim_token": "clm_...", "login_hint": "user@example.com" }
```

The response carries a new `claim` block (same shape as in Step 3). If it returns
`410 claim_expired` (the 24-hour registration window closed), restart at Step 3.

## Step 5 — Exchange the assertion

When the access token expires, exchange the `identity_assertion` for a new one:

```http
POST https://api2.globalfinreg.com/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=gfa_...
```

Response (200): `{"access_token": "...", "token_type": "Bearer", "expires_in": 3600, "scope": "gfr.agent"}`.
`invalid_grant` means the assertion expired or was revoked: restart at Step 3.

## Step 6 — Use the access_token

```http
GET https://api2.globalfinreg.com/api/Lei/GetLEISUser
Authorization: Bearer <access_token>
```

- The token acts as the user, but only on the endpoints listed at https://api2.globalfinreg.com/agentapi. Other endpoints return
  `403 {"error": "insufficient_scope"}`.
- On `401`, exchange the assertion once (Step 5) and retry. A `401` on a specific resource can also mean the user
  does not have access to it.
- Requests under `/api/` are rate limited; on `429` wait a few seconds before retrying.

### Use the MCP server

The same access token works with GFR's MCP server (Streamable HTTP) at `https://api2.globalfinreg.com/mcp`.
Send it as `Authorization: Bearer <access_token>` on every MCP request. Without a token the server only lists the
public LEI tools (search, LEI record, prices, legal forms); with one it adds tools for the user's LEIs, renewals,
transfers and invoices. Server card: https://api2.globalfinreg.com/.well-known/mcp/server-card.json.

To renew an LEI, call `get_renewal_quote`, show the user the price and get their agreement, then call
`create_renewal_order` with the quoted total as `expectedTotal`. With `paymentMethod` `card` you get a
`paymentUrl` for the user to pay on the payment provider's page (never ask for card details); with `invoice`
the renewal is confirmed and you get the invoice PDF for the user to pay by bank transfer. `download_invoice`
fetches an invoice again later. If a quote adds VAT for an entity in another EU country, its `vatNote` says so:
ask the user for the entity's EU VAT number, check it with `verify_vat_number` and pass `vatCountry` and
`vatNumber` to the quote and the order tool. A number VIES confirms as valid takes the VAT off (reverse charge).

To transfer an LEI to GFR, ask the user how they want to deal with the transfer power of attorney (POA), then
call `start_lei_transfer` with `poaMethod` `sign` (the user signs online, as a director or board member; you
get a `signingUrl`), `email` (GFR emails a signing link to the signer, by default the user) or `download` (you
get the POA PDF; upload the copy the user signed with `upload_signed_transfer_poa`). `check_transfer_status`
confirms an online signature and reports progress. If the LEI expires within
2 months it must be renewed with the transfer: the answers say so
(`renewalRequired`), and you renew it as above.

If the user has no GFR account yet, call the public `create_user` tool with their email address, first name
and last name (all required; ask the user and confirm them first). GFR emails them a link to set a password; the tool returns no token. Once they have set
it, register as in Step 3. The tool answers the same whether or not the address already has an account,
and is limited to 5 calls per hour per client.

### Buy with ACP

The same access token also works with GFR's [Agentic Commerce Protocol](https://www.agenticcommerce.dev)
checkout (version `2026-04-17`) at `https://api2.globalfinreg.com/acp`, described by
https://api2.globalfinreg.com/.well-known/acp.json. The buyer is always the user of the token. Send
`API-Version: 2026-04-17` on every request and a new `Idempotency-Key` on every POST.

There is no product feed: the item id says what is bought. A session buys exactly one item.

| Item id | What it buys |
|---|---|
| `lei_renewal:{leiId}:{years}` | Renewing one of the user's LEIs for 1, 3 or 5 years (`leiId` from `GET /api/Lei/GetLEISUser` or `list_my_leis`) |
| `lei_registration:{leiId}:{years}` | A new LEI for 1, 3 or 5 years: one of the user's draft applications |

To register a new LEI, first create the draft with the entity's data (`POST /api/Lei`, see https://api2.globalfinreg.com/agentapi),
then buy `lei_registration:{id}:{years}` with the id it returns. The session is
`not_ready_for_payment` until the application can be paid for, as on the website (for example while the
limited power of attorney still has to be signed); its `messages` say why.

```http
POST https://api2.globalfinreg.com/acp/checkout_sessions
Authorization: Bearer <access_token>
API-Version: 2026-04-17
Idempotency-Key: <uuid>
Content-Type: application/json

{ "line_items": [{ "id": "lei_renewal:<leiId>:1" }], "currency": "eur", "capabilities": {} }
```

Show the user the session's `totals` (minor units, VAT included) and get their agreement, then
`POST .../checkout_sessions/{id}/complete` with `payment_data`:

- **Invoice**: `{"purchase_order_number": "<the user's PO number or any reference>"}`. The order is confirmed
  and paid by bank transfer; `order.id` is the renewal order id or the LEI id, and the MCP tool
  `download_invoice` returns the invoice PDF.
- **Card**: only when the session lists the `card_stripe_spt` handler in
  `capabilities.payment.handlers`: a Stripe shared payment token,
  `{"handler_id": "card_stripe_spt", "instrument": {"type": "card", "credential": {"type": "spt", "token": "spt_..."} } }`.
  Never ask the user for card details.

Sessions expire after 24 hours. `GET .../checkout_sessions/{id}` returns the current state and
`POST .../checkout_sessions/{id}/cancel` cancels an open session.

### Buy with AP2

GFR's A2A agent (https://api2.globalfinreg.com/.well-known/agent-card.json) also plays the `merchant` role of the
[Agent Payments Protocol](https://ap2-protocol.org) (AP2 v0.1). It sells the same items through the same
checkout as ACP. Send the access token as `Authorization: Bearer <access_token>` on these `SendMessage`
requests to `https://api2.globalfinreg.com/a2a`:

1. A message with a data part `{"ap2.mandates.IntentMandate": {...} }` whose `skus` hold one item id
   from the table above. The answer is a task (its id is the cart id) with a
   `ap2.mandates.CartMandate` artifact once the item can be paid for, or a rejected task saying why.
   `merchant_authorization` is an ES256 JWT over the cart (`cart_hash`: base64url SHA-256 of the
   `contents` in RFC 8785 canonical JSON), verifiable with https://api2.globalfinreg.com/.well-known/jwks.json.
2. After the user agrees to the cart, a message with `{"ap2.mandates.PaymentMandate": {...} }`:
   `payment_details_id` is the cart id, `payment_details_total` its total, and `payment_response.method_name`
   either `https://www.globalfinreg.com/payment-methods/invoice` (pay by invoice; optional
   `details.purchase_order_number`) or `CARD` with a Stripe shared payment token in `details.token`, only
   when the cart lists it. The answer is the completed task with a payment receipt artifact.

`GetTask` and `CancelTask` take the cart id.

## Errors

| Code | Where | What to do |
|---|---|---|
| `anonymous_not_enabled`, `issuer_not_enabled` | `/agent/identity` | Use `service_auth`. |
| `invalid_request` | any | Fix the request body (JSON for `/agent/identity` and `/agent/identity/claim`, form-encoded for `/oauth2/token`). |
| `invalid_claim_token` | `/agent/identity/claim` | Restart at Step 3. |
| `claimed_or_in_flight` | `/agent/identity/claim` | Already approved, denied or claimed: keep polling or restart at Step 3. |
| `claim_expired` (410) | `/agent/identity/claim` | Restart at Step 3. |
| `authorization_pending` | `/oauth2/token` | Keep polling every `interval` seconds. |
| `slow_down` | `/oauth2/token` | Add 5 seconds to your interval. |
| `expired_token` | `/oauth2/token` | Re-initiate at `/agent/identity/claim`. |
| `access_denied` | `/oauth2/token` | The user denied the request. Stop and tell the user. |
| `invalid_grant` | `/oauth2/token` | Claim token already used, or assertion expired/revoked: restart at Step 3. |
| `unsupported_grant_type` | `/oauth2/token` | Use one of the grant types above, or the OAuth grants below. |
| `insufficient_scope` (403) | API | The endpoint is not available to agents. |
| 429 | any | Back off and retry. |

## Revocation

Revoke your credential when you are done:

```http
POST https://api2.globalfinreg.com/oauth2/revoke
Content-Type: application/x-www-form-urlencoded

token=gfa_...
```

Always `200`. After revocation the assertion returns `invalid_grant`. Access tokens already issued stay valid
until they expire (at most one hour). Users can ask info@globalfinreg.com to revoke agent access to their account.

## Alternative: OAuth sign-in for MCP clients (Claude's Connect button)

MCP clients with built-in OAuth, such as Claude, don't need the steps above. Add the MCP server
`https://api2.globalfinreg.com/mcp/account`: it answers unauthenticated requests with `401` and
`WWW-Authenticate: Bearer resource_metadata="..."`, so the client runs a standard OAuth 2.1 sign-in. The user
clicks Connect, approves in a popup on www.globalfinreg.com and is back in the chat, with no code to type.

- Client registration (RFC 7591): `POST https://api2.globalfinreg.com/oauth2/register` with `redirect_uris`
  (https, or http on localhost/127.0.0.1/[::1]); public clients only (`token_endpoint_auth_method` `none`).
- Authorization: `GET https://api2.globalfinreg.com/oauth2/authorize` with `response_type=code`, `client_id`,
  `redirect_uri`, `state` and PKCE (`code_challenge`, `code_challenge_method=S256`).
- Token: `POST https://api2.globalfinreg.com/oauth2/token` with `grant_type=authorization_code` (`code`,
  `client_id`, `redirect_uri`, `code_verifier`), then `grant_type=refresh_token`. Refresh tokens rotate on
  every use and expire after 90 days without use; revoke one at
  `/oauth2/revoke`.

Access tokens are the same `gfr.agent` tokens as above.

## Alternative: provisioned API secret

Integrations without a person present can ask info@globalfinreg.com for an API secret on the account and exchange it
with `GET https://api2.globalfinreg.com/api/Users/GenerateApiToken?secret={apiSecret}` for a one-hour bearer token
(`{"token": "..."}`). These tokens have the user's full access. Keep the secret out of logs and prompts.