PlaySuper LogoPlaySuper
API Reference

Webhook Integration

Complete guide for integrating PlaySuper webhooks to receive real-time transaction notifications

PlaySuper Webhook Integration Guide

Overview

PlaySuper webhooks allow your server to receive real-time notifications when coin transactions occur in your games. Instead of polling our API, you'll receive HTTP POST requests to your configured endpoint whenever coins are credited, debited, or refunded.


Quick Start

1. Configure Your Webhook Endpoint

Studio is inferred from your API key, so studioId is not required in the request body.

curl -X POST 'https://api.playsuper.club/studio-webhooks/configure' \
  -H 'Content-Type: application/json' \
  -H 'x-api-key: YOUR_STUDIO_API_KEY' \
  -d '{
    "webhookUrl": "https://your-server.com/webhooks/playsuper",
    "subscribedEvents": ["COINS_CREDITED", "COINS_DEBITED", "COINS_REFUNDED"],
    "environment": "PRODUCTION"
  }'

For Development Environment:

Use https://dev.playsuper.club/studio-webhooks/configure with "environment": "PRODUCTION"

2. Receive Webhook Events

Your endpoint will receive POST requests like:

{
  "event_id": "evt_abc123",
  "event_type": "COINS_CREDITED",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "app_id": "game_xyz",
  "user_uuid": "player_12345",
  "coin_id": "gold_coins",
  "delta": 100,
  "new_balance": 1500,
  "reference_id": "txn_789",
  "reason": "GAME_CREDIT"
}

3. Respond with 2xx Status

Return any 2xx status code to acknowledge receipt. We'll retry on failures.


Webhook Events

Event TypeDescription
COINS_CREDITEDCoins were added to a player's balance
COINS_DEBITEDCoins were deducted from a player's balance
COINS_REFUNDEDCoins were refunded to a player's balance (e.g., failed purchase reversal)

Webhook Payload Structure

interface WebhookPayload {
  event_id: string;        // Unique event identifier
  event_type: string;      // "COINS_CREDITED", "COINS_DEBITED", or "COINS_REFUNDED"
  timestamp: string;       // ISO 8601 timestamp
  app_id: string;          // Your game ID
  user_uuid: string;       // Player's identifier in your system
  coin_id: string;         // Coin type identifier
  delta: number;           // Amount changed (positive for credit, negative for debit)
  new_balance: number;     // Player's new coin balance
  reference_id: string;    // Transaction reference ID
  reason: string;          // Transaction source (see below)
}

Transaction Sources (reason field)

The reason field in the webhook payload indicates what triggered the transaction. Use this to distinguish your own API calls from PlaySuper-originated events.

Currently supported reason values:

  • GAME_CREDIT
  • GAME_DEBIT
  • PURCHASE_DEBIT
  • REFUND_CREDIT
SourceEvent TypeDescription
PURCHASE_DEBITCOINS_DEBITEDPlayer purchased a reward from the PlaySuper store
REFUND_CREDITCOINS_REFUNDEDCoins refunded due to failed/expired payment

Avoiding Double-Processing:

If you're using /distribute and /deduct APIs to sync balances, you should filter out webhook events with GAME_CREDIT or GAME_DEBIT sources in your server code to avoid processing your own sync calls:

app.post('/webhooks/playsuper', (req, res) => {
  const { reason } = req.body;

  // Skip events from your own sync calls
  if (reason === 'GAME_CREDIT' || reason === 'GAME_DEBIT') {
    return res.status(200).json({ received: true, skipped: true });
  }

  // Process PlaySuper-originated events (store purchases, refunds)
  // ...
});

HTTP Headers

Every webhook request includes these headers:

HeaderDescriptionExample
Content-TypeAlways JSONapplication/json
User-AgentIdentifies PlaySuperPlaySuper-Webhook/1.0
X-PlaySuper-Event-IDUnique event IDevt_abc123
X-PlaySuper-Event-TypeEvent typeCOINS_CREDITED
X-PlaySuper-TimestampUnix timestamp1705312200
X-PlaySuper-AttemptDelivery attempt number1
X-PlaySuper-SignatureHMAC-SHA256 signaturet=1705312200,v1=5257a...

Webhook Security (Signature Verification)

Every webhook includes an HMAC-SHA256 signature that you should verify to ensure the request is genuinely from PlaySuper.

Your Signing Secret

When you configure a webhook, you receive a signing secret (e.g., whsec_abc123...). Store this securely - it's used to verify webhook signatures.

Response from configure endpoint:

{
  "id": "whc_abc123",
  "signingSecret": "whsec_7f8a9b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a",
  "webhookUrl": "https://your-server.com/webhooks/playsuper",
  ...
}

