Back to API documentation
# OAuth Authentication Guide This guide describes how third-party integrations authenticate against Lightning Payroll using the hosted OAuth flow. Base URL used in examples: ```text https://api.dev.intellitron.com.au ``` ## Before you start You will need: 1. an approved API-admin partner account 2. a `client_id` and `client_secret` created for that partner account 3. at least one registered redirect URI 4. a backend service that can safely store the client secret and refresh tokens If you have not created your OAuth client yet, start with the **API Admin Setup and Management Guide** first. ## What this guide covers This guide explains: - how to start the hosted customer authorization flow - how to exchange the returned code for tokens - how to refresh tokens safely - what scopes and errors to expect This guide does not cover partner branding, connection monitoring, or API-admin setup tasks. ## Roles in the flow There are three separate actors: 1. **API admin partner**: owns the OAuth client and manages branding and connection monitoring. 2. **Partner application**: redirects users, exchanges codes, stores refresh tokens, and calls the API. 3. **Lightning Payroll customer**: signs in to Lightning Payroll and authorizes access to their payroll account. The OAuth client belongs to the API-admin partner, but the resulting access token represents the customer who completed the hosted login and consent flow. ## Supported authentication model Lightning Payroll currently supports: - OAuth 2.0 authorization code flow - refresh-token rotation - bearer access tokens Lightning Payroll does **not** currently support: - client credentials grant - password grant - PKCE - dynamic client registration ## Supported scopes Supported OAuth scopes are: | Scope | Purpose | |---|---| | `openid` | Required on `/api/oauth/authorize` for modern clients | | `openapi` | Legacy alternative to `openid`; still accepted | | `payroll.read` | Read-only payroll API access | | `payroll.write` | Read/write payroll API access | | `partner.checkout.preview` | Partner checkout discovery and preview | | `partner.checkout.write` | Partner checkout execution | | `partner.checkout.cancel` | Partner checkout cancellation | ### Scope recommendations - Request `openid payroll.read` for read-only payroll integrations. - Request `openid payroll.write` for integrations that create or update payroll data. - Only request partner-checkout scopes for API-admin partner flows that actually use those endpoints. - Prefer `openid` over the legacy `openapi` scope. ### How scopes are enforced For the main payroll API: - `GET` requests require `payroll.read` or `payroll.write` - non-`GET` requests require `payroll.write` For partner-checkout endpoints, the server checks the explicit partner scope required by that endpoint. ## High-level flow 1. The API admin provisions an OAuth client and registers redirect URIs. 2. Your app redirects the browser to `GET /api/oauth/authorize`. 3. Lightning Payroll redirects the user to its hosted login page. 4. The customer signs in and approves access. 5. Lightning Payroll redirects the browser back to your registered `redirect_uri` with `code` and `state`. 6. Your backend exchanges the code at `POST /api/oauth/token`. 7. Your backend stores the returned refresh token securely. 8. Your backend uses the access token as a bearer token on API calls. 9. When the access token expires, your backend exchanges the refresh token for a new token pair. ## Step 1) Redirect the browser to `/api/oauth/authorize` This endpoint is unauthenticated. It validates the client and kicks off the hosted login flow. ### Request ```bash curl -i -sS --get "$BASE_URL/api/oauth/authorize" \ --data-urlencode "client_id=your-client-id" \ --data-urlencode "redirect_uri=https://partner.example.com/oauth/callback" \ --data-urlencode "state=partner-state-123" \ --data-urlencode "scope=openid payroll.write" ``` ### Expected behaviour - Returns HTTP `302`. - Redirects to Lightning Payroll's hosted login page at `/auth/oauth-login`. - Preserves your original `state` internally and later returns it unchanged to your callback. - Passes through the exact `redirect_uri` you supplied. ### Required query parameters | Parameter | Required | Notes | |---|---|---| | `client_id` | Yes | Must be a valid client owned by an API-admin customer | | `redirect_uri` | Yes | Must exactly match one of the registered redirect URIs | | `state` | Yes | Opaque value from your app; returned unchanged to your callback | | `scope` | Yes in practice | Defaults to `openid`, but most integrations should request `openid payroll.read` or `openid payroll.write` | ### Important behaviour - `redirect_uri` matching is exact-string matching. - `openid` is required on authorize. `openapi` is still accepted for older clients. - Unsupported scopes return `400`. - If the client exists but its owner is not an API admin, the endpoint returns `403`. - This step starts in the browser, not as a background server-to-server request. ### Common `authorize` errors | Status | Response detail | Meaning | |---|---|---| | `400` | `Invalid client_id` | Unknown client | | `400` | `Invalid redirect_uri` | Redirect URI is not registered on the client | | `400` | `At least one scope is required.` | Scope set was empty | | `400` | `Unsupported OAuth scope(s): ...` | One or more scopes are not allowed | | `400` | `The openid scope is required. Legacy 'openapi' is also accepted.` | `openid`/`openapi` missing | | `403` | `Forbidden: Only API-admin customers can initiate OAuth` | Client owner is not API-admin enabled | | `422` | FastAPI validation error | A required query parameter was missing | ## Step 2) Hosted login and consent After `/api/oauth/authorize`, Lightning Payroll takes over in the browser. Your app should **not** call `/api/oauth/complete` directly. That endpoint is part of the hosted UI flow. The hosted flow signs the user in, collects consent, creates a one-time authorization code, and redirects back to your `redirect_uri`. The hosted login URL is single-use and expires after 10 minutes. If the user sits on an old login page or retries a stale completion URL, restart the flow from `/api/oauth/authorize`. In other words: your app starts the flow, Lightning Payroll handles login and consent, and your app takes over again only after the browser returns to your redirect URI. ## Step 3) Receive the callback After successful login and approval, the browser returns to your redirect URI: ```text https://partner.example.com/oauth/callback?code=<authorization_code>&state=<your_original_state> ``` ### Authorization-code properties - single use - valid for 10 minutes - tied to the original `client_id` - tied to the original `redirect_uri` Your backend should exchange it immediately and should never reuse it. ## Step 4) Exchange the authorization code for tokens Use `POST /api/oauth/token` with form-encoded data. ### Request ```bash curl -sS "$BASE_URL/api/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=authorization_code" \ --data-urlencode "code=<authorization_code>" \ --data-urlencode "client_id=your-client-id" \ --data-urlencode "client_secret=your-client-secret" \ --data-urlencode "redirect_uri=https://partner.example.com/oauth/callback" ``` ### Response ```json { "access_token": "<jwt-access-token>", "token_type": "Bearer", "expires_in": 1800, "refresh_token": "<opaque-refresh-token>", "refresh_expires_in": 2592000 } ``` ### Token response fields | Field | Meaning | |---|---| | `access_token` | Bearer token for API calls | | `token_type` | Always `Bearer` | | `expires_in` | Access-token lifetime in seconds; currently 1800 | | `refresh_token` | Opaque refresh token shown once | | `refresh_expires_in` | Refresh-token lifetime in seconds; currently 2592000 | ### Important behaviour - `client_secret` is sent in the request body, not via HTTP Basic auth. - `redirect_uri` is required for the authorization-code grant. - `redirect_uri` must exactly match the URI used when the code was issued. - The code is marked used as part of a successful exchange. - This token exchange should happen on your backend, not in browser JavaScript. ## Step 5) Refresh the token pair When the access token expires, exchange the refresh token for a fresh access token and a fresh refresh token. ### Request ```bash curl -sS "$BASE_URL/api/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=refresh_token" \ --data-urlencode "refresh_token=<current_refresh_token>" \ --data-urlencode "client_id=your-client-id" \ --data-urlencode "client_secret=your-client-secret" ``` ### Response ```json { "access_token": "<new-jwt-access-token>", "token_type": "Bearer", "expires_in": 1800, "refresh_token": "<new-opaque-refresh-token>", "refresh_expires_in": 2592000 } ``` ### Refresh-token rotation rules - Refresh tokens are single-use. - The previous refresh token is revoked as soon as it is used successfully. - If a refresh token is expired, revoked, or unknown, the server returns `400`. - Store the replacement refresh token immediately and discard the old one. ## Step 6) Call the API with the bearer token Use the access token in the `Authorization` header: ```bash ACCESS_TOKEN="<jwt-access-token>" curl -sS "$BASE_URL/api/company" \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` For write operations, request and use a token that includes `payroll.write`. ## Exact token-handling guidance ### Treat access tokens as bearer tokens Lightning Payroll access tokens are JWTs, but integrations should treat them as bearer tokens rather than relying on undocumented claims. Today the token includes claims such as: - `sub` - `admin_mode` - `api_client_id` - `scope` - `exp` Do not build hard dependencies on claim shape beyond what the API contract documents. There is no published JWKS or `aud`/`iss` verification contract in this integration surface. ### Store refresh tokens server-side only Refresh tokens should be stored only in your backend or secure server-side secret store. Do not: - expose refresh tokens to browsers - log refresh tokens - persist superseded refresh tokens after rotation ## Full error reference for `/api/oauth/token` | Status | Response detail | Meaning | |---|---|---| | `200` | token response | Success | | `400` | `Missing code for grant_type=authorization_code` | `code` missing | | `400` | `Missing redirect_uri for grant_type=authorization_code` | `redirect_uri` missing on code exchange | | `400` | `Invalid or expired code` | Unknown, expired, used, wrong-client, or wrong-redirect code | | `400` | `Missing refresh_token for grant_type=refresh_token` | `refresh_token` missing | | `400` | `Invalid or expired refresh token` | Unknown, expired, or revoked refresh token | | `401` | `Invalid client credentials` | Bad `client_id` / `client_secret` combination | | `404` | `Customer not found` | Token subject no longer exists | | `422` | FastAPI validation error | Unsupported `grant_type` or missing required form fields | ## Design constraints and implementation notes These are worth accounting for in your integration design: 1. There is no PKCE today, so keep the client secret on a confidential backend. 2. There is no client-credentials grant, so you must use the hosted customer authorization flow. 3. `state` is preserved and returned unchanged. Use it for CSRF protection and request correlation. 4. Redirect URI matching is strict. Keep environment-specific callback URLs registered exactly as used. 5. The same OAuth client can be used by many Lightning Payroll customers, and the API-admin account can inspect and revoke those connections through the API-admin management endpoints. ## Recommended production checklist 1. Register separate redirect URIs for dev, staging, and production. 2. Request the narrowest scope set you need. 3. Exchange authorization codes immediately. 4. Rotate refresh tokens exactly as returned by the token endpoint. 5. Retry token refreshes carefully; never reuse an old refresh token after a successful refresh. 6. Monitor customer connections and request errors from the API-admin endpoints.