OAuth API Endpoints

Technical reference for LUMA OAuth 2.0 endpoints.

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

EnvironmentURL
Authorizationhttps://luma.waytogrow.es/oauth
Token & APIhttps://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

ParameterTypeRequiredDescription
response_typestringYesMust be code
client_idstringYesYour application's client ID
redirect_uristringYesURI to redirect after authorization (must be registered)
scopestringYesSpace-separated list of scopes
statestringRecommendedOpaque value for CSRF protection
code_challengestringPKCEBase64-URL-encoded SHA-256 hash of code verifier
code_challenge_methodstringPKCEMust 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:

ParameterDescription
codeAuthorization code (valid for 10 minutes)
stateSame value you sent (verify this!)
https://yourapp.com/callback?code=AUTH_CODE_HERE&state=xyz789

#Error response

Redirects to your redirect_uri with:

ParameterDescription
errorError code
error_descriptionHuman-readable description
stateSame value you sent
https://yourapp.com/callback?error=access_denied&error_description=User%20denied%20access&state=xyz789

#Error codes

CodeDescription
invalid_requestMissing or invalid parameter
unauthorized_clientClient not authorized for this grant type
access_deniedUser denied authorization
invalid_scopeInvalid or unknown scope
server_errorInternal 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/json
  • application/x-www-form-urlencoded

#Authorization code grant

Exchange an authorization code for tokens.

#Request body

ParameterTypeRequiredDescription
grant_typestringYesMust be authorization_code
codestringYesAuthorization code from callback
redirect_uristringYesSame URI used in authorization
client_idstringYesYour application's client ID
client_secretstringConfidential clientsYour client secret
code_verifierstringPKCEOriginal 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"
}
FieldDescription
access_tokenToken for API requests
token_typeAlways Bearer
expires_inSeconds until expiration (3600 = 1 hour)
refresh_tokenToken to get new access tokens
scopeGranted scopes (space-separated)

#Refresh token grant

Get a new access token using a refresh token.

#Request body

ParameterTypeRequiredDescription
grant_typestringYesMust be refresh_token
refresh_tokenstringYesCurrent refresh token
client_idstringYesYour application's client ID
client_secretstringConfidential clientsYour client secret
scopestringNoRequest 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"
}
ErrorDescription
invalid_requestMissing required parameter
invalid_clientInvalid client credentials
invalid_grantInvalid, expired, or used code/token
unauthorized_clientClient not authorized for grant type
unsupported_grant_typeGrant type not supported

#Revocation endpoint

Revoke an access token or refresh token.

POST https://api.luma.waytogrow.es/v1/oauth/revoke

#Request body

ParameterTypeRequiredDescription
tokenstringYesToken to revoke
client_idstringYesYour application's client ID
client_secretstringConfidential clientsYour 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:

EndpointLimit
/oauth/authorize20 requests per 15 minutes per IP
/oauth/token20 requests per 15 minutes per IP
/oauth/revoke20 requests per 15 minutes per IP

Exceeding limits returns 429 Too Many Requests.


#Token lifetimes

Token typeLifetimeNotes
Authorization code10 minutesSingle use
Access token1 hourUse refresh token to renew
Refresh token30 daysRotates 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

  1. Store code_verifier securely (session storage)
  2. Send code_challenge in authorization request
  3. Send code_verifier in 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.