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).

CredentialUse caseLifespanScopes
Personal Access Token (PAT)Scripts, server-to-server jobs, SCIM, Digital Signage, most integrationsUp to 365 days; you choose at creation timeFine-grained per resource
OAuth 2.0 (authorization_code)Multi-user apps with a login flow; the Bonusly MCP server2-hour access tokens with refreshFine-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

  1. Sign in as a Global or Tech admin.
  2. Go to Profile Settings → API Tab or Company → Integrations → API & Tokens (if you're a Bonusly admin).
  3. Click Create token.
  4. 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.
  5. 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:

  1. Create a fresh token from the API & Tokens page with the
    same scopes (use the old token's name as a reference).
  2. Update the integration's stored credential with the new
    token.
  3. 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

  1. Register an OAuth application at Company → Integrations
    → OAuth Applications
    . Save the client_id; if you're
    building a confidential server-side client, save the
    client_secret shown once at creation time.

  2. 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.
  3. 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
  4. On callback, validate state first. If the returned
    state doesn't match the value you stored for this user's
    session, abort — that's the signal of a CSRF attempt. Only
    after state matches should you read the code.

  5. Exchange the code at POST https://bonus.ly/oauth/token,
    passing back the code_verifier you 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 one

    Doorkeeper returns an access token (2-hour lifetime) and a
    refresh token if you requested the offline scope.

  6. 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

StatusWhat it meansWhat to do
401 UnauthorizedThe bearer is missing, malformed, or unknown.Confirm the Authorization header includes Bearer and a non-empty token.
401 UnauthorizedThe bearer is expired, revoked, or the owner was deactivated.Create a new token.
403 ForbiddenThe 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 RequestsThe 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.