OpenFence Quick Start
Watch a real signed delivery happen, in 2 minutes. No install, no CLI, no signup.
Get a playground API key
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 }),
});
body := strings.NewReader(`{"target_url":"https://api.openfence.ai/api/v1/playground/echo/` + sessionID + `"}`)
req, _ := http.NewRequest("POST", "https://api.openfence.ai/api/v1/webhooks", body)
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
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 },
});
req, _ := http.NewRequest("POST", "https://api.openfence.ai/api/v1/webhooks/"+subID+"/test", nil)
req.Header.Set("Authorization", "Bearer "+key)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
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();
req, _ := http.NewRequest("GET", "https://api.openfence.ai/api/v1/playground/sessions/"+sessionID+"/inbox", nil)
req.Header.Set("Authorization", "Bearer "+key)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var entries struct {
Entries []map[string]interface{} `json:"entries"`
}
json.NewDecoder(resp.Body).Decode(&entries)
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, 8-step verifier algorithm, and Node/Go/Python samples — signature verification spec.
Reference
Event payload reference
Browse every field on every event type. Verify any event against your own session.
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.