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:
- Parse
tandv1from the header - Reject if
tis older than ~5 minutes (replay protection) - Compute
HMAC_SHA256(secret, `${t}.${rawBody}`) - Compare your computed hex to
v1in 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.