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. 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:
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.
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
{
"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:
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, 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:
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.
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.
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 |
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 clients, register themselves at POST https://api.onlucra.com/api/auth/oauth2/register; such a client works with the first account that authorizes it.