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 likehttps://<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.
- Sandbox (✅, self-service): in the sandbox you register a client
yourself in one call (
POST /v1/sandbox/clients) and even mint a passkey-free test user, so you can build the whole loop now with no access request. Getting in is a one-time email + terms magic link (/devaccess); the evaluation docs (concepts, this page, the API reference) stay open to read. - Production today (✅): the network operator provisions your client (the MVP runs
a single confidential client,
proforta, configured in the service environment). Send the operator your app name and redirect URI(s); you receive the credentials out of band. Keep the secret server-side. - Roadmap (🛣️): self-service production registration, secret rotation, multiple environments, and redirect-URI management land in the developer portal.
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.
2. Ask for consent ✅
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.textyou 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
- Authentication — the full OAuth/PKCE reference, sessions, errors
- Scopes & consent — the scope vocabulary, receipts, the ledger
- Authorization & tokens — token lifecycle, introspection, revocation
- Embedding the consent panel — drop-in data-rights UI
- API reference — every endpoint, request and response