Points API
Award and deduct points, query balances, and page through transaction history. Awards trigger automatic tier evaluation; when a participant crosses a threshold the response includes the tier upgrade and any unlocked badges. Expiring credits are consumed FIFO before non-expiring credits.
AuthAll endpoints require an X-API-Key header. Production keys are prefixed bq_live_; sandbox keys bq_test_.
ScopesParticipant key All points endpoints accept any valid API key. There is no separate admin tier for points.
Base URLhttps://YOUR_API_DOMAIN/api/v1/gamify
Endpoint inventory
| Method | Endpoint | Purpose |
|---|
| POST | /gamify/points/award | Award points to a participant. Idempotent. Returns new balance, tier upgrade, badges unlocked. |
| POST | /gamify/points/deduct | Deduct points. Idempotent. Returns 400 if the available balance is insufficient. |
| POST | /gamify/points/award-batch | Award up to 100 participants in one request. Idempotent. Per-item failures captured in results. |
| GET | /gamify/participants/{participant_id}/points | Current balance, total earned (lifetime), total spent (lifetime). |
| GET | /gamify/participants/{participant_id}/points/transactions | Paginated ledger entries (awards and deductions) for one participant. |
POST/api/v1/gamify/points/award
Participant keyIdempotent
Credit a participant with points. Triggers tier evaluation and badge unlock checks. Safe to retry via the Idempotency-Key header; a second call with the same key returns the original transaction without double-awarding.
Headers
| Field | Type | Description |
|---|
| Idempotency-Key | str | Optional. 1 to 255 chars matching ^[A-Za-z0-9_\-:.]{1,255}$. Recommended for every call. Descriptive keys ("order-ord_abc") give clean audit trails. |
Request body
| Field | Type | Description |
|---|
| participant_idrequired | str | Your participant identifier. 1 to 255 characters. Auto-creates the participant on first interaction. |
| amountrequired | int | Points to award. Must be greater than 0 and at most 1,000,000. |
| reason | Optional[str] | Free-form audit note (max 500 chars). Surfaces in transaction history and tier-change fact payloads. |
| metadata | Optional[dict[str, Any]] | Custom payload stored alongside the transaction (e.g. { order_id, amount_usd }). Returned by the transactions endpoint. |
| idempotency_key | Optional[str] | Legacy body field (max 255 chars). The Idempotency-Key header is preferred; supplying both with different values returns 400 IDEMPOTENCY_KEY_MISMATCH. |
| expires_at | Optional[datetime] | When set, the credit expires at this timestamp. Expiring credits are consumed FIFO before non-expiring credits during deductions. |
POST /api/v1/gamify/points/award
X-API-Key: bq_live_xxxxx
Content-Type: application/json
Idempotency-Key: order-ord_abc
{
"participant_id": "user_123",
"amount": 100,
"reason": "Purchase completed",
"metadata": {
"order_id": "ord_abc",
"amount_usd": 49.99
}
}
Response fields
| Field | Type | Description |
|---|
| transaction_id | str | Unique ledger id for this award. |
| participant_id | str | Echo of the credited participant. |
| amount | int | Points awarded. |
| new_balance | int | Participant's available balance after the award. |
| tier_upgrade | Optional[TierUpgradeInfo] | Populated when the award caused a tier change. Shape: { code, name, level, previous_code?, previous_level? }. null when no tier change occurred. |
| badges_unlocked | list[str] | Badge codes unlocked by this award. Empty list when no badges fired. Default: [] |
{
"transaction_id": "a1b2c3d4-e5f6-...",
"participant_id": "user_123",
"amount": 100,
"new_balance": 1500,
"tier_upgrade": {
"code": "gold",
"name": "Gold Tier",
"level": 3,
"previous_code": "silver",
"previous_level": 2
},
"badges_unlocked": ["first_purchase"]
}
Errors
| Status | Code | When it fires |
|---|
| 400 | IDEMPOTENCY_KEY_INVALID | Idempotency-Key header is empty, longer than 255 chars, or contains characters outside [A-Za-z0-9_\-:.]. |
| 400 | IDEMPOTENCY_KEY_MISMATCH | Both the Idempotency-Key header AND a body idempotency_key were provided and they differ. |
| 401 | unauthorized | X-API-Key header missing, invalid, expired, or revoked. |
| 403 | forbidden | Tenant is disabled. |
| 429 | RATE_LIMIT_PARTICIPANT_EXCEEDED | Per-participant write rate cap exceeded. The Retry-After response header tells you when to retry. |
POST/api/v1/gamify/points/deduct
Participant keyIdempotent
Debit a participant's available balance (e.g. reward redemption). FIFO-consumes any expiring credits first. Idempotent via the Idempotency-Key header.
Headers
| Field | Type | Description |
|---|
| Idempotency-Key | str | Optional. 1 to 255 chars matching ^[A-Za-z0-9_\-:.]{1,255}$. Recommended for every redemption to make retries safe. |
Request body
| Field | Type | Description |
|---|
| participant_idrequired | str | Your participant identifier. 1 to 255 characters. |
| amountrequired | int | Points to deduct. Must be greater than 0 and at most 1,000,000. |
| reason | Optional[str] | Free-form audit note (max 500 chars). |
| metadata | Optional[dict[str, Any]] | Custom payload stored alongside the transaction. |
| idempotency_key | Optional[str] | Legacy body field. Prefer the Idempotency-Key header. |
POST /api/v1/gamify/points/deduct
X-API-Key: bq_live_xxxxx
Content-Type: application/json
Idempotency-Key: redemption-rdm_42
{
"participant_id": "user_123",
"amount": 50,
"reason": "Reward redemption"
}
Response fields
| Field | Type | Description |
|---|
| transaction_id | str | Unique ledger id for this deduction. |
| participant_id | str | Echo of the debited participant. |
| amount | int | Points deducted. |
| new_balance | int | Participant's available balance after the deduction. |
| tier_upgrade | Optional[TierUpgradeInfo] | Always null on deductions (tiers track lifetime earnings, not available balance). |
| badges_unlocked | list[str] | Always empty on deductions. Default: [] |
{
"transaction_id": "b7c8d9e0-...",
"participant_id": "user_123",
"amount": 50,
"new_balance": 1450
}
Errors
| Status | Code | When it fires |
|---|
| 400 | insufficient_points | Available balance is below the requested amount. Body: { detail: "Insufficient points. Available: X, requested: N" }. |
| 400 | IDEMPOTENCY_KEY_INVALID | Idempotency-Key header is empty, too long, or contains illegal characters. |
| 400 | IDEMPOTENCY_KEY_MISMATCH | Header and body idempotency keys disagree. |
| 401 | unauthorized | X-API-Key header missing or invalid. |
| 403 | forbidden | Tenant is disabled. |
| 429 | RATE_LIMIT_PARTICIPANT_EXCEEDED | Per-participant write rate cap exceeded. |
POST/api/v1/gamify/points/award-batch
Participant keyIdempotent
Award points to up to 100 participants in a single request. Each item is processed independently; an individual failure does not roll back the rest. The endpoint itself never raises on per-item validation errors (failures appear inside results[].error).
Request body
| Field | Type | Description |
|---|
| awardsrequired | list[BatchAwardItem] | 1 to 100 award items. Each item has the same shape as POST /points/award minus expires_at. |
BatchAwardItem
| Field | Type | Description |
|---|
| participant_idrequired | str | 1 to 255 characters. |
| amountrequired | int | Greater than 0, at most 1,000,000. |
| reason | Optional[str] | Max 500 chars. |
| metadata | Optional[dict[str, Any]] | Custom payload. |
| idempotency_key | Optional[str] | Per-item idempotency. Recommended when retrying a batch. |
POST /api/v1/gamify/points/award-batch
X-API-Key: bq_live_xxxxx
Content-Type: application/json
{
"awards": [
{ "participant_id": "user_1", "amount": 100, "reason": "Weekly bonus" },
{ "participant_id": "user_2", "amount": 100, "reason": "Weekly bonus" },
{ "participant_id": "user_3", "amount": 50, "reason": "Referral bonus" }
]
}
Response fields
| Field | Type | Description |
|---|
| processed | int | Number of items that succeeded. |
| failed | int | Number of items whose results[].error is non-null. |
| results | list[BatchAwardResult] | One entry per input item, in the same order. Shape: { participant_id, transaction_id?, new_balance?, error? }. On success error is null; on failure transaction_id and new_balance are null and error describes the cause. |
{
"processed": 3,
"failed": 0,
"results": [
{ "participant_id": "user_1", "transaction_id": "...", "new_balance": 500, "error": null },
{ "participant_id": "user_2", "transaction_id": "...", "new_balance": 350, "error": null },
{ "participant_id": "user_3", "transaction_id": "...", "new_balance": 200, "error": null }
]
}
Errors
| Status | Code | When it fires |
|---|
| 401 | unauthorized | X-API-Key header missing or invalid. Per-item failures never raise at the request level; they appear in results[].error. |
| 403 | forbidden | Tenant is disabled. |
GET/api/v1/gamify/participants/{participant_id}/points
Participant key
Returns the participant's current available balance and lifetime totals. total_earned is never decremented by redemptions (it ranks tier and leaderboard progress); only balance moves on deduct.
Path parameters
| Field | Type | Description |
|---|
| participant_idrequired | str | Your participant identifier. 1 to 255 characters. |
GET /api/v1/gamify/participants/user_123/points
X-API-Key: bq_live_xxxxx
Response fields
| Field | Type | Description |
|---|
| participant_id | str | Echo of the requested participant id. |
| balance | int | Available points (lifetime earned minus lifetime spent, plus any expirations). |
| total_earned | int | Lifetime points awarded. Used for tier evaluation and leaderboards. Never decremented. |
| total_spent | int | Lifetime points deducted. |
{
"participant_id": "user_123",
"balance": 1500,
"total_earned": 5000,
"total_spent": 3500
}
Errors
| Status | Code | When it fires |
|---|
| 401 | unauthorized | X-API-Key header missing or invalid. |
| 403 | forbidden | Tenant is disabled. |
GET/api/v1/gamify/participants/{participant_id}/points/transactions
Participant key
Paginated ledger of all awards and deductions for one participant, newest first.
Path parameters
| Field | Type | Description |
|---|
| participant_idrequired | str | Your participant identifier. |
Query parameters
| Field | Type | Description |
|---|
| page | int | 1-indexed page number. Must be >= 1. Default: 1 |
| page_size | int | Items per page. Must be between 1 and 100. Default: 50 |
GET /api/v1/gamify/participants/user_123/points/transactions?page=1&page_size=20
X-API-Key: bq_live_xxxxx
Response fields
| Field | Type | Description |
|---|
| participant_id | str | Echo of the requested participant id. |
| transactions | list[PointsTransactionEntry] | Page of ledger entries. Each entry: { id, amount, reason?, transaction_type?, created_at }. |
| total | int | Total transactions across all pages for this participant. |
| page | int | Echo of the requested page. |
| page_size | int | Echo of the requested page size. |
{
"participant_id": "user_123",
"transactions": [
{
"id": "txn_abc",
"amount": 100,
"reason": "Purchase completed",
"transaction_type": "award",
"created_at": "2026-02-14T10:00:00Z"
},
{
"id": "txn_xyz",
"amount": -50,
"reason": "Reward redemption",
"transaction_type": "deduct",
"created_at": "2026-02-13T18:22:00Z"
}
],
"total": 156,
"page": 1,
"page_size": 20
}
Errors
| Status | Code | When it fires |
|---|
| 401 | unauthorized | X-API-Key header missing or invalid. |
| 403 | forbidden | Tenant is disabled. |
| 422 | validation_error | page is below 1, or page_size is outside the 1 to 100 range. |