00The problem this solves
A plain link from your app into Lightning Payroll only works while the user happens to have a live browser session with us. Sessions last 30 minutes of inactivity, so in practice a good share of click-throughs land on our login page instead of where you sent them.
The obvious workaround does not work. GET /api/redirect/company/{company_id} authenticates from the Authorization header and nothing else. A top-level browser navigation cannot set a header, and our web app holds its session token in localStorage rather than a cookie, so following that URL as a link returns 401 regardless of whether the user is signed in. It is a deep-link helper for a caller that can set a header, not a sign-in path. See the Company Redirect Guide for what it is actually for.
01Get a token that names the user
Previously an OAuth grant could only ever be completed by the account administrator, and the access token you received always acted as that administrator. On an account with several payroll users, you had one key for the whole business.
You can now run the normal authorization code flow for each individual user. Request openid session.handoff (plus any payroll API scopes your integration actually needs), send them to /api/oauth/authorize, let them approve the handoff permission, and exchange the code at /api/oauth/token as usual.
Scopes are fixed when the grant is authorized. An existing refresh token cannot acquire session.handoff after the fact; send that user through /api/oauth/authorize again with the new scope. Until they do, the handoff endpoint returns 403, and you should fall back to the normal authorization flow.
What comes back changes, not how you ask for it
The access token now carries the identity of whoever completed the flow:
| Who consented | admin_mode | sub | What the token can do |
|---|---|---|---|
| Account administrator | true | Their email address | Everything the administrator can do |
| Payroll user | false | Their username | Exactly what that user can do, no more |
A payroll user's token is restricted the same way their own login is, including which companies they may open. That is the point: you are acting as them, not as the business.
Refresh tokens keep the identity across rotation, so a grant made for a payroll user stays that user's grant for its whole life. If a user is deactivated or removed from the account, their token stops working at the next exchange rather than quietly falling back to administrator access.
Store one token pair per user, not one per business.
02Turn the token into a signed-in browser
When the user clicks through to Lightning Payroll from your app:
Exchange
Your server calls POST /api/partner/session-handoff with that user's access token.
Receive
You get back a signin_url.
Redirect
You redirect the user's browser to it as a top-level navigation.
Land
We sign them in and land them on the screen you asked for.
If you have no valid token for the user, or refreshing theirs fails, fall back to /api/oauth/authorize as normal and use the resulting token.
Request
Header: Authorization: Bearer <that user's access token>
| Field | Type | Required | Notes |
|---|---|---|---|
target | string | No | pays (default and currently the only supported target) |
company_id | positive integer | Yes | Company whose Pays screen should open. Must be a company the token's user is allowed to access, otherwise the call is refused rather than producing a link that fails on arrival. |
BASE_URL="https://<your-au-sandbox-api-host>" curl -sS -X POST "$BASE_URL/api/partner/session-handoff" \ -H "Authorization: Bearer $USER_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"target": "pays", "company_id": 123}'
Response
{
"signin_url": "https://<your-au-sandbox-app-host>/auth/partner-signin#ticket=...",
"expires_in": 60,
"target_path": "/admin/pays/company/123"
}
Redirect to signin_url immediately. It works once, and it expires after 60 seconds. Do not store it, email it, log it, or put it behind another redirect that might retry. The ticket rides in a URL fragment (#ticket=), never a query string, specifically so it is never written to a server access log.
If the user's session request cannot be honoured, you find out here rather than after they arrive. A company_id the user is not allowed to open is refused at this call, so you never hand somebody a link that fails on landing.
Common errors
| Status | Meaning |
|---|---|
| 403 | The token is not a partner access token, your OAuth client is not enabled, or the token lacks session.handoff |
| 403 | The token's user is not allowed to access that company |
| 422 | target is not pays, or company_id is missing or not a positive integer |
| 404 | No company with that company_id |
03Why not just hand the browser the access token
Because it leaks the token. A credential in a URL is written to the user's browser history, sent onward in the Referer header when the page loads anything external, and captured in the access logs of every proxy between you and us. Our access token is also a bearer credential for the whole payroll account behind a 30-day refresh chain, so a copy in a log file is a real incident.
The handoff ticket is the same idea without the exposure: it is single use, it lives for a minute, and it grants nothing beyond the session the token already stood for.
04What this does not do
- It does not create access. The session is exactly the one the presented token already represented.
- It does not skip authentication. Your user proved who they were, with a password and a one-time code, when they completed the OAuth flow.
- It requires explicit OAuth consent for
session.handoff; that consent is collected during the normal authorization flow, not during each click-through. - It does not extend the session. Once inside, the usual 30-minute idle timeout applies, and the user will need a fresh click-through afterwards.
- It is not a general SSO endpoint. It only signs in a user we already issued you a token for.
05Security notes
- Treat each user's tokens the way you would treat their password. One user's token must never be used to mint a link for a different user.
- The ticket in
signin_urlis a credential. Redirect to it, do not log it. - Sessions created this way are recorded as originating from your integration, so we can answer "who sent this user here" if a customer ever asks.
- Revoking a refresh token, or deactivating the user in Lightning Payroll, stops future handoffs for that person.
06Integration checklist
Run the OAuth flow once per user with session.handoff, not once per business.
Store and refresh each user's tokens separately.
Call the handoff endpoint from your server, with the clicking user's token.
Redirect straight away, and never reuse a link.
Fall back to /api/oauth/authorize when you have no working token for someone.
07Related guides
This guide assumes you already run the OAuth flow. These cover the rest of it.