Authentication
The Bonusly API accepts two credential types. You'll almost
always want a Personal Access Token (PAT). OAuth exists for
multi-user apps with a login flow (and for agentic integrations
via the Bonusly MCP server).
| Credential | Use case | Lifespan | Scopes |
|---|---|---|---|
| Personal Access Token (PAT) | Scripts, server-to-server jobs, SCIM, Digital Signage, most integrations | Up to 365 days; you choose at creation time | Fine-grained per resource |
OAuth 2.0 (authorization_code) | Multi-user apps with a login flow; the Bonusly MCP server | 2-hour access tokens with refresh | Fine-grained per resource |
Personal Access Tokens
PATs are bearer tokens created by a company admin from the
Company → Integrations → API & Tokens page.
Creating a token
- Sign in as a Global or Tech admin.
- Go to Profile Settings → API Tab or Company → Integrations → API & Tokens (if you're a Bonusly admin).
- Click Create token.
- Fill in:
- Name — anything memorable; this is how you'll
identify the token in the listing later. - Expiration — number of days, up to 365. Hard maximum,
no exceptions. - Scopes — pick the minimum set the integration needs.
Bonusly hides scopes your admin role can't grant.
- Name — anything memorable; this is how you'll
- Click Create. The token string is shown once —
copy it before closing the dialog. We store only a hash, so a
lost token can't be recovered; you'd have to create a new one.
Tip: create one PAT per integration. If a vendor changes
or a token leaks, you can revoke that one token without
disrupting anything else.
Sending a PAT on a request
Pass the token in the standard Authorization: Bearer … header:
curl https://bonus.ly/api/public/whoami \
-H "Authorization: Bearer YOUR_TOKEN_HERE"If your integration only supports an access_token=… query
parameter, that works too — but we recommend the header form
because query parameters can leak into HTTP server logs.
curl "https://bonus.ly/api/public/whoami?access_token=YOUR_TOKEN_HERE"Scopes
Scopes are the read/write/administer permissions you ask for at
creation time. They're grouped by resource family (users,
recognitions, awards, finance, company, analytics, uploads,
etc.) with a read, write, or administer flavor depending
on what you need. Pick the minimum set the integration actually
needs — adding the administer flavor to a resource that only
needs read is unnecessary blast radius if the token ever
leaks.
The set of scopes you can grant is gated by your admin role.
Global admins can grant any scope; Tech admins can grant a
subset. The full, authoritative list is shown to you in the
create-token modal with anything your role can't grant
already filtered out — you can't accidentally request a scope
the API will then refuse.
At request time, the API rejects calls whose token doesn't
include the scope the endpoint requires.
Finding the scope an endpoint needs: every endpoint's
documentation page in the API reference lists the scope (or
scopes) it requires under the Authorization heading. If
you're not sure what to ask for when creating a token, open
the docs for the endpoints your integration will call and
collect the union of their required scopes.
Expiration and renewal
Every PAT expires — there is no perpetual token. We email the
token owner twice before expiration:
- 30 days out — heads-up, plan a renewal.
- 7 days out — final reminder.
When a token expires it stops working. To renew:
- Create a fresh token from the API & Tokens page with the
same scopes (use the old token's name as a reference). - Update the integration's stored credential with the new
token. - Revoke the old, now-expired row from the table to keep things
tidy.
There's no auto-rotation; renewals are a deliberate human
action.
Revoking a token
Revocation is immediate. Find the row in the API & Tokens
table and click Revoke. The next request bearing that
token gets a 401 Unauthorized.
Bonusly will also revoke PATs automatically in one situation:
7 days after a company subscription is canceled, if the
cancellation hasn't been reversed. If you resubscribe inside that
7-day window, nothing happens to your tokens. If you resubscribe
after the auto-revocation, you'll need to create fresh tokens.
OAuth 2.0
If you're building a multi-user app — anything where end users
log in with their own Bonusly account — use OAuth's
authorization_code grant instead of a PAT. The same flow
powers Bonusly's MCP server for AI agents.
Bonusly only accepts the authorization_code grant
(implicit and password are not supported), and PKCE is
required for every authorization — confidential and public
clients alike. Authorization requests without a
code_challenge are rejected.
The flow
-
Register an OAuth application at Company → Integrations
→ OAuth Applications. Save theclient_id; if you're
building a confidential server-side client, save the
client_secretshown once at creation time. -
Generate a per-request PKCE pair and a CSRF
state.code_verifier: 43-128 chars of random URL-safe alphabet.code_challenge: base64url(SHA256(code_verifier)).state: an unguessable random string. Store the verifier
and the state server-side (or in a same-site, http-only
cookie) keyed off the user's session.
-
Redirect the user to the authorization endpoint with the
challenge and the state:https://bonus.ly/oauth/authorize ?client_id=<your-client-id> &redirect_uri=<your-redirect-uri> &response_type=code &scope=<space-separated scopes> &state=<random-state> &code_challenge=<base64url-sha256-of-verifier> &code_challenge_method=S256 -
On callback, validate
statefirst. If the returned
statedoesn't match the value you stored for this user's
session, abort — that's the signal of a CSRF attempt. Only
afterstatematches should you read thecode. -
Exchange the
codeatPOST https://bonus.ly/oauth/token,
passing back thecode_verifieryou stored in step 2:grant_type=authorization_code code=<code-from-callback> code_verifier=<verifier-from-step-2> client_id=<your-client-id> redirect_uri=<your-redirect-uri> # plus client_secret if you have oneDoorkeeper returns an access token (2-hour lifetime) and a
refresh token if you requested theofflinescope. -
Send the access token as
Authorization: Bearer …on
every API request — exactly like a PAT.
When the access token expires, use the refresh token to exchange
for a fresh access token without prompting the user again. The
same scope catalog applies to OAuth tokens as to PATs.
Sample request
A canonical authenticated call:
curl https://bonus.ly/api/public/whoami \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Accept: application/json"Response (truncated):
{
"success": true,
"message": "authenticated",
"company": { "id": "abc123", "name": "Acme Corp" },
"application": { "id": "def456", "name": "My Integration" },
"scopes": ["user:read", "awards:read"],
"user": null
}Errors you might see
| Status | What it means | What to do |
|---|---|---|
401 Unauthorized | The bearer is missing, malformed, or unknown. | Confirm the Authorization header includes Bearer and a non-empty token. |
401 Unauthorized | The bearer is expired, revoked, or the owner was deactivated. | Create a new token. |
403 Forbidden | The bearer is valid but doesn't carry the scope required by the endpoint. | Create a new token with the right scope, or use a different endpoint. |
429 Too Many Requests | The company has exceeded the rate limit for this operation. | Honor the Retry-After header before retrying. See Rate limits on the Getting started page; limits vary by operation and can be raised on request. |
For the full picture of error envelopes and rate-limit headers,
see Getting started.
See also
- Getting started — from zero to your
first API call. - The API reference in the sidebar — every endpoint shows the
scope it requires.
Updated about 23 hours ago