Skip to the content
The room

Documentation · Webhooks

Webhooks

A webhook tells another service when something happens with your games: a sale, a purchase inside one, a refund, a review, a wishlist, a new build, a new follower of your studio. Add up to five in your studio, each with the events it hears of. Each event is a signed POST of JSON; a Discord webhook’s address gets a message in plain words instead.

On this page

The events

What each event says
EventWhen, and what it says
saleA copy is bought or given as a gift: the game, what was paid (dollars), and whether it was a gift. Never who bought it.
purchaseA player buys inside a game (in-game purchases): the game, what was paid (dollars), the player’s id for the game, the payment’s id, whether it was a test (amount 0, no payment), and what it was: a product (key, kind, name, qty) or a charge (its id, name, and your ref). Your server gives it from this.
refundSomething sold is refunded: the game and how much went back (dollars). When Player Two refunds it (a player can’t refund an add-on or an in-game purchase themselves), it also has by "player-two" and the payment’s id, and, for an in-game purchase, what it was, as in purchase: a product (key, kind, name, qty) or a charge (its id, name and your ref), with the player’s id for the game; an add-on has its addon name. Player Two takes back what it holds (the copy, the add-on, the item, a consumable’s count, while none of it is used); your server takes back what it gave for a charge, and should re-read what the player owns.
reviewA review is written or changed: the game, its stars, whether it recommends the game, its words, and its writer's handle (reviews are public).
wishlistThe game is put on a wishlist: how many players have it there now. Never who.
buildA new build of the game is live (pushed, uploaded, rolled back to, or from a GitHub commit): its key and version (what its developer called it, or its number in the game's builds), where it came from, its files and size.
followA player follows your studio: how many follow it now. Never who.

Every event has event and at (when, ISO 8601), and a game (its slug, title and address) except follow, which has your studio. A test event comes when you press Send a test.

The request

a sale
POST https://your-service.com/player-two
content-type: application/json
user-agent: PlayerTwo-Webhooks/1
x-p2-event: sale
x-p2-delivery: 6a1f0d0e-5b8a-4f3c-9d2e-1c7b8a9e0f12
x-p2-timestamp: 1793102400
x-p2-signature: t=1793102400,v1=5d0c6e7a...

{
  "event": "sale",
  "at": "2026-10-27T12:00:00.000Z",
  "game": { "slug": "your-game", "title": "Your Game", "url": "https://playertwo.lol/game/your-game" },
  "amount": 4.99,
  "gift": false
}
a purchase inside a game (a charge your server made)
{
  "event": "purchase",
  "at": "2026-11-05T13:20:00.000Z",
  "game": { "slug": "your-game", "title": "Your Game", "url": "https://playertwo.lol/game/your-game" },
  "amount": 4.99,
  "charge": { "id": "4b8f2c1e-7a3d-4e9b-8c2f-1d6e5a9b0c3d", "name": "5,000 coins", "ref": "order-8812" },
  "player": "Xk3q9V2mA7cT1pLw8eR4sZ",
  "payment": "9c2e7f4a-1b3d-4a8e-9f6c-2d5b8e1a7c4f",
  "test": false
}
  • x-p2-delivery is the delivery’s own id: a retry has the same one, so handle each id once.
  • Answer with any 2xx within ten seconds. Redirects aren’t followed.
  • Webhooks go to public https addresses only, on the standard port.

Checking the signature

x-p2-signature is t=<seconds>,v1=<hex>: an HMAC-SHA256 of <seconds>.<the raw body> with the webhook’s secret (it starts p2whsec_; your studio shows it). Check it on the raw body before parsing it, and turn away anything older than a few minutes.

checking it
// Node: the raw body, as it arrived (before any JSON parsing), and the webhook's secret from your studio
import { createHmac, timingSafeEqual } from "node:crypto";

export function fromPlayerTwo(raw, header, secret) {
  const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;   // older than five minutes: a replay
  const want = createHmac("sha256", secret).update(`${t}.${raw}`).digest("hex");
  return v1.length === want.length && timingSafeEqual(Buffer.from(v1), Buffer.from(want));
}

Retries

A delivery that isn’t answered with a 2xx is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, then given up. A webhook that fails twenty times running is turned off, and you’re told; turn it back on in your studio once it’s fixed. Your studio shows each webhook’s last deliveries and what they were answered.

Discord

Give a Discord channel’s webhook address (Server Settings, Integrations, Webhooks, then Copy Webhook URL) and each event arrives as a message in plain words, such as “Someone bought Your Game ($4.99).”, with nothing in between to run.