Publish the passkey list and the base-token 422

GET /api/auth/passkeys was absent from the contract entirely — no path
entry, no schema — while the client teams had been pointed at a published
schema for it since v1.32.8. It is now registered with the shape the route
actually serves: id, name, credentialDeviceType, credentialBackedUp,
createdAt, and the nullable lastUsedAt that started this. The route itself
is unchanged; only the record of it was missing. PasskeyInfo is deliberately
separate from WebauthnMfaCredentialInfo — one lists sign-in credentials, the
other second-factor security keys, and merging them would blur two surfaces
that behave differently. The route test now compares the projection the
route asks Prisma for against the published property set, so neither end can
move without the other.

`invalid_base_updated_at` existed only in route tests. It is now on all
eight published endpoints that accept a `baseUpdatedAt` body token, and the
field description says the thing implementers keep getting wrong: an
unparseable token, `null` included, is rejected with 422 rather than falling
back to the unconditional write. Omitting the key is the only way to get
that write.

/api/auth/me/health-score-config also returns this 422 but is not in the
spec at all; publishing that route is a separate piece of work.
This commit is contained in:
Marc-André Bombeck committed 2026-08-01 11:49:40 +02:00
1 parent 43c591fe19
commit 6a69bad05e
9 files changed
+317 -83

No files matched your search

