LifeCare.ID® Developer Docs Public

Sandbox data source ✅

The sandbox gives you identities, grants and tokens. This page is the other half: a data source that honours those tokens, so an app or an agent has something to read under consent. It serves synthetic health data and nothing else.

It is a test fixture. It exists so you can exercise the whole loop (grant, token, read, ledger entry, revoke, refusal) against something that behaves the way a conforming data source must, before a real one is involved.

GET https://lifecare.id/sandbox-source/ describes the source and needs no token. Its configured field says whether the source has a sandbox to check tokens against; while it reads false, every data route answers 503.


What the source does on every request

It follows the resource-host contract in Data sources & connectors, with nothing cached between requests:

  1. Introspects the bearer token at the sandbox (POST /v1/token/introspect), presenting the source's own client credentials, as every resource server must (RFC 7662, section 2.1).
  2. Enforces active, token_type: "access", the scope and the resource owner.
  3. Records the access in the owner's ledger (POST /v1/access/record).
  4. Serves the persona's data, directly to you.

The record is written before the response, so an access that cannot be written to the ledger is not served. Because step 1 runs on every request, a revoked grant stops the very next call. What the source sends to LifeCare.ID® is {subject_did, scope, purpose, accessor, consent_id} and never a value; the consent_id is the one introspection returned for the token, so the ledger entry names the grant the read was made under (left out when introspection returned none).


Walkthrough

Everything below is copy and paste. On the hosted sandbox the two self-service calls need your developer access token (see Sandbox); a sandbox you run locally needs none.

SANDBOX=https://sandbox.lifecare.id
SOURCE=https://lifecare.id/sandbox-source
DEVACCESS=…   # the token from https://app.lifecare.id/devaccess (the portal's verify page)

1. Register a sandbox client

curl -s -X POST "$SANDBOX/v1/sandbox/clients" \
  -H "Authorization: Bearer $DEVACCESS" -H "Content-Type: application/json" \
  -d '{"name":"Pilot agent","redirect_uris":["http://localhost:4000/callback"]}'
CLIENT_ID=sbx_…   # client_id from the response

2. Mint a synthetic identity

curl -s -X POST "$SANDBOX/v1/sandbox/test-identity" \
  -H "Authorization: Bearer $DEVACCESS" -H "Content-Type: application/json" -d '{}'
DID=did:web:sandbox.lifecare.id:u:…   # did from the response
SESSION=lcs_…                          # session_token from the response

3. Grant a scope

The synthetic person grants your client the recovery and daily scopes. The call carries the person's own session, which is the credential a person acts with.

curl -s -X POST "$SANDBOX/v1/consent/grant" \
  -H "Authorization: Bearer $SESSION" -H "Content-Type: application/json" -d '{
  "subject_did":"'"$DID"'",
  "grantee":{"name":"Pilot agent","id":"'"$CLIENT_ID"'"},
  "scopes":["recovery","daily"],
  "purpose":{"text":"agent governance pilot"},
  "duration_days":1}'
CONSENT_ID=urn:lifecare:consent:…   # consent_id from the response

4. Get a token

Tokens are single scope and short lived. expires_at in the response says when to ask for another.

curl -s -X POST "$SANDBOX/v1/token" \
  -H "Authorization: Bearer $SESSION" -H "Content-Type: application/json" -d '{
  "subject_did":"'"$DID"'","scope":"recovery",
  "purpose":"agent governance pilot","requester":"'"$CLIENT_ID"'"}'
TOKEN=lc_…   # access_token from the response

5. Read synthetic data over REST

