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": "yourapp" },
  "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.

So does the grantee. The grantee.id must be the identity you authenticate as when you request the token (your client_id, or your DID): a token is issued only against a grant that names its requester. See Whose grant a token is issued against.


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, which the source calls with its own client credentials) 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