Search docs

Search the Vexo developer documentation

Get the ring

Platform

Authentication

Scoped OAuth 2.1 tokens that put the wearer in the loop for every grant.

POST/v1/oauth/token

How auth works

Vexo runs standard OAuth 2.1 with PKCE, with one addition: every grant is approved by the wearer on their phone, scope by scope, in plain language. There is no app-only path to a person's data. The lifecycle is short:

  1. Your app requests a set of scopes via vexo.oauth.authorize().
  2. The wearer reads each scope on their phone and approves or denies the grant.
  3. You exchange the returned code for a user-scoped token at POST /v1/oauth/token.
  4. The grant lives until it expires or the wearer revokes it. Revocation wins every race.

Access tokens expire after one hour. Refresh tokens rotate on every use. Nothing about a grant is permanent, and your code should assume that.

Token types

Three credential shapes, distinguishable by prefix. Log the prefix, never the token.

NameTypeDescription
vx_app_…app tokenIdentifies your application to the platform. Used to start authorization flows and manage webhooks you own. Grants access to zero wearer data on its own.
vx_user_…user tokenA wearer-scoped access token minted by the authorization code flow. Carries exactly the scopes the wearer approved, expires after 3600 seconds.
vx_pub_…publishable keySafe for client-side code. Can render the consent sheet and read your app's public metadata. Cannot read or write any capture or biometric.

Prefixes are stable. Everything after the prefix is opaque and may change length.

Authorization code flow

  1. Send the wearer to consent

    Build the authorization URL with the scopes you need and a PKCE verifier. The consent sheet renders on the wearer's phone, not in your app, so there is nothing for you to embed or style.

    auth.ts
    const { url, verifier } = vexo.oauth.authorize({
      scopes: ["captures:read", "biometrics:hrv"],
      redirectUri: "https://app.pulseboard.dev/callback",
    });
    
    // Persist the PKCE verifier server-side, then send the wearer to url.
    // They approve or deny on their phone, not in your app.
  2. Exchange the code

    After approval, Vexo redirects back with a one-time code that expires in 60 seconds. Exchange it server-side.

    POST/v1/oauth/token
    Token exchange
    curl https://api.vexo.dev/v1/oauth/token \
      -X POST \
      -H "Content-Type: application/json" \
      -d '{
        "grant_type": "authorization_code",
        "code": "ac_7t4mYQe2Lr0v",
        "client_id": "vx_app_31hd8s",
        "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
        "redirect_uri": "https://app.pulseboard.dev/callback"
      }'
    
    # 200 OK
    # {
    #   "access_token": "vx_user_9f2kQm4Lr0vXe81t",
    #   "refresh_token": "vx_refresh_p03aWn7cJd",
    #   "expires_in": 3600,
    #   "scope": "captures:read biometrics:hrv"
    # }
  3. Use the token

    Pass the access token as a bearer credential. The SDK does this for you once you call vexo.setAccessToken(grant.accessToken). Every request is checked against the live grant, not the token's original scope string, so a revoked scope fails immediately even on an unexpired token.

Scopes

Scopes are the unit of consent. The description below is the exact plain-language line the wearer reads before approving. Request the minimum set, you can always ask for more later.

NameTypeDescription
captures:readscope“Read the moments you have chosen to share.” Grants read access to the wearer's capture history and live stream.
captures:writescope“Create new moments on your behalf.” Allows the app to write captures attributed to the wearer.
biometrics:hrscope“See your heart rate.” Read access to derived heart rate metrics. Never raw waveforms.
biometrics:hrvscope“See your heart rate variability.” Read access to HRV windows and rolling baselines.
biometrics:tempscope“See your skin temperature trend.” Read access to nightly deviation from the wearer's baseline.
biometrics:motionscope“See your movement and activity.” Read access to fused motion state: still, walking, running, sleeping.
webhooks:managescope“Let this app receive updates automatically.” Create, list, and delete webhook endpoints for events the grant can see.
marketplace:publishscope“Let this developer publish context servers.” Required to submit and update MCP marketplace listings.

Wearer-facing copy is fixed per scope. You cannot rewrite it in your consent request.

Refreshing tokens

Access tokens live for one hour. Refresh before expiry, or catch the 401 and refresh then, both patterns are supported. Refresh tokens are single-use and rotate on every call.

Refresh
auth.ts
const refreshed = await vexo.oauth.refresh({
  refreshToken: grant.refreshToken,
});

// Refresh tokens rotate on every use. The old one is dead
// the moment this call resolves, so store the new pair atomically.
refreshed.accessToken;   // "vx_user_2c8xVt5Nq1wYe44j"
refreshed.refreshToken;  // "vx_refresh_k91mBz2fQe"

Revocation

Wearers revoke from their phone, per scope or per app, with no confirmation step on your side. Revocation propagates to every surface: REST calls start returning 401 token_revoked, and open streams close from the server.

Handling 4403
stream.ts
stream.on("close", (event) => {
  if (event.code === 4403) {
    // The wearer revoked a scope this stream depended on.
    // Do not retry. Drop cached data for this wearer and re-consent.
    await purgeWearer(event.wearerId);
  }
});

If you receive consent.revoked on a webhook, treat it the same way: stop processing that wearer, then purge what the lost scope covered.