# TEFCA sandbox API

> Build an app that discovers and retrieves synthetic TEFCA-style records with your FinchNode sandbox key.

Use an application sandbox key to run the synthetic discovery, retrieval, export, and revocation flow from your own backend. The data comes from FinchNode's fictional demo patient. No identity provider or exchange network is contacted.

The provider test exchange remains in the signed-in [TEFCA console](/docs/tefca/try-it-in-the-console). Its base64 document format differs from this simulation's FHIR collection.

## Start with a sandbox key

[Sign up](/signup), then create an application in [Applications](/console/applications), allow medications and labs, and create a sandbox key. The console shows the key once, so copy it then. Keep it in your server's `FINCHNODE_API_KEY` environment variable. A `ck_live_` key, delegated agent credential, or conformance-run key cannot use these endpoints.

Send requests to `https://api.finchnode.com/api/v1`. These routes use the usual [API authentication and errors](/docs/api), including request IDs and rate-limit headers.

## Run a complete backend example

With Node 24 and `FINCHNODE_API_KEY` set, save this as `tefca-demo.mjs` and run `node tefca-demo.mjs`. The synthetic consent acceptance below applies only to this fictional demonstration.

```js title="Node.js"
const base = 'https://api.finchnode.com/api/v1/tefca';
const key = process.env.FINCHNODE_API_KEY;
if (!key?.startsWith('ck_test_')) throw new Error('Set a sandbox FINCHNODE_API_KEY.');
async function request(path, body) {
  const response = await fetch(base + path, {
    method: body === undefined ? 'GET' : 'POST',
    redirect: 'error', signal: AbortSignal.timeout(15000),
    headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  const result = await response.json();
  if (!response.ok) throw new Error(`${response.status}: ${result.error?.message}`);
  return result;
}
const config = await request('/config');
const session = await request('/sessions', {
  mode: 'simulation', patientId: config.patientId,
  consent: true, consentVersion: config.consentVersion,
  scenario: 'matched', categories: ['medications', 'labs'],
});
try {
  const found = await request(`/sessions/${session.id}/discover`, {});
  if (!found.documents.length) {
    console.log('No records:', found.status, found.failures);
  } else {
    const retrieved = await request(`/sessions/${session.id}/retrieve`, {
      documentIds: found.documents.map(document => document.id),
    });
    if (retrieved.results.length) {
      const exported = await request(`/sessions/${session.id}/export`);
      console.log(JSON.stringify(exported.bundle, null, 2));
    }
    console.log('Document failures:', retrieved.failures);
  }
} finally {
  await request(`/sessions/${session.id}/revoke`, {});
}
```

## Try the browser sample app

Download and extract the [sample app](/tefca-sandbox-app.zip). Run `node server.mjs` in that directory with your sandbox key in the environment, then open `http://127.0.0.1:4318`. The app needs Node 24 and no package installation.

The sample has scenario selection, a synthetic consent checkbox, record-group selection, a FHIR viewer, JSON download, and revocation. It binds to loopback and keeps the key on its server. It is a local hackathon starter; add user authentication and durable browser-session storage before hosting your own app publicly.

## Understand the contract

| Request | Result |
| --- | --- |
| `GET /tefca/config` | Current synthetic consent, scenarios, and categories your app can request |
| `POST /tefca/sessions` | A session owned by your application, valid for 15 minutes |
| `GET /tefca/sessions/{id}` | The current session state |
| `POST /tefca/sessions/{id}/discover` | Matching synthetic record groups, or a no-match/source-unavailable outcome |
| `POST /tefca/sessions/{id}/retrieve` | Selected groups in `results`, and per-group `failures` |
| `GET /tefca/sessions/{id}/export` | One FHIR collection in `bundle`, plus consent and failures |
| `POST /tefca/sessions/{id}/revoke` | A revoked session; future discovery, retrieval, and export return `410` |

Discover and revoke take an empty body. Retrieve takes only `documentIds` from discovery. Creating a session accepts only the fields in the example, with optional `scenario` and `categories`. Omitting categories uses supported categories on the application's allowlist. Never submit personal demographics.

The export contains each retrieved resource once, including one Patient resource. Without the demographics category, that Patient contains only its resource type and synthetic ID. Labs exclude vital signs, even though both use FHIR Observation resources. Results remain separate from `/users/{subject}/records`; use the TEFCA export directly in your app.

## Read what comes back

Discovery lists the record groups you can retrieve (abbreviated, synthetic):

```json
{
  "id": "tefca_sim_a808a58f-15f9-4272-b35e-1685721cc757",
  "object": "tefca_session",
  "status": "discovered",
  "scenario": "partial",
  "categories": ["medications", "labs"],
  "matches": [{ "id": "synthetic-match", "source": "Northstar Health System (Synthetic)", "synthetic": true }],
  "documents": [
    { "id": "synthetic-Observation", "title": "Observation records (synthetic)", "resourceType": "Observation", "resourceCount": 2 },
    { "id": "synthetic-DiagnosticReport", "title": "DiagnosticReport records (synthetic)", "resourceType": "DiagnosticReport", "resourceCount": 1 },
    { "id": "synthetic-MedicationRequest", "title": "MedicationRequest records (synthetic)", "resourceType": "MedicationRequest", "resourceCount": 2 }
  ],
  "results": [],
  "failures": []
}
```

Retrieve with those `id` values. The export then holds the FHIR collection and any failures (abbreviated, synthetic):

```json
{
  "object": "tefca_export",
  "sessionId": "tefca_sim_a808a58f-15f9-4272-b35e-1685721cc757",
  "mode": "simulation",
  "synthetic": true,
  "status": "partial",
  "consent": { "version": "tefca-simulation-2026-09-09", "grantedAt": "2026-10-01T19:26:46.104Z", "revokedAt": null },
  "categories": ["medications", "labs"],
  "failures": [
    { "documentId": "synthetic-DiagnosticReport", "code": "simulated_retrieval_failure", "message": "This document is unavailable in the partial-results scenario." }
  ],
  "bundle": { "resourceType": "Bundle", "type": "collection", "entry": ["..."] }
}
```

The `TefcaSession` and `TefcaExport` schemas in [openapi.yaml](/openapi.yaml) describe these responses. Sessions also carry a few bookkeeping fields, such as `events`, that your app can ignore.

## Test failures and recovery

- `matched`: records are available.
- `partial`: the first selected group fails; other selected groups remain usable. Select at least two groups to see a partial success.
- `no-match`: discovery succeeds with no record groups.
- `unavailable`: discovery reports a simulated unavailable source.

Requests complete synchronously. A failed or no-match session is a valid scenario outcome, not an HTTP failure. Check `status`, `results`, and `failures` before exporting.

There are at most 20 active sessions per application, shared across its keys. Revoke finished sessions to release capacity. Session creation is not idempotent: keep the returned ID and use the read endpoint to recover. Retrieval of the same selection is repeatable; start a new session to change a completed selection. Expired sessions return `410`; old rows are removed when that application starts another session. Revoked rows may then return `404` after removal.

Another application's session returns `404`. A narrowed application allowlist returns `403 app_scope_changed`; revoke and start again. A concurrent mutation returns `409 session_changed`; read the current session before retrying. A `503 tefca_sandbox_unavailable` means the deployment is missing this feature's database migration. Revocation cannot remove a copy your app already downloaded.
