Skip to content

Webhook Integration

Webhooks let your application receive real-time notifications when events happen — new submissions, accepted playtests, completed payments, and more.

  1. Create a webhook endpoint in your application that accepts POST requests

  2. Register the webhook with the API:

    Terminal window
    curl -X POST https://app.weplaytestgames.com/api/v1/webhooks \
    -H "Authorization: Bearer wpg_sk_..." \
    -H "Content-Type: application/json" \
    -d '{
    "url": "https://yourapp.com/webhooks/wpg",
    "events": ["slot.submitted", "slot.accepted", "slot.rejected"]
    }'
  3. Save the webhook secret from the response — you’ll need it for signature verification

  4. Test the webhook:

    Terminal window
    curl -X POST https://app.weplaytestgames.com/api/v1/webhooks/wh_abc123/test \
    -H "Authorization: Bearer wpg_sk_..."

Your endpoint should:

  1. Verify the signature (see Webhooks for details)
  2. Process the event
  3. Return a 2xx status quickly (within 5 seconds)
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
app.post('/webhooks/wpg', (req, res) => {
// Verify signature
const signature = req.headers['x-wpg-signature'] as string;
const timestamp = req.headers['x-wpg-timestamp'] as string;
const secret = process.env.WPG_WEBHOOK_SECRET!;
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 300) {
return res.status(401).json({ error: 'Stale timestamp' });
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${req.rawBody}`)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the event
const { event, data } = req.body;
switch (event) {
case 'slot.submitted':
console.log(`New submission for slot ${data.slotId}`);
// Queue for review
break;
case 'slot.accepted':
console.log(`Submission accepted for slot ${data.slotId}`);
break;
case 'slot.rejected':
console.log(`Submission rejected for slot ${data.slotId}`);
break;
}
res.status(200).json({ received: true });
});
from flask import Flask, request, jsonify
import hmac
import hashlib
import time
import os
app = Flask(__name__)
@app.route('/webhooks/wpg', methods=['POST'])
def webhook():
signature = request.headers.get('X-WPG-Signature')
timestamp = request.headers.get('X-WPG-Timestamp')
secret = os.environ['WPG_WEBHOOK_SECRET']
# Verify timestamp
age = int(time.time()) - int(timestamp)
if age > 300:
return jsonify({'error': 'Stale timestamp'}), 401
# Verify signature
expected = hmac.new(
secret.encode(),
f"{timestamp}.{request.data.decode()}".encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected):
return jsonify({'error': 'Invalid signature'}), 401
# Process event
payload = request.json
event = payload['event']
data = payload['data']
if event == 'slot.submitted':
print(f"New submission for slot {data['slotId']}")
return jsonify({'received': True}), 200
  • Respond quickly — return 2xx within 5 seconds. Do heavy processing asynchronously (e.g., queue the event for background processing)
  • Handle duplicates — webhook deliveries may be retried. Use the event’s slotId/paymentId as a deduplication key
  • Verify signatures — always validate the HMAC signature to prevent spoofing
  • Check timestamps — reject events older than 5 minutes
  • Monitor failures — webhooks are auto-disabled after 10 consecutive failures. Set up alerts for failed deliveries
EventWhen It Fires
slot.reservedPlaytester claimed a slot
slot.submittedVideo submitted
slot.acceptedSubmission accepted
slot.rejectedSubmission rejected
slot.expiredDeadline passed
payout.completedPayout sent
payout.failedPayout failed
payment.completedPayment succeeded
payment.failedPayment failed
chat.messageNew chat message