Free spins API reference
Self-serve routes for issuing, listing, and cancelling free-spin grants for your own players — signed the same way as every other Operator API call. See Free spins for the guide. Campaign templates (reusable presets an admin configures) are managed by the platform admin team, not through this API — you reference one by ID if your integration contact has set one up for you, or supply the grant's fields directly.
POST /v1/operator/free-spins/grants
Request body:
type IssueGrantRequestBody = {
playerRef: string
campaignId?: string // if set, gameId/spins/betAmountMinor/currency below
// are IGNORED and read from the campaign instead
gameId?: string // required if campaignId is omitted
spins?: number // required (> 0) if campaignId is omitted
betAmountMinor?: number // optional; omit/0 to use your wallet's default stake
currency?: string // required if campaignId is omitted
idempotencyKey: string // required — see Idempotency below
expiresAt?: string // optional RFC3339 timestamp; informational only, not enforced by the platform today
}currency must be one the target game's provider supports, and the game must be visible to your operator tenant — the same checks launch applies.
Response: the created (or, on a repeat idempotency key, the original) grant:
type GrantStatus = 'pending' | 'active' | 'cancelled' | 'failed'
// 'expired' and 'completed' are reserved for a future phase that ingests
// provider-reported completion — nothing issues them today.
type GrantResponseBody = {
id: string
campaignId?: string
operatorId: string
providerId: string
gameId: string
playerRef: string
spins: number
betAmountMinor: number
currency: string
externalRef?: string // the provider's own identifier for this batch — empty until the provider call succeeds
status: GrantStatus
idempotencyKey: string
expiresAt?: string
lastError?: string // populated when status is 'failed'
createdAt: string
updatedAt: string
}Idempotency
idempotencyKey is required and scoped to (your operatorId, idempotencyKey): a repeat call with the same key returns the original call's grant unchanged, rather than issuing a second batch. This is a synchronous call to the provider, not a fire-and-forget queue — a provider-side failure marks the grant failed immediately (see lastError) and permanently consumes that idempotency key: retry with a fresh key, not the same one, if you want another attempt.
Error responses: 400 (missing/invalid fields, spins not positive, currency not supported, or — for a campaign-based grant — the campaign isn't active or is outside its validity window), 403 (the game isn't visible to your operator tenant), 404 (no such campaign, or no such game), 502 (the provider call itself failed — see lastError on the resulting failed grant), 401/429 (shared with every Operator API endpoint — see Errors & retry).
GET /v1/operator/free-spins/grants?playerRef=<ref>
Lists grants under your operator tenant. playerRef is optional — omit it to list every grant your tenant has issued.
Response: GrantResponseBody[].
GET /v1/operator/free-spins/grants/{id}
Returns a single grant by ID, scoped to your operator tenant (a grant ID belonging to a different operator behaves like it doesn't exist).
Error responses: 404 (no such grant under your tenant), 400 (id is not a valid identifier).
POST /v1/operator/free-spins/grants/{id}/cancel
Voids a grant's remaining, unused spins. Only an active grant can be cancelled — calling this on a grant that's pending, already cancelled, or failed returns 400 (ErrGrantNotCancellable). Like issuance, this is a synchronous call to the provider: if the provider's cancel call itself fails, your local grant is not marked cancelled (the spins may still be live on the provider's side), so a 502 here means retry the cancel rather than assuming it took effect.
Response: the updated GrantResponseBody.
Error responses: 404 (no such grant under your tenant), 400 (not cancellable — see above), 502 (the provider's cancel call failed), 401/429 (shared, see Errors & retry).