Skip to content

Webhooks

Webhooks allow you to receive real-time HTTP notifications when events occur on the platform — a playtest is submitted, a submission is accepted, a payment completes, etc.

EventTrigger
slot.reservedA playtester claimed a playtest slot
slot.submittedA playtester submitted a video
slot.acceptedGame owner accepted a submission
slot.rejectedGame owner rejected a submission
slot.expiredSlot deadline expired without a submission
payout.completedPayout was processed and sent
payout.failedPayout failed
payment.completedStripe payment succeeded
payment.failedStripe payment failed
chat.messageNew chat message received

All webhook deliveries send a JSON payload:

{
"event": "slot.submitted",
"timestamp": "2026-03-02T12:00:00Z",
"data": {
"slotId": "slot_abc",
"playtestId": "pt_xyz",
"gameId": "game_123",
"playerId": "usr_tester1",
"submittedAt": "2026-03-02T12:00:00Z"
}
}

The data object varies by event type and contains the relevant resource details.

Every webhook delivery includes headers for verifying authenticity:

HeaderDescription
X-WPG-SignatureHMAC-SHA256 hex digest of the signed payload
X-WPG-TimestampUnix timestamp (seconds) when the delivery was sent
X-WPG-Signature-VersionSignature version (v1)

The signature is computed over {timestamp}.{body}, where body is the raw JSON request body. The signing secret is the secret value returned when you create a webhook (prefixed with whsec_). Store it securely — it is only shown once.

import crypto from 'crypto';
function verifyWebhook(
body: string,
signature: string,
timestamp: string,
secret: string,
): boolean {
// Reject deliveries older than 5 minutes
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${body}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}
// In your Express handler:
app.post('/webhooks/wpg', (req, res) => {
const isValid = verifyWebhook(
JSON.stringify(req.body),
req.headers['x-wpg-signature'] as string,
req.headers['x-wpg-timestamp'] as string,
process.env.WPG_WEBHOOK_SECRET!,
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the event
const { event, data } = req.body;
console.log(`Received ${event}:`, data);
res.status(200).json({ received: true });
});

Always check the X-WPG-Timestamp header and reject deliveries older than 5 minutes. The timestamp is included in the HMAC signature, so it cannot be tampered with.

Failed deliveries (non-2xx response or network error) are retried with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
612 hours

After 5 failed retries (6 total attempts), the delivery is marked as failed.

Webhooks are automatically disabled after 10 consecutive failures across any deliveries. You receive an email notification when this happens.

To re-enable a disabled webhook: POST /api/v1/webhooks/:id/enable. This resets the failure count.

Rotate your webhook secret at any time via POST /api/v1/webhooks/:id/rotate-secret. The old secret is immediately invalidated — update your verification code before rotating.

See Webhook Management Endpoints for the full API reference.