Skip to content

Documentation

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.

Managing Webhooks

Web dashboard — Admin → Webhooks

Webhooks are set up on your league's dashboard, beside API Keys under Admin. Registering a webhook is a Pro feature.

  • Register — pick an event, paste your endpoint URL, and get a signing secret.
  • Edit the URL in place — repointing an endpoint keeps the same secret, so consumers verifying it keep working.
  • Send a test — fires a signed sample payload and shows the status code your endpoint returned.
  • Pause / resume — stop delivery while your endpoint is down without discarding the config.
  • Rotate the secret — mint a new signing secret when the old one may have leaked.
Open the dashboard

/settings webhook list

From Discord you can check what's registered — event type, URL, masked secret, and whether delivery is paused. The reply carries an Edit on Web button that opens the dashboard page for everything else.

/settings webhook list

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 press Send Test on the dashboard, 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...