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
curlor 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.
- Sign in at bonus.ly.
- Go to Profile Settings → API Tab or Company → Integrations → API & Tokens (if you're a Bonusly admin).
- 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. - 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.
| Surface | Base URL | When 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 v1 | https://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 server | https://bonus.ly/mcp | Model 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-DDon
every request. Use this once you're in production and need
contract stability. The response echoes the resolved version
back in theX-API-Versionheader.
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:
| Header | Meaning |
|---|---|
Retry-After | Seconds to wait before retrying. |
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset | The 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:
| Code | Meaning |
|---|---|
400 | Request payload was invalid. See errors. |
401 | Missing or invalid Authorization header. |
403 | Authenticated but the token lacks the required scope, or the user lacks the required role. |
404 | Resource not found, or you don't have access to it. |
409 | Conflict — typically a duplicate idempotency key with a different body, or a state-machine violation. |
429 | Rate-limited. Honor Retry-After. |
5xx | Bonusly 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.
Updated about 23 hours ago