+194 -81
View File
@@ -2196,6 +2196,24 @@ paths:
"401": *a1
"422": *a3
"429": *a4
/api/auth/passkeys:
get:
tags:
- Auth
summary: List the caller's registered passkeys
description: The passkeys registered for primary sign-in, newest first. Distinct from the second-factor security keys in
`GET /api/auth/2fa/status` (`webauthn`) — a passkey replaces the password, a security key is asserted after one.
`lastUsedAt` is stamped on every verified assertion and is null only for a passkey that has never signed in.
responses:
"200":
description: The caller's passkeys, newest first.
content:
application/json:
schema:
$ref: "#/components/schemas/PasskeyListEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/auth/me/sessions:
get:
tags:
@@ -3685,7 +3703,14 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"422": &a6
description: Request validation failed. When the body carried a `baseUpdatedAt` that could not be parsed as an ISO-8601
timestamp — including an explicit `null` — `meta.errorCode` = `invalid_base_updated_at` and nothing was
written. Omit the key entirely for the unconditional write; do not send `null` for it.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"429": *a4
delete:
tags:
@@ -4688,7 +4713,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"422": *a6
"429": *a4
/api/auth/me/disable-coach:
get:
@@ -4821,7 +4846,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"422": *a6
"429": *a4
/api/auth/me/source-priority:
get:
@@ -5847,7 +5872,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"422": *a6
"429": *a4
/api/auth/me/report-selection:
get:
@@ -6291,7 +6316,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"422": *a6
"429": *a4
delete:
tags:
@@ -6469,8 +6494,11 @@ paths:
baseUpdatedAt:
description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the
legacy unconditional write (older clients are unaffected). When present, the write is guarded on the
stored row still carrying this exact value — a stale token fails with 409 and changes nothing. Treat
as opaque: only ever echo a server-returned value, never parse or synthesise it."
stored row still carrying this exact value — a stale token fails with 409 and changes nothing. A
present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note
that this is NOT the unconditional write: sending `null` or a malformed string is rejected rather
than treated as an omitted token. Treat as opaque: only ever echo a server-returned value, never
parse or synthesise it."
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
@@ -6491,7 +6519,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"422": *a6
"429": *a4
/api/anamnesis/facts:
get:
@@ -6616,7 +6644,7 @@ paths:
schema:
$ref: "#/components/schemas/CorrectAnamnesisFactResponse"
"401": *a1
"404": &a6
"404": &a7
description: No current revision with this id exists for the authenticated account.
content:
application/json:
@@ -6652,7 +6680,7 @@ paths:
schema:
$ref: "#/components/schemas/RemoveAnamnesisFactResponse"
"401": *a1
"404": *a6
"404": *a7
"409":
description: Anamnesis fact changed since it was loaded (optimistic-concurrency conflict). No write happened. Re-read
the resource, re-apply the user's change against the fresh state, and resend with the new token.
@@ -7783,9 +7811,11 @@ paths:
- in: query
name: type
schema:
description: For VITALS_BASELINE only — the single vital to band (defaults to RESTING_HEART_RATE). Ignored by
composites. An unsupported value yields an `insufficient` value rather than a 422 so iOS metric
combinations stay forgiving.
description: The single measurement type a baseline metric works over. `VITALS_BASELINE` takes one of the eleven vitals
(`RESTING_HEART_RATE` through `WEIGHT`; defaults to `RESTING_HEART_RATE`). `SAME_TIME_BASELINE` takes one
of the four cumulative day metrics — `ACTIVITY_STEPS`, `ACTIVE_ENERGY_BURNED`, `WALKING_RUNNING_DISTANCE`,
`FLIGHTS_CLIMBED`. Ignored by the composite metrics. A type the named metric does not support yields an
`insufficient` value rather than a 422, so client metric combinations stay forgiving.
type: string
enum:
- RESTING_HEART_RATE
@@ -7799,9 +7829,15 @@ paths:
- BLOOD_GLUCOSE
- PULSE
- WEIGHT
description: For VITALS_BASELINE only — the single vital to band (defaults to RESTING_HEART_RATE). Ignored by
composites. An unsupported value yields an `insufficient` value rather than a 422 so iOS metric combinations
stay forgiving.
- ACTIVITY_STEPS
- ACTIVE_ENERGY_BURNED
- WALKING_RUNNING_DISTANCE
- FLIGHTS_CLIMBED
description: The single measurement type a baseline metric works over. `VITALS_BASELINE` takes one of the eleven vitals
(`RESTING_HEART_RATE` through `WEIGHT`; defaults to `RESTING_HEART_RATE`). `SAME_TIME_BASELINE` takes one of
the four cumulative day metrics — `ACTIVITY_STEPS`, `ACTIVE_ENERGY_BURNED`, `WALKING_RUNNING_DISTANCE`,
`FLIGHTS_CLIMBED`. Ignored by the composite metrics. A type the named metric does not support yields an
`insufficient` value rather than a 422, so client metric combinations stay forgiving.
responses:
"200":
description: The flat derived-metric value (ok or insufficient).
@@ -7830,12 +7866,16 @@ paths:
minLength: 1
maxLength: 1024
description: Comma-separated derived-metric tokens. Each is a `<DERIVED_METRIC_ID>` or
`<DERIVED_METRIC_ID>:<MeasurementType>` (the colon sub-targets a VITALS_BASELINE vital). An unknown id
422s; a `type` outside the MeasurementType enum 422s; at most 24 tokens; duplicates collapse.
`<DERIVED_METRIC_ID>:<MeasurementType>` (the colon sub-targets the single measurement type a baseline
metric works over — a vital for `VITALS_BASELINE`, a cumulative day metric for `SAME_TIME_BASELINE`; see
the `type` parameter on `GET /api/insights/derived`). An unknown id 422s; a `type` outside the
MeasurementType enum 422s; at most 24 tokens; duplicates collapse.
required: true
description: Comma-separated derived-metric tokens. Each is a `<DERIVED_METRIC_ID>` or
`<DERIVED_METRIC_ID>:<MeasurementType>` (the colon sub-targets a VITALS_BASELINE vital). An unknown id 422s;
a `type` outside the MeasurementType enum 422s; at most 24 tokens; duplicates collapse.
`<DERIVED_METRIC_ID>:<MeasurementType>` (the colon sub-targets the single measurement type a baseline metric
works over — a vital for `VITALS_BASELINE`, a cumulative day metric for `SAME_TIME_BASELINE`; see the `type`
parameter on `GET /api/insights/derived`). An unknown id 422s; a `type` outside the MeasurementType enum
422s; at most 24 tokens; duplicates collapse.
responses:
"200":
description: The map of derived-metric values, keyed by token.
@@ -8222,7 +8262,7 @@ paths:
schema:
$ref: "#/components/schemas/MoodCustomTagPatchEnvelope"
"401": *a1
"404": &a7
"404": &a8
description: Not found / not owned by the caller.
content:
application/json:
@@ -8257,7 +8297,7 @@ paths:
schema:
$ref: "#/components/schemas/MoodCustomTagDeleteEnvelope"
"401": *a1
"404": *a7
"404": *a8
"422": *a3
"429": *a4
/api/mood/tags/{key}/hidden:
@@ -8298,7 +8338,7 @@ paths:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"401": *a1
"404": *a7
"404": *a8
"422": *a3
"429": *a4
/api/mood/tags/groups:
@@ -8373,7 +8413,7 @@ paths:
schema:
$ref: "#/components/schemas/MoodTagGroupPatchEnvelope"
"401": *a1
"404": *a7
"404": *a8
"422": *a3
"429": *a4
delete:
@@ -8404,7 +8444,7 @@ paths:
schema:
$ref: "#/components/schemas/MoodTagGroupDeleteEnvelope"
"401": *a1
"404": *a7
"404": *a8
"422": *a3
"429": *a4
/api/mood/tags/layout:
@@ -8452,7 +8492,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"422": *a6
"429": *a4
/api/settings/reminder-thresholds:
get:
@@ -8648,7 +8688,7 @@ paths:
schema:
$ref: "#/components/schemas/MeasurementReminderEnvelope"
"401": *a1
"404": &a8
"404": &a9
description: Measurement reminder not found / not owned.
content:
application/json:
@@ -8682,7 +8722,7 @@ paths:
schema:
$ref: "#/components/schemas/MeasurementReminderPatchEnvelope"
"401": *a1
"404": *a8
"404": *a9
"422": *a3
"429": *a4
delete:
@@ -8704,7 +8744,7 @@ paths:
schema:
$ref: "#/components/schemas/MeasurementReminderDeleteEnvelope"
"401": *a1
"404": *a8
"404": *a9
"422": *a3
"429": *a4
/api/measurement-reminders/{id}/satisfy:
@@ -8728,7 +8768,7 @@ paths:
schema:
$ref: "#/components/schemas/MeasurementReminderSatisfyEnvelope"
"401": *a1
"404": *a8
"404": *a9
"422": *a3
"429": *a4
/api/measurement-reminders/{id}/complete:
@@ -8755,7 +8795,7 @@ paths:
schema:
$ref: "#/components/schemas/MeasurementReminderCompleteEnvelope"
"401": *a1
"404": *a8
"404": *a9
"422": *a3
"429": *a4
/api/labs:
@@ -8920,7 +8960,7 @@ paths:
schema:
$ref: "#/components/schemas/GetLabResultResponse"
"401": *a1
"404": &a9
"404": &a10
description: Lab result not found (or owned by another user).
content:
application/json:
@@ -8998,7 +9038,7 @@ paths:
schema:
$ref: "#/components/schemas/UpdateLabResultResponse"
"401": *a1
"404": *a9
"404": *a10
"422": *a3
"429": *a4
delete:
@@ -9020,7 +9060,7 @@ paths:
schema:
$ref: "#/components/schemas/DeleteLabResultResponse"
"401": *a1
"404": *a9
"404": *a10
"422": *a3
"429": *a4
/api/labs/restore:
@@ -9237,7 +9277,7 @@ paths:
schema:
$ref: "#/components/schemas/CreateBiomarkerResponse"
"401": *a1
"409": &a11
"409": &a12
description: A biomarker with this name already exists for the caller.
content:
application/json:
@@ -9265,7 +9305,7 @@ paths:
schema:
$ref: "#/components/schemas/GetBiomarkerResponse"
"401": *a1
"404": &a10
"404": &a11
description: Biomarker not found (or owned by another user).
content:
application/json:
@@ -9332,8 +9372,8 @@ paths:
schema:
$ref: "#/components/schemas/UpdateBiomarkerResponse"
"401": *a1
"404": *a10
"409": *a11
"404": *a11
"409": *a12
"422": *a3
"429": *a4
delete:
@@ -9356,7 +9396,7 @@ paths:
schema:
$ref: "#/components/schemas/DeleteBiomarkerResponse"
"401": *a1
"404": *a10
"404": *a11
"422": *a3
"429": *a4
/api/illness/episodes:
@@ -9481,7 +9521,7 @@ paths:
schema:
$ref: "#/components/schemas/GetIllnessEpisodeEnvelope"
"401": *a1
"404": &a12
"404": &a13
description: Illness episode not found (or owned by another user).
content:
application/json:
@@ -9559,7 +9599,7 @@ paths:
schema:
$ref: "#/components/schemas/UpdateIllnessEpisodeEnvelope"
"401": *a1
"404": *a12
"404": *a13
"422": *a3
"429": *a4
delete:
@@ -9582,7 +9622,7 @@ paths:
schema:
$ref: "#/components/schemas/DeleteIllnessEpisodeEnvelope"
"401": *a1
"404": *a12
"404": *a13
"422": *a3
"429": *a4
/api/illness/episodes/{id}/restore:
@@ -9608,7 +9648,7 @@ paths:
schema:
$ref: "#/components/schemas/RestoreIllnessEpisodeEnvelope"
"401": *a1
"404": *a12
"404": *a13
"422": *a3
"429": *a4
/api/illness/episodes/{id}/resolve:
@@ -9644,7 +9684,7 @@ paths:
schema:
$ref: "#/components/schemas/ResolveIllnessEpisodeEnvelope"
"401": *a1
"404": *a12
"404": *a13
"422": *a3
"429": *a4
/api/illness/episodes/{id}/day-logs:
@@ -9694,7 +9734,7 @@ paths:
schema:
$ref: "#/components/schemas/GetIllnessDayLogEnvelope"
"401": *a1
"404": *a12
"404": *a13
"422": *a3
"429": *a4
post:
@@ -9773,7 +9813,7 @@ paths:
schema:
$ref: "#/components/schemas/UpsertIllnessDayLogCreatedEnvelope"
"401": *a1
"404": *a12
"404": *a13
"422": *a3
"429": *a4
/api/illness/episodes/{id}/correlation:
@@ -9800,7 +9840,7 @@ paths:
schema:
$ref: "#/components/schemas/IllnessCorrelationEnvelope"
"401": *a1
"404": *a12
"404": *a13
"422": *a3
"429": *a4
/api/illness/insights:
@@ -10068,7 +10108,7 @@ paths:
schema:
$ref: "#/components/schemas/GetAllergyEnvelope"
"401": *a1
"404": &a13
"404": &a14
description: Allergy not found (or owned by another user).
content:
application/json:
@@ -10150,7 +10190,7 @@ paths:
schema:
$ref: "#/components/schemas/UpdateAllergyEnvelope"
"401": *a1
"404": *a13
"404": *a14
"422": *a3
"429": *a4
delete:
@@ -10173,7 +10213,7 @@ paths:
schema:
$ref: "#/components/schemas/DeleteAllergyEnvelope"
"401": *a1
"404": *a13
"404": *a14
"422": *a3
"429": *a4
/api/family-history:
@@ -10278,7 +10318,7 @@ paths:
schema:
$ref: "#/components/schemas/GetFamilyHistoryEnvelope"
"401": *a1
"404": &a14
"404": &a15
description: Family history entry not found (or owned by another user).
content:
application/json:
@@ -10346,7 +10386,7 @@ paths:
schema:
$ref: "#/components/schemas/UpdateFamilyHistoryEnvelope"
"401": *a1
"404": *a14
"404": *a15
"422": *a3
"429": *a4
delete:
@@ -10369,7 +10409,7 @@ paths:
schema:
$ref: "#/components/schemas/DeleteFamilyHistoryEnvelope"
"401": *a1
"404": *a14
"404": *a15
"422": *a3
"429": *a4
/api/documents/inbound:
@@ -11478,7 +11518,7 @@ paths:
schema:
$ref: "#/components/schemas/CreateCustomMetricResponse"
"401": *a1
"409": &a16
"409": &a17
description: A custom metric with this name already exists for the caller.
content:
application/json:
@@ -11506,7 +11546,7 @@ paths:
schema:
$ref: "#/components/schemas/GetCustomMetricResponse"
"401": *a1
"404": &a15
"404": &a16
description: Custom metric not found (or owned by another user).
content:
application/json:
@@ -11570,8 +11610,8 @@ paths:
schema:
$ref: "#/components/schemas/UpdateCustomMetricResponse"
"401": *a1
"404": *a15
"409": *a16
"404": *a16
"409": *a17
"422": *a3
"429": *a4
delete:
@@ -11594,7 +11634,7 @@ paths:
schema:
$ref: "#/components/schemas/DeleteCustomMetricResponse"
"401": *a1
"404": *a15
"404": *a16
"422": *a3
"429": *a4
/api/custom-metrics/{id}/entries:
@@ -11637,7 +11677,7 @@ paths:
schema:
$ref: "#/components/schemas/ListCustomMetricEntriesEnvelope"
"401": *a1
"404": *a15
"404": *a16
"422": *a3
"429": *a4
post:
@@ -11681,7 +11721,7 @@ paths:
schema:
$ref: "#/components/schemas/CreateCustomMetricEntryResponse"
"401": *a1
"404": *a15
"404": *a16
"422": *a3
"429": *a4
/api/custom-metrics/{id}/entries/{entryId}:
@@ -11731,7 +11771,7 @@ paths:
schema:
$ref: "#/components/schemas/UpdateCustomMetricEntryResponse"
"401": *a1
"404": &a17
"404": &a18
description: Custom metric entry not found (or owned by another user).
content:
application/json:
@@ -11763,7 +11803,7 @@ paths:
schema:
$ref: "#/components/schemas/DeleteCustomMetricEntryResponse"
"401": *a1
"404": *a17
"404": *a18
"422": *a3
"429": *a4
/api/onboarding/disclaimer:
@@ -11899,7 +11939,7 @@ paths:
schema:
$ref: "#/components/schemas/NutrientBatchEnvelope"
"401": *a1
"403": &a18
"403": &a19
description: The opt-in `nutrients` module is off for this account (errorCode `module.disabled`). Stop syncing and do
not retry; offer the enable-in-settings hint. Only run the HealthKit read-authorization flow while the
module is on.
@@ -11933,7 +11973,7 @@ paths:
schema:
$ref: "#/components/schemas/NutrientOverviewEnvelope"
"401": *a1
"403": *a18
"403": *a19
"422": *a3
"429": *a4
/api/nutrients/daily:
@@ -11969,7 +12009,7 @@ paths:
schema:
$ref: "#/components/schemas/NutrientDailySeriesEnvelope"
"401": *a1
"403": *a18
"403": *a19
"422": *a3
"429": *a4
/api/nutrients/water:
@@ -11996,7 +12036,7 @@ paths:
schema:
$ref: "#/components/schemas/NutrientWaterWriteEnvelope"
"401": *a1
"403": *a18
"403": *a19
"422": *a3
"429": *a4
/api/integrations/healthkit:
@@ -12271,7 +12311,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"422": *a6
"429": *a4
delete:
tags:
@@ -13297,8 +13337,10 @@ components:
baseUpdatedAt:
description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the
legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored
row still carrying this exact value — a stale token fails with 409 and changes nothing. Treat as opaque:
only ever echo a server-returned value, never parse or synthesise it."
row still carrying this exact value — a stale token fails with 409 and changes nothing. A
present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that
this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as
an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it."
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
@@ -13772,8 +13814,10 @@ components:
baseUpdatedAt:
description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the
legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored
row still carrying this exact value — a stale token fails with 409 and changes nothing. Treat as opaque:
only ever echo a server-returned value, never parse or synthesise it."
row still carrying this exact value — a stale token fails with 409 and changes nothing. A
present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that
this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as
an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it."
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
@@ -13836,8 +13880,10 @@ components:
baseUpdatedAt:
description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the
legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored
row still carrying this exact value — a stale token fails with 409 and changes nothing. Treat as opaque:
only ever echo a server-returned value, never parse or synthesise it."
row still carrying this exact value — a stale token fails with 409 and changes nothing. A
present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that
this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as
an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it."
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
@@ -14000,8 +14046,10 @@ components:
baseUpdatedAt:
description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the
legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored
row still carrying this exact value — a stale token fails with 409 and changes nothing. Treat as opaque:
only ever echo a server-returned value, never parse or synthesise it."
row still carrying this exact value — a stale token fails with 409 and changes nothing. A
present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that
this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as
an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it."
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
@@ -14207,8 +14255,10 @@ components:
baseUpdatedAt:
description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the
legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored
row still carrying this exact value — a stale token fails with 409 and changes nothing. Treat as opaque:
only ever echo a server-returned value, never parse or synthesise it."
row still carrying this exact value — a stale token fails with 409 and changes nothing. A
present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that
this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as
an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it."
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
@@ -14445,8 +14495,10 @@ components:
baseUpdatedAt:
description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the
legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored
row still carrying this exact value — a stale token fails with 409 and changes nothing. Treat as opaque:
only ever echo a server-returned value, never parse or synthesise it."
row still carrying this exact value — a stale token fails with 409 and changes nothing. A
present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that
this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as
an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it."
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
@@ -15027,8 +15079,10 @@ components:
baseUpdatedAt:
description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the
legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored
row still carrying this exact value — a stale token fails with 409 and changes nothing. Treat as opaque:
only ever echo a server-returned value, never parse or synthesise it."
row still carrying this exact value — a stale token fails with 409 and changes nothing. A
present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that
this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as
an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it."
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
@@ -17344,6 +17398,65 @@ components:
- data
- error
additionalProperties: false
PasskeyListEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/PasskeyListResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
PasskeyListResponse:
type: array
items:
$ref: "#/components/schemas/PasskeyInfo"
PasskeyInfo:
type: object
properties:
id:
type: string
description: Row id; the handle for DELETE.
name:
type: string
description: User-supplied label, defaulting to “Passkey”.
credentialDeviceType:
type: string
description: "Authenticator device type as reported at registration: `singleDevice` for a credential bound to one
authenticator, `multiDevice` for a syncable one."
credentialBackedUp:
type: boolean
description: Whether the authenticator reported the credential as backed up (synced to the platform keychain) at
registration time. Not re-read afterwards.
createdAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
description: Registered at.
lastUsedAt:
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: When this passkey last completed a verified assertion; null when it has never been used to sign in. Stamped
on every successful login, so a client can render a real 'last used' rather than assuming never.
required:
- id
- name
- credentialDeviceType
- credentialBackedUp
- createdAt
- lastUsedAt
additionalProperties: false
SessionListEnvelope:
type: object
properties: