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 "", 200

Testing 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.