# OAuth

> Let other Lucra accounts connect your app.

Source: https://www.onlucra.com/docs/oauth

Use OAuth when your app acts for Lucra accounts you don't own. A member signs in to Lucra and approves your app, and you get an access token instead of asking them for an [API key](https://www.onlucra.com/docs/authentication#api-keys). For your own account, a key is simpler.

The flow is the standard authorization code flow with PKCE: register your app once, send people to authorize it, exchange the code for tokens, and refresh them.

## Register your app

Owners and admins create an app in **Settings → Developers → OAuth apps** with **Create OAuth app**:

| Field         | Rule                                                                         |
| ------------- | ---------------------------------------------------------------------------- |
| Name          | Up to 200 characters; it can't contain "Lucra"                              |
| Callback URLs | One per line, up to 20; HTTPS only, no `localhost`, credentials or fragment |
| Access        | **Read**, or **Read and write**                                              |

Lucra then shows the client ID (`lucra_…`) and client secret (`lucra_oauth_…`). The secret is shown once; keep it on your server. Every app is confidential and authenticates to the token endpoint with HTTP Basic. Any Lucra account can authorize it.

**Copy setup instructions** on the app copies its client ID, endpoints and callbacks, never the secret.

## Authorize

Send the person to the authorization URL. Generate a fresh `state` and PKCE code verifier each time:

```bash
https://api.onlucra.com/api/auth/oauth2/authorize
  ?response_type=code
  &client_id=lucra_…
  &redirect_uri=https://app.example.com/oauth/callback
  &scope=read%20write%20offline_access
  &resource=https://api.onlucra.com/v1
  &state=…
  &code_challenge=…
  &code_challenge_method=S256
```

They sign in if they need to, then approve on the consent screen. If they belong to more than one account, they pick which one your app gets, and the grant is for that account. Creators can't authorize apps.

Lucra then redirects to your `redirect_uri` with `code` and `state`. The `redirect_uri` must match a registered callback exactly. Check that `state` matches the one you sent.

`resource` names the API the token is for. Leave it out and the grant is for `/v1`. The code, its tokens and every refresh keep that audience, so the token request can leave it out too.

## Exchange the code

The code lasts 10 minutes and works once.

```bash
curl https://api.onlucra.com/api/auth/oauth2/token \
  -u "$LUCRA_CLIENT_ID:$LUCRA_CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code=$CODE \
  -d code_verifier=$CODE_VERIFIER \
  -d redirect_uri=https://app.example.com/oauth/callback \
  -d resource=https://api.onlucra.com/v1
```

```json
{
  "access_token": "eyJ…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "expires_at": 1791306245,
  "refresh_token": "…",
  "scope": "read write offline_access"
}
```

## Call the API

Use the access token like a key:

```bash
curl https://api.onlucra.com/v1/account \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Lucra-Version: 2026-10-01"
```

It acts for the account that authorized your app. If that account is a [partner](https://www.onlucra.com/docs/api-partners), send `Lucra-Account: acct_…` to reach one of its client brands, as with a partner key.

With the SDK, pass the access token where you'd pass a key: `new Lucra(accessToken)`.

## Refresh

Access tokens last an hour. Swap the refresh token for a new pair:

```bash
curl https://api.onlucra.com/api/auth/oauth2/token \
  -u "$LUCRA_CLIENT_ID:$LUCRA_CLIENT_SECRET" \
  -d grant_type=refresh_token \
  -d refresh_token=$REFRESH_TOKEN
```

Store the new refresh token and drop the old one: reusing a refresh token ends the grant. A refresh token lasts 30 days, so refresh at least that often.

A grant lasts until the person disconnects your app, leaves the account, or a year passes. Then they authorize again.

## Scopes

| Scope            | Grants                                                   |
| ---------------- | -------------------------------------------------------- |
| `read`           | Every read, like a `read` key                            |
| `write`          | Reads and writes, like a `write` key                     |
| `offline_access` | A refresh token, so your app stays connected             |

Ask only for what your app registered: a **Read** app can't ask for `write`. A token never does more than the person's own role; members who aren't owners or admins read everything but payments. See [Access](https://www.onlucra.com/docs/access).

## Revoke

Revoking a refresh token ends the grant, like the person disconnecting your app: your app leaves their **Settings → Connected apps** and every token from that grant stops working. Revoking an access token ends only that token.

```bash
curl https://api.onlucra.com/api/auth/oauth2/revoke \
  -u "$LUCRA_CLIENT_ID:$LUCRA_CLIENT_SECRET" \
  -d token=$REFRESH_TOKEN \
  -d token_type_hint=refresh_token
```

People disconnect your app in **Settings → Connected apps**, and its tokens stop working at once. Editing your app, rotating its secret or revoking it disconnects everyone; they authorize again.

## Errors

| Result                       | Means                                                       |
| ---------------------------- | ----------------------------------------------------------- |
| `error=access_denied`        | The person declined, or can't grant access to that workspace |
| `invalid_grant`              | The code expired or was used, or the refresh token was reused |
| `invalid_client`             | Unknown client, or a missing or wrong client secret         |
| `unsupported_grant_type`     | Only `authorization_code` and `refresh_token`; for machine-to-machine access use an [API key](https://www.onlucra.com/docs/authentication#api-keys) |
| `401 invalid_key`            | The token expired or was revoked; refresh or authorize again |
| `403 permission_denied`      | A `read` token called a write                               |
| `403 account_not_reachable`  | `Lucra-Account` isn't one the token reaches                 |
| `403 internal_route`         | The endpoint is only for Lucra's apps                       |

## Discovery

Server metadata is at `https://api.onlucra.com/.well-known/oauth-authorization-server/api/auth`, and the API's protected-resource metadata at `https://api.onlucra.com/.well-known/oauth-protected-resource/v1`. A `401` from `/v1` names it in `WWW-Authenticate` (`resource_metadata`). Clients that can't register ahead, like [MCP](https://www.onlucra.com/docs/mcp) clients, register themselves at `POST https://api.onlucra.com/api/auth/oauth2/register`; such a client works with the first account that authorizes it.
