API Reference
The PlaylogiQ public REST API is versioned under /api/v1.
- Base URL (reference deployment):
https://crm-api.logiqdesk.com/api/v1 - Machine-readable spec (OpenAPI 3.1):
/api/v1/openapi.json
The API is self-hosted, so your installation’s host may differ — substitute your own domain. The OpenAPI document is the source of truth; paste it into any OpenAPI viewer (Swagger UI, Redoc, Insomnia, Postman) to explore request/response schemas interactively.
Authentication
Every endpoint except GET /health and the spec itself requires an operator API
key in the x-api-key header:
x-api-key: plq_your_api_keyKeys can be brand-pinned (recommended) or tenant-wide (legacy), and carry least-privilege scopes. See PAM Integration → Authentication for the full model. In short:
- A brand-pinned key writes only to its brand; any
x-brandhint is ignored. - A tenant-wide key selects the brand per request with
x-brand(header) or?brand=(query) — a brand id or slug; the tenant’s default brand is used when omitted.
x-api-key: plq_your_api_key
x-brand: main # tenant-wide keys only — brand id or slugConventions
- Content type:
application/jsonfor all request and response bodies. - Single resources return
{ "data": { … } }. - Collections return one of:
{ "data": [ … ], "pagination": { page, limit, total, pages } }— Players, Missions.{ "campaigns": [ … ], "total": n }/{ "templates": [ … ], "total": n }— withlimit/offsetquery params.{ "data": [ … ] }— most gamification/segment/bonus/tournament lists.{ "data": [ … ], "next_cursor": string|null }— cursor/keyset exports.
- Errors return
{ "error": string, "code"?: string, "details"?: object }with an HTTP status.codeis one ofUNAUTHORIZED,BAD_REQUEST,NOT_FOUND,FORBIDDEN,INTERNAL_ERROR.
Endpoint index
| Resource | Endpoints |
|---|---|
| System | GET /health |
| Events | POST /events |
| Players | GET /players · POST /players · GET /players/{id} · PATCH /players/{id} · GET /players/{id}/next-best-action |
| Segments | GET /segments · POST /segments · GET /segments/{id}/export |
| Campaigns | GET /campaigns · POST /campaigns · GET /campaigns/{id} · PATCH /campaigns/{id} · DELETE /campaigns/{id} |
| Notifications | GET/POST /notifications/templates · GET/PATCH/DELETE /notifications/templates/{id} · POST /notifications/send |
| Gamification | GET/POST + GET/PATCH/DELETE {id} for levels, missions, badges, currencies · GET /gamification/player/{playerId} |
| Bonuses | GET/POST /bonuses · GET/PATCH/DELETE /bonuses/{id} |
| Tournaments | GET/POST /tournaments · GET/PATCH/DELETE /tournaments/{id} |
| Analytics | GET /analytics/summary · /timeseries · /revenue · /cohorts · GET /reports/run |
| Export | GET /export/{dataset} · GET /segments/{id}/export |
System
GET /health
Public (no API key). Returns { status: "ok"|"degraded", version, service, checks, timestamp }. 503 when degraded.
Events
POST /events
Ingest a single event or a batch (up to 100). Requires the events:write scope.
Players are upserted by player_id. See
Event ingestion API for the full contract
(idempotency, mapping, brand pinning).
Single:
{ "player_id": "player-123", "event_type": "deposit",
"event_id": "evt-9a7f", "payload": { "amount": 100, "currency": "EUR" },
"timestamp": "2026-07-09T10:15:30Z", "session_id": "sess-42" }Batch: { "events": [ … ] } (1–100).
Response 200: { received, stored, deduped, rejected?, ignored?, unknown_event_types?, rejected_event_types?, status: "queued", timestamp }.
Players
GET /players
Query: page (1), limit (20, max 100), search (email or external_id),
status (active/suspended/self_excluded/closed). Brand-scoped.
→ { data: Player[], pagination }.
POST /players
Idempotent on (brand, external_id); syncs language/currency onto the profile.
Body: external_id (required); email, phone, status, metadata,
language (en/it/es), currency (3-letter ISO), brand (id or slug).
→ 201 { data: Player }.
GET /players/{id}
id is the PlaylogiQ player UUID (not external_id). Returns the player plus
its joined profile. → { data: Player }.
PATCH /players/{id}
Body: status, email, phone, metadata (shallow-merged). → { data: Player }.
GET /players/{id}/next-best-action
Recommendation derived from churn/value/engagement scores + lifecycle stage.
→ { data: { player_id, action, priority: high|medium|low, reason, channelHint: personal|email|push|any } }.
Player: id, external_id, email, phone, status, metadata,
created_at, updated_at.
Segments
GET /segments
Query: type. Brand-scoped. → { data: Segment[] }.
POST /segments
Body: name, type (required); rules (object), brand.
→ 201 { data: Segment }.
GET /segments/{id}/export
Cursor-paginated keyset export of members (columns: external_id, email, phone,
name, consent, country). Query: cursor, limit (1–1000, default 200),
format (json/csv). CSV returns the cursor in the x-next-cursor header.
→ { segment, data[], next_cursor }.
Segment: id, name, type, rules, created_at, updated_at.
Campaigns
GET /campaigns
Query: status (draft/scheduled/running/paused/completed/cancelled),
limit (20, max 100), offset. Brand-scoped. → { campaigns: Campaign[], total }.
POST /campaigns
Body: name, type (one_shot/recurring/triggered/journey),
channels (≥1 of email/telegram/push/in_app); description,
target_segment_id, brand. → 201 { data: Campaign }.
The public API exposes four campaign channels (
telegram,push,in_app). The dashboard additionally offers SMS and WhatsApp editors; those two are managed in the UI, not via thePOST /campaignsAPI.
GET /campaigns/{id} → { data: Campaign }.
PATCH /campaigns/{id}
Update fields or transition status (validated: draft→scheduled|running,
scheduled→running|cancelled, running→paused|cancelled, paused→running|cancelled;
completed & cancelled are terminal). Body: name, description, status,
channels, target_segment_id, config, schedule.
DELETE /campaigns/{id}
Soft-cancel (sets status: cancelled). → { data: Campaign }.
Campaign: id, name, description, status, type, channels,
target_segment_id, schedule, config, stats, created_at, updated_at.
Notifications
GET /notifications/templates
Query: channel (email/telegram/push/in_app), active (default true),
limit, offset. Brand-scoped. → { templates: NotificationTemplate[], total }.
POST /notifications/templates
Variables are auto-extracted from {{handlebars}} placeholders in body +
subject. Body: name, channel, body; locale (en/it/es),
subject, variables, brand. → 201 { data: NotificationTemplate }.
GET/PATCH/DELETE /notifications/templates/{id}
PATCH updates any field (+ active); DELETE soft-deletes (active: false).
POST /notifications/send
Enqueue a single notification to a player (async delivery). Body: player_id,
channel; template_id, payload. → 202 { queued, notification_id, data }.
NotificationTemplate: id, name, channel, locale, subject, body,
variables, active, created_at, updated_at.
Gamification
Every resource follows the same shape: GET/POST on the collection
(brand-scoped) and GET/PATCH/DELETE on {id}. DELETE returns
{ data: { id } }.
Levels
POST body: name, tier (≥1), min_points (≥0); max_points,
benefits, brand. Level: id, name, tier, min_points,
max_points, benefits, created_at.
Missions
GET query: page, limit, status (active/completed/expired/cancelled),
type (daily/weekly/one_time/progressive/collaborative) → paginated.
POST body: name, type; description, status, rules, rewards,
max_completions_per_player, starts_at, ends_at, brand.
Badges
POST body: name; description, icon_url, criteria, rarity
(common/rare/epic/legendary), brand.
Currencies (points)
POST body: name, symbol, type (real/virtual/bonus);
brand.
GET /gamification/player/{playerId}
A player’s gamification profile: { player_id, current_level, point_balances[], active_missions[], badges_earned[] }.
Bonuses
GET /bonuses (brand-scoped) · POST /bonuses
POST body: name, type (welcome/reload/cashback/free_spins/
no_deposit/custom), value, value_type; wagering_requirement,
config, expires_in_hours, active, brand.
GET/PATCH/DELETE /bonuses/{id}
Bonus: id, name, type, value, value_type, wagering_requirement,
config, expires_in_hours, active, created_at.
Tournaments
GET /tournaments (brand-scoped) · POST /tournaments
POST body: name; description, status
(draft/active/ended/cancelled), game_config, prize_pool,
max_participants, starts_at, ends_at, brand.
GET/PATCH/DELETE /tournaments/{id}
Tournament: id, name, description, status, game_config,
prize_pool, max_participants, starts_at, ends_at, created_at,
updated_at.
Analytics & reports
Read-only. Brand-scoped via the optional brand hint.
GET /analytics/summary
Headline KPIs: players, active, revenue (GGR/NGR), deposits, avg LTV, lifecycle
stage counts. → { data }.
GET /analytics/timeseries
Daily players + event volume. Query: from (ISO, default 30d ago), tz (IANA,
default UTC). → { data[] }.
GET /analytics/revenue
Query: dimension (country/device/os/browser/platform/game/
provider/category/heatmap/bonus_roi), from, tz. → { data[] }.
GET /analytics/cohorts
Query: months (1–36, default 12). → { data }.
GET /reports/run
Ad-hoc metric × dimension. Query: metric (players/events/deposits/
wagered), dimension (day/week/status/country/game/event_type),
from. → { data[] }.
Export (BI / warehouse sync)
GET /export/{dataset}
Bulk, cursor-paginated keyset export for incremental sync. dataset ∈
players/events/transactions. Query: cursor, limit (1–1000, default
200). Pass the returned next_cursor to continue. → { data[], next_cursor }.
GET /segments/{id}/export (above) is the segment-membership export.
SDK
The official @playlogiq/sdk wraps track(), trackBatch(), and identify()
and sets x-api-key for you. Point it at your installation:
import { PlaylogiQ } from "@playlogiq/sdk"
const plq = new PlaylogiQ({
apiKey: "plq_your_api_key",
baseUrl: "https://crm-api.logiqdesk.com", // your API host
})See the Introduction for a quick-start, or the PAM Integration section for the ingestion contract.