Overview
# YesCash Authentication API
This OpenAPI file is the partner-facing contract for ctomer authentication.
It covers:
- login;
- registration;
- token refresh;
- logout;
- password reset;
- identifier management;
- device registration;
- MFA response handling where applicable;
- authentication error responses.
Partner applications e tokens returned by this API to call the Core API.
Base URL:
```text
https://api.yes.cash
```
Servers
https://api.yes.cash — Production API host.
Endpoint Reference
POST /v1/auth/start
Initiate ctomer login (step 1 of 2).
First step of the two-step ctomer-login flow per `§A.2.1`. The
ctomer submits an identifier (email or phone); the principal
resolves the identifier against the per-YesCash
`iam.IdentifierRegistry` and returns a `loginAttemptId` opaque
session reference. The response is uniform across the three
Identifier Registry resolution outcomes per `§A.2.1.2`
neutral-response posture (defeats identifier enumeration). Timing
is normalised at the BFF.
Anonymo on the ctomer side (APIM Posture A per ADR 20 v2.3.4
`§D3.7.8`). Partner context provided via APIM subscription key;
APIM derives `X-Client-Id` server-side per Architecture `§14.3`.
No password is submitted at this step — the two-step separation
defeats step-1 credential validation by an attacker holding the
subscription key but not the ctomer's password.
Flow-level idempotency via `loginAttemptId`; no header-based
idempotency key per FM.5.2.
Parameters
Request body
application/json: AuthStartRequest
Responses
POST /v1/auth/login
Complete ctomer login (step 2 of 2; also MFA challenge / enrolment continuation).
Second step of the two-step ctomer-login flow per `§A.2.2`. The
ctomer-app presents the `loginAttemptId` (from `/auth/start`)
and the ctomer's password; the BFF resolves the login-attempt
session, calls Cognito `InitiateAuth`, and on credential-match
returns the token bundle.
**Three call shapes — `authStat` discrimination per `§A.12.7.a`:**
- **Initial credential submission** — `loginAttemptId` + `password`.
Response shape `AUTHENTICATED` (token bundle) or
`MFA_CHALLENGE_REQUIRED` (MFA challenge) or
`MFA_ENROLMENT_REQUIRED` (enrolment required).
- **MFA challenge completion** — `loginAttemptId` +
`mfaChallengeId` + `mfaCode` + `mfaMethod`. Same
`loginAttemptId` from the prior call.
- **MFA enrolment commit** — `loginAttemptId` +
`mfaEnrolmentSessionId` + `mfaEnrolmentMethod` +
`mfaEnrolmentCode`.
Generic-rejection family discipline per Charter `§11` row 2: a
non-existent identifier and an incorrect password BOTH surface as
`auth.credentialMismatch` (AML-tipping-off framework). No
sub-codes are emitted to distinguish the two cases.
Cognito-side state is consulted read-only at the first call;
`Subscription` allocation occurs atomically with the
`AUTHENTICATED` response per `§A.2.2.3`.
Anonymo on the ctomer side (APIM Posture A). Flow-level
idempotency via `loginAttemptId`; per-`loginAttemptId` 1-attempt
limit per Step 6a Q2.
Parameters
Request body
application/json: AuthLoginRequest
Responses
POST /v1/auth/refresh
Refresh the access token.
Mint a fresh access-token ing the current refresh-token. APIM
Posture B per ADR 20 v2.3.4 `§D3.7.8` — the JWT cryptographic
integrity validation is performed but the expiry check is skipped;
the ctomer-app MT present an access-token (expired or
near-expiry) in the `Authorization` header as the carrier for
APIM's `X-Client-Id` derivation.
Refresh-token rotation per `§A.3.1`: on success the response
contains a rotated refresh-token; the prior refresh-token is
invalidated at Cognito atomically with the rotation. The partner-
app MT persist the rotated value.
Ctomer-app that has discarded its access-token cannot e
`/auth/refresh`; the ctomer mt re-authenticate from
`/auth/start`.
Parameters
Request body
application/json: AuthRefreshRequest
Responses
POST /v1/auth/logout
Revoke the current ctomer session.
Per-session refresh-token revocation per `§A.3.3` and ADR 20
v2.3.4 `§10` entry 3 leading recommendation (Cognito-side
revocation semantic S2). APIM Posture C — a valid (non-expired)
access-token is required. Ctomer-app recovery semantic for
expired access-token per `§A.3.4`: call `/auth/refresh` first to
mint a fresh access-token, then retry `/auth/logout`.
Effect: the presented refresh-token is invalidated at Cognito.
Other refresh-tokens for the same `MasterCtomer` (other devices,
other partner-apps) REMAIN VALID. The ctomer's mental model is
"log out of this app on this device" — matched by the S2
semantic. Administrative global sign-out is a separate
operations-mediated path per `§A.3.5`.
Parameters
Request body
application/json: AuthLogoutRequest
Responses
POST /v1/auth/register/start
Initiate ctomer-direct registration (step 1 of 3).
First step of the ctomer-direct registration flow per `§A.4`.
The ctomer submits an identifier (email or phone); the principal
dispatches a 6-digit OTP to that identifier and returns a
`registrationId` opaque session reference.
Anonymo endpoint (APIM Posture A); partner context via APIM
subscription key. Neutral-response posture per `§A.4.2` — the
response shape is uniform across the three Identifier Registry
resolution outcomes (defeats identifier enumeration).
The OTP carrier-side branding per `§A.4.5` preserves Charter `§11`
row 5 cosmetic-separation defence: from-display carries the YCMT
entity name; partner brand appears in body content for cohesion.
Flow-level idempotency via `registrationId`; no header-based
idempotency key per FM.5.2.
Parameters
Request body
application/json: RegisterStartRequest
Responses
POST /v1/auth/register/verify-otp
Verify registration OTP (step 2 of 3).
Second step of the ctomer-direct registration flow per `§A.5`.
The ctomer-app presents the `registrationId` and the OTP
delivered to the ctomer's identifier; on OTP success the BFF
returns a `branch` discriminator (`NEW_CTOMER` /
`EXISTING_CTOMER`) per `§A.5.3` branch-keying, directing the
partner-app's `/register/set-password` UI flow.
Branch keying is on Cognito-credential existence per `§A.5.3`,
NOT on `MasterCtomer` existence — a skeletal `MasterCtomer`
(post-abandoned-flow) routes back to `NEW_CTOMER`.
Branch disclosure is gated on OTP success per ADR 20 v2.3.4
`§D3.7.1` (post-OTP-ownership-proof); does not weaken the
`§A.4.2` neutral-response defence at the prior step.
Flow-level idempotency via `registrationId` (single-e after
success); per-`registrationId` 3-attempt OTP counter per
`§A.5.5`.
Parameters
Request body
application/json: RegisterVerifyOtpRequest
Responses
POST /v1/auth/register/set-password
Complete registration by setting / verifying password (step 3 of 3).
Final step of the ctomer-direct registration flow per `§A.6`.
The endpoint path is `/v1/auth/register/set-password` per
`§A.6.1` wire convention and Appendix A wire traces; this path
supersedes the `§A.0.3` section-label editorial title
"Registration — complete".
**Branch behaviour per `§A.6.2` / `§A.6.3`:**
- `NEW_CTOMER` branch: validates password against YesCash
policy `§A.6.5`; creates Cognito credential; creates
`MasterCtomer` (or rebinds skeletal `MasterCtomer` per
`§A.6.7`); creates `Subscription` atomically; emits token
bundle. `ctomerStat = PENDING` at issuance (KYC not yet
completed).
- `EXISTING_CTOMER` branch: verifies password against existing
Cognito credential; on success creates or resolves
`Subscription` for the `(MasterCtomer, App Client)` pair;
emits token bundle. On `auth.credentialMismatch` the response
may include the `details.next = PASSWORD_FORGOT` carry-forward
hint per `§A.6.4`.
Idempotency: per-`registrationId` (single-e after success);
`X-Idempotency-Key` header SHOULD be supplied per FM.5 for the
partner-app's retry policy; absence is acceptable per FM.5.2
anonymo-ctomer-driven-flow carve-out. Server-side dedup is
keyed on `(clientId, idempotencyKey, endpoint)` when the header
is present.
Parameters
Request body
application/json: RegisterSetPasswordRequest
Responses
GET /v1/auth/identifiers
List the authenticated ctomer's active identifiers.
Returns the currently-active identifier set for the authenticated
`MasterCtomer` per `§A.8.2`. Ctomer-context bearer token
required (APIM Posture C).
The identifier set is `MasterCtomer`-scoped at the YesCash
level per `§A.7.6` — the same set is visible to any partner-app
whose ctomer-app authenticates the same `MasterCtomer`.
Activity history is partner-scoped via `clientId` audit-event
attribution (`identifier.list_accessed` carries the calling
`clientId`).
Identifier-management-context surface; structurally distinct from
the Part B `/v1/core/profile` endpoint per `§A.8.2.2` — `canRemove`
is identifier-management-specific and has no place in profile read.
Safe read; no idempotency key.
Parameters
Responses
POST /v1/auth/identifiers/add/start
Initiate identifier-add (step 1 of 2).
First step of the two-step OTP-protected identifier-add per
`§A.8.3`. The ctomer-app submits a new identifier; the
principal evaluates against the per-YesCash
`iam.IdentifierRegistry` per the distinguishable-rejection posture
of `§A.8.3.2` Q2:
- Identifier already owned by the authenticated `MasterCtomer`
→ `validation.identifierAlreadyOwned` (400).
- Identifier owned by a DIFFERENT `MasterCtomer` →
`validation.identifierNotAvailable` (400; neutral wording per
AML-tipping-off framework — no disclosure of the other
ctomer's existence).
- Identifier not yet owned → OTP dispatched to the new identifier;
`identifierAddId` returned.
Ctomer-context (APIM Posture C). Flow-level idempotency via
`identifierAddId`.
Parameters
Request body
application/json: IdentifierAddStartRequest
Responses
POST /v1/auth/identifiers/add/verify-otp
Complete identifier-add (step 2 of 2).
Verifies the OTP dispatched at `/identifiers/add/start` and writes
the new identifier to `iam.IdentifierRegistry` bound to the
authenticated `MasterCtomer` per `§A.8.4`. The
`iam.IdentifierRegistry` per-YesCash uniqueness constraint is
enforced at write time as defence-in-depth against staging-vs-
commit race conditions.
Ctomer-context (APIM Posture C). Flow-level idempotency via
`identifierAddId` (single-e after success); per-`identifierAddId`
3-attempt OTP counter.
Parameters
Request body
application/json: IdentifierAddVerifyOtpRequest
Responses
POST /v1/auth/identifiers/{identifierId}/remove/start
Initiate identifier-remove (step 1 of 2).
First step of the two-step OTP-protected identifier-remove per
`§A.8.5`. The ctomer-app specifies (via `otpDeliveryIdentifierId`)
which of the ctomer's OTHER active identifiers receives the
verification OTP — the ctomer proves continued control of an
identifier they are RETAINING per the substantive remove-scenario
framing.
At-least-one-active-identifier invariant per `§A.8.1 Q4`:
if the path-parameter `identifierId` is the ctomer's only active
identifier, returns `validation.identifierLastActiveCannotBeRemoved`
(400). Ctomer directed to `§A.10` recovery posture for sole-
identifier-lost cases.
Ctomer-context (APIM Posture C). Flow-level idempotency via
`identifierRemoveId`.
Parameters
Request body
application/json: IdentifierRemoveStartRequest
Responses
POST /v1/auth/identifiers/{identifierId}/remove/verify-otp
Complete identifier-remove (step 2 of 2).
Verifies the OTP dispatched at `/identifiers/{identifierId}/remove/start`
and deactivates the path-parameter identifier in
`iam.IdentifierRegistry` per `§A.8.6`. Defence-in-depth: at commit
time the at-least-one-active-identifier invariant is re-checked;
a rare race condition where another concurrent flow leaves the
path-parameter identifier as the sole active one returns
`validation.identifierLastActiveCannotBeRemoved` (400).
Ctomer-context (APIM Posture C). Flow-level idempotency via
`identifierRemoveId`.
Parameters
Request body
application/json: IdentifierRemoveVerifyOtpRequest
Responses
POST /v1/auth/password/forgot
Initiate password reset (step 1 of 2).
First step of the two-step OTP-protected password reset per
`§A.9.2`. The ctomer-app submits an identifier (and optionally
a carry-forward `registrationId` from a prior
`auth.credentialMismatch` rejection); the principal dispatches a
6-digit OTP to the ctomer's verified identifier and returns an
opaque `passwordResetId`.
Anonymo endpoint (APIM Posture A); partner context via APIM
subscription key. Neutral-response posture per `§A.9.2.2` —
response shape uniform across the three Identifier Registry
resolution outcomes. On the not-resolved path the BFF performs a
timing-equalised no-op dispatch.
Carry-forward path per `§A.9.2.3`: when `registrationId` is
present and valid, the principal binds the `passwordResetId` to
the same `MasterCtomer`, places the `registrationId` in
paed-TTL state, and (on successful `/password/reset`)
reactivates with refreshed 30-min TTL.
Flow-level idempotency via `passwordResetId`; no header-based
idempotency key per FM.5.2.
Parameters
Request body
application/json: PasswordForgotRequest
Responses
POST /v1/auth/password/reset
Complete password reset (step 2 of 2).
Verifies the OTP dispatched at `/password/forgot`, validates the
new password against the YesCash password policy per `§A.6.5`,
and sets the new password at Cognito per `§A.9.3`.
On success, the BFF invokes Cognito `AdminerGlobalSignOut` per
`§A.9.3.5` Q8 — ALL refresh-tokens for the `MasterCtomer`
across all partner-app sessions are invalidated. The ctomer's
first post-reset authentication produces a fresh session bound to
the fresh credential.
The endpoint does NOT issue new tokens at success — the response
directs the ctomer-app to re-authenticate via `/auth/start` +
`/auth/login` (standalone path; `next = LOGIN`) or to re-present
at `/register/set-password` with the reactivated `registrationId`
and fresh password (carry-forward path; `next = REGISTER_COMPLETE`).
Anonymo endpoint (APIM Posture A). Flow-level idempotency via
`passwordResetId`.
Parameters
Request body
application/json: PasswordResetRequest
Responses
POST /v1/auth/device-registration/start
Initiate device-registration ceremony (step 1 of 2).
First step of device-bound authentication registration per
`§A.11.2`. The ctomer-app initiates a device-registration
ceremony; the principal generates a fresh `registrationChallenge`
(32 bytes cryptographically-random; 5-minute TTL) and returns it
pl `publicKeyRequirements` to the ctomer-app. The ctomer-
app's device generates an RSA key-pair locally (private key into
OS keystore; public key in memory for transmission at `/complete`)
and signs the registration proof JWS over the challenge pl the
public-key thumbprint.
**For a ctomer with an existing active registration**: returns
`202 Accepted` with `stepUpRequired = LIVENESS_CHECK` per
`§A.11.2.6`. The ctomer-app mt route to the liveness webview
(re-ing the `§B.4 / §B.5` KYC capture surface as a step-up
surface), then re-call `/device-registration/start` with the
resulting `stepUpToken` to receive a 201 Created with
`stepUpApplied = true`.
Ctomer-stat gating per `§A.11.2.2`:
`PENDING` / `VALID` / `MONITORED` allow; `RESTRICTED` / `BLOCKED`
/ `CLOSED` reject with `ctomer.statBlocking` (403).
Ctomer-context (APIM Posture C). Idempotency-Key required per
FM.5.
Parameters
Request body
application/json: DeviceRegistrationStartRequest
Responses
POST /v1/auth/device-registration/complete
Complete device-registration with proof-of-possession (step 2 of 2).
Submits the freshly-generated RSA public key together with the
`DeviceRegistrationProof` JWS signed by the corresponding device
private key per `§A.11.3`. The proof binds to the
`registrationChallenge` issued at `/start` pl the RFC 7638
SHA-256 JWK thumbprint of the submitted public key per App C
`§C.2`.
**Cryptographic discipline — RS256 + RSA-2048 minimum.** Per App
C `§C.0.2` + ADR 25 v1.1 `§D5.1` + FAPI 1.0 Baseline + NIST SP
800-131A. v1 closes on RS256 only; ES256 / EdDSA are forward-
watched at v1.1. **The Step 23 Appendix B flow B.3 step 5 erratum
is corrected by App C `§C.3` corrective reference and by the
`DeviceRegistrationProof` schema description in this document.**
Verification chain (principal-side) per App C `§C.2.7` — five
steps: header discipline → public-key acceptance → self-signature
verification → challenge binding → thumbprint binding. Each step
produces a specific error code on failure per `§A.14.2.3`.
On success: the public key is stored against
`(MasterCtomerId, ctomer_device_id)` per ADR 25 `§D5`; the
principal-allocated `deviceId` (ULID, `DEV-` prefix) is returned;
the partner-app es this `deviceId` in subsequent `§B.12`
transfer-confirmation calls. The verbatim `registrationProof`
JWS is preserved in the audit trail per `§A.13.5`.
Ctomer-stat gating identical to `/start` per `§A.11.3.2`.
Idempotency-Key required per FM.5.
Parameters
Request body
application/json: DeviceRegistrationCompleteRequest
Responses
Schemas
Error
The canonical error envelope per FM.6.1. Every 4xx and 5xx response on every
Part A endpoint follows this shape. The `error.code` value is the partner-
consumed contract surface; `error.message` is partner-side diagnostic text
in English (ctomer-facing localisation is partner-app responsibility);
`error.correlationId` mirrors the `X-Correlation-Id` response header for
partner log-correlation when intermediate infrastructure strips headers;
`error.details` carries code-specific structured detail per `§A.14`.
| Field | Required | Type | Description |
error | Yes | object | |
ValidationIssue
Per-field validation issue per FM.6.5. The summary code on the envelope
is one of `validation.fieldInvalid`, `validation.fieldMissing`,
`validation.bodyMalformed`, `validation.contentTypeUnsupported`; the
per-field issue carries the specific issue code drawn from the
Part-A-specific controlled vocabulary at `§A.14.3` and
`§A.14.5.3`.
| Field | Required | Type | Description |
field | Yes | string | Field path ing dotted notation for nested fields (e.g.,
`recipient.bankAccount.iban`); array indices e bracket notation
(e.g., `fees[2].amount`). |
code | Yes | string | Specific per-field issue code. Drawn from the controlled vocabulary
at `§A.14.3` (Part-A-specific) + `§A.14.5.3` (Part-A-generic). |
message | No | string | Optional per-field diagnostic message. |
PasswordPolicyDetails
`details` shape for `validation.passwordPolicyViolation` per `§A.6.5`.
Surfaced on `POST /v1/auth/register/set-password` and
`POST /v1/auth/password/reset` when the ctomer-supplied password fails
the YesCash password policy.
| Field | Required | Type | Description |
rules | Yes | array<string> | The full set of rules in force at the YesCash. Stable for a given
YesCash; surfacing the full set helps partner-app implementers
build the ctomer-app's password-strength indicator UI. |
violations | Yes | array<string> | The subset of rules the submitted password failed. The ctomer-app
surfaces remediation guidance per the specific violations; the partner-
app does NOT see the password itself. |
OtpAttemptDetails
`details` shape for `auth.otpInvalid` carrying remaining-attempt
diagnostic per `§A.5.5` / `§A.8.4.4` / `§A.9.3.4`.
| Field | Required | Type | Description |
attemptsRemaining | No | integer | Remaining OTP-verification attempts before the per-session OTP
counter exhats (3-attempt limit per `§A.4.5`). On the final
attempt's failure, the response code transitions to
`auth.otpAttemptsExhated` or `auth.otpExhated` (the latter at
password-reset per `§A.9.3.4`) and `attemptsRemaining` is no
longer carried. |
CustomerStatusDetails
`details` shape for `ctomer.statBlocking` / `ctomer.statInsufficient`
per FM.6.7. Carries the current ctomer stat and a remediation hint
per `§B.17.3` / App G `remediation` identifier registry.
| Field | Required | Type | Description |
currentStatus | No | string | The ctomer's current matrix v4 stat. Enum values per
`§B.2 / matrix v4 §2`. |
requiredStatus | No | string | For `ctomer.statInsufficient`, the minimum stat the endpoint
requires. Omitted for `ctomer.statBlocking` (where the stat
is in an absolutely blocking state regardless of endpoint). |
remediationHint | No | string | App G `remediation` identifier per App G `§G.2`, indicating the
ctomer-UX action to surface. The partner-app's UX layer
translates the identifier to localised ctomer-facing copy. |
RateLimitDetails
`details` shape for `validation.rateLimitExceeded` (429) per FM.7.
| Field | Required | Type | Description |
scope | No | string | The rate-limit scope that breached. Per FM.7 scopes:
`partnerCtomer` `(partnerId, MasterCtomerId)`,
`partnerIp` `(partnerId, sourceIp)`,
`partnerIdentifier` `(partnerId, identifier)`,
`partnerGlobal` `(partnerId)`. |
retryAfterSeconds | No | integer | Seconds to wait before retry; mirrors `Retry-After` header. |
IdentifierType
Ctomer identifier type. Disambiguates the OTP-dispatch carrier path
(SMS vs email) and the format-validation rule (RFC 5321/5322 vs E.164).
| Field | Required | Type | Description |
Identifier
Ctomer login handle in normalised form. Email lowercase RFC 5321/5322
compliant; phone in E.164 format with leading `+`. The principal
normalises before `iam.IdentifierRegistry` lookup; partner-app SHOULD
submit pre-normalised values for predictable UX (e.g., the partner-app's
text-input field MAY enforce E.164 entry, but the BFF does not require
it — the BFF normalises and validates).
| Field | Required | Type | Description |
IdentifierMasked
Ctomer-display-safe masked form of an identifier. Email: first character
+ `****` + `@` + domain. Phone: country-code prefix + `*` digits + last
four digits. The raw identifier value is NEVER returned via the
ctomer-context API; the masked form is the ctomer-recognition signal.
| Field | Required | Type | Description |
TokenBundle
The Cognito-issued token bundle per `§A.2.2.5` / `§A.6` / `§A.3`.
Treated as opaque by partner-apps except for the documented inspection
carve-outs at FM.4.2.
| Field | Required | Type | Description |
accessToken | Yes | string | The Cognito-issued access-token JWT. ed as
`Authorization: Bearer <accessToken>` on every ctomer-context
endpoint. TTL bounded at 60 minutes by Architecture `§14.3`
invariant (ADR 20 v2.2 token-TTL invariant); concrete value
communicated via `expiresIn`. |
refreshToken | Yes | string | The Cognito-issued refresh-token. Opaque to the partner-app.
Submitted to `POST /v1/auth/refresh` to mint fresh access-tokens.
TTL operationally configured per YesCash (typically 10 days
for YesCash phase-1, bounded at 30 days by Architecture `§14.3`). |
idToken | No | string | The Cognito-issued ID-token JWT. Optional; some partner-app
integrations surface limited ctomer-identity claims from the
ID token; the partner-app's authorisation decisions remain
governed by SQL-resolved request context, NOT by ID-token claims. |
tokenType | Yes | string | |
expiresIn | Yes | integer | Access-token TTL in seconds. Per `§A.3` YesCash-operational
configuration; 3600 (60 minutes) for YesCash phase-1. |
accessTokenExpiresAt | No | string | Absolute access-token expiry timestamp; mirrors the JWT `exp`
claim. Surfaced for partner-app proactive-refresh logic that
avoids inspecting the JWT directly. |
refreshTokenExpiresAt | No | string | Absolute refresh-token expiry timestamp. |
idTokenExpiresAt | No | string | Absolute ID-token expiry timestamp. |
AuthStartRequest
Request body for `POST /v1/auth/start`. The first step of the two-step
ctomer-login flow per `§A.2.1`. Anonymo on the ctomer-side
(partner-app context provided via APIM subscription key). No password
at this step — the two-step separation defeats step-1 credential
validation by an attacker holding the subscription key but not the
ctomer password.
AuthStartResponse
Response body for `POST /v1/auth/start`. Uniform across the three
Identifier Registry resolution outcomes per `§A.2.1.2` neutral-response
posture; the response shape is identical whether the ctomer-supplied
identifier resolves to a registered `MasterCtomer`, a skeletal
`MasterCtomer`, or no `MasterCtomer`. Timing is normalised at the
BFF per `§A.2.1.2` to defeat side-channel inference.
| Field | Required | Type | Description |
loginAttemptId | Yes | string | Opaque, single-e, short-lived login-attempt session identifier
per ADR 20 v2.3.4 `§D3.7.7`. Server-side-bound to the ctomer-
supplied identifier (hashed), the App Client, the resolution
outcome at this step (not surfaced; consumed at `/auth/login`),
and `loginAttemptExpiresAt`. Format illtrative; partners treat
opaquely. |
loginAttemptExpiresAt | Yes | string | Absolute expiry timestamp. TTL 10 minutes from issuance per Step
6a Q1 decision; shorter than `registrationId`'s 30 minutes becae
login is single-action with no intermediate OTP gate. |
AuthLoginRequest
Request body for `POST /v1/auth/login`. Three legal call shapes:
(1) initial password submission (after `/auth/start`), (2) MFA challenge
completion (after a `MFA_CHALLENGE_REQUIRED` response per
`§A.12.7.a`), (3) MFA enrolment commit (after a `MFA_ENROLMENT_REQUIRED`
response per `§A.12.7.a`). The `loginAttemptId` is preserved across the
multi-call sequence per `§A.12`; the partner-app does NOT generate a
fresh `loginAttemptId` for the MFA continuation calls.
| Field | Required | Type | Description |
loginAttemptId | Yes | string | The login-attempt session identifier from `/auth/start`. |
password | No | string | Ctomer-supplied password. Required on the initial (post-`/auth/start`)
call; omitted on MFA challenge/enrolment continuation calls. The BFF
passes the password to Cognito `InitiateAuth`; the BFF does NOT
enforce password policy at this surface (policy is enforced at
`/auth/register/set-password` and `/auth/password/reset` per
`§A.6.5`). |
mfaChallengeId | No | string | Required for MFA challenge completion (after a
`MFA_CHALLENGE_REQUIRED` response). The principal-issued challenge
identifier from the prior `/auth/login` response. |
mfaCode | No | string | Required for MFA challenge completion. The MFA code from the
ctomer's enrolled factor (TOTP code from authenticator app;
SMS-MFA code dispatched out-of-band). |
mfaMethod | No | string | Required for MFA challenge completion or enrolment commit.
The factor the ctomer is ing. |
mfaEnrolmentSessionId | No | string | Required for MFA enrolment commit (after a
`MFA_ENROLMENT_REQUIRED` response). The enrolment-session
identifier from the prior `/auth/login` response. |
mfaEnrolmentMethod | No | string | Required for MFA enrolment commit. The selected enrolment factor. |
mfaEnrolmentCode | No | string | Required for MFA enrolment commit. The first TOTP code from the
authenticator app (proves the ctomer scanned the setup secret)
or the SMS-MFA code dispatched to the ctomer's enrolling phone. |
AuthLoginResponse
Response body for `POST /v1/auth/login`. The polymorphic response is
discriminated by `authStat` per `§A.12.7.a`:
- `AUTHENTICATED` — ctomer authenticated; token bundle issued
- `MFA_CHALLENGE_REQUIRED` — MFA challenge issued; partner-app collects
MFA code and re-calls `/auth/login` with `mfaChallengeId` + `mfaCode`
+ `mfaMethod`
- `MFA_ENROLMENT_REQUIRED` — MFA enrolment required; partner-app
surfaces the `supportedMethods[]` array, collects ctomer enrolment
selection and verification code, re-calls `/auth/login` with
`mfaEnrolmentSessionId` + `mfaEnrolmentMethod` + `mfaEnrolmentCode`
The partner-app MT recognise all three `authStat` values per
`§B.17.2.3` and route correctly.
| Field | Required | Type | Description |
AuthLoginResponseAuthenticated
`AUTHENTICATED` response shape per `§A.2.2.5`. The ctomer's session
is established; the token bundle is issued; the partner-scoped
`subscriptionId` is allocated or reed per `§A.2.2.3`.
| Field | Required | Type | Description |
authStatus | Yes | string | |
accessToken | Yes | string | Cognito access-token JWT per `TokenBundle.accessToken`. |
refreshToken | Yes | string | Cognito refresh-token per `TokenBundle.refreshToken`. |
idToken | No | string | Optional Cognito ID-token JWT per `TokenBundle.idToken`. |
tokenType | Yes | string | |
expiresIn | Yes | integer | |
accessTokenExpiresAt | No | string | |
refreshTokenExpiresAt | No | string | |
idTokenExpiresAt | No | string | |
subscriptionId | Yes | string | Partner-scoped ctomer reference allocated per ADR 22 v3 `§D2.6`.
Always populated. Informational on the wire per `§B.1.5` — partner-
app does NOT e `subscriptionId` in subsequent Part B URLs
(self-referential URL convention per `§B.1.4`); recorded for
partner-side logging and per-partner-app ctomer-listing. |
customerStatus | No | string | Matrix v4 ctomer stat at the moment of token issuance.
Subsequent stat reads via `GET /v1/core/profile` (Part B). |
mfaEnrolment | No | object | Present on the enrolment-commit success path per Appendix A flow
A.5; carries the jt-enrolled factor information. |
AuthLoginResponseMfaChallengeRequired
`MFA_CHALLENGE_REQUIRED` response shape per `§A.12.7.a`. The ctomer's
credentials verified; a risk signal fired (EU posture per `§A.12.4`) or
the YesCash posture requires MFA at every login ( YesCash per
`§500.12`); the partner-app collects the MFA code and re-calls
`/auth/login` with the challenge fields.
| Field | Required | Type | Description |
authStatus | Yes | string | |
mfaChallengeId | Yes | string | Principal-issued challenge identifier; consumed at challenge completion. |
mfaMethod | Yes | string | The factor the ctomer has enrolled (singular at v1 per
single-active-factor invariant). |
mfaChallengeExpiresAt | Yes | string | 5-minute TTL per `§A.12.7.a`. |
supportedMethods | No | array<string> | For ctomers with multiple enrolled factors (forward gap;
single-factor at v1), enumerates the methods available to the
ctomer. Singular array at v1. |
AuthLoginResponseMfaEnrolmentRequired
`MFA_ENROLMENT_REQUIRED` response shape per `§A.12.7.a` / `§A.12.6.1`.
Ctomer-app surfaces the `supportedMethods[]` array with TOTP first
(the recommended default); partner-app SHOULD display the `rationale`
field accompanying each method so the ctomer is informed of the
assurance-tradeoff between TOTP and SMS_MFA.
| Field | Required | Type | Description |
authStatus | Yes | string | |
mfaEnrolmentSessionId | Yes | string | |
supportedMethods | Yes | array<object> | |
mfaEnrolmentSessionExpiresAt | Yes | string | 5-minute TTL per `§A.12.7.a`. |
AuthRefreshRequest
| Field | Required | Type | Description |
refreshToken | Yes | string | The current refresh-token. Consumed at Cognito `InitiateAuth`
`REFRESH_TOKEN_AUTH`; rotated per `§A.3.1` YesCash policy.
The ctomer-app's `Authorization` header carries the expired
or near-expiry access-token; APIM Posture B per ADR 20 v2.3.4
`§D3.7.8` skips the expiry check for `/auth/refresh`. |
AuthRefreshResponse
Fresh access-token issuance per `§A.3.2.4`. The refresh-token is
rotated per the `§A.3.1` rotation posture (always present in the
response; if rotation is operationally disabled the value is the same
as submitted, preserving contract uniformity).
| Field | Required | Type | Description |
accessToken | Yes | string | |
refreshToken | Yes | string | |
idToken | No | string | |
tokenType | Yes | string | |
expiresIn | Yes | integer | |
accessTokenExpiresAt | No | string | |
idTokenExpiresAt | No | string | |
AuthLogoutRequest
| Field | Required | Type | Description |
refreshToken | Yes | string | The current refresh-token. The BFF passes to Cognito `RevokeToken`
per `§A.3.3.2`. Per ADR 20 v2.3.4 `§10` entry 3 leading recommendation
and Step 6b Q6 acceptance, the revocation semantic is S2 (per-session
revocation) — only the presented refresh-token is invalidated;
other refresh-tokens for the same `MasterCtomer` (other devices,
other partner-apps) remain valid. |
RegisterStartRequest
| Field | Required | Type | Description |
identifier | Yes | Identifier | |
identifierType | Yes | IdentifierType | |
partnerCustomerRef | No | string | Optional partner-side opaque reference for the ctomer per
`§A.4.3`. Recorded on `Subscription.OriginatingChannelMetadata`
per ADR 22 v3 `§D2` for partner-side correlation. NOT ed as
an identity input to SQL per Architecture `§14.3` invariant 3. |
RegisterStartResponse
Response body for `POST /v1/auth/register/start`. Uniform across the
three Identifier Registry resolution outcomes per `§A.4.2`
neutral-response posture.
| Field | Required | Type | Description |
registrationId | Yes | string | Opaque, single-e, short-lived registration session identifier
per ADR 20 v2.3.4 `§D3.7.7`. Server-side-bound to the ctomer-
supplied identifier (normalised), the App Client, the resolution
outcome at this step, and `registrationIdExpiresAt`. Format
illtrative. |
next | Yes | string | Always `OTP` at v1 — directs the partner-app to
`POST /v1/auth/register/verify-otp`. |
registrationIdExpiresAt | Yes | string | 30-minute TTL from issuance per `§A.4.4` Step 5 Q5 / Q7b decision.
The OTP dispatched alongside has its own shorter 5-minute TTL. |
RegisterVerifyOtpRequest
| Field | Required | Type | Description |
registrationId | Yes | string | |
otp | Yes | string | 6-digit numeric OTP. Accepted as a string to preserve leading
zeros; non-numeric or non-6-character input rejected as
`validation.fieldFormatInvalid`. |
RegisterVerifyOtpResponse
Response body for `POST /v1/auth/register/verify-otp`. The
`branch` discriminator drives the partner-app's `/register/set-password`
UI flow per `§A.5.3`: `NEW_CTOMER` → "create your password" UI;
`EXISTING_CTOMER` → "verify your password to register this app" UI.
Both values target the same endpoint at the next step; the value
distinguishes the ctomer-app UI mode.
| Field | Required | Type | Description |
registrationId | Yes | string | |
branch | Yes | string | Per `§A.5.3` branch keying. Keyed on Cognito-credential existence
for the resolved `MasterCtomer`, NOT on `MasterCtomer`
existence; the third row of the §A.5.3 mapping (skeletal
`MasterCtomer`) routes to `NEW_CTOMER`. The substantive
disclosure of branch is gated on OTP success per ADR 20 v2.3.4
`§D3.7.1` (post-OTP-ownership-proof; does not weaken the
`§A.4.2` neutral-response defence at the prior step). |
next | Yes | string | Directs the partner-app to `POST /v1/auth/register/set-password`.
`SET_PASSWORD` for `NEW_CTOMER`; `VERIFY_PASSWORD` for
`EXISTING_CTOMER`. The value `PASSWORD` is preserved at v1 as
an Appendix A wire-trace convention; partner-apps SHOULD accept
any of the three values as a directive to call
`/register/set-password`. |
RegisterSetPasswordRequest
Request body for `POST /v1/auth/register/set-password`. The endpoint
path is per `§A.6.1` wire convention and per the Appendix A wire
traces; the path supersedes the §A.0.3 section-label
"Registration — complete" editorial title. Both `NEW_CTOMER` and
`EXISTING_CTOMER` branches consume the same request shape; the
BFF differentiates semantics by the post-OTP-state recorded against
the `registrationId` per `§A.6.2` / `§A.6.3`.
| Field | Required | Type | Description |
registrationId | Yes | string | |
password | Yes | string | For `NEW_CTOMER` branch: the new password to create against the
Cognito er. Validated against the YesCash password policy per
`§A.6.5` BEFORE the Cognito-create-er call. For `EXISTING_CTOMER`
branch: the ctomer's existing password, verified against Cognito. |
RegisterSetPasswordResponse
On `NEW_CTOMER` branch success: the Cognito credential is created,
the `MasterCtomer` and `Subscription` records are written
atomically per ADR 22 v3 `§D2`, and the initial token bundle is
issued. On `EXISTING_CTOMER` branch success: the credential is
verified, the partner-scoped `Subscription` is created or resolved,
and the initial token bundle is issued. Both branches return the
same token-bundle shape with `ctomerStat = PENDING` for new
ctomers (KYC not yet completed) or the ctomer's pre-existing
stat for the `EXISTING_CTOMER` branch.
| Field | Required | Type | Description |
accessToken | Yes | string | |
refreshToken | Yes | string | |
idToken | No | string | |
tokenType | Yes | string | |
expiresIn | Yes | integer | |
accessTokenExpiresAt | No | string | |
refreshTokenExpiresAt | No | string | |
idTokenExpiresAt | No | string | |
subscriptionId | No | string | Partner-scoped ctomer reference per ADR 22 v3 `§D2.6`. |
customerStatus | Yes | string | |
IdentifierListResponse
| Field | Required | Type | Description |
identifiers | Yes | array<IdentifierListItem> | Zero-or-more currently-active identifiers for the authenticated
`MasterCtomer`. Order: ascending by `verifiedAt`. The
at-least-one-active-identifier invariant per `§A.8.1 Q4` means
an authenticated ctomer always has at least one active
identifier; the empty-array case is structurally impossible at
this endpoint but the shape accommodates it. |
IdentifierListItem
| Field | Required | Type | Description |
identifierId | Yes | string | Partner-visible UUID v4 (ULID rendered with `ID-` prefix in the
wire form per FM.3.3). ed as the path parameter at the remove
endpoint (`§A.8.5`). |
identifierType | Yes | IdentifierType | |
identifierMasked | Yes | IdentifierMasked | |
verifiedAt | Yes | string | |
canRemove | Yes | boolean | `true` if removal would not violate the at-least-one-active-
identifier invariant. Partner-app's UI surfaces the per-identifier
remove control conditional on this field. |
IdentifierAddStartRequest
| Field | Required | Type | Description |
identifier | Yes | Identifier | The new identifier being added. Normalised per FM.3 before
`iam.IdentifierRegistry` lookup. Ctomer-context endpoint
implements distinguishable-rejection posture per `§A.8.3.2` Q2:
already-owned-by-this-ctomer surfaces
`validation.identifierAlreadyOwned`; owned-by-other-ctomer
surfaces `validation.identifierNotAvailable`; not-yet-owned
proceeds with OTP dispatch. |
identifierType | Yes | IdentifierType | |
IdentifierAddStartResponse
| Field | Required | Type | Description |
identifierAddId | Yes | string | Opaque, single-e, short-lived identifier-add session
identifier. Server-side-bound to the authenticated
`MasterCtomerId`, the ctomer-supplied new identifier
(normalised), the App Client, and the expiry timestamp. |
next | Yes | string | |
identifierAddIdExpiresAt | Yes | string | 30-minute TTL from issuance per `§A.8.3.4`. |
IdentifierAddVerifyOtpRequest
| Field | Required | Type | Description |
identifierAddId | Yes | string | |
otp | Yes | string | |
IdentifierAddVerifyOtpResponse
Shape matches the per-identifier shape from
`GET /v1/auth/identifiers` per `§A.8.4.4` — allows the partner-app
to update its in-app identifier-list cache directly from this
response without a follow-up list-fetch.
| Field | Required | Type | Description |
IdentifierRemoveStartRequest
| Field | Required | Type | Description |
otpDeliveryIdentifierId | Yes | string | The `identifierId` of the ctomer-selected OTHER active
identifier that will receive the verification OTP. Mt be (a)
bound to the authenticated `MasterCtomer`, (b) currently
active, and (c) distinct from the path-parameter `identifierId`
being removed. The substantive control per `§A.8.5.2`: the
ctomer proves continued control of an identifier they are
RETAINING; the typical remove scenario is the ctomer is
losing access to the identifier being removed. |
IdentifierRemoveStartResponse
| Field | Required | Type | Description |
identifierRemoveId | Yes | string | |
next | Yes | string | |
otpDeliveryIdentifierMasked | Yes | IdentifierMasked | Masked form of the ctomer-selected OTHER identifier; surfaces
to the ctomer-app's UI so the ctomer sees confirmation of
where the OTP was dispatched. |
identifierRemoveIdExpiresAt | Yes | string | |
IdentifierRemoveVerifyOtpRequest
| Field | Required | Type | Description |
identifierRemoveId | Yes | string | |
otp | Yes | string | |
IdentifierRemoveVerifyOtpResponse
| Field | Required | Type | Description |
identifierId | Yes | string | |
deactivatedAt | Yes | string | |
PasswordForgotRequest
| Field | Required | Type | Description |
identifier | Yes | Identifier | |
identifierType | Yes | IdentifierType | |
registrationId | No | string | Optional carry-forward `registrationId` from a prior
`auth.credentialMismatch` rejection at `/register/set-password`
`EXISTING_CTOMER` branch per `§A.9.2.3` Q5 pae-and-refresh
decision. When present, the principal binds the
`passwordResetId` to the same `MasterCtomer` as the
`registrationId`, places the `registrationId` in paed-TTL
state, and on successful `/password/reset` reactivates the
`registrationId` with refreshed 30-min TTL and directs the
ctomer-app back to `/register/set-password`. |
PasswordForgotResponse
Uniform response across the three Identifier Registry resolution
outcomes per `§A.9.2.2` neutral-response posture. On the
identifier-does-not-resolve path the BFF performs a timing-equalised
no-op dispatch — no OTP is actually sent but the response timing
matches the dispatch-success path to defeat enumeration via
side-channel inference.
| Field | Required | Type | Description |
passwordResetId | Yes | string | |
next | Yes | string | |
passwordResetIdExpiresAt | Yes | string | 30-minute TTL; OTP has 5-minute TTL. |
PasswordResetRequest
| Field | Required | Type | Description |
passwordResetId | Yes | string | |
otp | Yes | string | |
newPassword | Yes | string | The new password, validated against the YesCash password
policy per `§A.6.5` (same policy as registration-time). |
PasswordResetResponse
Polymorphic response per `§A.9.3.4`:
- Standalone path: `next = LOGIN` directs the ctomer-app to the
login screen; the ctomer re-authenticates via `/auth/start` +
`/auth/login` with the fresh password.
- Carry-forward path: `next = REGISTER_COMPLETE` directs the ctomer-
app to re-call `/auth/register/set-password` with the reactivated
`registrationId` and the fresh password.
On successful reset the BFF invokes Cognito `AdminerGlobalSignOut`
per `§A.9.3.5` Q8 — all existing refresh-tokens across the
ctomer's active partner-app sessions are invalidated.
| Field | Required | Type | Description |
PasswordResetResponseStandalone
| Field | Required | Type | Description |
next | Yes | string | |
PasswordResetResponseCarryForward
| Field | Required | Type | Description |
next | Yes | string | |
registrationId | Yes | string | |
registrationIdExpiresAt | Yes | string | Refreshed 30-minute TTL per the Q5 pae-and-refresh decision. |
DeviceRegistrationStartRequest
Request for `POST /v1/auth/device-registration/start`. Two legal
call shapes:
- First call (no `stepUpToken`): onboarding-time registration for a
ctomer who has no active device, OR initial step-up-required
probe for a ctomer who already has an active device.
- Retry call (with `stepUpToken`): re-registration after the liveness
step-up flow per `§A.11.4` choreography.
| Field | Required | Type | Description |
deviceMetadata | No | DeviceMetadata | |
stepUpToken | No | string | Single-e 10-minute-TTL token issued by the liveness webview
closure per `§A.11.2.4`. Present on the re-registration retry
call following a `202` step-up-required response. |
DeviceRegistrationStartResponse
Polymorphic response per `§A.11.2.5` / `§A.11.2.6`:
- `201 Created` happy-path: registration challenge issued.
- `202 Accepted` step-up-required: ctomer has existing active
registration; partner-app mt route to the liveness webview
before retrying with `stepUpToken`.
| Field | Required | Type | Description |
DeviceRegistrationStartHappyPath
| Field | Required | Type | Description |
deviceRegistrationId | Yes | string | Principal-issued per-attempt identifier (ULID, `DRG-` prefix). |
registrationChallenge | Yes | string | Base64url-encoded 32 bytes (256 bits) of cryptographically-random
data. Per-`deviceRegistrationId` single-e. The ctomer's
freshly-generated device private key signs over this challenge
in the `DeviceRegistrationProof` JWS at `/complete`. |
expiresAt | Yes | string | 5-minute TTL from issuance. |
publicKeyRequirements | Yes | object | |
stepUpApplied | Yes | boolean | `false` for onboarding-time first registration; `true` after
successful re-registration with `stepUpToken` per `§A.11.4`. |
DeviceRegistrationStartStepUpRequired
| Field | Required | Type | Description |
stepUpRequired | Yes | string | |
kycSessionId | Yes | string | |
livenessWebviewUrl | Yes | string | Principal-served webview URL per `§B.4 / §B.5` pattern. The
ctomer-app opens this in a webview; on successful liveness
the principal emits a webview-closure signal carrying a
`stepUpToken` (10-min TTL); the ctomer-app re-calls
`/device-registration/start` with the token. |
expiresAt | Yes | string | |
nextStep | Yes | string | |
PublicKeyJwk
RSA public key in JWK form per RFC 7517. Submitted at
`/device-registration/complete` for principal-side registration.
Constraints per ADR 25 v1.1 `§D5.1` + App C `§C.0.2`: `kty=RSA`,
`alg=RS256`, `e=sig`, modul ≥ 2048 bits.
| Field | Required | Type | Description |
kty | Yes | string | |
n | Yes | string | Base64url-encoded RSA modul (≥ 2048 bits). |
e | Yes | string | Base64url-encoded RSA exponent. |
kid | No | string | Device-or-app-generated key identifier the device retains for its
own bookkeeping. Opaque to the principal at registration; the
principal correlates by `publicKeyThumbprint` (RFC 7638 SHA-256),
not `kid`. After successful registration the principal-allocated
canonical `deviceId` is ed by the partner-app for subsequent
`§B.12` calls. |
alg | Yes | string | |
use | Yes | string | |
DeviceRegistrationProof
**Device-registration proof JWS (Compact Serialization per RFC 7515).**
Signed by the ctomer device's freshly-generated RSA private key
(corresponding to the `publicKey` submitted alongside) per App C
`§C.2`.
**Algorithm: RS256 + RSA-2048 minimum.** Per App C `§C.0.2` + ADR
25 v1.1 `§D5.1`. v1 closes on RS256 only.
**The Step 23 Appendix B flow B.3 step 5 erratum** — which silently
surfaced `alg: ES256` for the device-bound assertion — **is
corrected by App C `§C.0.4` and `§C.3` and is superseded by this
schema.** The device-registration-proof JWS (App C `§C.2`) and the
device-bound-assertion JWS at `§B.12` (App C `§C.3`) both e
RS256.
**Protected header.** `{"alg":"RS256","kid":"<publicKey.kid>","typ":"JWT"}`.
Closed-header discipline per App C `§C.1.3` (no `crit`, `x5c`).
**JCS-canonical payload (RFC 8785)** per App C `§C.2.4`:
```json
{
"device_registration_id": "DRG-01HX...",
"iat": 1715608822,
"public_key_thumbprint": "<RFC 7638 SHA-256 JWK thumbprint of publicKey>",
"registration_challenge": "<base64url echo of start-issued challenge>",
"registration_proof_v1": "v1"
}
```
**Verification chain (principal-side at `/complete`)** per App C `§C.2.7`:
1. Verify `header.alg == "RS256"`; reject with
`device.publicKeyAlgorithmUnsupported` otherwise.
2. Verify `submittedPublicKey.kty == "RSA"`,
`submittedPublicKey.alg == "RS256"`,
`submittedPublicKey.modul_bits >= 2048`,
`submittedPublicKey.e == "sig"`.
3. Self-signature verification: RSASSA-PKCS1-v1_5-SHA-256 against
`submittedPublicKey` over JWS Signing Input. Reject with
`device.registrationProofInvalid` on failure.
4. Challenge binding: `payload.registration_challenge ==
expectehallenge`. Reject with
`device.registrationProofChallengeMismatch`.
5. Thumbprint binding: RFC 7638 SHA-256 JWK thumbprint of
`submittedPublicKey` equals `payload.public_key_thumbprint`. Reject
with `device.registrationProofThumbprintMismatch`.
The verbatim JWS string is preserved in the principal-side audit
trail per `§A.13.5` + ADR 25 `§D5.2` evidence pattern.
**The payload does NOT carry a `master_ctomer_id` claim.** Ctomer-
binding is at the registration-record layer per `§A.11.3` + ADR 25
`§D5.1` — the principal looks up the JWT-resolved
`(MasterCtomerId, PartnerId)` from `iam.requestContext_Resolve` and
binds the registration record to that ctomer.
| Field | Required | Type | Description |
DeviceRegistrationCompleteRequest
DeviceRegistrationCompleteResponse
| Field | Required | Type | Description |
deviceId | Yes | string | Principal-allocated stable per-device identifier (ULID, `DEV-`
prefix). The partner-app stores this for e in the device-bound
assertion at `§B.12` transfer-confirmation calls (`device_id`
claim per App C `§C.3`). |
registeredAt | Yes | string | |
publicKeyThumbprint | Yes | string | RFC 7638 SHA-256 JWK thumbprint, base64url-encoded. Echo of the
submitted thumbprint per defence-in-depth. |
stepUpApplied | Yes | boolean | `true` if the registration was preceded by liveness step-up
(re-registration path); `false` for onboarding-time first
registration. |
priorDeviceIdDeactivated | No | string | The prior `deviceId` that was deactivated on re-registration
success, if any. `null` for first-time registration. |