Base URL: https://api.commonshub.brussels/v1
All responses are JSON. Errors return { "error": "message" } with appropriate HTTP status codes.
The API is identity-provider agnostic. It doesn't know or care about Discord, Telegram, or any specific platform. Instead, it trusts registered apps to vouch for their users.
- An app (Discord bot, CLI, agent) registers with the API and receives credentials
- The app authenticates its users however it wants (Discord OAuth, device code, etc.)
- The app calls the API on behalf of the user
Every authenticated request includes:
Authorization: Bearer <appSecret>
X-User-Id: <userId>
The API trusts the app to have verified the user's identity. The userId is an API-level identifier (not a Discord ID or platform-specific ID).
Users are created when they first interact through any approved app. The app provides:
- A unique user ID (the app can use its own platform ID, e.g. Discord user ID)
- Display name and username
The API maintains a unified user record across all apps.
Register a new app. Admin only (requires master API key).
Request:
{
"name": "ElinorBot"
}Response:
{
"appId": "app_abc123",
"appSecret": "chb_sk_live_x7k9...",
"name": "ElinorBot",
"createdAt": "2026-03-18T16:00:00Z"
}appSecret is shown once. Store it securely. The API only stores a hash.
List registered apps. Admin only.
Response:
{
"apps": [
{
"appId": "app_abc123",
"name": "ElinorBot",
"createdAt": "2026-03-18T16:00:00Z",
"lastUsedAt": "2026-03-18T16:30:00Z"
}
]
}Revoke an app. Admin only. All requests using this app's secret will immediately return 401.
The CLI authenticates users directly against the API — no Discord needed.
Prerequisite: User must already have an API account (created via any app, e.g. Discord bot).
- CLI calls
POST /v1/auth/devicewith its app credentials → getsdeviceCode+userCode(6 digits) - CLI displays: "Open https://api.commonshub.brussels/auth/verify and enter code: 482901"
- User opens the URL, logs in (email, existing session, etc.), enters the code
- CLI polls
GET /v1/auth/device/:deviceCodeuntil status is"approved" - API returns a user-scoped token the CLI uses for subsequent requests
$ chb login
Open https://api.commonshub.brussels/auth/verify and enter code: 482901
Waiting for authorization... ✓
Logged in as Xavier Damman (@xdamman)Start a device authorization flow.
Request:
Authorization: Bearer <appSecret>
Response:
{
"deviceCode": "dev_abc123",
"userCode": "482901",
"verifyUrl": "https://api.commonshub.brussels/auth/verify",
"expiresIn": 900
}Poll for authorization status.
Response (pending):
{ "status": "pending" }Response (approved):
{
"status": "approved",
"userId": "u_849888126",
"token": "chb_ut_...",
"displayName": "Xavier Damman"
}Verify a device code. Called from the browser after user logs in.
Request:
{
"userCode": "482901"
}Device codes:
- 6 digits, numeric
- Expire after 15 minutes
- Single use
- Rate limited: max 5 attempts per code
List all bookable rooms.
Response:
{
"rooms": [
{
"id": "ostrom",
"name": "Ostrom Room",
"calendarId": "c_72861d...@group.calendar.google.com",
"capacity": 12,
"amenities": ["projector", "whiteboard"]
}
]
}Get room availability for a date range.
Query params:
date— single date (YYYY-MM-DD), defaults to todayfrom/to— date range (YYYY-MM-DD)
Response:
{
"room": "ostrom",
"date": "2026-03-19",
"events": [
{
"id": "abc123",
"title": "Board Meeting",
"start": "2026-03-19T10:00:00+01:00",
"end": "2026-03-19T12:00:00+01:00",
"bookedBy": "xdamman"
}
],
"availableSlots": [
{ "start": "08:00", "end": "10:00" },
{ "start": "12:00", "end": "22:00" }
]
}Book a room.
Request:
{
"title": "Team Standup",
"date": "2026-03-19",
"start": "14:00",
"end": "15:00",
"description": "Weekly sync"
}Response:
{
"eventId": "abc123",
"title": "Team Standup",
"room": "ostrom",
"start": "2026-03-19T14:00:00+01:00",
"end": "2026-03-19T15:00:00+01:00"
}Errors:
409 Conflict— room is already booked for that time400 Bad Request— invalid time range, room doesn't exist
Cancel a room booking. Only the person who booked it (or an admin) can cancel.
Response:
{ "ok": true }List shifts for a date range.
Query params:
date— single date (YYYY-MM-DD), defaults to todayfrom/to— date rangeuserId— filter by user (show only shifts a user is signed up for)
Response:
{
"date": "2026-03-19",
"slots": [
{
"index": 0,
"start": "08:30",
"end": "11:30",
"maxSignups": 3,
"signups": [
{
"userId": "u_849888126",
"username": "xdamman",
"displayName": "Xavier Damman",
"signedUpAt": "18/03/2026 14:15"
}
],
"roomEvents": [
{
"title": "Yoga Class",
"room": "Ostrom",
"start": "2026-03-19T10:00:00+01:00",
"end": "2026-03-19T12:00:00+01:00"
}
],
"spotsLeft": 2
}
]
}Same as above but for a single date.
Sign up for a shift.
Request:
{
"email": "xavier@example.com"
}Email is optional. If provided, the user receives a Google Calendar invite.
Response:
{
"ok": true,
"slot": { "start": "08:30", "end": "11:30" },
"date": "2026-03-19",
"spotsLeft": 1
}Errors:
409 Conflict— already signed up for this slot422 Unprocessable— slot is full
Cancel a shift signup.
Response:
{ "ok": true }Mint token rewards for shift participants. Requires minter role.
Request:
{
"participants": ["u_849888126", "u_123456789"],
"amountPerUser": 3,
"token": "CHT"
}Response:
{
"results": [
{
"userId": "u_849888126",
"username": "xdamman",
"amount": 3,
"txHash": "0xabc...",
"success": true
}
]
}Get the authenticated user's profile.
Response:
{
"userId": "u_849888126",
"username": "xdamman",
"displayName": "Xavier Damman",
"email": "xavier@example.com",
"walletAddress": "0x1234...",
"roles": ["minter", "admin"]
}Update the authenticated user's profile.
Request:
{
"email": "new@example.com",
"walletAddress": "0x5678..."
}Get a user's public profile (username, displayName, roles). No email or wallet.
Any app (ElinorBot, agent) can prepare an action for a user to confirm. This lets an AI agent set everything up, then the user just clicks "Confirm".
Prepare an action for a user to confirm.
Request:
{
"action": "shift_signup",
"userId": "u_849888126",
"params": {
"date": "2026-03-19",
"slotIndex": 0
},
"expiresIn": 3600
}Response:
{
"actionId": "act_abc123",
"confirmUrl": "https://api.commonshub.brussels/v1/actions/act_abc123/confirm",
"expiresAt": "2026-03-19T17:00:00Z"
}The calling app (e.g. Discord bot) shows a single "✅ Confirm" button that triggers execution.
Get action details (for showing the confirmation UI).
Response:
{
"actionId": "act_abc123",
"action": "shift_signup",
"params": { "date": "2026-03-19", "slotIndex": 0 },
"userId": "u_849888126",
"status": "pending",
"expiresAt": "2026-03-19T17:00:00Z"
}Execute a prepared action. Must be called by (or on behalf of) the target user.
Response:
{
"ok": true,
"result": {
"slot": { "start": "08:30", "end": "11:30" },
"date": "2026-03-19",
"spotsLeft": 1
}
}Errors:
403— wrong user410 Gone— expired or already executed
All errors follow this format:
{
"error": "slot_full",
"message": "This shift slot is full (3/3 spots taken)",
"status": 422
}Standard error codes:
400— bad request (missing params, invalid format)401— not authenticated (missing/invalid app credentials)403— not authorized (wrong user, missing role)404— resource not found409— conflict (already booked, already signed up)410— gone (expired action)422— unprocessable (slot full, invalid state)429— rate limited500— server error