API documentation · v1
Build culture data into your stack.
REST + webhooks for BetterUs Workplace. Read your workspace data, subscribe to events, write into your CRM / data warehouse / wherever the work lives. Same shape for every workspace, every plan.
Overview
All endpoints are scoped to a workspace. The base URL is https://api.workplace.thebetterusproject.com with paths under /api/v1/workspace/:workspaceId/....
Responses are JSON. Successful calls return 200 with the resource body; failures return a non-2xx status with { "error": "...", "message": "..." }.
Read-only by default. Write keys are whitelisted per workspace — email us if you need them.
Authentication
Every request needs an X-API-Key header carrying a key minted at /admin/integrations/api-keys.
curl https://api.workplace.thebetterusproject.com/api/v1/workspace/WORKSPACE_ID/dashboard \
-H "X-API-Key: bu_live_xxxxxxxxxxxxxxxxxxxx"Keys are revealed once on creation and never displayed again — the admin panel stores only the prefix + last 4 characters. Lose the key, revoke it and mint a new one.
Rate limits
Default: 120 requests / minute per API key. Burst up to 30 in 5 seconds. The current window is exposed in response headers:
X-RateLimit-Limit— your limit per minuteX-RateLimit-Remaining— what's left in this windowX-RateLimit-Reset— unix seconds until the window resets
Hitting the cap returns 429 with Retry-Afterin seconds. Build a small backoff in your client and you'll never see one.
Errors
Standard HTTP semantics:
200— fine400— bad request body / params401— missing or invalidX-API-Key403— key valid but doesn't have scope for this resource404— workspace or resource not found429— rate-limited5xx— our problem; safe to retry with backoff
{
"error": "rate_limited",
"message": "Slow down — 120 req/min cap. Retry in 12s."
}GET /dashboard
Workspace-wide culture metrics. Refreshed nightly.
GET /api/v1/workspace/:workspaceId/dashboard
200 OK
{
"cultureScore": 72,
"cultureScoreChange": 4,
"belongingScore": 68,
"engagementScore": 74,
"wellbeingScore": 71,
"followThroughScore": 76,
"pillarBalanceScore": 70,
"totalMembers": 42,
"activeMembers": 38,
"averageMood": 4.1,
"totalSparks": 1284
}GET /members
Every packmate in the workspace.
GET /api/v1/workspace/:workspaceId/members
200 OK
{
"members": [
{
"id": "mem_abc123",
"name": "Sarah Chen",
"role": "manager",
"email": "sarah@company.com",
"sparks": 142,
"currentStreak": 12,
"lastActive": "2026-05-02T08:14:00Z",
"mood": 4
}
]
}Detail per member: GET /members/:memberId returns the same shape plus joinedAt, totalCheckIns, longestStreak, and pillarScores.
GET /recognitions
Recognition wall, newest first. Optional ?limit=N param (max 1000).
GET /api/v1/workspace/:workspaceId/recognitions?limit=50
200 OK
{
"recognitions": [
{
"id": "rec_xyz789",
"fromMemberId": "mem_abc123",
"fromMemberName": "Sarah Chen",
"toMemberId": "mem_def456",
"toMemberName": "James Kim",
"message": "You held the customer call with calm. Bought us the week.",
"pillar": "pack",
"createdAt": "2026-05-02T09:42:00Z"
}
]
}GET /pulses
Pulse questions fired by admins / managers, with response counts.
GET /api/v1/workspace/:workspaceId/pulses
200 OK
{
"pulses": [
{
"id": "pul_qwe456",
"question": "How energised do you feel about this week?",
"scale": "1-5",
"audience": "everyone",
"responses": 24,
"sentAt": "2026-05-01T07:00:00Z"
}
]
}GET /audit
Workspace audit log, newest first. Useful for compliance reviews.
GET /api/v1/workspace/:workspaceId/audit?limit=200
200 OK
{
"entries": [
{
"id": "aud_001",
"action": "wolfmode.set",
"actorId": "mem_abc123",
"actorName": "Sarah Chen",
"target": "den",
"createdAt": "2026-05-02T07:32:00Z"
}
]
}Webhook events
Subscribe at /admin/integrations/webhooks. Events are POSTed to your endpoint as JSON. Six event types:
recognition.sentwolfmode.setpulse.createdmember.joinedcheckin.completedmoment.posted
Payload shape:
POST https://your-endpoint.com/betterus
Content-Type: application/json
X-BetterUs-Event: recognition.sent
X-BetterUs-Timestamp: 1714723200
X-BetterUs-Signature: sha256=4f5a9b...
{
"event": "recognition.sent",
"deliveredAt": "2026-05-02T09:42:00Z",
"workspace": { "id": "ws_123", "name": "School of Play" },
"data": {
"id": "rec_xyz789",
"fromMemberId": "mem_abc123",
"fromMemberName": "Sarah Chen",
"toMemberId": "mem_def456",
"toMemberName": "James Kim",
"message": "...",
"pillar": "pack",
"createdAt": "2026-05-02T09:42:00Z"
}
}Your endpoint must respond with a 2xx within 5 seconds. We retry up to 4 times with exponential backoff (1s, 5s, 30s, 5min) before quietly disabling the webhook and emailing the workspace admin.
Signature verification
When you set a signing secret on the webhook, every payload carries X-BetterUs-Signature and X-BetterUs-Timestamp. Compute HMAC-SHA256 over timestamp.raw_body with your secret; if it matches the header, the payload is authentic.
// Node.js verification example
import crypto from "node:crypto";
function isValid(req, secret) {
const sig = req.headers["x-betterus-signature"];
const ts = req.headers["x-betterus-timestamp"];
if (!sig || !ts) return false;
// Reject anything older than 5 minutes — replay protection
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${req.rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(sig.replace(/^sha256=/, "")),
Buffer.from(expected),
);
}Always do the timestamp check first — that's the cheap part of rejecting replays. Constant-time compare on the signature itself.
Stuck on something?
The API is small enough that real humans answer questions about it. One working day, usually less.
Email a developer