Dashboard APIAPI Overview

Connect accounts with OAuth

Register an integration, invite testers, and request account permissions with OAuth 2.0

Building a product for other R+D users? Start with Build an app for developer access, registration, and platform-specific examples. This page is the protocol reference.

OAuth is available when enabled for your environment. Request developer access at /app/developers. This page is intentionally absent from the dashboard navigation. Personal API tokens continue to work independently.

Register an app

Submit your developer application, including your integration and data-use details. After approval, create an app with its name, website, privacy policy, support email, callback URLs, and requested scopes. Each app has its own review.

Choose one platform per registration:

PlatformClient authenticationCallback requirements
Backendclient_secret_basic (recommended) or registered client_secret_postExact HTTPS URL; loopback HTTP for development
Browsernone; never embed a secretExact callback and browser origin
Native / desktopnone; use the system browserReviewed app identifier and callback ownership evidence; claimed HTTPS, reverse-domain scheme, or IP-loopback listener

All platforms require S256 PKCE. An account can register five apps and an app can register ten callbacks. Wildcards, URL credentials and fragments are rejected. Native IP-loopback callbacks may vary their port; the actual URL is bound to the code.

Before first app approval, its owner can invite five additional R+D accounts by exact email address or username search. Pending and accepted invitations both count. Invitees accept under /app/developers and can then test account linking. Invitations grant testing access only; they do not grant app management or partner/device access. Removing an invite also revokes that account's app connections.

App changes create a new review submission. The current approved configuration remains active while the new submission is reviewed. Removing scopes restricts existing grants immediately. Approval revokes prior connections so users can review the new configuration. Disabling or suspending an app revokes connections; restoring it requires fresh consent.

Authorization code flow

Use the issuer for your environment, such as https://staging.researchanddesire.com. Discover endpoints from /.well-known/oauth-authorization-server. Production and staging registrations and tokens are separate.

Generate a cryptographically random state and a new PKCE verifier for each login:

import { randomBytes, createHash } from "node:crypto";

const state = randomBytes(32).toString("base64url");
const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
const url = new URL("/oauth/authorize", issuer);
url.search = new URLSearchParams({
  response_type: "code",
  client_id: clientId,
  redirect_uri: callbackUrl,
  scope: "profile:read lkbx:read lkbx:control offline_access",
  state,
  code_challenge: challenge,
  code_challenge_method: "S256",
});
// Store state and verifier in the initiating user's session, then redirect to url.

The user signs in, sees the app's identity and permissions, and allows a subset or cancels. Shared-account access and offline access require separate opt-in on the consent form. At your callback, validate state and the returned iss against the initiating session. Handle error=access_denied; otherwise exchange code immediately:

curl "$ISSUER/oauth/token" \
  --user "$CLIENT_ID:$CLIENT_SECRET" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=$CODE" \
  --data-urlencode "redirect_uri=$CALLBACK_URL" \
  --data-urlencode "code_verifier=$VERIFIER"

Public browser/native clients omit HTTP Basic and send client_id in the form body. Use one client authentication method per request. Token endpoints accept form encoding, not JSON. Browser token requests must originate from a registered browser origin and must not include cookies.

The response contains access_token, token_type: "Bearer", expires_in, and the actual scope. A refresh_token is returned only when the user grants offline_access. Treat tokens as opaque; use the granted scope rather than assuming all requested scopes were accepted. No Supabase JWT or OpenID Connect ID token is returned.

curl "$ISSUER/api/v1/users/me" -H "Authorization: Bearer $ACCESS_TOKEN"

Scopes

ScopeAccess
profile:readAccount ID and profile information
lkbx:readLockbox devices, templates, sessions and state
lkbx:controlStart/end locks and adjust durations within existing permissions
lkbx:keyholdersAssign keyholders when starting a lock; also requires lkbx:control
dtt:readDTT devices, templates, history and statistics
dtt:writeEdit and activate DTT templates
ossm:readOSSM device information; no remote control
shared:accessExtend granted product scopes to accounts the user can manage, including accounts linked later
offline_accessReceive rotating refresh tokens

Without shared:access, only the consenting account's resources are accessible, including by ID and through aliases. Scope permissions do not override account sharing, keyholder, self-lock or other device rules. Unknown routes and methods are denied. OAuth responses omit pairing codes, internal identifiers and other fields outside the external contract. Training statistics on profile endpoints also require dtt:read.

Refresh, revoke and rotate

Authorization codes last five minutes and can be used once. Access tokens last ten minutes. Refresh tokens expire after 30 days without use and at most 180 days after initial consent. Each successful refresh replaces the refresh token. Serialize refreshes per connection and persist the replacement atomically. Reusing any prior refresh token revokes its entire family, including access tokens issued from that family. There is no replay grace period; after an ambiguous refresh outcome, the user may need to reconnect.

curl "$ISSUER/oauth/token" --user "$CLIENT_ID:$CLIENT_SECRET" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "refresh_token=$REFRESH_TOKEN"

curl "$ISSUER/oauth/revoke" --user "$CLIENT_ID:$CLIENT_SECRET" \
  --data-urlencode "token=$REFRESH_TOKEN"

Revocation disconnects that grant. Unknown tokens return success without revealing whether they existed. Users can disconnect all grants for an app under Settings → Connected apps. API calls are limited to 60 requests per minute per app and user; respect 429 and Retry-After.

Backend secrets are shown once. Normal rotation gives the previous secret a 24-hour overlap, with at most two active secrets. Emergency rotation invalidates previous secrets and user connections immediately. Store secrets on your backend, outside source control and logs.

Webhooks, client-credentials grants, password grants, implicit grants, dynamic client registration and OpenID Connect are outside this version.

On this page