Documentation navigation
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 servertest- 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
- Navigate to Dashboard > My Servers > Vote Rewards
- Enter your Callback URL (must be publicly accessible HTTPS endpoint)
- Optionally set a Secret Key for signature verification
- Save your configuration
- Use the Send Test Webhook button to verify delivery
| Field | Required | Description |
|---|---|---|
| Callback URL | Yes | The URL that will receive webhook POST requests |
| Secret Key | No | Used 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
| Header | Value |
|---|---|
| Content-Type | application/json |
| User-Agent | PalworldServers-Vote-Webhook/1.0 |
| Authorization | Your secret key (if configured) |
| X-Webhook-Signature | HMAC-SHA256 hex digest (if secret configured) |
Body
{
"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
| Field | Type | Description |
|---|---|---|
| server | string | UUID of the server that received the vote |
| playerId | string | null | Player's in-game ID (Steam ID, etc.) if provided during vote |
| user | string | null | Website user UUID if the voter was logged in |
| type | string | Event type (e.g., "vote", "test") |
| timestamp | string | ISO 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 valueX-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
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
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}), 200PHP
<?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
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
| Event | Trigger | Notes |
|---|---|---|
vote | A user votes for your server | Includes playerId if the voter provided their in-game ID |
test | You click "Send Test Webhook" in dashboard | Uses playerId: "TEST_PLAYER_123" |
Failure Handling
Webhook delivery is fire-and-forget. Understanding the failure model helps you build resilient integrations.
| Behavior | Details |
|---|---|
| Timeout | 5 seconds. If your endpoint doesn't respond in time, it counts as a failure. |
| Retries | No automatic retries. Each delivery attempt is a single fire-and-forget request. |
| Failure Count | Each failed delivery increments the failureCount on the webhook record. |
| Degradation | After 10 consecutive failures, the webhook should be considered degraded. Check your endpoint. |
| Monitoring | Last 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
- Configure your webhook URL in the dashboard
- Click "Send Test Webhook"
- A test payload will be delivered with
type: "test" - Verify your endpoint received and processed the payload
Test 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.