LifeCare.ID® Developer Docs Public

Getting started (relying-party app)

This walks a brand-new application from zero to reading consented data. It assumes you're building an app (a requester). If you're building a data source, start at Data sources & connectors instead.

Base URL. In the MVP, LifeCare.ID is served behind the home node's ingress at /id, so every path below is relative to a base like https://<node-domain>/id. We write paths as {base}/v1/.... Ask your network operator for the base URL of the deployment you're integrating with.

Prefer to read code? The quickstart repos are clone-and-run, zero-dependency reference apps — a relying-party app (Node) and a data source (Python) — that implement this whole walkthrough. Skim this page, then run one.


0. Register your application ✅ (operator-mediated) · 🛣️ (self-service)

You need a confidential client: a client_id, a client_secret, and an exact-match list of redirect URIs.

You must be able to keep client_secret confidential — the token exchange is a server-to-server call. Pure SPA/native clients should proxy the exchange through their own backend.


1. Sign the user in ✅

LifeCare.ID uses OAuth 2.0 authorization code + PKCE. Full detail and error handling are in Authentication; the short version:

a. Redirect the user to /authorize:

GET {base}/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.example/callback
  &state=RANDOM_OPAQUE
  &code_challenge=BASE64URL(SHA256(verifier))
  &code_challenge_method=S256

If the user has no LifeCare.ID session, they're sent to the LifeCare.ID web surface to run (or create) their passkey — onboarding and sign-in are the same front door — then returned here. On success the browser is redirected to:

https://yourapp.example/callback?code=ONE_TIME_CODE&state=RANDOM_OPAQUE

Verify state matches what you sent.

b. Exchange the code (server-to-server):

POST {base}/token
Content-Type: application/json

{
  "grant_type": "authorization_code",
  "code": "ONE_TIME_CODE",
  "redirect_uri": "https://yourapp.example/callback",
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET",
  "code_verifier": "THE_ORIGINAL_VERIFIER"
}

Response:

{
  "access_token": "lcs_…",
  "token_type": "Bearer",
  "expires_at": "2026-06-18T12:00:00+00:00",
  "did": "did:web:node0.example:u:9f2a1c4d8b3e0f57"
}

In your language — the same exchange ({base} and the code/verifier from the redirect):

# cURL
curl -s -X POST "$BASE/token" -H "Content-Type: application/json" -d '{
  "grant_type":"authorization_code","code":"ONE_TIME_CODE",
  "redirect_uri":"https://yourapp.example/callback",
  "client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET",
  "code_verifier":"THE_ORIGINAL_VERIFIER"}'
// Node 18+ (server-side — never ship the secret to a browser)
const res = await fetch(`${BASE}/token`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    grant_type: "authorization_code", code,
    redirect_uri: REDIRECT_URI, client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET, code_verifier: verifier,
  }),
});
const { did, access_token } = await res.json();
# Python (stdlib)
import json, urllib.request
req = urllib.request.Request(f"{BASE}/token", method="POST",
    data=json.dumps({
        "grant_type": "authorization_code", "code": code,
        "redirect_uri": REDIRECT_URI, "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET, "code_verifier": verifier,
    }).encode(), headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as r:
    data = json.loads(r.read())
did, access_token = data["did"], data["access_token"]

You now know who the user is (did) and hold a session token (access_token) you can use to drive their consent panel. Store the DID against your user record; treat the session token like a session cookie.


Reading data requires an active grant for a scope, bound to a purpose. You can create one via the user-facing panel using the session token from step 1 (no subject_did needed — it's derived from the session):

POST {base}/v1/panel/grant
Authorization: Bearer lcs_…
Content-Type: application/json

{
  "grantee": { "name": "Your App", "id": "did:web:yourapp.example" },
  "scopes": ["cgm", "daily"],
  "purpose": { "text": "weekly health synthesis",
               "dpv": "dpv:PersonalisedRecommendation" },
  "duration_days": 90
}

Response carries a signed receipt and a consent_id. The grant is now visible in the user's ledger. (You can also render the whole panel — summary, ledger feed, grant/revoke — see Embedding the consent panel.)

Purpose matters. The purpose.text you declare here must match the purpose you later request a token for — tokens are purpose-bound. Pick a stable, human-readable purpose and reuse it.


3. Get a data-access token ✅

To actually read data you exchange the grant for a data-access token (a UMA-pattern RPT). This is a different token from the session token:

POST {base}/v1/token
Content-Type: application/json

{
  "subject_did": "did:web:node0.example:u:9f2a1c4d8b3e0f57",
  "scope": "cgm",
  "purpose": "weekly health synthesis",
  "requester": "yourapp"
}

Response:

{
  "access_token": "lc_…",
  "token_type": "Bearer",
  "scope": "cgm",
  "consent_id": "urn:lifecare:consent:…",
  "expires_at": "2026-06-17T12:20:00+00:00"
}

The token is minimum-scope and time-boxed to min(token TTL, grant expiry).

In your language:

# cURL
curl -s -X POST "$BASE/v1/token" -H "Content-Type: application/json" -d '{
  "subject_did":"'"$DID"'","scope":"cgm",
  "purpose":"weekly health synthesis","requester":"yourapp"}'
// Node 18+
const res = await fetch(`${BASE}/v1/token`, {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ subject_did: did, scope: "cgm",
    purpose: "weekly health synthesis", requester: "yourapp" }),
});
const { access_token } = await res.json();   // an lc_… data-access token
# Python (stdlib)
import json, urllib.request
req = urllib.request.Request(f"{BASE}/v1/token", method="POST",
    data=json.dumps({"subject_did": did, "scope": "cgm",
        "purpose": "weekly health synthesis", "requester": "yourapp"}).encode(),
    headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as r:
    access_token = json.loads(r.read())["access_token"]

4. Read the data — directly from the source ✅

Present the lc_… token to the data source (e.g. the user's home node API), not to LifeCare.ID. The source validates it by introspection (POST {base}/v1/token/introspect) and, if active, returns the data directly to you. LifeCare.ID is never in the data path.

The source is expected to record the access in the ledger (POST {base}/v1/access/record), so the user sees "Your App read your cgm data" in their panel.

What does the source actually serve, and how do I call it? That is the node's API, not LifeCare.ID's — and it serves interpreted context (scores, bands, narratives), never raw values. Build a healthspan app, end to end documents the live node read/write surface (/dashboard, /workout, /journey, /experience, …), the two-system model, and the full capability map (what's live vs roadmap). Start there once you hold a token.


5. Revocation is the user's, any time ✅

The user can revoke from their panel (or you can, on their behalf, via POST {base}/v1/panel/revoke). Revocation kills dependent tokens in < 500 ms and updates the public revocation registry. Design for revocation: treat a 403 / inactive introspection as a normal, expected state and degrade gracefully.


Where to go next