Security
Public endpoint URLs are designed to be safe to share, but every public ingestion path attracts spam and abuse over time. yorkyy gives you four layers of defense; combine them based on your endpoint's exposure.
Defense in depth
Pick protections based on threat model rather than maxing out every option. Quick recommendations:
| Scenario | Origin allowlist | Turnstile | HMAC signing | Honeypot |
|---|---|---|---|---|
| Public HTML form (marketing site) | ✓ | ✓ | — | ✓ (always) |
| SPA / React component on a public site | ✓ | ✓ | — | ✓ |
| Server-to-server (your backend → yorkyy) | — | — | ✓ | — |
| Mobile app | — | — | ✓ (via your backend) | — |
| Internal / private testing | ✓ (localhost) | — | — | ✓ |
Origin allowlist
By default any origin can POST to an endpoint. Adding origins to the allowlist tells yorkyy to reject POSTs with an Origin or Referer header that doesn't match. Requests with no Origin header (server-to-server) always pass — origins are a browser concept, set by the browser, not by the JavaScript making the request.
Add origins exactly as the browser sends them: https://your-site.com, https://www.your-site.com (these are different — add both if needed), no trailing slash, no path.
Turnstile (Cloudflare CAPTCHA)
Turnstile is a privacy-respecting CAPTCHA from Cloudflare. When enabled, every submission must include a valid Turnstile token in the cf-turnstile-response field. Tokens are verified server-side against Cloudflare's API before the submission is accepted.
To use Turnstile:
- Get a sitekey at dash.cloudflare.com → Turnstile
- Add the Turnstile widget to your form (Cloudflare's docs cover this)
- Enable Require Turnstile on the yorkyy endpoint
Token is appended to the submission as cf-turnstile-response automatically by Cloudflare's widget — you don't need to do anything special.
HMAC request signing
When you can't trust the client (typically anything running in a browser you don't fully control), use HMAC signing. yorkyy generates a 32-byte secret, you sign every request with it, and yorkyy rejects unsigned or wrongly-signed requests with 401 invalid_signature.
The signature format matches what we use for outgoing webhook deliveries — symmetric on both sides.
Header format
X-Yorkyy-Signature: t=<unix-seconds>,v1=<hex-hmac-sha256>The signature is computed over the literal string `${t}.${rawBody}` — that's the timestamp, a dot, then the unmodified request body bytes. Including the timestamp in the signed payload prevents replay attacks.
Signing in Node / TypeScript
import { createHmac } from "node:crypto";
function sign(secret: string, body: string): { sig: string; ts: number } {
const t = Math.floor(Date.now() / 1000);
const v1 = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
return { sig: `t=${t},v1=${v1}`, ts: t };
}
async function send(endpointUrl: string, secret: string, data: unknown) {
const body = JSON.stringify(data);
const { sig } = sign(secret, body);
return fetch(endpointUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Yorkyy-Signature": sig,
},
body,
});
}Signing in Python
import hmac, hashlib, time, json, requests
def sign_and_send(endpoint_url: str, secret: str, data: dict):
body = json.dumps(data)
t = int(time.time())
v1 = hmac.new(secret.encode(), f"{t}.{body}".encode(), hashlib.sha256).hexdigest()
return requests.post(endpoint_url, data=body, headers={
"Content-Type": "application/json",
"X-Yorkyy-Signature": f"t={t},v1={v1}",
})Signing in Go
import (
"crypto/hmac"; "crypto/sha256"; "encoding/hex"
"fmt"; "net/http"; "strings"; "time"
)
func sign(secret, body string) string {
t := time.Now().Unix()
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "%d.%s", t, body)
return fmt.Sprintf("t=%d,v1=%s", t, hex.EncodeToString(mac.Sum(nil)))
}
func send(url, secret, body string) (*http.Response, error) {
req, _ := http.NewRequest("POST", url, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Yorkyy-Signature", sign(secret, body))
return http.DefaultClient.Do(req)
}Rotation
Rotating a secret invalidates the old one immediately. Stagger deployments accordingly: ship the new secret to your servers first, then rotate, then confirm traffic continues to flow.
Honeypot fields
Honeypots are hidden form fields that humans don't see and bots auto-fill. Every yorkyy endpoint has one configured (default name _gotcha). Submissions with a value in this field are silently dropped — they return 200 OK so the bot doesn't realize it was caught.
To add a honeypot to a form:
<input
type="text"
name="_gotcha"
style="position:absolute;left:-9999px"
tabindex="-1"
autocomplete="off"
/>Don't use display:none — some bots specifically skip those.
Rate limits
Every endpoint has two automatic limits:
- Per IP — default 60 requests/minute. Catches single-source spam.
- Total — default 600 requests/minute. Catches distributed abuse.
Adjust both per endpoint. For low-volume forms (a personal contact form), consider 10/min and 60/min. For an event-registration endpoint expecting spikes, raise the total to several thousand.
Payload limits
| Limit | Value | Behavior on exceed |
|---|---|---|
| Total body size | 32 KB | 413 Payload Too Large |
| Field count | 50 | Excess fields silently dropped |
| Field value length | 10 KB | Truncated with … (truncated) |
| Field name length | 200 chars | Truncated |
Data handling
- Submission values are stored as-is. We don't strip HTML or normalize values — output is escaped at render time (in email HTML, dashboard UI, etc.)
- IP addresses and User-Agents are recorded for audit and abuse investigation only
- Signing secrets are encrypted at rest with AES-256-GCM. We can never recover them — only verify against them
- API keys are stored as SHA-256 hashes only; full plaintext is never persisted
- Webhook signing secrets are encrypted at rest with AES-256-GCM
Reporting a security issue
If you discover a vulnerability, please email security@yorkyy.comrather than opening a public issue. We'll acknowledge within 48 hours and patch critical issues within 7 days.