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 minute
  • X-RateLimit-Remaining— what's left in this window
  • X-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 — fine
  • 400 — bad request body / params
  • 401 — missing or invalid X-API-Key
  • 403— key valid but doesn't have scope for this resource
  • 404 — workspace or resource not found
  • 429 — rate-limited
  • 5xx — 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.sent
  • wolfmode.set
  • pulse.created
  • member.joined
  • checkin.completed
  • moment.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