Lightning Payroll OAuth Authentication Back to API documentation →
Partner integration guide

Redirect, consent, call the API.

How third-party integrations authenticate against Lightning Payroll using the hosted OAuth 2.0 authorization code flow. Your app redirects a browser, Lightning Payroll handles login and consent, and your backend exchanges the result for a bearer token that represents the customer who said yes.

6 steps 9 scopes PKCE S256 30 min access token 30 day refresh chain
Your app redirects the browser Hosted login sign-in + consent Callback code + state Your backend exchanges for tokens GET /oauth/authorize redirect_uri POST /oauth/token

Steps 2-5 collapse into one emailed link when you provision through partner checkout (see the flow-at-a-glance note below).

00Before you start

You will need:

  • an approved API-admin partner account
  • a client_id and client_secret created for that partner account
  • at least one registered redirect URI
  • 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.

This guide does not cover partner branding, connection monitoring, or API-admin setup tasks. For partner branding and white-label configuration, see the API Branding & Co-Branding Guide.

Base URLs
BASE_URL="https://<your-au-sandbox-api-host>"     # AU sandbox
# BASE_URL="https://<your-nz-sandbox-api-host>"  # NZ sandbox

Set BASE_URL to the AU or NZ host that matches the customer-facing deployment you are testing. Sandbox hostnames are issued with your partner credentials.

01Roles in the flow

There are three separate actors. 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.

ActorRole
API admin partnerOwns the OAuth client and manages branding and connection monitoring.
Partner applicationRedirects users, exchanges codes, stores refresh tokens, and calls the API.
Lightning Payroll customerSigns in to Lightning Payroll and authorizes access to their payroll account.

Who can complete consent

By default, only an account administrator can authorize a connection. That grant is scoped to a Lightning Payroll customer account rather than to an individual person or a single company, and the resulting token can read every payroll company in that account.

Lightning Payroll accounts can also have payroll users (sometimes called non-admin users), who sign in with their own credentials and may be restricted to a subset of companies. A payroll user cannot complete the consent step unless your OAuth client has been enabled for per-user grants. Without it, the hosted login screen says so before asking for a password, and /api/oauth/complete independently returns 403 for any payroll-user session, so a caller driving the flow directly gets the same answer.

Per-user grants, where enabled, let each person authorize your integration in their own name. The flow is unchanged; what changes is the token you get back:

Who consentedadmin_mode claimsub claimReach
Account administratortrueTheir email addressThe whole account
Payroll userfalseTheir usernameOnly what that user may do, including their company restrictions

The identity survives refresh-token rotation, so a grant made for a payroll user stays that user's grant. If the user is deactivated or removed, the next token exchange fails rather than falling back to administrator access. Ask us if you want this enabled; it is off unless we have discussed it with you. See the Partner Session Handoff Guide for what per-user grants are usually used for.

This matters when the same email address is used twice, which is common for bookkeepers and advisors: an address can belong to an administrator on one account and to a payroll user on a different account. The hosted login screen therefore asks which type of account is signing in before it asks for a password. If your integration deep-links a user into the flow, expect that extra step. The same ambiguity applies when your own integration signs in with a username and password; send account_type=admin to state the identity up front (see "Fetching that bearer token" in the API Admin Setup and Management Guide).

What to tell your customer: unless you are using per-user grants, the person who authorizes the connection must be the account administrator for the payroll account you want access to. Once they have authorized it, the connection covers the whole account, and their payroll users do not need to authorize anything themselves.

02Authentication model & scopes

Supported

  • OAuth 2.0 authorization code flow
  • Refresh-token rotation
  • Bearer access tokens
  • PKCE (RFC 7636), S256 only

Not supported

  • Client credentials grant
  • Password grant
  • Open dynamic client registration. An RFC 7591 registration_endpoint is advertised, but it only accepts exact matches against a fixed allow-list and refuses to persist anything else. Partner clients are provisioned by Lightning Payroll, not self-registered.

PKCE is optional for confidential clients that authenticate with a client_secret, and required for public clients: a client registered with no stored secret must send code_challenge on /oauth/authorize and the matching code_verifier on /oauth/token, because PKCE takes the place of the secret. plain is rejected; only S256 is accepted.

Supported scopes

ScopePurpose
openidRequired on /api/oauth/authorize for modern clients
openapiLegacy alternative to openid; still accepted
payroll.readRead-only payroll API access
payroll.writeRead/write payroll API access
session.handoffExchange the user's access token for a single-use browser sign-in link
partner.checkout.previewPartner checkout discovery and preview
partner.checkout.writePartner checkout execution, and renewal management
partner.checkout.cancelPartner 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.
  • Request openid session.handoff when users should click through into Lightning Payroll without signing in again. Add payroll scopes only if your API integration also needs them.
  • 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.

