Webhook Integration
Webhooks let your application receive real-time notifications when events happen — new submissions, accepted playtests, completed payments, and more.
Quick Setup
Section titled “Quick Setup”-
Create a webhook endpoint in your application that accepts POST requests
-
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"]}' -
Save the webhook secret from the response — you’ll need it for signature verification
-
Test the webhook:
Terminal window curl -X POST https://app.weplaytestgames.com/api/v1/webhooks/wh_abc123/test \-H "Authorization: Bearer wpg_sk_..."
Handling Webhooks
Section titled “Handling Webhooks”Your endpoint should:
- Verify the signature (see Webhooks for details)
- Process the event
- Return a 2xx status quickly (within 5 seconds)
Express.js Example
Section titled “Express.js Example”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 });});Flask Example
Section titled “Flask Example”from flask import Flask, request, jsonifyimport hmacimport hashlibimport timeimport 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}), 200Best Practices
Section titled “Best Practices”- 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/paymentIdas 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
Available Events
Section titled “Available Events”| Event | When It Fires |
|---|---|
slot.reserved | Playtester claimed a slot |
slot.submitted | Video submitted |
slot.accepted | Submission accepted |
slot.rejected | Submission rejected |
slot.expired | Deadline passed |
payout.completed | Payout sent |
payout.failed | Payout failed |
payment.completed | Payment succeeded |
payment.failed | Payment failed |
chat.message | New chat message |