curl -s "$SOURCE/v1/source/recovery?owner=$DID" -H "Authorization: Bearer $TOKEN"
{
  "fixture": true,
  "notice": "Synthetic persona data from the LifeCare.ID® sandbox reference source. Not a person. Zero PHI. Test fixture.",
  "resource_owner": "did:web:sandbox.lifecare.id:u:817612d13f3219b6",
  "scope": "recovery",
  "purpose": "agent governance pilot",
  "requester": "sbx_Zy47k_NDW4E",
  "consent_id": "urn:lifecare:consent:8e1f…",
  "persona": { "persona_id": "p3-under-recovered", "display_name": "Under-Recovered Athlete", "selected_by": "did-hash" },
  "as_of": "2026-10-01",
  "access_recorded": true,
  "ledger_entry_hash": "43bcec11…",
  "data": {
    "covered": true,
    "pillar": "recovery",
    "score": 0.0,
    "band": "off-track",
    "as_of": "2026-10-01",
    "weekly": [
      { "week_index": 0, "first_day": "2026-09-02", "last_day": "2026-09-08",
        "covered": true, "score": 51.0, "band": "drifting", "as_of": "2026-09-08", "direction": "first-reading" }
    ],
    "sentence": "Recovery is off track: sleep is short and overnight recovery sits well below this persona's own baseline.",
    "sleep_label": "under-rested"
  }
}

The read is now in the person's ledger, attributed to sandbox-source:synthetic-personas, and its payload carries the consent_id the token was issued under, so the entry links back to the grant from step 2:

curl -s "$SANDBOX/v1/consent/ledger/$DID" -H "Authorization: Bearer $SESSION"
{ "event_type": "data_access",
  "description": "sandbox-source:synthetic-personas read your recovery data. Purpose: agent governance pilot.",
  "payload": { "scope": "recovery", "purpose": "agent governance pilot",
               "accessor": "sandbox-source:synthetic-personas",
               "consent_id": "urn:lifecare:consent:8e1f…" } }

6. Connect an MCP client

The MCP endpoint is https://lifecare.id/sandbox-source/mcp (streamable HTTP). The bearer token on the transport is the same sandbox access token. Any MCP client that can send a header will connect; for example:

{
  "mcpServers": {
    "lifecare-sandbox-source": {
      "type": "http",
      "url": "https://lifecare.id/sandbox-source/mcp",
      "headers": { "Authorization": "Bearer lc_…" }
    }
  }
}

Or by hand:

curl -s -X POST "$SOURCE/mcp" -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"read_scope","arguments":{"scope":"recovery","owner":"'"$DID"'"}}}'

The tool result carries the same document as step 5, as structuredContent and as JSON text.

7. Revoke, and see the next call fail

curl -s -X POST "$SANDBOX/v1/consent/revoke" \
  -H "Authorization: Bearer $SESSION" -H "Content-Type: application/json" \
  -d '{"consent_id":"'"$CONSENT_ID"'"}'

curl -s -i "$SOURCE/v1/source/recovery?owner=$DID" -H "Authorization: Bearer $TOKEN"
HTTP/2 401
www-authenticate: Bearer error="invalid_token"

{"detail":"token inactive or unknown","error":"invalid_token"}

The MCP endpoint refuses the same token the same way, on every method.


REST reference

All data routes take Authorization: Bearer <sandbox access token> and answer Cache-Control: no-store.

Route Token scope Returns
GET /sandbox-source/ none the source's description; configured says whether it has a sandbox to check tokens against, client_credentials whether it presents its own client credentials there (introspection needs them)
GET /sandbox-source/v1/scopes any the scope vocabulary, the one scope this token reads, the owner and the persona. Records nothing
GET /sandbox-source/v1/source/{scope}?owner={did} {scope} that scope for the token's owner
GET /sandbox-source/v1/weekly-summary?owner={did}&week={n} daily one week across every pillar, plus the persona's headline. week is optional and defaults to the latest

owner is required on every read and must be the DID the token was issued for. That is the subject check a data source owes the person, and it is never skipped.

Scopes

The source follows the scope vocabulary. Each payload carries covered and a sentence; a scope a persona has no data for says so instead of inventing a reading.

Scope What comes back
recovery the recovery band and score, weekly direction, a sleep label
activity the muscle stimulus band and score from training, weekly direction, the training modality, a movement band
cgm the glucose stability band and score, weekly direction, an anomaly flag. Covered for one persona only
body_composition a muscle retention band and the direction of weight and lean mass. No mass, no percentage
daily every pillar's current band, coverage day by day, the focus, the challenge week
insights the newest derived insight per family, each a categorical label
labs covered: false. No persona has laboratory results

told_to_guardian is not served: it is what a person tells their guardian, held by their node, and no data source holds it. A read of it answers 404 unknown_scope.

