OpenFence Quick Start
Watch a real signed delivery happen, in 2 minutes. No install, no CLI, no signup.
Mint a playground session
We'll ask OpenFence for a 24-hour playground API key. No signup — anyone can mint one. The key gives you access to a sandboxed tenant with playground rate limits.
curl -X POST https://api.openfence.ai/api/v1/playground/sessions
const r = await fetch('https://api.openfence.ai/api/v1/playground/sessions', { method: 'POST' });
const { api_key } = await r.json();
resp, _ := http.Post("https://api.openfence.ai/api/v1/playground/sessions", "", nil)
defer resp.Body.Close()
import requests
r = requests.post("https://api.openfence.ai/api/v1/playground/sessions")
session = r.json()
Create a webhook subscription
We'll register a webhook subscription whose target URL points at OpenFence's own echo receiver — https://api.openfence.ai/api/v1/playground/echo/<your-session-id>. The loopback is visible by design: the demo IS the real delivery path.
curl -X POST https://api.openfence.ai/api/v1/webhooks \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"target_url": "https://api.openfence.ai/api/v1/playground/echo/'$SESSION_ID'"}'
const r = await fetch('https://api.openfence.ai/api/v1/webhooks', {
method: 'POST',
headers: { Authorization: 'Bearer ' + key, 'Content-Type': 'application/json' },
body: JSON.stringify({ target_url: echoUrl }),
});
// http.NewRequest("POST", "https://api.openfence.ai/api/v1/webhooks", body)
// req.Header.Set("Authorization", "Bearer "+key)
r = requests.post(
"https://api.openfence.ai/api/v1/webhooks",
headers={"Authorization": f"Bearer {key}"},
json={"target_url": echo_url},
)
Fire a test delivery
We'll trigger a synthetic geofence.enter event for this subscription. OpenFence will sign it and POST it to the loopback URL — the same delivery worker that ships real production events.
curl -X POST https://api.openfence.ai/api/v1/webhooks/$SUB/test -H "Authorization: Bearer $KEY"
await fetch('https://api.openfence.ai/api/v1/webhooks/' + subId + '/test', {
method: 'POST',
headers: { Authorization: 'Bearer ' + key },
});
// POST /api/v1/webhooks/{sub}/test with Authorization: Bearer ...
r = requests.post(
f"https://api.openfence.ai/api/v1/webhooks/{sub_id}/test",
headers={"Authorization": f"Bearer {key}"},
)
Receive the signed delivery
We poll the session's inbox for the delivery OpenFence just sent itself. When it lands, we see the raw body bytes and all X-OpenFence-* headers — bit-identical to what your real receiver would see in production.
curl https://api.openfence.ai/api/v1/playground/sessions/$SESSION/inbox -H "Authorization: Bearer $KEY"
const r = await fetch('https://api.openfence.ai/api/v1/playground/sessions/' + sessionId + '/inbox', {
headers: { Authorization: 'Bearer ' + key },
});
const { entries } = await r.json();
// GET /api/v1/playground/sessions/{id}/inbox with Authorization: Bearer ...
r = requests.get(
f"https://api.openfence.ai/api/v1/playground/sessions/{session_id}/inbox",
headers={"Authorization": f"Bearer {key}"},
)
Verify the signature in your browser
HMAC-SHA256 over "{timestamp}.{body_bytes}" keyed by the signing secret from step 2. We run it right here in your browser via WebCrypto — no Node, no Python required.
// Parse signature header: "t=<unix>,v1=<hex>"
const sig = entry.headers['x-openfence-signature'];
const parts = Object.fromEntries(sig.split(',').map(s => s.split('=')));
const body = Uint8Array.from(atob(entry.body_b64), c => c.charCodeAt(0));
const signedInput = new TextEncoder().encode(parts.t + '.');
const input = new Uint8Array(signedInput.length + body.length);
input.set(signedInput);
input.set(body, signedInput.length);
const key = await crypto.subtle.importKey(
'raw', new TextEncoder().encode(signingSecret),
{ name: 'HMAC', hash: 'SHA-256' }, false, ['verify']
);
const sigBytes = Uint8Array.from(parts.v1.match(/../g).map(h => parseInt(h, 16)));
const ok = await crypto.subtle.verify('HMAC', key, sigBytes, input);
// ok === true ✓
Building this in your own stack? Node sample · Go sample · Python sample
What just happened? Payload anatomy
The OpenFence delivery worker built a signed input as
f"{timestamp}.".encode("ascii") + body_bytes
— the timestamp followed by a literal period followed by the exact bytes of the JSON body.
It then computed HMAC_SHA256(signing_secret, signed_input),
encoded the digest as 64 hex characters, and sent it in the X-OpenFence-Signature header
as t=<unix>,v1=<hex>.
Your verifier reconstructs the signed input from the timestamp + raw body bytes, runs the same HMAC, and compares — with a constant-time comparator (see the verify-webhooks page for why).
If anything tampers with the body or the timestamp in flight, the HMAC won't match. Try it ↓
Try tampering with the payload
What if an attacker modified the body in flight? The HMAC won't match — your receiver rejects the delivery. Click below to flip one byte of the captured body and re-verify.
Original
Signature verified ✓
Tampered (1 byte flipped)
Signature verification failed.
The body was modified after signing; HMAC no longer matches.
Contract
Read the integration contract
Wire format, freshness window, retries, dead-letter, idempotency — the full spec.
Real tenant
Request a real API key
Tell us what you're building. We'll set up a tenant for your use case.
Build your verifier
Node / Go / Python samples
Zero-dep, byte-equal to the samples that ship with the repo.