API голосов
Плагин сервера узнаёт о голосах игроков и выдаёт награду. Ключ и вебхук бесплатны для подтверждённых владельцев: их выдаёт кабинет сервера.
Ключ
Создайте ключ в кабинете сервера, блок «Награды за голоса». Ключ получает права votes:read и votes:write и работает с одним сервером, пока владение подтверждено.
Authorization: Bearer sp_live_ВАШ_КЛЮЧ
Ключ показывается один раз. Храните его на игровом сервере и не отправляйте игровому клиенту.
Список голосов
GET /api/integration/v1/votes?status=provisional&claimed=false&limit=100
{
"votes": [{
"voteId": "uuid",
"serverId": "uuid",
"inGameName": "76561198000000000",
"votedAt": "2026-10-23T18:04:11.000Z",
"voteDay": "2026-10-23",
"status": "provisional",
"claimedAt": null
}],
"nextAfterId": null
}- status
provisionalпринят и ждёт проверки,countedзасчитан через 48 часов,allещё и аннулированные- claimed
falseбез отметки о награде,trueс отметкой- after_id
- продолжение списка с
nextAfterId - limit
- до 100
inGameName приходит, только если игрок указал ник или SteamID64 и согласился передать его владельцу. Данных аккаунта SpawnPulse в ответе нет.
Отметка о награде
POST /api/integration/v1/votes/{voteId}/claim
Idempotency-Key: {voteId}
200 {"voteId":"…","claimed":true,"claimedAt":"…","replay":false}
409 {"error":"ALREADY_CLAIMED","claimedAt":"…"}Повтор с тем же Idempotency-Key вернёт тот же ответ 200 с "replay": true, поэтому запрос можно повторить после обрыва связи. Голос на проверке и аннулированный голос отметить нельзя: ответ 404.
# Новые голоса, ещё без награды curl -H "Authorization: Bearer sp_live_ВАШ_КЛЮЧ" \ "https://spawnpulse.com/api/integration/v1/votes?status=provisional&claimed=false&limit=100" # Отметить голос как награждённый (повтор с тем же ключом идемпотентности безопасен) curl -X POST -H "Authorization: Bearer sp_live_ВАШ_КЛЮЧ" \ -H "Idempotency-Key: ID_ГОЛОСА" \ "https://spawnpulse.com/api/integration/v1/votes/ID_ГОЛОСА/claim"
Вебхук
Укажите в кабинете адрес HTTPS. SpawnPulse отправит POST на каждое событие голоса:
- X-SpawnPulse-Event
vote.provisional,vote.countedилиvote.annulled- X-SpawnPulse-Delivery
- UUID доставки, один и тот же при повторах
- X-SpawnPulse-Signature
t=<unix>,v1=<hex>, где v1 = HMAC-SHA256(секрет, t + "." + тело)
{
"voteId": "3f0c6c2e-8a4b-4f7e-9d1a-2b6f0e4c9a17",
"serverId": "1b4e28ba-2fa1-11d2-883f-0016d3cca427",
"inGameName": "76561198000000000",
"votedAt": "2026-10-23T18:04:11.000Z",
"voteDay": "2026-10-23",
"status": "provisional"
}Ответьте кодом 2xx за 5 секунд. Без него SpawnPulse повторит доставку через 1 мин, 5 мин, 30 мин, 2 ч, 6 ч и 12 ч. Перенаправления не выполняются, адреса локальных и внутренних сетей не принимаются. Данных о сериях голосов в вебхуке нет.
Проверка подписи (Node.js)
const crypto = require("node:crypto");
// rawBody: the request body exactly as received, before JSON.parse.
function verifySpawnPulse(secret, rawBody, header, now = Math.floor(Date.now() / 1000)) {
if (!header) return false;
let t = null;
const signatures = [];
for (const part of header.split(",")) {
const [key, value] = part.split("=", 2).map((item) => item.trim());
if (key === "t") t = Number(value);
if (key === "v1") signatures.push(value);
}
if (!Number.isInteger(t) || Math.abs(now - t) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(t + "." + rawBody, "utf8").digest();
return signatures.some((value) => {
const actual = Buffer.from(value, "hex");
return actual.length === expected.length && crypto.timingSafeEqual(actual, expected);
});
}EasyVote и другие плагины
Эндпоинт /api/vote/v1 отвечает в формате Listforge, поэтому EasyVote и похожие плагины для Rust, ARK и 7 Days to Die работают без доработки. Ключ можно передать параметром key, но заголовок Authorization надёжнее.
# 0: голоса за 24 часа нет; 1: голос есть, награда не выдана; 2: награда выдана
GET https://spawnpulse.com/api/vote/v1?object=votes&element=claim&key=sp_live_ВАШ_КЛЮЧ&steamid=76561198000000000
# Отметить награду выданной: 1, если отметили сейчас; 0, если отмечать нечего
GET https://spawnpulse.com/api/vote/v1?action=post&object=votes&element=claim&key=sp_live_ВАШ_КЛЮЧ&steamid=76561198000000000
# Ники и голоса за месяц: [{"nickname":"…","votes":12}]
GET https://spawnpulse.com/api/vote/v1?object=servers&element=voters&key=sp_live_ВАШ_КЛЮЧ&month=current&format=jsonEasyVote: свой сайт
"VoteSitesAPI": {
"SpawnPulse": {
"API Claim Reward (GET URL)": "https://spawnpulse.com/api/vote/v1?action=post&object=votes&element=claim&key={0}&steamid={1}",
"API Vote status (GET URL)": "https://spawnpulse.com/api/vote/v1?object=votes&element=claim&key={0}&steamid={1}",
"Vote link (URL)": "https://spawnpulse.com/games/{0}#vote",
"Site Uses Username Instead of Player Steam ID?": false
}
},
"Servers": {
"MyServer": {
"SpawnPulse": "rust/my-server:sp_live_ВАШ_КЛЮЧ"
}
}Вместо rust/my-server подставьте часть адреса страницы сервера после /games/.
Oxide и Carbon: свой плагин
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core.Libraries;
namespace Oxide.Plugins
{
[Info("SpawnPulseVotes", "SpawnPulse", "1.0.0")]
[Description("Награда за голос на SpawnPulse")]
public class SpawnPulseVotes : RustPlugin
{
private const string ApiKey = "sp_live_ВАШ_КЛЮЧ";
private const string Api = "https://spawnpulse.com/api/integration/v1/votes";
private class Vote { public string voteId; public string inGameName; }
private class VoteList { public List<Vote> votes; }
private Dictionary<string, string> Headers() =>
new Dictionary<string, string> { ["Authorization"] = "Bearer " + ApiKey };
private void OnServerInitialized() => timer.Every(60f, Poll);
private void Poll()
{
webrequest.Enqueue(Api + "?status=provisional&claimed=false&limit=100", null, (code, body) =>
{
if (code != 200 || string.IsNullOrEmpty(body)) return;
foreach (var vote in JsonConvert.DeserializeObject<VoteList>(body).votes)
{
var player = vote.inGameName == null ? null : BasePlayer.Find(vote.inGameName);
if (player == null) continue; // игрок не в сети: голос подождёт следующего опроса
var headers = Headers();
headers["Idempotency-Key"] = vote.voteId;
webrequest.Enqueue(Api + "/" + vote.voteId + "/claim", "", (claimCode, _) =>
{
if (claimCode == 200) GiveReward(player);
}, this, RequestMethod.POST, headers);
}
}, this, RequestMethod.GET, Headers());
}
private void GiveReward(BasePlayer player)
{
player.inventory.GiveItem(ItemManager.CreateByName("scrap", 25));
player.ChatMessage("Спасибо за голос на SpawnPulse");
}
}
}