Signature Format

The X-PlaySuper-Signature header contains:

t=1705312200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t = Unix timestamp when the signature was generated
  • v1 = HMAC-SHA256 signature

How to Verify

  1. Extract the timestamp and signature from the header
  2. Compute the expected signature: HMAC-SHA256(timestamp + "." + raw_body, signing_secret)
  3. Compare your computed signature with the one in the header
  4. Optionally, check that the timestamp is recent (within 5 minutes) to prevent replay attacks

Verification Examples

Node.js

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const [tPart, v1Part] = signature.split(',');
  const timestamp = tPart.split('=')[1];
  const receivedSig = v1Part.split('=')[1];

  // Check timestamp is within 5 minutes
  const currentTime = Math.floor(Date.now() / 1000);
  if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
    return false; // Timestamp too old
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Constant-time comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(receivedSig),
    Buffer.from(expectedSig)
  );
}

// Express middleware
app.post('/webhooks/playsuper', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-playsuper-signature'];
  const payload = req.body.toString();

  if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(payload);
  // Process verified webhook...

  res.status(200).json({ received: true });
});

Python

import hmac
import hashlib
import time

def verify_webhook_signature(payload: str, signature: str, secret: str) -> bool:
    parts = dict(p.split('=') for p in signature.split(','))
    timestamp = parts.get('t')
    received_sig = parts.get('v1')

    if not timestamp or not received_sig:
        return False

    # Check timestamp is within 5 minutes
    current_time = int(time.time())
    if abs(current_time - int(timestamp)) > 300:
        return False

    # Compute expected signature
    signed_payload = f"{timestamp}.{payload}"
    expected_sig = hmac.new(
        secret.encode('utf-8'),
        signed_payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison
    return hmac.compare_digest(received_sig, expected_sig)

# Flask example
@app.route('/webhooks/playsuper', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-PlaySuper-Signature')
    payload = request.get_data(as_text=True)

    if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
        return jsonify({'error': 'Invalid signature'}), 401

    event = request.json
    # Process verified webhook...

    return jsonify({'received': True}), 200

Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "math"
    "strconv"
    "strings"
    "time"
)

func verifyWebhookSignature(payload, signature, secret string) bool {
    parts := make(map[string]string)
    for _, p := range strings.Split(signature, ",") {
        kv := strings.SplitN(p, "=", 2)
        if len(kv) == 2 {
            parts[kv[0]] = kv[1]
        }
    }

    timestamp := parts["t"]
    receivedSig := parts["v1"]

    // Check timestamp is within 5 minutes
    ts, _ := strconv.ParseInt(timestamp, 10, 64)
    if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
        return false
    }

    // Compute expected signature
    signedPayload := timestamp + "." + payload
    h := hmac.New(sha256.New, []byte(secret))
    h.Write([]byte(signedPayload))
    expectedSig := hex.EncodeToString(h.Sum(nil))

    return hmac.Equal([]byte(receivedSig), []byte(expectedSig))
}

Retry Policy

If your endpoint fails to respond with a 2xx status, we'll retry with exponential backoff:

AttemptDelay After Failure
11 minute
25 minutes
315 minutes
41 hour
54 hours

After 5 failed attempts, the webhook is marked as failed and moved to dead-letter queue.

Timeout: Your endpoint must respond within 30 seconds.


API Reference

Configure Webhook

Create or update webhook configuration for your studio.

curl -X POST 'https://api.playsuper.com/studio-webhooks/configure' \
  -H 'Content-Type: application/json' \
  -H 'x-api-key: YOUR_STUDIO_API_KEY' \
  -d '{
    "webhookUrl": "https://your-server.com/webhooks/playsuper",
    "subscribedEvents": ["COINS_CREDITED", "COINS_DEBITED", "COINS_REFUNDED"],
    "environment": "PRODUCTION",
    "description": "Production webhook for coin events"
  }'

Response:

{
  "id": "whc_abc123",
  "studioId": "studio_123",
  "webhookUrl": "https://your-server.com/webhooks/playsuper",
  "subscribedEvents": ["COINS_CREDITED", "COINS_DEBITED"],
  "environment": "PRODUCTION",
  "isActive": true,
  "createdAt": "2025-01-15T10:00:00.000Z"
}

List Webhook Configs

curl -X GET 'https://api.playsuper.com/studio-webhooks/config?studioId=studio_123' \
  -H 'x-api-key: YOUR_STUDIO_API_KEY'

Get Specific Config

curl -X GET 'https://api.playsuper.com/studio-webhooks/config/whc_abc123?studioId=studio_123' \
  -H 'x-api-key: YOUR_STUDIO_API_KEY'

Update Webhook Config

