RoundPlanAPI reference

Getting started

Verifying webhooks

Anyone can send a request to your URL. Check the RoundPlan-Signature header before you trust a call.

The signature

The header looks like t=1790500000,v1=5257a8…. t is when it was sent, in seconds. Each v1 is the HMAC-SHA256, in hex, of t, a dot and the raw body, with your webhook's signing secret as the key. After you create a new secret, calls carry two v1 values for 24 hours: one per secret. Accept the call if either matches.

Use the raw body exactly as received: parsing the JSON and serialising it again changes the bytes, and the signature no longer matches. Reject a t more than five minutes old, so a recorded call cannot be replayed later.

Node

import crypto from "node:crypto";

// rawBody: the request body exactly as received (a string, not parsed JSON).
function verifyRoundPlan(rawBody, header, secret, toleranceSec = 300) {
  const parts = header.split(",").map((p) => p.split("="));
  const t = Number(parts.find(([k]) => k === "t")?.[1]);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = crypto.createHmac("sha256", secret).update(t + "." + rawBody).digest("hex");
  return parts.some(([k, v]) =>
    k === "v1" && v && v.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(v, "hex"), Buffer.from(expected, "hex")));
}

Python

import hashlib, hmac, time

# raw_body: the request body exactly as received (bytes, not parsed JSON).
def verify_roundplan(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = [p.split("=", 1) for p in header.split(",")]
    t = next((v for k, v in parts if k == "t"), None)
    if t is None or not t.isdigit() or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(k == "v1" and hmac.compare_digest(v, expected) for k, v in parts)

Testing it

In Settings → API, Send test posts a ping event, signed the same way, and tells you what your server answered.