PAM IntegrationEvent ingestion API

Event ingestion API

The single endpoint a PAM needs.

POST /api/v1/events
x-api-key: plq_…            # brand-pinned key
Content-Type: application/json

Request — a single event

{
  "player_id": "player-123",
  "event_type": "CashIn",
  "event_id": "pam-evt-9a7f3",
  "payload": { "amt": 2500, "currency": "EUR", "method": "card" },
  "timestamp": "2026-07-09T10:15:30Z",
  "session_id": "sess-42"
}
FieldRequiredNotes
player_idYour internal player id (stored as the player’s external_id).
event_typeWhatever string your PAM emits. Translated by the mapping.
event_idIdempotency key (≤128 chars). Re-sends with the same key are de-duped.
payloadFree-form JSON object. Defaults to {}.
timestampISO-8601. Defaults to server receive time. Set it for replays/backfills.
session_idOptional grouping id for a play session.

Request — a batch (up to 100)

{
  "events": [
    { "player_id": "p1", "event_type": "Login" },
    { "player_id": "p2", "event_type": "CashIn", "payload": { "amt": 5000 } }
  ]
}

A batch is up to 100 events. For higher throughput, send batches concurrently — the endpoint is stateless per request.

Players are upserted automatically

You do not pre-create players. On ingest, PlaylogiQ upserts the player by (brand, player_id): unknown players are created, known players are matched. If your mapping has a player field map, mapped attributes (email, name, …) are written on the same upsert.

Idempotency & retries

Send an event_id and you can safely retry on any network/5xx error: a repeat with the same event_id is stored once (deduplicated on event_id within the tenant). This makes the PAM’s job simple — at-least-once delivery with retries is sufficient; you never risk double-counting a deposit.

Response

{
  "received": 2,
  "stored": 2,
  "deduped": 0,
  "status": "queued",
  "timestamp": "2026-07-09T10:15:30.412Z"
}
FieldMeaning
receivedEvents in the request.
storedNewly stored (excludes idempotent duplicates).
dedupedDuplicates skipped via event_id.
rejected(if any) Events refused by the mapping’s reject policy.
ignored(if any) Events dropped by the mapping’s ignore policy.
unknown_event_types(if any) Types stored but neither canonical nor mapped — a hint to extend your mapping.
rejected_event_types(if any) The distinct types that were rejected.
statusAlways "queued" — accepted for asynchronous derivation.

Error responses

StatuscodeCauseRetry?
400BAD_REQUESTBody isn’t valid JSON, or fails schema validation (see details).❌ Fix the payload
401UNAUTHORIZEDMissing/invalid/expired API key.❌ Fix the key
403FORBIDDENKey lacks the events:write scope.❌ Fix the key
429RATE_LIMITEDOver the per-minute budget (see below).✅ After Retry-After
500INTERNAL_ERRORUnexpected server error.✅ With backoff

A validation failure returns Zod-style details:

{ "error": "Invalid event payload", "details": { "fieldErrors": { "player_id": ["player_id is required"] } } }

Rate limits

Ingestion has its own generous per-tenant budget, separate from the admin/read API so the event firehose can’t starve it.

TierDefault budgetEnv override
Ingestion (POST /events)6000 requests / minute per tenantRATE_LIMIT_INGEST_PER_MIN
Admin/read API1200 requests / minute per tenantRATE_LIMIT_PER_MIN
  • With batches of 100, 6000 req/min = up to 600,000 events/minute.
  • A 429 carries Retry-After (seconds) plus X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Honour Retry-After before retrying.
  • The limiter fails open — if the platform’s cache is unavailable, requests are allowed rather than rejected. Do not rely on 429 as a hard ceiling.
  • Budgets are the fixed-window per-tenant defaults; a specific installation may raise them. Confirm the effective limit with the operator if you expect sustained high volume.

Capacity planning

The limit counts requests, not events — so batching is the throughput lever:

Sending styleThroughput per tenant (default)
Unbatched (1 event/request)100 events/second
Batched (100 events/request)10,000 events/second (600k/min)

For a brand with tens of thousands of concurrent players this is comfortable when batched — e.g. 50,000 active players × 10 events/min = 500k events/min, well inside the batched budget. If you send unbatched you’ll hit the 100 req/s request cap first, so batch wherever you can.

Raising it is a one-line operator change — set RATE_LIMIT_INGEST_PER_MIN in the api environment and recreate the container; no code change. The budget is per-tenant and Redis-backed (fail-open). Past a certain volume the real ceiling becomes database write throughput, not the limiter.

Limits & constraints

ConstraintValue
Events per request (batch)1–100
event_id length≤ 128 chars
player_idrequired, non-empty string (your internal id)
event_typerequired, non-empty string
payloadJSON object (defaults to {})
timestampISO-8601; send in UTC. Defaults to server receive time
TransportHTTPS only

cURL example

curl -X POST https://crm-api.logiqdesk.com/api/v1/events \
  -H "x-api-key: plq_your_brand_key" \
  -H "Content-Type: application/json" \
  -d '{
    "player_id": "player-123",
    "event_type": "CashIn",
    "event_id": "pam-evt-9a7f3",
    "payload": { "amt": 2500, "currency": "EUR" }
  }'

The base URL above is the reference self-hosted deployment (crm-api.logiqdesk.com). Use whatever host your installation exposes.

Next: translate CashIn / amt into the canonical deposit / amount with the mapping engine.