curl -X PUT 'https://api.playsuper.com/studio-webhooks/config/whc_abc123?studioId=studio_123' \
  -H 'Content-Type: application/json' \
  -H 'x-api-key: YOUR_STUDIO_API_KEY' \
  -d '{
    "webhookUrl": "https://new-server.com/webhooks/playsuper",
    "isActive": true
  }'

Delete Webhook Config

curl -X DELETE 'https://api.playsuper.com/studio-webhooks/config/whc_abc123?studioId=studio_123' \
  -H 'x-api-key: YOUR_STUDIO_API_KEY'

Send Test Event

Test your webhook integration without affecting real data.

curl -X POST 'https://api.playsuper.com/studio-webhooks/test?studioId=studio_123&configId=whc_abc123' \
  -H 'Content-Type: application/json' \
  -H 'x-api-key: YOUR_STUDIO_API_KEY' \
  -d '{
    "eventType": "COINS_CREDITED"
  }'

Get Delivery History

View webhook delivery attempts and their status.

curl -X GET 'https://api.playsuper.com/studio-webhooks/deliveries?studioId=studio_123&status=FAILED&limit=10' \
  -H 'x-api-key: YOUR_STUDIO_API_KEY'

Query Parameters:

  • status: Filter by status (SUCCESS, FAILED, PENDING, RETRYING)
  • configId: Filter by webhook config
  • limit: Number of results (default: 50)
  • offset: Pagination offset

Retry Failed Delivery

Manually retry a failed webhook delivery.

curl -X POST 'https://api.playsuper.com/studio-webhooks/deliveries/del_xyz789/retry?studioId=studio_123' \
  -H 'x-api-key: YOUR_STUDIO_API_KEY'

Get Webhook Stats

Get delivery statistics for your webhooks.

curl -X GET 'https://api.playsuper.com/studio-webhooks/stats?studioId=studio_123' \
  -H 'x-api-key: YOUR_STUDIO_API_KEY'

Response:

{
  "totalEvents": 1250,
  "delivered": 1200,
  "failed": 30,
  "pending": 20,
  "successRate": 96.0
}

Implementation Examples

Node.js (Express)

const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/playsuper', (req, res) => {
  const eventId = req.headers['x-playsuper-event-id'];
  const eventType = req.headers['x-playsuper-event-type'];

  console.log(`Received${eventType} event:${eventId}`);

  const { user_uuid, coin_id, delta, new_balance, reference_id, reason } = req.body;

  // Skip events from your own sync calls to avoid double-processing
  if (reason === 'GAME_CREDIT' || reason === 'GAME_DEBIT') {
    console.log(`Skipping self-originated event:${reason}`);
    return res.status(200).json({ received: true, skipped: true });
  }

  // Process PlaySuper-originated events
  switch (req.body.event_type) {
    case 'COINS_CREDITED':
      // Update your database, notify user, etc.
      console.log(`User${user_uuid} received${delta}${coin_id}`);
      break;
    case 'COINS_DEBITED':
      // Handle debit event (e.g., PURCHASE_DEBIT from PlaySuper store)
      console.log(`User${user_uuid} spent${Math.abs(delta)}${coin_id}`);
      break;
    case 'COINS_REFUNDED':
      // Handle refund event (REFUND_CREDIT - coins returned due to failed purchase)
      console.log(`User${user_uuid} was refunded${delta}${coin_id}`);
      break;
  }

  // Always respond quickly with 200
  res.status(200).json({ received: true });
});

app.listen(3000);

Python (Flask)

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/playsuper', methods=['POST'])
def handle_webhook():
    event_id = request.headers.get('X-PlaySuper-Event-ID')
    event_type = request.headers.get('X-PlaySuper-Event-Type')

    print(f"Received{event_type} event:{event_id}")

    payload = request.json
    user_uuid = payload['user_uuid']
    delta = payload['delta']
    new_balance = payload['new_balance']
    reason = payload.get('reason')

    # Skip events from your own sync calls to avoid double-processing
    if reason in ('GAME_CREDIT', 'GAME_DEBIT'):
        print(f"Skipping self-originated event:{reason}")
        return jsonify({'received': True, 'skipped': True}), 200

    # Process PlaySuper-originated events
    if payload['event_type'] == 'COINS_CREDITED':
        print(f"User{user_uuid} received{delta} coins")
    elif payload['event_type'] == 'COINS_DEBITED':
        # PURCHASE_DEBIT - player bought from PlaySuper store
        print(f"User{user_uuid} spent{abs(delta)} coins")
    elif payload['event_type'] == 'COINS_REFUNDED':
        # REFUND_CREDIT - coins returned due to failed purchase
        print(f"User{user_uuid} was refunded{delta} coins")

    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(port=3000)

