Get started

Webhooks

Baynoy POSTs every state change to URLs you register at /dashboard/developers/webhooks. Each delivery is HMAC-SHA256 signed so you can prove it came from us and wasn't tampered with.

Delivery shape

POST with Content-Type: application/json:

POST /your-handler HTTP/1.1
Host: yourapp.com
Content-Type: application/json
Baynoy-Signature: t=1748180400000,v1=a3b1c4d8e9f6...
Baynoy-Event-Id: evt_x1y2z3
Baynoy-Event-Type: payment.succeeded

{
  "id": "evt_x1y2z3",
  "type": "payment.succeeded",
  "created": 1748180400,
  "data": {
    "object": {
      "id": "pay_abc",
      "amount": 2000,
      "currency": "chf",
      "status": "succeeded",
      "customer": "cus_def"
    }
  }
}

Verifying the signature

Compute HMAC-SHA256 over ${timestamp}.${raw_body}with the endpoint's secret (shown once when you create the endpoint) and compare the hex digest to the v1= portion of the header.

Reject anything older than 5 minutes — that's our replay-defence window.

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyBaynoy(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.trim().split("="))
  );
  const t = Number(parts.t);
  if (Math.abs(Date.now() - t) > 5 * 60 * 1000) return false;
  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  return timingSafeEqual(
    Buffer.from(expected, "utf8"),
    Buffer.from(parts.v1, "utf8")
  );
}

Python

import hmac, hashlib, time

def verify_baynoy(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.strip().split("=") for p in header.split(","))
    t = int(parts["t"])
    if abs(time.time() * 1000 - t) > 5 * 60 * 1000:
        return False
    expected = hmac.new(
        secret.encode(),
        f"{t}.{raw_body.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

PHP

function verify_baynoy($raw_body, $header, $secret) {
    $parts = [];
    foreach (explode(',', $header) as $p) {
        list($k, $v) = explode('=', trim($p), 2);
        $parts[$k] = $v;
    }
    $t = (int) $parts['t'];
    if (abs((int)(microtime(true) * 1000) - $t) > 5 * 60 * 1000) {
        return false;
    }
    $expected = hash_hmac('sha256', $t . '.' . $raw_body, $secret);
    return hash_equals($expected, $parts['v1']);
}

Event types

payment.succeededpayment.failedpayment.refundedcustomer.createdcustomer.updatedinvoice.paidinvoice.voidedpayout.paidpayout.failedsubscription.createdsubscription.renewedsubscription.cancelleddispute.opened

Retries

Non-2xx responses retry with exponential backoff for up to 72 hours. Respond with any 2xx (including 200 with empty body) to acknowledge. Don't do real work synchronously — push to a queue, ack fast, process later.