LifeCare.ID® Developer Docs Public

Sandbox ✅

Engage with LifeCare.ID without requesting access. The sandbox is an isolated instance where you self-register an app and mint a synthetic signed-in user in seconds, then run the full sign-in to grant to token to introspect loop. It holds only synthetic, zero-PHI data, and it is separate from production.


1. Register your app (self-service)

On the hosted sandbox, pass your developer access token (from the portal, https://app.lifecare.id/devaccess) as a Bearer. (A local sandbox you run yourself needs no token.)

DEVACCESS=…   # the token from https://app.lifecare.id/devaccess (the portal's verify page)
curl -s -X POST "$SANDBOX/v1/sandbox/clients" \
  -H "Authorization: Bearer $DEVACCESS" \
  -H "Content-Type: application/json" \
  -d '{"name":"My app","redirect_uris":["http://localhost:4000/callback"]}'
{
  "client_id": "sbx_Zy47k_NDW4E",
  "client_secret": "qDkujP10i72…",
  "redirect_uris": ["http://localhost:4000/callback"],
  "note": "Sandbox client. Store the secret now - it is not shown again."
}

You now have a client_id and client_secret, the same credentials an operator would otherwise issue you. redirect_uris is an exact-match allowlist; pass every callback URL you will use.

Delete your app

When you are done with a client (a test run that registered one, a secret you want to retire), delete it with its own secret as the credential, as HTTP Basic. Whoever holds the secret holds the client: nobody is signed in to a sandbox client, so there is no other owner to ask.

curl -s -X DELETE "$SANDBOX/v1/sandbox/clients/$CLIENT_ID" \
  -u "$CLIENT_ID:$CLIENT_SECRET"
{ "deleted": true, "client_id": "sbx_Zy47k_NDW4E" }

A wrong or missing secret, a credential for a different client, and an unknown client_id all answer 401 alike, so the route says nothing about which ids exist. On the hosted sandbox the Authorization header carries the client's secret, so pass your developer access token as the lc_dev cookie instead (--cookie "lc_dev=$DEVACCESS"); a local sandbox needs neither.

What the deletion ends is the credential: the client can no longer exchange an authorization code, sign in a user or authenticate a server-to-server call, and its redirect URIs leave the allowlist. What stays is the member's: a grant that names the client as grantee and the ledger entries about it are the member's records, and a data-access token already issued against such a grant runs to its expiry (it is bound to the grant, not to the client's secret; revoke the grant to end it early).

2. Mint a test identity (no passkey)

Production sign-in needs a real WebAuthn passkey on a device, which you cannot script. In the sandbox you can mint a synthetic signed-in user instead, so the whole loop runs headlessly (CI, curl, tests):

curl -s -X POST "$SANDBOX/v1/sandbox/test-identity" -H "Content-Type: application/json" -d '{}'
{
  "did": "did:web:sandbox.example:u:817612d13f3219b6",
  "session_token": "lcs_…",
  "expires_at": "2026-06-24T09:00:00+00:00",
  "note": "Synthetic sandbox identity (no passkey)."
}

The session_token is a real user/session token. Use it two ways:

3. Run the whole loop

With a client and a test identity you can exercise everything:

# (a) act as the signed-in user: set the session cookie, then run /authorize -> /token
#     (this is what the app quickstart does for you)
# (b) grant consent for a scope
curl -s -X POST "$SANDBOX/v1/consent/grant" -H "Content-Type: application/json" -d '{
  "subject_did":"'"$DID"'","grantee":{"name":"My app","id":"'"$CLIENT_ID"'"},
  "scopes":["cgm"],"purpose":{"text":"demo"},"duration_days":1}'

# (c) issue a data-access token, then introspect it as your client
curl -s -X POST "$SANDBOX/v1/token" -H "Content-Type: application/json" -d '{
  "subject_did":"'"$DID"'","scope":"cgm","purpose":"demo","requester":"'"$CLIENT_ID"'"}'
curl -s -X POST "$SANDBOX/v1/token/introspect" -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/json" -d '{"token":"lc_…"}'

Introspection is the one call here that needs your client credentials, the client_id and client_secret from step 1 (HTTP Basic, as -u sends them). It follows RFC 7662, section 2.1: the endpoint authenticates the resource server that asks, so holding a token string is not enough to read whose data it opens. No credential is 401; a member's session or a holder signature in place of a client credential is 403. Both come before the token is looked up, on the sandbox as in production. See Authorization & tokens § Who may introspect.

The quickstart repos run this end to end. Point them at your sandbox base URL and the credentials from step 1.

A token needs something to open. The sandbox data source honours sandbox tokens and serves synthetic health data: five personas, interpreted outputs only, over REST and over MCP. It introspects every request with its own client credentials, records every read in the ledger, and stops on the call after you revoke. Its page has the walkthrough from registering a client to watching a revoked token fail.


What the sandbox is (and isn't)


Run it locally

The sandbox is the same service with one flag. To run your own:

LIFECAREID_SANDBOX=true \
LIFECAREID_STATE_DIR=/tmp/lifecareid-sandbox \
LIFECAREID_DOMAIN=sandbox.local \
uv run uvicorn --factory lifecareid:create_app --port 8821
# → http://localhost:8821   (set SANDBOX=http://localhost:8821 for the curls above)

Operators host a shared sandbox by running this profile as a separate instance with isolated state; the production instance leaves LIFECAREID_SANDBOX unset, so it never exposes these routes.