Badges API
Award badges to participants, list earned and unearned badges with rarity and icons, and manage badge definitions through admin endpoints. Awards are composite-idempotent on (participant_id, badge_code), so duplicate calls return the original earned timestamp instead of erroring.
AuthAll endpoints require an X-API-Key header. Production keys are prefixed bq_live_; sandbox keys bq_test_.
ScopesParticipant key Award badges and read participant badge state with any valid API key.
Admin scope Manage badge definitions: requires admin or gamify:admin scope.
Base URLhttps://YOUR_API_DOMAIN/api/v1/gamify
Endpoint inventory
| Method | Endpoint | Purpose |
|---|
| POST | /gamify/badges/award | Award a badge to a participant. Composite-idempotent on (participant_id, badge_code). |
| GET | /gamify/participants/{participant_id}/badges | List badges for a participant with earned and unearned status. |
| GET | /gamify/admin/badges | List badge definitions. Admin scope. |
| POST | /gamify/admin/badges | Create a badge definition. Admin scope. |
| GET | /gamify/admin/badges/{badge_id} | Get one badge definition. Admin scope. |
| PATCH | /gamify/admin/badges/{badge_id} | Update a badge definition. Admin scope. |
| DELETE | /gamify/admin/badges/{badge_id} | Deactivate (soft-delete) a badge. Admin scope. |
POST/api/v1/gamify/badges/award
Participant key
Award a badge to a participant. Awards are composite-idempotent on (participant_id, badge_code): a duplicate call returns the original award with already_earned: true. No Idempotency-Key header is needed (and the endpoint does not accept one).
Request body
| Field | Type | Description |
|---|
| participant_idrequired | str | Your participant identifier. 1 to 255 characters. |
| badge_coderequired | str | Code of an active badge definition for the tenant. 1 to 50 characters. |
| metadata | Optional[dict[str, Any]] | Free-form context attached to the award (trigger source, originating campaign, etc.). |
POST /api/v1/gamify/badges/award
X-API-Key: bq_live_xxxxx
Content-Type: application/json
{
"participant_id": "user_123",
"badge_code": "power_user",
"metadata": { "trigger": "10th_login" }
}
Response fields
| Field | Type | Description |
|---|
| participant_id | str | Echo of the requested participant id. |
| badge_code | str | Echo of the awarded badge code. |
| badge_name | str | Display name from the badge definition. |
| icon | Optional[str] | Badge icon URL from the definition, if set. |
| rarity | str | Rarity classification of the badge. Default: "common" |
| earned_at | datetime | When the badge was first earned. On duplicate awards this is the original timestamp, not the retry time. |
| already_earned | bool | true when this call hit the composite-idempotency path (the participant already had the badge). Default: false |
{
"participant_id": "user_123",
"badge_code": "power_user",
"badge_name": "Power User",
"icon": "https://cdn.example.com/badges/power.png",
"rarity": "rare",
"earned_at": "2026-02-14T10:00:00Z",
"already_earned": false
}
Errors
| Status | Code | When it fires |
|---|
| 401 | unauthorized | X-API-Key header missing, invalid, expired, or revoked. |
| 403 | forbidden | Tenant is disabled. |
| 404 | badge_not_found | No active badge definition exists for the supplied badge_code ("Badge '{badge_code}' not found"). |
| 429 | RATE_LIMIT_PARTICIPANT_EXCEEDED | Per-participant write rate cap exceeded. The Retry-After response header tells you when to retry. |
GET/api/v1/gamify/participants/{participant_id}/badges
Participant key
List badges for a participant. By default returns every badge definition for the tenant with earned status flagged per row; use the query parameters to narrow the result.
Path parameters
| Field | Type | Description |
|---|
| participant_idrequired | str | Your participant identifier. 1 to 255 characters. |
Query parameters
| Field | Type | Description |
|---|
| earned_only | bool | When true, only badges the participant has earned are returned. Default: false |
| badge_codes | Optional[str] | Comma-separated list of badge codes to filter to (CSV). |
GET /api/v1/gamify/participants/user_123/badges
X-API-Key: bq_live_xxxxx
# Only earned badges
GET /api/v1/gamify/participants/user_123/badges?earned_only=true
# Specific badges by code
GET /api/v1/gamify/participants/user_123/badges?badge_codes=power_user,first_steps
Response fields
| Field | Type | Description |
|---|
| participant_id | str | Echo of the requested participant id. |
| badges | list[ParticipantBadgeEntry] | Per-badge rows. Each entry: code, name, description (optional), icon (optional), rarity (defaults to "common"), category (optional), earned (defaults to false), earned_at (optional). |
| total | int | Number of badge rows returned. |
| earned_count | int | Number of badges the participant has actually earned across the returned set. |
{
"participant_id": "user_123",
"badges": [
{
"code": "power_user",
"name": "Power User",
"description": "Log in 10 times.",
"icon": "https://cdn.example.com/badges/power.png",
"rarity": "rare",
"category": "engagement",
"earned": true,
"earned_at": "2026-02-14T10:00:00Z"
}
],
"total": 8,
"earned_count": 3
}
Errors
| Status | Code | When it fires |
|---|
| 401 | unauthorized | X-API-Key header missing, invalid, expired, or revoked. |
| 403 | forbidden | Tenant is disabled. |
Query parameters
| Field | Type | Description |
|---|
| category | Optional[str] | Filter by category. |
| is_active | Optional[bool] | When set, filters to definitions where is_active matches. |
| page | int | 1-indexed page number. Default: 1 |
| page_size | int | Items per page. Max 100. Default: 50 |
GET /api/v1/gamify/admin/badges?is_active=true&page=1&page_size=50
X-API-Key: bq_live_xxxxx # must carry admin or gamify:admin scope
{
"items": [
{
"id": "01HZ...",
"tenant_id": "01HZ...",
"code": "power_user",
"name": "Power User",
"description": "Log in 10 times.",
"icon_url": "https://cdn.example.com/badges/power.png",
"badge_color": "#7C3AED",
"rarity": "rare",
"category": "engagement",
"is_active": true,
"is_hidden": false,
"display_order": 10,
"created_at": "2026-01-10T08:00:00Z",
"updated_at": "2026-02-12T15:30:00Z"
}
],
"total": 12,
"page": 1,
"page_size": 50
}
POST/api/v1/gamify/admin/badges
Admin scope
Create a badge definition. Returns 201 with the full BadgeResponse on success.
Request body
| Field | Type | Description |
|---|
| coderequired | str | Unique badge code for the tenant. 1 to 50 chars. Cannot be changed after creation; downstream awards reference it. |
| namerequired | str | Display name. 1 to 100 chars. |
| description | Optional[str] | Optional long-form description. |
| icon_url | Optional[str] | Optional URL for the badge icon shown in the SDK and dashboards. |
| badge_color | Optional[str] | Optional hex color. Max 20 chars. |
| rarity | str | One of common, uncommon, rare, epic, legendary. Default: "common" |
| category | Optional[str] | Optional category grouping. Max 50 chars. |
| is_hidden | bool | When true, the badge is hidden in default listings (useful for surprise drops). Default: false |
| display_order | int | Sort order in admin and SDK listings. Default: 0 |
POST /api/v1/gamify/admin/badges
X-API-Key: bq_live_xxxxx # admin or gamify:admin scope
Content-Type: application/json
{
"code": "power_user",
"name": "Power User",
"description": "Log in 10 times.",
"icon_url": "https://cdn.example.com/badges/power.png",
"badge_color": "#7C3AED",
"rarity": "rare",
"category": "engagement",
"display_order": 10
}
Errors
| Status | Code | When it fires |
|---|
| 400 | badge_code_conflict | Another badge definition already uses that code for this tenant ("Badge with code '{code}' already exists"). |
| 403 | forbidden | API key is missing the admin or gamify:admin scope. |
GET/api/v1/gamify/admin/badges/{badge_id}
Admin scope
Retrieve one badge definition by its UUID. Response shape matches the items in the list endpoint.
Path parameters
| Field | Type | Description |
|---|
| badge_idrequired | UUID | UUID of the badge definition. |
Errors
| Status | Code | When it fires |
|---|
| 404 | badge_not_found | No badge exists for the supplied badge_id. |
PATCH/api/v1/gamify/admin/badges/{badge_id}
Admin scope
Partial update of a badge definition. All body fields are optional; only supplied fields are changed. The code cannot be updated.
Request body (all optional)
| Field | Type | Description |
|---|
| name | Optional[str] | 1 to 100 chars. |
| description | Optional[str] | Long-form description. |
| icon_url | Optional[str] | Replacement icon URL. |
| badge_color | Optional[str] | Hex color, max 20 chars. |
| rarity | Optional[str] | One of common, uncommon, rare, epic, legendary. |
| category | Optional[str] | Category grouping. |
| is_hidden | Optional[bool] | Toggle visibility in default listings. |
| is_active | Optional[bool] | Set false to pause the badge without losing existing awards. |
| display_order | Optional[int] | Sort order in listings. |
Errors
| Status | Code | When it fires |
|---|
| 404 | badge_not_found | No badge exists for the supplied badge_id. |
DELETE/api/v1/gamify/admin/badges/{badge_id}
Admin scope
Soft-delete the badge definition by setting is_active to false. Existing participant awards are preserved; new awards stop succeeding for this badge_code.
{ "message": "Badge deactivated" }
Errors
| Status | Code | When it fires |
|---|
| 404 | badge_not_found | No badge exists for the supplied badge_id. |