Webhooks

Receive real-time notifications when events occur on your server, such as player votes.

Overview

Webhooks allow your game server or external service to receive HTTP POST notifications when specific events happen. Instead of polling our API, your server gets notified instantly.

Real-time

Events are delivered within seconds of occurring

Secure

Optional HMAC-SHA256 signature verification

Supported Events

  • vote - A user voted for your server
  • test - Test webhook from the dashboard

Configuration Methods

  • Simple: Set a single webhook callback URL on your server settings
  • Advanced: Configure multiple VoteWebhook records for different endpoints

Configuration

Setup Steps

  1. Navigate to Dashboard > My Servers > Vote Rewards
  2. Enter your Callback URL (must be publicly accessible HTTPS endpoint)
  3. Optionally set a Secret Key for signature verification
  4. Save your configuration
  5. Use the Send Test Webhook button to verify delivery
FieldRequiredDescription
Callback URLYesThe URL that will receive webhook POST requests
Secret KeyNoUsed to generate HMAC signature for payload verification

Limits

  • Maximum of 5 webhooks per server
  • Webhook endpoint must respond within 5 seconds with a 2xx status code
  • Only HTTPS URLs are recommended for production use

Payload Format

All webhook deliveries are HTTP POST requests with a JSON body.

Headers

HeaderValue
Content-Typeapplication/json
User-AgentPalworldServers-Vote-Webhook/1.0
AuthorizationYour secret key (if configured)
X-Webhook-SignatureHMAC-SHA256 hex digest (if secret configured)

Body

Webhook Payload (application/json)
{
  "server": "server-uuid",
  "playerId": "STEAM_123 (if provided)",
  "user": "website-user-uuid (if logged in)",
  "type": "vote",
  "timestamp": "2026-07-19T12:00:00.000Z"
}

Field Descriptions

FieldTypeDescription
serverstringUUID of the server that received the vote
playerIdstring | nullPlayer's in-game ID (Steam ID, etc.) if provided during vote
userstring | nullWebsite user UUID if the voter was logged in
typestringEvent type (e.g., "vote", "test")
timestampstringISO 8601 timestamp of when the event occurred

Signature Verification

When a secret key is configured, every webhook delivery includes two authentication headers:

  • Authorization - The raw secret value
  • X-Webhook-Signature - HMAC-SHA256 hex digest of the JSON request body, using your secret as the key

Always verify the signature in production to ensure the webhook came from PalworldServers.io and hasn't been tampered with.

Node.js

Node.js - Express Middleware
const crypto = require('crypto');

function verifyWebhookSignature(req, res, next) {
  const secret = process.env.WEBHOOK_SECRET;
  const signature = req.headers['x-webhook-signature'];

  if (!secret || !signature) {
    return res.status(401).json({ error: 'Missing signature' });
  }

  const body = JSON.stringify(req.body);
  const expected = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');

  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  next();
}

app.post('/webhook', verifyWebhookSignature, (req, res) => {
  const { server, playerId, type, timestamp } = req.body;
  console.log(`Vote received for player ${playerId}`);
  
  // Process asynchronously
  processVoteReward(playerId).catch(console.error);
  
  // Respond immediately
  res.status(200).json({ received: true });
});

Python

Python - Flask
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_here"

def verify_signature(payload: bytes, signature: str) -> bool:
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Webhook-Signature', '')
    
    if not verify_signature(request.data, signature):
        return jsonify({"error": "Invalid signature"}), 401
    
    data = request.json
    player_id = data.get('playerId')
    event_type = data.get('type')
    
    print(f"Received {event_type} event for player {player_id}")
    
    # Process the vote reward
    if event_type == 'vote' and player_id:
        grant_vote_reward(player_id)
    
    return jsonify({"received": True}), 200

PHP

PHP - Webhook Handler
<?php
$secret = getenv('WEBHOOK_SECRET');
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

$expected = hash_hmac('sha256', $payload, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

$data = json_decode($payload, true);
$playerId = $data['playerId'] ?? null;
$type = $data['type'] ?? '';

if ($type === 'vote' && $playerId) {
    // Grant vote reward to player
    grantReward($playerId);
}

http_response_code(200);
echo json_encode(['received' => true]);

Go

Go - HTTP Handler
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io"
    "net/http"
    "os"
)

var webhookSecret = os.Getenv("WEBHOOK_SECRET")

type WebhookPayload struct {
    Server    string `json:"server"`
    PlayerID  string `json:"playerId"`
    User      string `json:"user"`
    Type      string `json:"type"`
    Timestamp string `json:"timestamp"`
}

func verifySignature(body []byte, signature string) bool {
    mac := hmac.New(sha256.New, []byte(webhookSecret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }

    signature := r.Header.Get("X-Webhook-Signature")
    if !verifySignature(body, signature) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    var payload WebhookPayload
    if err := json.Unmarshal(body, &payload); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }

    // Process vote reward asynchronously
    go processVoteReward(payload.PlayerID)

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]bool{"received": true})
}

func main() {
    http.HandleFunc("/webhook", webhookHandler)
    http.ListenAndServe(":8080", nil)
}

Event Types

EventTriggerNotes
voteA user votes for your serverIncludes playerId if the voter provided their in-game ID
testYou click "Send Test Webhook" in dashboardUses playerId: "TEST_PLAYER_123"

Failure Handling

Webhook delivery is fire-and-forget. Understanding the failure model helps you build resilient integrations.

BehaviorDetails
Timeout5 seconds. If your endpoint doesn't respond in time, it counts as a failure.
RetriesNo automatic retries. Each delivery attempt is a single fire-and-forget request.
Failure CountEach failed delivery increments the failureCount on the webhook record.
DegradationAfter 10 consecutive failures, the webhook should be considered degraded. Check your endpoint.
MonitoringLast success/failure timestamps are available in your dashboard for each webhook.

Testing

Before going live, verify your webhook endpoint works correctly using the built-in testing tools.

Using the Dashboard Test Button

  1. Configure your webhook URL in the dashboard
  2. Click "Send Test Webhook"
  3. A test payload will be delivered with type: "test"
  4. Verify your endpoint received and processed the payload

Test Payload

Test Webhook Payload
{
  "server": "your-server-uuid",
  "playerId": "TEST_PLAYER_123",
  "user": null,
  "type": "test",
  "timestamp": "2026-07-19T12:00:00.000Z"
}

External Testing Tools

For initial development, use a request inspection service to see exactly what payloads are being sent:

  • webhook.site - Free webhook inspection and testing
  • RequestBin - Collect and inspect HTTP requests
  • ngrok - Expose your local development server to the internet

Best Practices

Respond Quickly

Return a 200 status immediately within 5 seconds. Process the webhook payload asynchronously (queue, background job, etc.).

Verify Signatures

Always validate the X-Webhook-Signature header in production to ensure authenticity.

Handle Duplicates

Design your handler to be idempotent. Use the timestamp and playerId to deduplicate events if needed.

Log Everything

Log incoming webhook payloads for debugging. Include headers, body, and your processing result.

Use HTTPS

Always use HTTPS endpoints in production to protect payload data in transit.

Monitor Failures

Check your dashboard regularly for webhook failure counts. Fix issues before reaching the 10-failure degradation threshold.