Complete technical reference for LUMA's OAuth 2.0 implementation. These endpoints follow the OAuth 2.0 RFC 6749 and PKCE RFC 7636 specifications.
#Base URLs
| Environment | URL |
|---|---|
| Authorization | https://luma.waytogrow.es/oauth |
| Token & API | https://api.luma.waytogrow.es/v1 |
#Authorization endpoint
Initiates the OAuth flow by redirecting users to log in and authorize your app.
GET https://luma.waytogrow.es/oauth/authorize
#Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
response_type | string | Yes | Must be code |
client_id | string | Yes | Your application's client ID |
redirect_uri | string | Yes | URI to redirect after authorization (must be registered) |
scope | string | Yes | Space-separated list of scopes |
state | string | Recommended | Opaque value for CSRF protection |
code_challenge | string | PKCE | Base64-URL-encoded SHA-256 hash of code verifier |
code_challenge_method | string | PKCE | Must be S256 |
#Example request
https://luma.waytogrow.es/oauth/authorize?
response_type=code&
client_id=mid_client_abc123&
redirect_uri=https://yourapp.com/callback&
scope=transactions.read%20invoices.read&
state=xyz789&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
#Success response
Redirects to your redirect_uri with:
| Parameter | Description |
|---|---|
code | Authorization code (valid for 10 minutes) |
state | Same value you sent (verify this!) |
https://yourapp.com/callback?code=AUTH_CODE_HERE&state=xyz789
#Error response
Redirects to your redirect_uri with:
| Parameter | Description |
|---|---|
error | Error code |
error_description | Human-readable description |
state | Same value you sent |
https://yourapp.com/callback?error=access_denied&error_description=User%20denied%20access&state=xyz789
#Error codes
| Code | Description |
|---|---|
invalid_request | Missing or invalid parameter |
unauthorized_client | Client not authorized for this grant type |
access_denied | User denied authorization |
invalid_scope | Invalid or unknown scope |
server_error | Internal server error |
#Token endpoint
Exchange authorization codes for access tokens, or refresh existing tokens.
POST https://api.luma.waytogrow.es/v1/oauth/token
#Content types
Accepts both:
application/jsonapplication/x-www-form-urlencoded
#Authorization code grant
Exchange an authorization code for tokens.
#Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
grant_type | string | Yes | Must be authorization_code |
code | string | Yes | Authorization code from callback |
redirect_uri | string | Yes | Same URI used in authorization |
client_id | string | Yes | Your application's client ID |
client_secret | string | Confidential clients | Your client secret |
code_verifier | string | PKCE | Original code verifier |
#Example request (confidential client)
curl -X POST https://api.luma.waytogrow.es/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "AUTH_CODE",
"redirect_uri": "https://yourapp.com/callback",
"client_id": "mid_client_abc123",
"client_secret": "mid_secret_xyz789"
}'
#Example request (public client with PKCE)
curl -X POST https://api.luma.waytogrow.es/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "AUTH_CODE",
"redirect_uri": "https://yourapp.com/callback",
"client_id": "mid_client_abc123",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}'
#Success response
{
"access_token": "mid_at_xxxxxxxxxxxxx",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "mid_rt_xxxxxxxxxxxxx",
"scope": "transactions.read invoices.read"
}
| Field | Description |
|---|---|
access_token | Token for API requests |
token_type | Always Bearer |
expires_in | Seconds until expiration (3600 = 1 hour) |
refresh_token | Token to get new access tokens |
scope | Granted scopes (space-separated) |
#Refresh token grant
Get a new access token using a refresh token.
#Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
grant_type | string | Yes | Must be refresh_token |
refresh_token | string | Yes | Current refresh token |
client_id | string | Yes | Your application's client ID |
client_secret | string | Confidential clients | Your client secret |
scope | string | No | Request subset of original scopes |
#Example request
curl -X POST https://api.luma.waytogrow.es/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "refresh_token",
"refresh_token": "mid_rt_xxxxxxxxxxxxx",
"client_id": "mid_client_abc123",
"client_secret": "mid_secret_xyz789"
}'
#Response
Same format as authorization code grant. The refresh token may rotate (new token returned).
#Token endpoint errors
{
"error": "invalid_grant",
"error_description": "The authorization code has expired"
}
| Error | Description |
|---|---|
invalid_request | Missing required parameter |
invalid_client | Invalid client credentials |
invalid_grant | Invalid, expired, or used code/token |
unauthorized_client | Client not authorized for grant type |
unsupported_grant_type | Grant type not supported |
#Revocation endpoint
Revoke an access token or refresh token.
POST https://api.luma.waytogrow.es/v1/oauth/revoke
#Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
token | string | Yes | Token to revoke |
client_id | string | Yes | Your application's client ID |
client_secret | string | Confidential clients | Your client secret |
#Example request
curl -X POST https://api.luma.waytogrow.es/v1/oauth/revoke \
-H "Content-Type: application/json" \
-d '{
"token": "mid_at_xxxxxxxxxxxxx",
"client_id": "mid_client_abc123",
"client_secret": "mid_secret_xyz789"
}'
#Response
Always returns success, even if token was already invalid:
{
"success": true
}
#Rate limits
OAuth endpoints have specific rate limits to prevent abuse:
| Endpoint | Limit |
|---|---|
/oauth/authorize | 20 requests per 15 minutes per IP |
/oauth/token | 20 requests per 15 minutes per IP |
/oauth/revoke | 20 requests per 15 minutes per IP |
Exceeding limits returns 429 Too Many Requests.
#Token lifetimes
| Token type | Lifetime | Notes |
|---|---|---|
| Authorization code | 10 minutes | Single use |
| Access token | 1 hour | Use refresh token to renew |
| Refresh token | 30 days | Rotates on use |
#PKCE implementation
PKCE adds security for public clients (mobile apps, SPAs).
#1. Generate code verifier
Create a random string (43-128 characters, URL-safe):
function base64UrlEncode(buffer: Uint8Array): string {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
function generateCodeVerifier(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
#2. Create code challenge
SHA-256 hash of the verifier, base64-URL encoded:
async function generateCodeChallenge(verifier: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest("SHA-256", data);
return base64UrlEncode(new Uint8Array(hash));
}
#3. Use in flow
- Store
code_verifiersecurely (session storage) - Send
code_challengein authorization request - Send
code_verifierin token exchange
#Security considerations
#State parameter
Always use and validate the state parameter:
// Generate
const state = crypto.randomUUID();
sessionStorage.setItem("oauth_state", state);
// Validate on callback
const storedState = sessionStorage.getItem("oauth_state");
if (callbackState !== storedState) {
throw new Error("State mismatch - possible CSRF attack");
}
#Redirect URI validation
- Register all redirect URIs in your app settings
- Use exact match validation (no wildcards)
- Always use HTTPS in production
#Token storage
- Store tokens securely (encrypted, server-side preferred)
- Never expose tokens in URLs or logs
- Clear tokens on logout
#Client secret protection
- Never include client secrets in client-side code
- Use environment variables on servers
- Rotate secrets if compromised
#Error handling examples
#Handle authorization errors
app.get("/callback", (req, res) => {
const { error, error_description, code, state } = req.query;
if (error) {
console.error(`OAuth error: ${error} - ${error_description}`);
return res.redirect("/connect?error=" + encodeURIComponent(error as string));
}
// Verify state
if (state !== req.session.oauthState) {
return res.status(400).send("Invalid state");
}
// Exchange code for tokens
// ...
});
#Handle token errors
async function exchangeCode(code: string) {
const response = await fetch("https://api.luma.waytogrow.es/v1/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Token error: ${error.error} - ${error.error_description}`);
}
return response.json();
}
#Handle refresh failures
async function refreshTokens(refreshToken: string) {
try {
const response = await fetch("https://api.luma.waytogrow.es/v1/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
const tokens = await response.json();
if (tokens.error) {
// Refresh token expired or revoked
// Redirect user to re-authorize
return null;
}
return tokens;
} catch (error) {
console.error("Refresh failed:", error);
return null;
}
}
#Using the SDK with OAuth tokens
Once you have an access token, use the LUMA SDK for API requests:
import { LUMA } from "@luma-ai/sdk";
const luma = new LUMA({
token: accessToken, // OAuth access token
});
// List transactions
const transactions = await luma.transactions.list({
pageSize: 50,
});
// Get invoices
const invoices = await luma.invoices.list({
statuses: ["unpaid", "overdue"],
});
// Get financial metrics
const profit = await luma.metrics.profit({
from: "2024-01-01",
to: "2024-12-31",
});
See the SDK documentation for all available methods.
#Related
- Build an OAuth App — Getting started guide
- OAuth Scopes Reference — Available permissions
- App Review Process — Get your app verified
- API Reference — Full API documentation