Skip to content

Session revoke webhook

One of the platform → provider webhooks: this direction goes the other way, the platform calls your server, not the reverse. It's how an operator kicking a player reaches a still-open game client — see the inbound webhooks section of the server-integration guide for when and why this fires.

Implement POST /v1/game/session/revoke (that exact path, at the baseUrl your integration contact registered for you) and verify every call with @moosehq/provider-sdk's verifyPlatformSignature before trusting it — this is the platform calling you, so the usual "the SDK signs it for me" story is reversed here.

Request

Signed the same way your own outbound calls are: X-Tenant-ID, X-Timestamp, X-Nonce, X-Signature headers, HMAC-SHA256 over method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + body using your provider secret. X-Tenant-ID carries your own provider tenant ID (the platform is asserting "this call is for you", not naming itself).

Body:

ts
type RevokeSessionRequest = {
  sessionToken: string
  playerRef: string
  gameId: string
  reason?: string // e.g. "operator kick" — may be empty
}

Response

Return 200 on success. Any other status is logged by the platform but never retried and never changes the kick's outcome — the session is already deleted regardless (see above).

Example (Node/Express)

ts
import { verifyPlatformSignature, type RevokeSessionRequest } from '@moosehq/provider-sdk'

// req.body must be the exact raw string the platform signed, so this
// route needs the raw body, not JSON-parsed middleware.
app.post('/v1/game/session/revoke', express.text({ type: '*/*' }), (req, res) => {
  const result = verifyPlatformSignature({
    method: req.method,
    path: req.path,
    headers: req.headers,
    body: req.body,
    secret: process.env.PLATFORM_SECRET!,
    now: new Date(),
    expectedTenantId: 'acme-studio', // your own provider tenant ID
  })
  if (!result.ok) return res.status(401).json({ error: result.reason })

  const revoke: RevokeSessionRequest = JSON.parse(req.body)
  // End revoke.sessionToken on your side, and — for a browser game —
  // tell the running client via your own channel (e.g. the game-client
  // SDK's notifySessionRevoked) so it exits immediately instead of
  // waiting for its next wallet call to 401.
  res.sendStatus(200)
})

verifyPlatformSignature

ts
function verifyPlatformSignature(input: VerifyPlatformSignatureInput): VerifyPlatformSignatureResult

type VerifyPlatformSignatureInput = {
  method: string
  path: string                 // URL path only — no scheme/host/query
  headers: IncomingHeaders     // e.g. Node's req.headers — lowercase keys
  body: string                 // the exact raw request body the platform signed
  secret: string               // your provider secret
  now: Date
  maxSkewSeconds?: number      // defaults to 300 (5 minutes)
  expectedTenantId?: string    // optional defense-in-depth check
}

type VerifyPlatformSignatureResult =
  | { ok: true; tenantId: string }
  | {
      ok: false
      reason:
        | 'missing-tenant-id'
        | 'tenant-id-mismatch'
        | 'missing-timestamp'
        | 'timestamp-skew'
        | 'missing-nonce'
        | 'invalid-signature'
    }

Unlike the platform's own HMACVerifier, this doesn't consult a previous-secret grace period during rotation — that's a platform-side, per-tenant concern; a provider verifying inbound calls only ever checks against its own single current secret.