Skip to main content

How do I set up webhook integration?

Quick answer: Throne can send an HTTPS POST to your endpoint the moment a gift, contribution, or crowdfunded gift comes in. Every request is signed so you can verify it actually came from Throne.

How it works

When you enable the Webhook integration and configure a subscriber URL, Throne sends an HTTPS POST request to that URL each time a qualifying event occurs. Every request includes a cryptographic signature so you can confirm it came from Throne.

Your responsibility: Respond with a 2xx status within 10 seconds.

Request format

Property

Value

Method

POST

Content-Type

application/json; charset=utf-8

Timeout

10 seconds

Headers

Header

Description

X-Signature-Timestamp

Unix timestamp in seconds (decimal string, e.g. "1700000000")

X-Signature-Ed25519

Hex-encoded Ed25519 signature (128 hex characters = 64 bytes)

Payload structure

Every request body is a JSON object with this envelope:

Field

Type

Description

contract_version

"1"

Contract version

event_id

string

Unique delivery ID (UUID)

event_type

string

One of: gift_purchased, contribution_purchased, gift_crowdfunded

data

object

Event-specific fields (see below)

gift_purchased

{
  "creator_id": "string",
  "creator_username": "string",
  "gifter_username": "string",
  "message": "string (optional)",
  "item_name": "string",
  "item_thumbnail_url": "string",
  "price": 1099,
  "currency": "USD",
  "is_surprise_gift": true
}

price is in the smallest currency unit × 100 (e.g. 1099 = $10.99). currency is an ISO 4217 code (e.g. "USD", "EUR"). gifter_username is "Anonymous" when the sender chose to remain anonymous. message is omitted when there is no gift message.

contribution_purchased

{
  "creator_id": "string",
  "creator_username": "string",
  "gifter_username": "string",
  "message": "string (optional)",
  "item_name": "string",
  "item_thumbnail_url": "string",
  "amount": 1099,
  "currency": "USD"
}

amount is in the smallest currency unit × 100 (e.g. 1099 = $10.99). currency is an ISO 4217 code (e.g. "USD", "EUR").

gift_crowdfunded

{
  "creator_id": "string",
  "creator_username": "string",
  "item_name": "string",
  "item_thumbnail_url": "string",
  "price": 1099,
  "currency": "USD",
  "is_surprise_gift": true
}

gifter_username is "Anonymous" when the sender chose to remain anonymous. message is omitted when there is no gift message. price is in the smallest currency unit × 100 (e.g. 1099 = $10.99). currency is an ISO 4217 code (e.g. "USD", "EUR").

Verifying signatures

All requests are signed so you can validate that the request came from Throne.

Throne's public key (Ed25519, PEM):

-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAPXbUfxh7XL4SYUVcfhmYMIbxvtR9E9LDd8gPJ1PwSD8=
-----END PUBLIC KEY-----

How to verify

  1. Read T from the X-Signature-Timestamp header.

  2. Read B as the raw request body (do not re-encode a parsed object).

  3. Decode X-Signature-Ed25519 from hex to 64 bytes → S.

  4. Build the signed message: M = T + "." + B

  5. Verify the Ed25519 signature S over the UTF-8 bytes of M using the public key above.

Additional checks:

  • Reject requests where T is missing or non-numeric.

  • Reject requests where S does not decode to exactly 64 bytes.

  • If you have already processed event_id, return 2xx without repeating side effects (idempotency).

Example (Node.js)

import { createVerify } from "crypto";function verifyThroneWebhook(rawBody, timestamp, signatureHex) {
  const message = `${timestamp}.${rawBody}`;
  const verify = createVerify("Ed25519");
  verify.update(message);
  return verify.verify(THRONE_PUBLIC_KEY_PEM, Buffer.from(signatureHex, "hex"));
}

Security checklist

  • Subscriber URLs must use HTTPS in production

  • Always verify the signature before any business logic

  • Validate the timestamp to limit replay attacks

  • Deduplicate on event_id

  • Do not log the full signature, a short prefix is enough for debugging

Need help?

Contact us through the Throne support portal if you run into issues setting up your integration.

Did this answer your question?