Examples
Complete handlers you can adapt, with verification and acknowledgement in the right order.
ProposedNot yet callable
This documents a design under review. Endpoints, payloads and field names may change before release.
Node and Express
import express from "express";
import crypto from "node:crypto";
const app = express();
const SECRET = process.env.ONETAP_WEBHOOK_SECRET!;
// express.raw, not express.json — the signature covers the raw bytes.
app.post(
"/hooks/onetap",
express.raw({ type: "application/json" }),
async (req, res) => {
const header = req.get("X-Onetap-Signature") ?? "";
const raw = req.body.toString("utf8");
if (!verify(raw, header, SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(raw);
// Acknowledge first. Anything slow happens after this line.
res.sendStatus(200);
try {
await enqueue(event);
} catch (err) {
// Log and alert. Do not throw — the response is already sent.
console.error("webhook enqueue failed", event.id, err);
}
},
);enqueue should insert event.id with a unique constraint. A duplicate delivery then fails the insert and is safely skipped.
Python and Flask
import hmac, hashlib, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["ONETAP_WEBHOOK_SECRET"].encode()
@app.post("/hooks/onetap")
def onetap_webhook():
raw = request.get_data() # bytes, unparsed
parts = dict(
p.split("=", 1) for p in request.headers.get("X-Onetap-Signature", "").split(",")
)
expected = hmac.new(
SECRET, f"{parts.get('t')}.".encode() + raw, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")):
abort(400)
if abs(time.time() - int(parts["t"])) > 300:
abort(400)
enqueue(request.get_json())
return "", 200Testing locally
Onetap cannot reach localhost. Expose your handler through a tunnel and register that URL against a test endpoint.
# Expose port 3000 and register the tunnel URL
ngrok http 3000
# Then fire a test delivery at it
curl -X POST https://api.onetaplabs.com/v1/webhook_endpoints/whe_8f21c0/test \
-H "Authorization: Bearer sk_test_..." \
-H 'Content-Type: application/json' \
-d '{"type":"attendance.recorded"}'Use a test-mode key and a separate endpoint. Registering a tunnel against your production endpoint sends real attendance to a laptop.