Getting Started

The Bonusly API lets you read and write your company's data
programmatically — users, recognitions, rewards, redemptions, and
the analytics that roll up from them. Use it to build internal
tooling, sync to your data warehouse, or drive integrations with
HRIS, communication, and BI systems.

This guide walks you from zero to your first successful request in
a few minutes.

What you'll need

  • A Bonusly account on a paid plan.
  • Global admin or Tech admin access on that account. (If
    you're not sure, ask the person who manages your Bonusly
    subscription.)
  • A terminal with curl or your favorite HTTP client.

Step 1 — Create a Personal Access Token

Bonusly authenticates API requests with Personal Access Tokens
(PATs)
. PATs replaced the older "API access tokens" — they
expire (up to one year), carry fine-grained permissions, and can
be revoked individually.

  1. Sign in at bonus.ly.
  2. Go to Profile Settings → API Tab or Company → Integrations → API & Tokens (if you're a Bonusly admin).
  3. Click Create token. Give it a name (e.g. "Quickstart
    notebook"), pick an expiration up to 365 days, and select the
    scopes your integration needs. For this walkthrough, choose
    user:read.
  4. Click Create. Bonusly will show you the token string
    once — copy it now. We store only a hash, so if you close
    the dialog without copying you'll need to create a new one.

For a deeper dive into tokens, scopes, and the difference
between PATs and OAuth, see Authentication.

Step 2 — Call the API

The Bonusly Public API lives at https://bonus.ly/api/public/.
Send your PAT as a Bearer token in the Authorization header.
The smallest request you can make is GET /whoami — it returns
the company, application, scopes, and user associated with the
token:

curl https://bonus.ly/api/public/whoami \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"

A successful response looks like:

{
  "success": true,
  "message": "authenticated",
  "company": { "id": "abc123", "name": "Acme Corp" },
  "application": { "id": "def456", "name": "My Integration" },
  "scopes": ["user:read", "awards:read"],
  "user": null
}

If you get a 401 Unauthorized, double-check that the
Authorization header includes the word Bearer before the
token. If you get a 403 Forbidden, the token is valid but
doesn't carry the required scope for the endpoint you called.

Available API surfaces

Bonusly currently exposes three customer-facing surfaces. New
integrations should target the Public API.

SurfaceBase URLWhen to use it
Public API (date-versioned)https://bonus.ly/api/public/The recommended surface. Stricter contracts, cursor pagination, idempotency, structured rate-limit headers, and date-based versioning. Start here.
Legacy v1https://bonus.ly/api/v1/The original API surface. Still supported and covers some resources the Public API hasn't yet, but no new development happens here. Existing integrations on v1 keep working.
MCP serverhttps://bonus.ly/mcpModel Context Protocol endpoint for agentic / LLM integrations. Authorized via OAuth, not PATs.

Resource coverage on the Public API is expanding actively. If
the resource you need isn't there yet, you can call the v1
endpoint with the same PAT — the same token works on both
surfaces — and migrate later when it lands on Public.

API versioning

The Public API is date-versioned. Each release is identified
by a YYYY-MM-DD date that matches a version cut in our docs
sidebar.

  • Omit the version header and your request routes to the
    latest current version by date. That's a good default for
    exploration and for integrations that want to track us
    forward.
  • Pin a version by sending X-API-Version: YYYY-MM-DD on
    every request. Use this once you're in production and need
    contract stability. The response echoes the resolved version
    back in the X-API-Version header.
curl https://bonus.ly/api/public/whoami \
  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
  -H "X-API-Version: 2026-05-12"

Older versions don't stick around forever — we do remove them
eventually. Before a version is fully removed, we'll notify
integrators via email and via a Deprecation response header
on every call to that version, giving you time to migrate.
Pin a version once you're in production, watch for the
deprecation signal, and move forward when prompted.

The legacy /api/v1/ surface is stable — we add fields and
endpoints but won't remove or rename existing ones in a
backwards-incompatible way. It has no version header.

Cross-cutting features

A few capabilities apply broadly across the API and are worth
knowing about before you build:

Idempotency

State-changing requests on the Public API (POST / PATCH /
DELETE under /api/public/*) accept an optional
Idempotency-Key header — any opaque string you generate
client-side. Bonusly caches the response keyed on
(company, key) for 24 hours and replays the same response if
you retry with the same key. Use it to make network or worker
retries safe.

curl https://bonus.ly/api/public/awards \
  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "...": "..." }'

If the same key is replayed with a different body inside the
24-hour window you'll get a 409 Conflict. If the original
request is still in flight you'll get a 409 Conflict with a
Retry-After hint.

Cursor pagination

Public API list endpoints use opaque cursor pagination. The
response includes a next_cursor field; pass it back as the
cursor query parameter on the next request to get the
following page. Don't try to parse, mutate, or persist the
cursor for longer than the listing session — its contents are
implementation details that can change.

When you pass a cursor, the API ignores any other filter or
sort parameters you also pass — the cursor already encodes them.

Date versioning (Public API)

/api/public/* endpoints accept an X-API-Version: YYYY-MM-DD
header to pin the contract version. Omit it to route to the
latest current version. Older versions are eventually removed;
deprecated versions return a Deprecation response header and
we email integrators before any final removal. See the
API versioning section above for the full
lifecycle.

Audit log

Successful calls to /api/public/* write an entry to the
Admin → Security → Sign-in logs view alongside interactive
sign-ins. Each entry captures the user, the IP and user agent,
the login method (either personal_access_token or oauth),
and the timestamp.

To keep the log readable for heavy integrations, only the
first request per (user, IP, login method) each day writes
a new row — subsequent requests on the same tuple that day
authenticate normally but don't add a row.

What you can do next

  • Browse the full API reference in the sidebar — every
    resource and endpoint has request/response schemas and a "Try
    It" button.
  • Read Authentication for the details on
    PATs and OAuth.
  • If you're integrating an LLM or agent, look at the MCP
    reference — it's the recommended path for agentic clients.

Rate limits

Bonusly rate-limits API requests per company to protect
shared infrastructure. The limits aren't a single global
number — they vary by operation. Heavier endpoints (bulk
analytics queries, list endpoints fanning out to large
collections) have lower limits than light reads. The current
defaults are tuned to support normal integrations comfortably,
but if you're building something with unusual volume needs
we're happy to raise specific limits on request —
contact support with the integration
shape and we'll work with you.

When you exceed a limit you'll get a 429 Too Many Requests
response. Two headers tell you what to do:

HeaderMeaning
Retry-AfterSeconds to wait before retrying.
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-ResetThe current bucket size, your remaining budget, and the wall-clock at which the bucket resets. Use these to back off proactively, not just after a 429.

Best practice: implement exponential backoff with jitter, honor
Retry-After, and treat X-RateLimit-Remaining < 10% as a
cue to slow down.

Errors

Errors return a JSON envelope:

{
  "success": false,
  "message": "Human-readable description",
  "errors": [ { "field": "...", "code": "..." } ]
}

The HTTP status code is the primary signal. Common codes:

CodeMeaning
400Request payload was invalid. See errors.
401Missing or invalid Authorization header.
403Authenticated but the token lacks the required scope, or the user lacks the required role.
404Resource not found, or you don't have access to it.
409Conflict — typically a duplicate idempotency key with a different body, or a state-machine violation.
429Rate-limited. Honor Retry-After.
5xxBonusly is having a bad time. Retry with exponential backoff.

Need help?

  • Search the API reference in the left sidebar.
  • For account-level questions or help with your token,
    contact support.