How It Works
The metin2.gg Vote Rewards API lets your Metin2 server automatically detect when players vote and grant in-game rewards. The flow is simple: players vote on metin2.gg with their character name, and your server queries our API to check and claim those votes.
There are two integration modes — choose whichever fits your setup:
Polling (Recommended)
Your server periodically calls our API to check for new votes. Simplest to implement — works with any Metin2 server setup. Just add one HTTP request to your game server code.
Webhooks (Advanced)
metin2.gg pushes a notification to your server the instant a player votes. Real-time, but requires a publicly accessible HTTPS endpoint on your side.
Authentication
All API requests require an API key sent via the X-API-Key header. Keys follow the format mg_live_... and are generated from your server panel.
X-API-Key: mg_live_5bef...1a70Your API key is tied to one server and shown only once when generated. If you lose it, regenerate a new one from the panel (the old key is immediately invalidated).
How to Get Your API Key
- 1Go to My Panel → My Servers. Click the name of your server to open its management page.
- 2On the management page, verify ownership (DNS TXT record or meta tag) if you haven't already.
- 3Add a backlink to metin2.gg on your server's website to unlock Partner status.
- 4On your server's management page, click the "API Settings" button. Then click "Generate API Key" and copy it — it's shown only once.
Check Vote Status
Check whether a specific player has an unclaimed vote. If a pending vote exists, it is automatically claimed (marked as used) so the same vote cannot be claimed twice.
GET /api/v1/vote/check?player_id=PlayerName HTTP/1.1
Host: metin2.gg
X-API-Key: mg_live_YOUR_KEY_HEREQuery Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| player_id | string | Yes | The character name the player used when voting. Case-insensitive. |
Response
Player has an unclaimed vote (now claimed)
{
"voted": true,
"player_id": "PlayerName",
"vote_id": "550e8400-e29b-41d4-a716-446655440000",
"voted_at": "2026-03-02T14:30:00Z"
}No pending vote for this player
{
"voted": false
}List Unclaimed Votes
Retrieve all unclaimed votes for your server. Useful for batch processing — for example, a cron job that runs every 5 minutes and grants rewards for all pending votes at once.
GET /api/v1/vote/unclaimed?limit=50 HTTP/1.1
Host: metin2.gg
X-API-Key: mg_live_YOUR_KEY_HEREQuery Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| limit | integer | No | Maximum number of votes to return (1–100, default 50). |
Response
{
"votes": [
{
"vote_id": "550e8400-e29b-41d4-a716-446655440000",
"player_id": "PlayerName",
"voted_at": "2026-03-02T14:30:00Z"
},
{
"vote_id": "660e8400-e29b-41d4-a716-446655440001",
"player_id": "AnotherPlayer",
"voted_at": "2026-03-02T14:32:00Z"
}
],
"total": 2
}Batch Claim Votes
Mark multiple votes as claimed in one request. Use this after processing votes from the unclaimed endpoint.
POST /api/v1/vote/claim HTTP/1.1
Host: metin2.gg
Content-Type: application/json
X-API-Key: mg_live_YOUR_KEY_HERE
{
"vote_ids": [
"550e8400-e29b-41d4-a716-446655440000",
"660e8400-e29b-41d4-a716-446655440001"
]
}Request Body (JSON)
| Parameter | Type | Required | Description |
|---|---|---|---|
| vote_ids | string[] | Yes | Array of vote UUIDs to claim. Maximum 100 per request. |
Response
{
"claimed": 2,
"failed": 0
}Webhooks
Instead of polling, you can receive real-time push notifications when a player votes. Configure your webhook URL in the server panel under API Settings.
Webhook Payload
POST https://your-server.com/api/vote-webhook HTTP/1.1
Content-Type: application/json
X-Metin2GG-Signature: a1b2c3d4e5f6...
{
"event": "vote.created",
"vote_id": "550e8400-e29b-41d4-a716-446655440000",
"server_id": "your-server-uuid",
"player_id": "PlayerName",
"voted_at": "2026-03-02T14:30:00Z",
"timestamp": "2026-03-02T14:30:01Z"
}Verifying the Signature
Every webhook request includes an X-Metin2GG-Signature header containing an HMAC-SHA256 hex digest of the raw request body, signed with your API secret. Always verify this signature before trusting the payload.
import hmac
import hashlib
def verify_webhook(request_body: bytes, signature: str, secret: str) -> bool:
"""Verify the X-Metin2GG-Signature header."""
expected = hmac.new(
secret.encode("utf-8"),
request_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
# Usage in your webhook handler:
# body = request.body (raw bytes)
# sig = request.headers["X-Metin2GG-Signature"]
# if not verify_webhook(body, sig, "your_api_secret"):
# return HttpResponse(status=403)<?php
function verifyWebhook(string $body, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $body, $secret);
return hash_equals($expected, $signature);
}
// Usage in your webhook handler:
// $body = file_get_contents('php://input');
// $sig = $_SERVER['HTTP_X_METIN2GG_SIGNATURE'] ?? '';
// if (!verifyWebhook($body, $sig, 'your_api_secret')) {
// http_response_code(403);
// exit('Invalid signature');
// }Metin2 Integration: Python Command
Drop this script into your game server's Python command system. When a player types /reward in-game, it checks metin2.gg for a pending vote and grants the reward.
# vote_reward.py — Drop into your game server's command scripts
# Works with Python 2.7+ (common in Metin2 servers)
import json
import os
METIN2GG_API_KEY = "mg_live_YOUR_KEY_HERE"
METIN2GG_API_URL = "https://metin2.gg/api/v1/vote/check"
def check_vote(player_name):
"""Check if player has a pending vote and auto-claim it."""
url = "%s?player_id=%s" % (METIN2GG_API_URL, player_name)
# Method 1: Use urllib2 (Python 2) or urllib.request (Python 3)
try:
try:
import urllib2
req = urllib2.Request(url)
req.add_header("X-API-Key", METIN2GG_API_KEY)
resp = urllib2.urlopen(req, timeout=5)
data = json.loads(resp.read())
except ImportError:
import urllib.request
req = urllib.request.Request(url)
req.add_header("X-API-Key", METIN2GG_API_KEY)
resp = urllib.request.urlopen(req, timeout=5)
data = json.loads(resp.read().decode("utf-8"))
return data.get("voted", False)
except Exception:
pass
# Method 2: Fallback to curl (works even with broken SSL)
try:
cmd = 'curl -s -H "X-API-Key: %s" "%s"' % (METIN2GG_API_KEY, url)
raw = os.popen(cmd).read()
data = json.loads(raw)
return data.get("voted", False)
except Exception:
return None
# Example usage in your game command handler:
# def cmd_reward(player):
# result = check_vote(player.GetName())
# if result is True:
# player.GiveItem(50011, 10) # Example: 10x Dragon God Coins
# player.ChatInfoMessage("Vote reward claimed!")
# elif result is False:
# player.ChatInfoMessage("No pending vote. Vote at metin2.gg!")
# else:
# player.ChatInfoMessage("Could not reach metin2.gg. Try again.")Metin2 Integration: Quest File (Lua)
This quest file creates an NPC interaction where players can check their vote status. It calls a server-side command that triggers the Python vote check.
quest vote_reward begin
state start begin
when login or letter begin
send_letter("Vote Reward")
end
when button or info begin
say_title("Vote Reward")
say("")
say("Vote for our server on metin2.gg")
say("and claim your reward here!")
say("")
local s = select("Check My Vote", "How to Vote", "Close")
if s == 1 then
-- Triggers the Python command that checks the API
cmdchat("checkvote "..pc.get_name())
elseif s == 2 then
say_title("How to Vote")
say("")
say("1. Open metin2.gg in your browser")
say("2. Find our server page")
say("3. Enter your character name")
say("4. Click the Vote button")
say("5. Come back here to claim your reward!")
say("")
end
end
end
endMetin2 Integration: PHP Web Panel
If your server has a web panel (PHP-based), use this snippet to check and claim votes from the panel. Works with any PHP 7+ environment with cURL.
<?php
/**
* check_vote.php — Vote check for PHP-based web panels
* Requires PHP 7.0+ with cURL extension
*/
$apiKey = "mg_live_YOUR_KEY_HERE";
$playerName = $_GET["player"] ?? "";
if (empty($playerName)) {
echo json_encode(["error" => "Missing player name"]);
exit;
}
// Check vote status
$ch = curl_init(
"https://metin2.gg/api/v1/vote/check?player_id=" . urlencode($playerName)
);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-API-Key: $apiKey"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
echo json_encode(["error" => "API returned HTTP $httpCode"]);
exit;
}
$data = json_decode($response, true);
if ($data["voted"] ?? false) {
// Player has voted — grant reward via your game DB
// Example: INSERT INTO player.reward_queue (player, item_vnum, count) ...
echo json_encode([
"success" => true,
"message" => "Vote reward granted!",
"vote_id" => $data["vote_id"],
]);
} else {
echo json_encode([
"success" => false,
"message" => "No pending vote found.",
]);
}Rate Limits & Error Codes
The API enforces rate limits to ensure fair usage. Current limits:
- 60 requests per minute per API key
- 1,000 requests per hour per API key
| Status | Description |
|---|---|
| 200 OK | Request succeeded. |
| 400 Bad Request | Missing or invalid parameters. Check the player_id or vote_ids format. |
| 401 Unauthorized | Missing or invalid API key. Make sure you include the X-API-Key header. |
| 403 Forbidden | API access is disabled — usually because the backlink was removed. Re-add the backlink and check again. |
| 404 Not Found | The endpoint doesn't exist. Double-check the URL path. |
| 429 Too Many Requests | Rate limit exceeded. Wait and retry after the Retry-After header value (in seconds). |
| 500 Internal Server Error | Something went wrong on our end. Retry after a few seconds. |
Retry Strategy
On 429 errors, read the Retry-After header (value in seconds) and wait before retrying. For 500 errors, retry with exponential backoff (1s, 2s, 4s).
Troubleshooting FAQ
I get 401 Unauthorized on every request
Make sure you're sending the API key in the X-API-Key header (not as a query parameter or in the body). The key must start with mg_live_.
The API says voted: false but the player claims they voted
The player may have voted without entering their character name, or used a different name. Check the vote link URL — it must include ?player_id=EXACT_NAME. Names are case-insensitive.
I get 403 Forbidden
Your API access is paused, likely because the backlink to metin2.gg was removed from your website. Re-add it and click 'Check Now' in the panel to re-verify.
Can I test the API without real votes?
Currently there's no sandbox mode. Vote on your own server listing with a test character name to verify the integration end-to-end.
My Python script can't connect (timeout or SSL error)
Most Metin2 servers run Python 2.7 which has outdated SSL certificates. Use the os.popen('curl ...') fallback shown in the Python example, or upgrade your server's Python CA bundle.
How often should I poll the API?
For the /check endpoint (per-player): call it when the player requests their reward (e.g. /reward command). For /unclaimed (bulk): every 3–5 minutes via cron is ideal. Don't poll more than once per minute.
Can multiple servers share one API key?
No. Each API key is tied to one server listing. If you run multiple servers, generate a separate key for each from their respective panel pages.
What happens if my webhook endpoint is down?
metin2.gg retries failed webhook deliveries up to 5 times with exponential backoff. If all retries fail, the vote remains unclaimed and can still be picked up via the polling endpoints.
Ready to Integrate?
Add your server to metin2.gg, generate an API key, and start rewarding voters in minutes.