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/webhook

Returns 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 Updated

Payload 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 ID
  • username - Discord username
  • rating_before - Elo before match
  • rating_after - Elo after match
  • rating_change - Elo delta
  • matches - Total matches played
  • max_elo - Highest rating achieved
  • min_elo - Lowest rating achieved
  • placements - Placement counts

Team Entry Fields

For team rating types, entries include:

  • team_id - Team identifier
  • members - 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:

HeaderDescription
Content-Typeapplication/json
X-SignatureHMAC-SHA256 signature (sha256=...)
X-TimestampUnix timestamp when sent
X-Event-TypeEvent type (e.g., leaderboard_ratings)
User-AgentTeamUpBot/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', 200

Best 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...