Webhooks
Receive real-time notifications when ratings change in your leaderboards
What are Webhooks?
Webhooks allow your server or application to receive automatic notifications when events occur in Team Up. Instead of polling the API for changes, your endpoint receives a POST request with the event data.
Currently, webhooks are available for leaderboard_ratings events,
which fire whenever a match is recorded and player or team ratings are updated.
Discord Commands
/webhook register
Register a webhook URL to receive event notifications.
/webhook register event_type: Leaderboard Ratings Updated url: https://your-server.com/webhookReturns a secret key that you should save immediately - it won't be shown again.
/webhook list
View all registered webhooks for your server.
/webhook list/webhook test
Send a test payload to verify your webhook endpoint is working.
/webhook test event_type: Leaderboard Ratings Updated/webhook delete
Remove a registered webhook. You must delete before re-registering.
/webhook delete event_type: Leaderboard Ratings UpdatedPayload Structure
leaderboard_ratings Event
Sent when a match is recorded and ratings are updated. Includes all affected rating types.
{
"event": "leaderboard_ratings",
"timestamp": "2024-12-10T15:30:00.000Z",
"guild_id": "123456789012345678",
"leaderboard": {
"leaderboard_id": "ranked",
"name": "Ranked"
},
"match": {
"match_id": "abc123",
"recorded_by": "987654321098765432",
"recorded_at": "2024-12-10T15:30:00.000Z",
"versus": "1v1",
"teams": [
{
"team_number": 1,
"result": "win",
"place": 1,
"players": [
{ "user_id": "111111111111111111", "username": "Winner" }
]
},
{
"team_number": 2,
"result": "loss",
"place": 2,
"players": [
{ "user_id": "222222222222222222", "username": "Loser" }
]
}
]
},
"rating_updates": [
{
"rating_type": "player_global_all",
"total_entries": 150,
"affected_entries": [
{
"user_id": "111111111111111111",
"username": "Winner",
"rating_before": 1500,
"rating_after": 1525,
"rating_change": 25,
"matches": 42,
"max_elo": 1550,
"min_elo": 1400,
"placements": { "1": 20, "2": 15 }
}
]
}
]
}Player Entry Fields
user_id- Discord user IDusername- Discord usernamerating_before- Elo before matchrating_after- Elo after matchrating_change- Elo deltamatches- Total matches playedmax_elo- Highest rating achievedmin_elo- Lowest rating achievedplacements- Placement counts
Team Entry Fields
For team rating types, entries include:
team_id- Team identifiermembers- Array of team members- ...plus all rating fields above
Note: Leaderboard rank is not included in webhook payloads to optimize performance. Use the REST API to fetch current rankings when needed.
HTTP Headers
Each webhook request includes the following headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Signature | HMAC-SHA256 signature (sha256=...) |
X-Timestamp | Unix timestamp when sent |
X-Event-Type | Event type (e.g., leaderboard_ratings) |
User-Agent | TeamUpBot/1.0 |
Verifying Signatures
Security Note
Signature verification is optional but recommended. It ensures the webhook payload came from Team Up and hasn't been tampered with in transit.
To verify a webhook signature, compute the HMAC-SHA256 of the raw request body using your secret,
then compare it to the X-Signature header.
JavaScript / Node.js
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expectedSignature = 'sha256=' +
crypto.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// In your webhook handler:
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const payload = JSON.stringify(req.body);
if (!verifySignature(payload, signature, YOUR_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process the webhook...
res.status(200).send('OK');
});Python
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# In your webhook handler (Flask example):
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Signature')
payload = request.get_data()
if not verify_signature(payload, signature, YOUR_SECRET):
return 'Invalid signature', 401
data = request.json
# Process the webhook...
return 'OK', 200Best Practices
Do
- Use HTTPS endpoints
- Verify signatures in production
- Respond quickly (<10 seconds)
- Return 2xx status codes on success
- Process webhooks asynchronously
- Store your secret securely
Don't
- Expose your webhook secret
- Do heavy processing synchronously
- Rely on webhook order
- Ignore signature verification
- Use HTTP in production
Delivery Behavior
Fire and Forget
Webhooks are sent asynchronously and do not block match recording. If your endpoint fails to respond, the match is still recorded successfully. Webhooks are not retried on failure.
Timeout
Your endpoint must respond within 10 seconds. If processing takes longer, return a 200 status immediately and process the data asynchronously.
Redirects
Redirects are not followed for security reasons. Ensure your webhook URL points directly to your final endpoint.
Test Payloads
When you use /webhook test, the payload includes a "test": true field. You can use this to identify test
requests in your webhook handler.
// Check if it's a test webhook
if (payload.test === true) {
console.log('Received test webhook');
return res.status(200).send('Test received');
}
// Process real webhook...