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 Type | Description |
|---|---|
COINS_CREDITED | Coins were added to a player's balance |
COINS_DEBITED | Coins were deducted from a player's balance |
COINS_REFUNDED | Coins 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_CREDITGAME_DEBITPURCHASE_DEBITREFUND_CREDIT
| Source | Event Type | Description |
|---|---|---|
PURCHASE_DEBIT | COINS_DEBITED | Player purchased a reward from the PlaySuper store |
REFUND_CREDIT | COINS_REFUNDED | Coins 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:
| Header | Description | Example |
|---|---|---|
Content-Type | Always JSON | application/json |
User-Agent | Identifies PlaySuper | PlaySuper-Webhook/1.0 |
X-PlaySuper-Event-ID | Unique event ID | evt_abc123 |
X-PlaySuper-Event-Type | Event type | COINS_CREDITED |
X-PlaySuper-Timestamp | Unix timestamp | 1705312200 |
X-PlaySuper-Attempt | Delivery attempt number | 1 |
X-PlaySuper-Signature | HMAC-SHA256 signature | t=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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt= Unix timestamp when the signature was generatedv1= HMAC-SHA256 signature
How to Verify
- Extract the timestamp and signature from the header
- Compute the expected signature:
HMAC-SHA256(timestamp + "." + raw_body, signing_secret) - Compare your computed signature with the one in the header
- 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}), 200Go
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:
| Attempt | Delay After Failure |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 15 minutes |
| 4 | 1 hour |
| 5 | 4 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 configlimit: 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
-
Check your endpoint is publicly accessible
-
Verify firewall allows incoming requests
-
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
| Endpoint | Use Case | Environment Value |
|---|---|---|
https://dev.playsuper.club/studio-webhooks/... | Testing and development | PRODUCTION |
https://api.playsuper.club/studio-webhooks/... | Live production traffic | PRODUCTION |
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"
}'Related Guides
- API Reference - REST API for player management, coins, and rewards
- Touchpoints API - Configure visual reward widgets
- Gift Card API - Purchase and distribute gift card vouchers
- Console Setup - Set up your account and generate API keys