Bands are on-track, drifting and off-track; retention bands are excellent, good and watch; directions are improving, steady, declining and first-reading. Scores run from 0 to 100 and are derived, in the same sense as the node's own contract: no response field holds a reading, a mass, a percentage or a count of anything measured, and no sentence holds a digit.

Errors

Every refusal is {"detail": "...", "error": "<code>"}. A 401 also carries WWW-Authenticate: Bearer.

Status error When
401 missing_token no bearer token
401 invalid_token the sandbox says the token is not active: unknown, expired, or its grant was revoked
403 wrong_token_type a session token, or any token that is not a data-access token
403 insufficient_scope the token covers a different scope
403 wrong_subject owner is not the DID the token was issued for
400 owner_required no owner named
404 unknown_scope the scope is not in the vocabulary
404 unknown_week the persona has no such week
503 introspection_unavailable the sandbox could not be asked about the token. Nothing is served
503 source_not_authorized the sandbox refused the source's own client credentials (missing or wrong), so it could not check the token. Your token was not looked up. Nothing is served
503 access_record_failed the access could not be written to the ledger. Nothing is served
503 source_not_configured the source has no sandbox to check against

MCP reference

POST https://lifecare.id/sandbox-source/mcp: streamable HTTP, stateless, JSON responses. There is no session and no server-initiated stream, so GET answers 405.

A test fixture for agent governance pilots. This is not the member MCP surface, and its tools are not a preview of one. It exists so a gateway, a firewall or a policy layer in front of an agent has a consent-bound health data server to be tested against.

Tool Arguments Token scope
list_scopes none any
read_scope scope, owner scope
get_weekly_summary owner, optional week daily

The personas

Five synthetic members, thirty days each, generated by the challenge readiness simulation that the node is tested against. Each is built to show one thing.

Persona What it shows
p1-adherent-responder the healthy case: weight comes down while lean mass holds
p2-glp1-muscle-loser weight comes down fast, a meaningful share of it lean, with exercise that is all cardio
p3-under-recovered frequent training on short sleep: training on track, recovery off track
p4-sparse-returner data that arrives, stops for a long stretch and resumes
p5-glucose-anomaly glucose stability falling through the month while everything else stays flat

Which persona you get. The source maps the resource owner's DID onto a persona: SHA-256 of the DID, the first four bytes read as a big-endian unsigned integer, modulo five, indexing the list above in order. The same identity always reads the same persona.

Choosing one. A grant whose purpose text carries persona:<id> pins that persona, for example "purpose":{"text":"agent governance pilot persona:p2-glp1-muscle-loser"} (and the same text as purpose on the token request). The pin lives in the purpose the synthetic person consented to, so it cannot be changed per request. The response's persona.selected_by says which rule applied: did-hash or purpose.

The window is fixed. The data covers the thirty days ending on as_of. It does not move with the calendar, and nothing new arrives.


Limits you should know about

Run it locally

The source, its tests, a Compose file that runs it on your machine and an example client that runs the whole loop are the public sandbox kit, github.com/enbarca/lifecareid-sandbox (Apache-2.0). The source you run checks every token at the hosted sandbox, as the hosted source does, with a client of its own: register one with your developer access token (step 1 of the sandbox guide) and put its id and secret in compose/.env. There is no identity server to run: sandbox mode is a runtime flag on the production binary, so no image of it is published, and identity, consent and tokens stay at https://sandbox.lifecare.id.

git clone https://github.com/enbarca/lifecareid-sandbox && cd lifecareid-sandbox/compose
cp .env.example .env     # SOURCE_CLIENT_ID and SOURCE_CLIENT_SECRET: the client you registered
docker compose up
# SOURCE=http://localhost:8799/sandbox-source   (SANDBOX stays https://sandbox.lifecare.id)

The walkthrough above then runs unchanged with SOURCE pointed at your machine, developer access token included; the kit's compose/README.md has it by hand, and examples/run-loop.mjs runs every step and checks each answer (LIFECAREID_DEVACCESS_TOKEN=... SOURCE=http://localhost:8799/sandbox-source node examples/run-loop.mjs). The hosted source itself is a Cloudflare Pages Function; the kit's pages/ folder is that deployment, with the two project variables it reads.