Custom Lock API
For a door system that is your own: an in-house controller, a building system, a lock brand we do not drive directly, or a bridge you wrote. OpenTeeSheet sends your HTTPS endpoint one signed request per action and your server programs the door. It runs behind the same lock interface as our built-in integrations, with the same failure handling, on every plan.
Set up
Dashboard, Settings, Smart Locks, Add Lock, Custom Lock API: your door id (sent as door_id), your endpoint (https:// on the public internet; private, loopback and local addresses are refused), and whether your endpoint can open the door on demand. OpenTeeSheet then shows the signing secret, which signs both directions, and the events URL (/api/webhooks/locks/<lock id>) your system posts door events to.
The API is for a door system you control. It does not change which hardware we recommend: the brands our hardware guidance excludes stay excluded, and we build no bridge to them.
Requests to your endpoint
POST, JSON, with OTS-Event, OTS-Delivery (unique per request: de-duplicate on it) and OTS-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "t.body")>. Verify the raw body and refuse anything more than 5 minutes old.
| Event | When | Answer |
|---|---|---|
access.grant | A booking is confirmed (also staff bookings, extensions, re-issued codes) | 2xx { access_id, pin?, access_url? } |
access.revoke | Cancel, refund or a re-issued code: data { access_id, booking_id } | 2xx, or 404 (already gone) |
door.unlock | Unlock door, by the golfer inside the window or by staff. Only if you said your endpoint can | 2xx = opened; anything else is a refusal |
ping | Test connection and the door monitor | 2xx { name?, door_open? } |
POST https://doors.yourstudio.com/openteesheet
OTS-Event: access.grant
OTS-Delivery: 2f6b0c8e-...
OTS-Signature: t=1793466000,v1=5d41402abc4b2a76b9719d911017c592...
{ "event": "access.grant", "delivery_id": "2f6b0c8e-...", "sent_at": "2026-11-02T17:31:00.000Z", "door_id": "door-7",
"data": { "booking_id": "6f1c...", "pin": "4821", "guest_name": "Pat",
"starts_at": "2026-11-02T17:50:00.000Z", "ends_at": "2026-11-02T19:00:00.000Z",
"time_zone": "America/New_York", "doors": ["street", "door-7"] } }
-> 200 { "access_id": "your-id-for-this-grant", "pin": "907311", "access_url": "https://..." }The door must take the code only between starts_at and ends_at. access_id is required: it is how a grant is revoked and how your door events are matched to the booking (booking_id and guest_name are for your records). Return your own 4-8 digit pin if your system picks codes, and an access_url if a link opens the door; time_zone is the venue's.
import { createHmac, timingSafeEqual } from "node:crypto"
function verify(secret, rawBody, header, nowS = Math.floor(Date.now() / 1000)) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)))
if (Math.abs(nowS - Number(t)) > 300) return false
const want = Buffer.from(createHmac("sha256", secret).update(t + "." + rawBody).digest("hex"))
const got = Buffer.from(String(v1))
return got.length === want.length && timingSafeEqual(got, want)
}Door events to OpenTeeSheet
Signed the same way with the same secret; one event or {"events": [...]}. event is unlock, door_opened, door_closed, denied, forced_open, held_open or your own name. Send the grant's access_id: that is the evidence the no-show rules and the "door PIN used" report read. A bad signature answers 401; an event we could not store answers 503 so you retry.
POST https://openteesheet.com/api/webhooks/locks/<lock id>
OTS-Signature: t=<unix>,v1=<HMAC-SHA256(secret, "<t>.<body>")>
{ "id": "evt_123", "event": "unlock", "success": true, "at": "2026-11-02T18:01:00Z", "door_id": "door-7", "access_id": "your-id-for-this-grant" }When your server does not answer
Every request times out after 8 seconds. A timeout, an error status, a missing access_id or a malformed pin is a refusal, handled as for every lock: the booking stands, it is flagged for staff, no code is shown to the golfer as working, you are alerted with the reason, and your backup code applies. Grants go out when the booking is confirmed, ahead of the session, so a door that validates codes it already holds keeps opening while OpenTeeSheet or your internet is down.
Reference server
scripts/custom-lock-reference.mjs in the OpenTeeSheet source is a complete door server with no dependencies: signature checks, de-duplication, grants held for their window, revoke, ping, and POST /keypad standing in for the keypad (inside the window it opens and reports a signed unlock; outside it reports denied). Our test suite books through it end to end.
OTS_LOCK_SECRET=<signing secret> \
OTS_EVENTS_URL=https://openteesheet.com/api/webhooks/locks/<lock id> \
PORT=8787 node scripts/custom-lock-reference.mjs