POST /api/partner/session-handoff requires the exact session.handoff scope as well as per-client enablement. OAuth scopes cannot be added during refresh-token rotation, so users with an older grant must complete /api/oauth/authorize again to approve this scope.

03The flow at a glance

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.

Provisioning + consent in one flow (partner checkout). If you provision customers through partner checkout, you can collapse steps 2-5 into a single emailed flow: pass an oauth_onboarding block to POST /api/partner-checkout/orders and the new customer is emailed a single-use magic link that signs them in and lands them straight on the consent screen, with no separate login or authorize redirect on your side. You still receive code + state at your redirect_uri and exchange them exactly as in steps 3-4. See "OAuth single-flow onboarding" in the Partner Checkout Admin Endpoints Guide.

04Step 1 – Redirect to /api/oauth/authorize

This endpoint is unauthenticated. It validates the client and kicks off the hosted login flow.

GET/api/oauth/authorize
GET /api/oauth/authorize
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

ParameterRequiredNotes
client_idYesMust be a valid client owned by an API-admin customer
redirect_uriYesMust exactly match one of the registered redirect URIs
stateYesOpaque value from your app; returned unchanged to your callback
scopeYes, in practiceDefaults 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

StatusResponse detailMeaning
400Invalid client_idUnknown client
400Invalid redirect_uriRedirect URI is not registered on the client
400At least one scope is required.Scope set was empty
400Unsupported OAuth scope(s): ...One or more scopes are not allowed
400The openid scope is required. Legacy 'openapi' is also accepted.openid/openapi missing
403Forbidden: Only API-admin customers can initiate OAuthClient owner is not API-admin enabled
422FastAPI validation errorA required query parameter was missing

05Step 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.

06Step 3 – Receive the callback

After successful login and approval, the browser returns to your redirect URI:

Your redirect_uri
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.

07Step 4 – Exchange the code for tokens

Use POST /api/oauth/token with form-encoded data.

POST/api/oauth/token
POST /api/oauth/token
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

200 OK
{
  "access_token": "<jwt-access-token>",
  "token_type": "Bearer",
  "expires_in": 1800,
  "refresh_token": "<opaque-refresh-token>",
  "refresh_expires_in": 2592000
}

Token response fields

FieldMeaning
access_tokenBearer token for API calls
token_typeAlways Bearer
expires_inAccess-token lifetime in seconds; currently 1800
refresh_tokenOpaque refresh token shown once
refresh_expires_inRefresh-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.

08Step 5 – Refresh the token pair

When the access token expires, exchange the refresh token for a fresh access token and a fresh refresh token.

POST /api/oauth/token (refresh)
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"

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.

09Step 6 – Call the API with the bearer token

GET /api/company
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.

10Rate limits

Limits are counted per API client, not per IP address. Your end customers do not share a bucket with each other, and you do not share one with other partners, so a busy pay cycle for one customer will not throttle the rest.

CallsLimit
/api/oauth/token60 per minute
Pay reads and writes (/api/company/{company_id}/pays*, /api/company/{company_id}/pay-runs, both *-workers-comp-pay* routes)60 per minute
Everything else on /api25 per minute, or 120 per minute on list endpoints

Two things worth building in.

Cache the access token. It is valid for 30 minutes, and expires_in on the token response tells you how long you have. Exchanging a fresh token per API call spends your token-endpoint budget on nothing and is the most common cause of an unexpected 429.

Batch where the endpoint accepts a list. PUT /api/company/{company_id}/pays/create and PUT /api/company/{company_id}/create-workers-comp-pays both take an array of pay objects, so one call covers every employee in a company for a given pay date. A per-employee loop is the same work spread across many more requests.

On 429, back off and retry rather than failing the run. These figures are enforced approximately and may be tuned, so treat them as the working envelope rather than a contractual guarantee, and do not tune your client to sit exactly on the boundary.

11Token-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 and 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

12Full error reference for /api/oauth/token

StatusResponse detailMeaning
200token responseSuccess
400Missing code for grant_type=authorization_codecode missing
400Missing redirect_uri for grant_type=authorization_coderedirect_uri missing on code exchange
400Invalid or expired codeUnknown, expired, used, wrong-client, or wrong-redirect code
400Missing refresh_token for grant_type=refresh_tokenrefresh_token missing
400Invalid or expired refresh tokenUnknown, expired, or revoked refresh token
401Invalid client credentialsBad client_id / client_secret combination
404Customer not foundToken subject no longer exists
422FastAPI validation errorUnsupported grant_type or missing required form fields

13Design notes & production checklist

Design constraints and implementation notes

Worth accounting for in your integration design:

  1. PKCE is supported and required for public clients; confidential clients should still keep the client secret on a server-side backend, never in browser JavaScript.
  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.

14Related guides