Webhooks

Receive form submissions in real time as signed HTTPS POSTs. Verify the signature to ensure the request actually came from yorkyy.

Setup

Open dashboard / Channels, click Add webhook, and enter your endpoint URL. yorkyy shows your signing secretonce — save it. You'll need it to verify deliveries.

Delivery format

Each submission triggers a POST to your URL with these headers:

POST /your-endpoint HTTP/1.1
Content-Type: application/json
User-Agent: yorkyy-webhooks/1.0
X-Yorkyy-Event: submission.created
X-Yorkyy-Signature: t=1715817600,v1=4f3a2b...

And this body:

{
  "event": "submission.created",
  "submission_id": "01h...",
  "form_id": "01h...",
  "form_title": "Customer feedback",
  "data": {
    "Your name": "Ada Lovelace",
    "Email address": "ada@example.com",
    "Message": "Loving the product."
  },
  "created_at": "2026-05-15T18:42:00.000Z"
}

Verify the signature

The X-Yorkyy-Signature header has the form t=<unix-seconds>,v1=<hex-hmac-sha256>. To verify:

  1. Parse t and v1 from the header
  2. Reject if t is older than ~5 minutes (replay protection)
  3. Compute HMAC_SHA256(secret, `${t}.${rawBody}`)
  4. Compare your computed hex to v1 in constant time

Node.js / TypeScript

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

function verifySignature(rawBody: string, signatureHeader: string, secret: string): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("=") as [string, string]),
  );
  const t = parts.t;
  const v1 = parts.v1;
  if (!t || !v1) return false;

  // Replay protection: reject signatures older than 5 minutes.
  const age = Math.abs(Math.floor(Date.now() / 1000) - parseInt(t, 10));
  if (age > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  if (expected.length !== v1.length) return false;
  return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(v1, "hex"));
}

// Express example:
app.post("/webhooks/yorkyy", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.header("x-yorkyy-signature");
  if (!sig || !verifySignature(req.body.toString("utf8"), sig, process.env.YORKYY_WEBHOOK_SECRET!)) {
    return res.status(401).send("invalid signature");
  }
  const payload = JSON.parse(req.body.toString("utf8"));
  // ... handle payload ...
  res.status(200).send("ok");
});

Python

import hmac
import hashlib
import time

def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    t = parts.get("t")
    v1 = parts.get("v1")
    if not t or not v1:
        return False
    if abs(int(time.time()) - int(t)) > 300:
        return False
    expected = hmac.new(
        secret.encode(),
        f"{t}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, v1)

Go

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "strconv"
    "strings"
    "time"
)

func VerifySignature(rawBody []byte, signatureHeader, secret string) bool {
    parts := map[string]string{}
    for _, p := range strings.Split(signatureHeader, ",") {
        kv := strings.SplitN(p, "=", 2)
        if len(kv) == 2 {
            parts[kv[0]] = kv[1]
        }
    }
    t, v1 := parts["t"], parts["v1"]
    if t == "" || v1 == "" {
        return false
    }
    ts, err := strconv.ParseInt(t, 10, 64)
    if err != nil || abs(time.Now().Unix()-ts) > 300 {
        return false
    }
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(t + "."))
    mac.Write(rawBody)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(v1))
}

func abs(n int64) int64 { if n < 0 { return -n }; return n }

Retries

yorkyy treats your endpoint as healthy on any 2xx response. Anything else (4xx, 5xx, timeout, network error) triggers up to 3 retries with exponential backoff. After the final attempt fails, the delivery is recorded as failed and won't be retried again — fix the underlying issue and re-submit the form to test.

Endpoints should respond within 10 seconds. If your work takes longer, return 2xx immediately and process asynchronously.

Idempotency

Use submission_idas your idempotency key. Even though retries are rare, network conditions can cause the same delivery twice; check whether you've already processed a given submission_id before acting.

Testing locally

Use a tunnel like ngrok or cloudflared to expose your local server on a public HTTPS URL. yorkyy will reject non-localhost HTTP endpoints, so a tunnel is required during local development against the hosted yorkyy.