Přeskočit na hlavní obsah

Bonus flow

Bidirectional contract for issuing + tracking bonuses on the player's wallet.

Lifecycle

[bonus.credited]
CRM grant → adapter.grantBonus → ───────────────────→ active

[bonus.wagered] (N×)
←────────────────────────┤

[bonus.completed] │
←────────────────────────┴──→ completed (TERMINAL)

alternative terminals:
[bonus.expired] → expired
[bonus.cancelled] → cancelled (admin / fraud)
[bonus.forfeited] → forfeited (max bet violation, etc.)

CRM → Adapter (outbound)

When a CRM journey, campaign, or admin issues a bonus, UCRM calls your adapter:

POST <your-base>/v1/bonuses/grant

Headers:

  • Authorization: Bearer <CRM-API-key-or-platform-token> (your auth scheme)
  • X-UCRM-Signature: <hex-hmac> (UCRM signs outbound calls)
  • Content-Type: application/json

Body:

{
"grant_id": "ucrm-uuid-correlation-key", // store + echo on every webhook
"tenant_id": "ucrm-tenant-uuid",
"player_external_id": "casino_player_001",
"template": {
"type": "deposit_match",
"amount_subunit": 5000, // €50 in cents (issue value)
"currency": "EUR",
"wagering": {
"multiplier": 30,
"basis": "bonus_plus_deposit",
"max_bet_subunit": 500 // €5 in cents
},
"expires_at": "2026-06-08T...",
"config": { // type-specific config blob
"match_percent": 100,
"cap_amount": 5000,
"deposit_amount": 5000 // for deposit_match — the trigger deposit
}
},
"granted_by": {
"kind": "journey", // manual | api | journey | campaign | auto
"ref": "journey_run_uuid"
}
}

Success response (201):

{
"platform_bonus_id": "platform-side-bonus-id",
"status": "credited" // "credited" if synchronous, "pending" if async
}

Error responses:

// 4xx — non-retriable
{ "code": "PLAYER_BLOCKED", "message": "Player is self-excluded" }
{ "code": "DUPLICATE_GRANT", "message": "Already issued for grant_id X" }
{ "code": "TEMPLATE_NOT_SUPPORTED", "message": "..." }

// 5xx — retriable
{ "code": "INTERNAL_ERROR", "message": "..." }

POST <your-base>/v1/bonuses/cancel

{
"grant_id": "ucrm-uuid",
"tenant_id": "...",
"platform_bonus_id": "platform-side-id",
"reason": "fraud_review"
}

Returns 200 { ok: true } then fires bonus.cancelled webhook (asynchronously) when the cancel actually completes on platform.

GET <your-base>/v1/bonuses/:platform_bonus_id

Used by daily reconciliation to compare CRM state vs platform state.

// 200 response
{
"status": "active", // active | completed | forfeited | expired | cancelled
"wagering_progress_subunit": 60000, // weighted progress in subunit
"total_wagered_subunit": 60000,
"total_won_from_bonus_subunit": 0
}

404 if platform doesn't recognise the id (means CRM's view is wrong — log + audit).

Adapter → CRM (webhooks)

Fire these as the bonus moves through its lifecycle. Always include grant_id so CRM can correlate.

POST https://api.casinocrm.io/v1/adapters/<your-slug>/webhooks/bonus-events

Headers:

  • X-UCRM-Signature: <hex-hmac-sha256-over-timestamp.body>
  • X-UCRM-Timestamp: <unix-seconds>

bonus.credited:

{
"event": "bonus.credited",
"event_id": "evt-uuid-unique",
"ts": "2026-05-08T12:00:00Z",
"grant_id": "ucrm-uuid",
"platform_bonus_id": "platform-id",
"data": {
"credited_amount_subunit": 5000,
"currency": "EUR"
}
}

bonus.wagered (fire after each settled bet that contributes to wagering):

{
"event": "bonus.wagered",
"event_id": "evt-uuid",
"ts": "...",
"grant_id": "ucrm-uuid",
"platform_bonus_id": "platform-id",
"data": {
"wagered_amount_subunit": 1000,
"weighted_progress_subunit": 1000, // amount × game_contribution_pct / 100
"bet_id": "bet-uuid"
}
}

bonus.completed:

{
"event": "bonus.completed",
"event_id": "evt-uuid",
"ts": "...",
"grant_id": "ucrm-uuid",
"platform_bonus_id": "platform-id",
"data": {
"completed_at": "...",
"total_wagered_subunit": 150000,
"total_won_from_bonus_subunit": 12000
}
}

bonus.expired:

{
"event": "bonus.expired",
"event_id": "evt-uuid",
"ts": "...",
"grant_id": "ucrm-uuid",
"platform_bonus_id": "platform-id",
"data": {}
}

bonus.cancelled:

{
"event": "bonus.cancelled",
"event_id": "evt-uuid",
"ts": "...",
"grant_id": "ucrm-uuid",
"platform_bonus_id": "platform-id",
"data": { "reason": "fraud_review" }
}

bonus.forfeited:

{
"event": "bonus.forfeited",
"event_id": "evt-uuid",
"ts": "...",
"grant_id": "ucrm-uuid",
"platform_bonus_id": "platform-id",
"data": { "reason": "max_bet_violation:8500_max_500" }
}

Reconciliation

UCRM runs pnpm reconcile:bonuses daily. For each active grant in your adapter's tenants:

  1. Calls GET <your-base>/v1/bonuses/:platform_bonus_id
  2. If platform's status is a forward transition from CRM's status, replays the missing webhook locally (synthetic event through the same state-sync pipeline).
  3. If status is incompatible (CRM active but platform completed, missed webhook), forces transition + logs structured drift entry for ops.

This handles webhook drops + partial outages gracefully — you don't need to implement perfect at-most-once delivery; UCRM closes the gap nightly.