@dooraccess/opendoor-web-sdk
v1.0.2
Published
JavaScript/TypeScript SDK for embedding door code access into web applications
Readme
OpenDOOR Web SDK
JavaScript/TypeScript SDK for embedding door code access into your web application. Your guests authenticate via email OTP, and the SDK fetches and displays their door codes.
Installation
npm install @dooraccess/opendoor-web-sdkOr load via CDN:
<script src="https://unpkg.com/@dooraccess/opendoor-web-sdk"></script>How It Works
The integration has two parts: your backend handles authentication (because it requires secrets), and the web SDK runs in the guest's browser and fetches door codes.
Guest's Browser Your Backend DOOR
─────────────── ──────────── ────
1. Guest enters email ──> POST /passwordless/start ──> DOOR sends OTP email
(includes client_id + secret)
2. Guest enters OTP code ──> POST /oauth/token ──> DOOR returns JWT
(includes client_id + secret) + refresh_token
<── Return JWT to browser <──
3. Browser initializes SDK
with the JWT
4. SDK calls getLocks() ──> Returns locks + door codes
5. JWT expires, SDK calls
onTokenExpired() ──> POST /oauth/token ──> DOOR refresh_token grant
(includes client_id + secret)
<── Return new JWT <──Steps 1, 2, and 5 go through your backend because they require client_id and client_secret, which must never be exposed in browser code. The SDK's lock/device calls run in the browser and call the DOOR API directly with the JWT.
Step 1: Your Backend — Authentication
Your backend handles the passwordless OTP flow. The SDK does not manage authentication — it receives a JWT from your backend.
Obtain credentials
You'll receive a client_id and client_secret from DOOR during onboarding. These are scoped to your account.
Send the OTP email
When a guest wants to access their door codes, your backend triggers an OTP email:
POST https://auth.prod.latch.com/passwordless/start
Content-Type: application/json
{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"connection": "email",
"send": "code",
"email": "[email protected]"
}This sends a 6-digit code to the guest's email.
Exchange the OTP for a JWT
After the guest enters the code, your backend exchanges it for tokens:
POST https://auth.prod.latch.com/oauth/token
Content-Type: application/json
{
"grant_type": "http://auth0.com/oauth/grant-type/passwordless/otp",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"username": "[email protected]",
"otp": "123456",
"realm": "email",
"scope": "openid profile email offline_access",
"audience": "https://rest.latchaccess.com/access/sdk"
}The audience parameter is required. Without it, the JWT will not have the correct permissions to call the DOOR API.
Response:
{
"access_token": "eyJhbGciOi...",
"refresh_token": "v1.MjY0OTFk...",
"token_type": "Bearer",
"expires_in": 86400
}access_token: Pass this to the SDK. Valid for 24 hours.refresh_token: Store this securely on your backend. Used to get a newaccess_tokenwhen the current one expires.
Return the JWT to the browser
Your backend returns the access_token to the browser (e.g., via a JSON response). Never return the refresh_token or client_secret to the browser.
Step 2: Browser — Initialize the SDK
Once the browser has the JWT, initialize the SDK:
import { OpenDOORClient } from '@dooraccess/opendoor-web-sdk';
const client = new OpenDOORClient({
token: jwtFromYourBackend,
onTokenExpired: async () => {
// Call your backend to refresh the token (see Step 3)
const res = await fetch('/api/auth/refresh', { method: 'POST' });
const { token } = await res.json();
return token;
},
});
// Fetch all locks and door codes
const locks = await client.getLocks();
locks.forEach(lock => {
console.log(`${lock.name}: ${lock.doorCode ?? 'No code — use app to unlock'}`);
});
// Clean up when done
client.destroy();Via CDN / script tag:
<script src="https://unpkg.com/@dooraccess/opendoor-web-sdk"></script>
<script>
var client = new OpenDOOR.OpenDOORClient({
token: jwtFromYourBackend,
onTokenExpired: function () {
return fetch('/api/auth/refresh', { method: 'POST' })
.then(function (res) { return res.json(); })
.then(function (data) { return data.token; });
},
});
client.getLocks().then(function (locks) {
// Render locks in your UI
});
</script>Step 3: Your Backend — Token Refresh
JWTs expire after 24 hours. The SDK detects expiry and calls your onTokenExpired callback automatically. Your backend should use the stored refresh_token to get a new access_token:
POST https://auth.prod.latch.com/oauth/token
Content-Type: application/json
{
"grant_type": "refresh_token",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"refresh_token": "STORED_REFRESH_TOKEN",
"audience": "https://rest.latchaccess.com/access/sdk"
}The audience parameter is required on refresh too. Without it, the new JWT won't have the correct permissions.
Response:
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 86400
}Return the new access_token to the browser. If a new refresh_token is included in the response, update your stored copy.
How the SDK handles expiry
The SDK checks the JWT's exp claim before every API call. If expired (or within 30 seconds of expiry):
- If
onTokenExpiredis provided: The SDK calls it, waits for the new token, and retries the request automatically. The guest never sees an interruption. - If
onTokenExpiredis not provided: The SDK throws anAuthErrorwith a message explaining what happened.
If a request returns 401 (token rejected server-side), the same flow applies — the SDK calls onTokenExpired and retries once.
Avoiding Repeated OTP Verification
The OTP flow only needs to happen once. After the initial verification, your backend has a refresh_token that can mint new JWTs for the duration of the guest's stay — no additional OTP emails needed.
The recommended approach is to tie the DOOR refresh_token to your existing user session:
- Guest verifies OTP once — your backend receives the
access_tokenandrefresh_tokenfrom DOOR - Store the
refresh_tokenin your session — associate it with the guest's session in your app (e.g., in your session store, database, or server-side cache, keyed to your session cookie) - On subsequent page loads — the guest is already logged into your app. Your backend checks the session, finds the stored DOOR
refresh_token, mints a freshaccess_token, and passes it to the SDK. No OTP required. - The SDK's
onTokenExpiredcallback — follows the same path. It calls your backend refresh endpoint, which uses the storedrefresh_tokento get a new JWT silently.
First Visit Subsequent Visits
─────────── ─────────────────
Guest enters email Guest loads page (already logged in)
↓ ↓
OTP email sent Your backend checks session
↓ ↓
Guest enters code Finds stored DOOR refresh_token
↓ ↓
Backend gets JWT + refresh_token Calls DOOR to mint fresh JWT
↓ ↓
Stores refresh_token in session Returns JWT to browser
↓ ↓
Returns JWT to browser SDK initialized — door codes load
↓
SDK initialized — door codes loadThe guest only sees the OTP screen on their very first visit. Every visit after that, door codes load automatically as part of your normal page load.
API Reference
OpenDOORClient
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| token | string | Yes | — | JWT access_token from the auth flow |
| onTokenExpired | () => Promise<string> \| string | No | — | Called when the token expires. Return a fresh JWT. |
| timeout | number | No | 15000 | Request timeout in milliseconds |
| maxRetries | number | No | 2 | Max retries for 5xx, 429, and network errors |
| includeAllDevices | boolean | No | false | When true, retrieves all credentials for doors the user can unlock. When false, retrieves only credentials generated from accesses granted by the Partner that minted the user's JWT. |
Methods
| Method | Description |
|--------|-------------|
| getLocks(): Promise<Lock[]> | Fetch all locks accessible to the authenticated user |
| getLock(lockId: string): Promise<Lock> | Fetch a single lock by its device UUID |
| updateToken(newToken: string): void | Manually replace the current JWT |
| isAuthenticated(): boolean | Returns true if the current token has not expired |
| destroy(): void | Clean up the client. All subsequent calls will throw. |
Types
interface Lock {
id: string; // Device UUID
name: string; // e.g. "Building Entrance", "Unit 4B"
buildingId: string; // Building UUID
startTime: Date; // Access window start
endTime: Date | null; // Access window end (null = no end date)
doorCode: string | null; // Door code PIN, or null if not available
}Error Types
All errors extend SDKError.
| Error | When | Properties |
|-------|------|------------|
| AuthError | Token expired with no onTokenExpired callback, or refresh returned an invalid token | statusCode |
| APIError | DOOR API returned a non-success response (4xx, 5xx) | statusCode, responseBody |
| NotFoundError | getLock(lockId) could not find a matching lock in the returned device list | — |
| NetworkError | Network failure — DNS, timeout, connection refused | cause |
| ConfigError | Invalid configuration (e.g., empty token) | — |
Error Handling Example
import { OpenDOORClient, AuthError, APIError, NetworkError } from '@dooraccess/opendoor-web-sdk';
try {
const locks = await client.getLocks();
} catch (error) {
if (error instanceof AuthError) {
// Token expired and refresh failed — redirect to login
redirectToLogin();
} else if (error instanceof APIError) {
// Server error — show message, maybe retry later
console.error(`API error ${error.statusCode}:`, error.responseBody);
} else if (error instanceof NetworkError) {
// Offline or connectivity issue
showOfflineMessage();
}
}Security Notes
- Never expose
client_idorclient_secretin browser code. All auth calls must go through your backend. - Never send
refresh_tokento the browser. Store it server-side and expose a refresh endpoint that returns a newaccess_token. - The
audienceparameter is required on both the initial token exchange and refresh calls. Without it, the JWT will lack the necessary permissions.
Browser Support
The SDK works in all modern browsers:
- Chrome 60+
- Firefox 55+
- Safari 11+
- Edge 79+
Support
Contact your DOOR account representative for integration assistance.
License
MIT