Go

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

type WebhookPayload struct {
    EventID     string `json:"event_id"`
    EventType   string `json:"event_type"`
    Timestamp   string `json:"timestamp"`
    AppID       string `json:"app_id"`
    UserUUID    string `json:"user_uuid"`
    CoinID      string `json:"coin_id"`
    Delta       int    `json:"delta"`
    NewBalance  int    `json:"new_balance"`
    ReferenceID string `json:"reference_id"`
    Reason      string `json:"reason"`
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    eventID := r.Header.Get("X-PlaySuper-Event-ID")
    eventType := r.Header.Get("X-PlaySuper-Event-Type")

    log.Printf("Received %s event: %s", eventType, eventID)

    var payload WebhookPayload
    if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }

    // Skip events from your own sync calls to avoid double-processing
    if payload.Reason == "GAME_CREDIT" || payload.Reason == "GAME_DEBIT" {
        log.Printf("Skipping self-originated event: %s", payload.Reason)
        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(map[string]interface{}{"received": true, "skipped": true})
        return
    }

    // Process PlaySuper-originated events
    switch payload.EventType {
    case "COINS_CREDITED":
        log.Printf("User %s received %d coins", payload.UserUUID, payload.Delta)
    case "COINS_DEBITED":
        // PURCHASE_DEBIT - player bought from PlaySuper store
        log.Printf("User %s spent %d coins", payload.UserUUID, -payload.Delta)
    case "COINS_REFUNDED":
        // REFUND_CREDIT - coins returned due to failed purchase
        log.Printf("User %s was refunded %d coins", payload.UserUUID, payload.Delta)
    }

    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]bool{"received": true})
}

func main() {
    http.HandleFunc("/webhooks/playsuper", webhookHandler)
    log.Fatal(http.ListenAndServe(":3000", nil))
}

Best Practices

1. Respond Quickly

Process webhooks asynchronously. Return 200 immediately and handle the event in a background job.

app.post('/webhooks/playsuper', async (req, res) => {
  // Acknowledge immediately
  res.status(200).json({ received: true });

  // Process asynchronously
  processWebhookAsync(req.body).catch(console.error);
});

2. Handle Duplicates (Idempotency)

Webhooks may be delivered more than once. Use event_id to detect duplicates.

app.post('/webhooks/playsuper', async (req, res) => {
  const eventId = req.body.event_id;

  // Check if already processed
  if (await isEventProcessed(eventId)) {
    return res.status(200).json({ received: true, duplicate: true });
  }

  // Mark as processing
  await markEventProcessing(eventId);

  // Process event...

  res.status(200).json({ received: true });
});

3. Verify Signatures

Always verify the X-PlaySuper-Signature header to ensure requests are genuinely from PlaySuper. See the Webhook Security section for implementation examples.

app.post('/webhooks/playsuper', (req, res) => {
  const signature = req.headers['x-playsuper-signature'];

  if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process verified webhook...
});

4. Use HTTPS

Always use HTTPS endpoints in production to ensure data security.


Troubleshooting

Webhook Not Received

  1. Check your endpoint is publicly accessible

  2. Verify firewall allows incoming requests

  3. Check delivery history for errors:

    curl -X GET 'https://api.playsuper.com/studio-webhooks/deliveries?studioId=studio_123&status=FAILED' \
      -H 'x-api-key: YOUR_STUDIO_API_KEY'

Timeout Errors

  • Ensure your endpoint responds within 30 seconds
  • Process webhooks asynchronously
  • Return 200 immediately, handle logic in background

Duplicate Events

  • Implement idempotency using event_id
  • Store processed event IDs in your database
  • Check before processing

Environment Configuration

EndpointUse CaseEnvironment Value
https://dev.playsuper.club/studio-webhooks/...Testing and developmentPRODUCTION
https://api.playsuper.club/studio-webhooks/...Live production trafficPRODUCTION

Both development and production use "environment": "PRODUCTION" in the request body. The environment is determined by which API endpoint you call (dev vs api subdomain).

Configure separate webhook URLs for each environment:

# Development
curl -X POST 'https://dev.playsuper.club/studio-webhooks/configure' \
  -H 'x-api-key: YOUR_DEV_STUDIO_API_KEY' \
  -d '{
    "webhookUrl": "https://dev.your-server.com/webhooks/playsuper",
    "environment": "PRODUCTION"
  }'

# Production
curl -X POST 'https://api.playsuper.club/studio-webhooks/configure' \
  -H 'x-api-key: YOUR_PROD_STUDIO_API_KEY' \
  -d '{
    "webhookUrl": "https://your-server.com/webhooks/playsuper",
    "environment": "PRODUCTION"
  }'