Matching & Sync
Pending imports & linking (human, in the portal)
There is no automatic matching in the API — lacking PII, the server cannot map pseudonyms to people. This happens in the portal:
- First uploads (
first_visit) accumulate as pending entries, grouped bydevicePseudonym. - A staff member links a pseudonym to a real patient record in the portal. This creates the mapping
devicePseudonym → clientId(the device-pseudonym table) and apersistentTokenfor that patient. - From then on the mapping sync (see below) delivers
mkClientIdfor the pseudonym to the client, and the client subsequently sends asfollow_up/token— directly attributed.
GET /api/v1/imports/list — pending overview (for your portal/debugging)
Query: ?status=pending|rejected|all (default pending), optional &countOnly=1.
Full list:
{
"success": true,
"data": {
"pseudonyms": [
{ "devicePseudonym": "a1b2…", "initialsHint": "AB", "dobMonthYearHint": "1985-03",
"status": "pending", "measurementCount": 5, "newestMeasuredAt": "2026-07-15T10:30:00.000Z" }
],
"count": 50,
"total": 120
}
}
Counts only (countOnly=1):
{ "success": true, "data": {
"pendingPseudonyms": 12, "pendingMeasurements": 48,
"rejectedPseudonyms": 3, "rejectedMeasurements": 7 } }
Mapping sync — GET /api/v1/practice/mappings?since=<ISO-8601>
Authenticated. The client polls incrementally (typically 1×/h) and caches locally to know which pseudonyms are linked (→ follow_up) or revoked.
since is required (ISO-8601; for the first sync e.g. 1970-01-01T00:00:00.000Z). Missing/invalid → 400 VALIDATION_ERROR.
Response 200 (Cache-Control: no-store):
{
"success": true,
"data": {
"mappings": [
{ "devicePseudonym": "a1b2…", "mkClientId": "<uuid>", "revoked": false },
{ "devicePseudonym": "f5e6…", "mkClientId": null, "revoked": true, "revokedAt": "2026-07-14T09:00:00.000Z" }
],
"nextSince": "2026-07-15T10:30:00.001Z"
}
}
- Active link:
mkClientIdset,revoked: false. - Tombstone:
revoked: true(+revokedAt,mkClientId: null) ⇒ invalidate the mapping locally, stop uploading for this pseudonym. Triggers: manual reset of the link, deletion of the patient, or consent withdrawal. - Use
nextSinceassinceon the next poll (cursor). The cursor is deterministic (secondary sort on the pseudonym) so equal timestamps cannot cause an infinite loop.
Rejected pseudonyms — GET /api/v1/rejected-clients
Authenticated. Returns the full, deduplicated list of all currently rejected pseudonyms of the practice. The client caches it and skips these pseudonyms on the next export.
Response 200:
{ "rejected": [ { "pseudonym": "a1b2…" }, { "pseudonym": "f5e6…" } ] }
An empty list = nothing rejected (no 404). Content is identical to skip_pseudonyms[] from the import response — both are the same source.
Rejection lifecycle (portal-side): A staff member rejects a pseudonym → its existing pending entries become rejected, and newly arriving measurements for that pseudonym are automatically buffered as rejected (not pending). Reactivating resets them to pending.