Use a Lockii app when you want operators to connect your product from Settings → Integrations and grant your servers offline access to their company data. Good fits include accounting syncs, reporting tools, messaging add-ons, and operations automation.
If you are building a marketplace or OTA that lists inventory and creates paid bookings as merchant of record, see Building a Marketplace App instead.
| Method | Best for | Access |
|---|---|---|
| Lockii app (this guide) | Third-party products installed by operators | Company-scoped offline tokens with the scopes the operator approved |
| API key | Server-to-server scripts and Zapier | Company-level key created by the operator |
| Login with Lockii | Tools acting as a signed-in user (including MCP) | That user's role and permissions |
App tokens are just another way to authenticate to the standard Lockii REST API. There is no separate API surface for apps — your granted scopes are ordinary Lockii permissions.
Apps are curated. Email [email protected] to request permission to have your app listed in the Lockii directory.
Include:
Once approved, Lockii registers your app and issues:
client_id and client_secretTreat the client_secret like a password. Store it in a secrets manager — never in client-side code or version control. Rotation is available on request.
Scopes are Lockii permission strings. Only scopes approved for your app at registration can be requested. Operators see plain-language descriptions on the consent screen.
| Scope | Grants |
|---|---|
booking:read |
View bookings, schedule, customers, and products |
booking:write |
Create and update bookings |
inventory:write |
Manage products, stock, and inventory details |
reporting:read |
Read reporting data and dashboards |
Request the minimum set your product needs. Marketplace channel scopes (channel:read / channel:write) are for sales-channel apps only — see the marketplace guide.
Do not send external_account_id on authorize unless you are requesting channel scopes.
| Purpose | URL |
|---|---|
| API | https://dash.lockii.app/api/v1 |
| OAuth | https://dash.lockii.app/api/oauth |
Lockii uses the OAuth 2.0 authorization code grant with refresh tokens. Each successful grant creates a connection (installation) for one operator company. PKCE (S256) is supported and recommended.
Redirect the operator to:
https://dash.lockii.app/api/oauth/authorize
?client_id={your client_id}
&redirect_uri={one of your registered redirect URIs}
&response_type=code
&scope=booking:read reporting:read
&state={opaque anti-CSRF value}
Replace scope with the space-separated scopes approved for your app.
If you provided a connect URL, operators can also start from Settings → Integrations in Lockii (the Connect button links to your URL). Otherwise Lockii can start the authorize URL directly.
The operator signs in to Lockii, picks which company to connect, and approves the scopes. Only users who can manage integrations for that company can approve.
redirect_uri with ?code=...&state=... (code expires in 10 minutes, single-use)?error=access_denied&state=...Always verify state before continuing.
POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code={code}
&client_id={client_id}
&client_secret={client_secret}
&redirect_uri={same redirect_uri as authorize}
Response:
{
"access_token": "lk_at_...",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "lk_rt_...",
"scope": "booking:read reporting:read",
"connection_id": "conn_01K3ZQ..."
}
Store the token pair keyed by connection_id (and your own customer/account id).
POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token={current refresh token}
&client_id={client_id}
&client_secret={client_secret}
Refresh a few minutes before expiry, or on a 401 with error.code: "token_expired". If the refresh token expired or the operator disconnected, they must authorize again.
403 with connection_pausedconnection.disconnectedconnection_id and issues fresh tokensPOST /api/oauth/revoke (RFC 7009: token + client credentials)Send the connection's access token on every request:
GET /api/v1/booking?status=active&limit=10 HTTP/1.1
Host: dash.lockii.app
Authorization: Bearer lk_at_xxxxxxxxxxxxxxxx
The token is scoped to one company. You do not pass a company ID. Handlers enforce the granted scopes the same way they enforce user permissions.
| Status | Code | Meaning |
|---|---|---|
401 |
token_expired |
Refresh and retry |
401 |
token_invalid |
Token revoked or unknown — re-authorize |
403 |
insufficient_scope |
Connection lacks the required scope |
403 |
connection_paused |
Operator paused the connection |
For endpoint details, see the Lockii REST API.
If you registered a webhook URL, Lockii POSTs JSON events for your connections.
| Event | When |
|---|---|
connection.activated |
Operator completed OAuth (or reconnected) |
connection.disconnected |
Operator disconnected — stop using their tokens |
Marketplace listing and booking events apply only to apps with channel scopes.
Payloads are thin (ids + timestamps). Re-fetch canonical state via the API. Respond with 2xx within 10 seconds. Delivery order is not guaranteed; deduplicate on event id.
Each delivery includes:
Lockii-Event-Id: evt_...
Lockii-Event-Type: connection.activated
Lockii-Signature: t={unix},v1={hmac_hex}
The signature is HMAC-SHA256 of {t}.{raw_request_body} using your webhook signing secret. Reject deliveries older than 5 minutes. Compute the HMAC over the raw body bytes before JSON parsing.
Example:
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyLockiiSignature(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (age > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
client_secret and tokens stored securely; tokens keyed by connection_idstate verified; PKCE implementedexternal_account_id unless you are a marketplace app401 token_expired triggers refresh-and-retryconnection.disconnected stops API use for that company promptlyinsufficient_scope / connection_paused