Skip to content

feat(sca): SCA management surface — enrollment, login/session, beneficiary trust, 2FA reset - #600

Merged
jklein24 merged 10 commits into
mainfrom
feat/striga-sca-management
Jul 17, 2026
Merged

feat(sca): SCA management surface — enrollment, login/session, beneficiary trust, 2FA reset#600
jklein24 merged 10 commits into
mainfrom
feat/striga-sca-management

Conversation

@jklein24

@jklein24jklein24 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

The SCA management surface for EU (Striga) customers — the customer-level setup, session, and recovery flows that sit alongside per-payment authorization (not inside it). This single PR collapses the original design's PR3–PR7 (enrollment, passkey, login/session, beneficiary trust, 2FA reset). Stacks on #558, which defines the per-transaction authorization core and the shared schemas this PR reuses. All endpoints are EU-only — 409 for providers that don't require SCA (the invisibility guarantee) and mirror the sparkcore handlers' exact routes + JSON shapes. Surfaced after an audit of #558 found these existed only as sparkcore temp types, not in the spec.

Covered by this PR

  • Factor enrollment (design PR3/PR4): POST /customers/{id}/sca/factors/totp (+/confirm), .../passkey (+/confirm), GET .../sca/factors, DELETE .../passkey/{credentialId}.
  • Login / 180-day session (PR5): POST .../sca/login/start (+/complete); POST .../sca/record-event.
  • Beneficiary trust (PR6): POST .../external-accounts/{extId}/trust (issues the challenge + whitelistedId), .../trust/confirm, .../untrust/confirm — the whitelisting exemption that lets recurring payees skip per-transaction SCA.
  • 2FA reset / recovery (PR7): POST .../sca/factors/reset (201) → GET .../reset/{resetId} (poll to LIVENESS_PASSED) → POST .../reset/{resetId}/complete.

Reuses ScaFactor / ScaChallenge / ScaAuthorization from #558; confirm/login bodies carry the code | passkeyAssertion + origin proof, and beneficiary confirm threads whitelistedId back so confirm never re-whitelists (no duplicate SMS). TwoFactorResetStatus / ScaLoginComplete document their provider-passthrough terminal values (LIVENESS_PASSED / SUCCESS).

NOT in this PR

  • Per-transaction authorization — the challenge on a debit, the authorize / resend endpoints, and inline scaAuthorization / scaFactor — that's feat(sca): add Strong Customer Authentication surface #558.
  • The sparkcore backend for these endpoints (separate stack), and client-side WebAuthn ceremony logic (Grid relays opaque passkey blobs; it performs no crypto).

Known gaps / follow-ups (tracked in the design doc addendum)

  • Untrust has confirm but no start, and there is no trust-list read — a caller who loses whitelistedId (or needs the untrust OTP re-sent) is stuck (addendum F4). The trusted-beneficiary exemption benefit (a trusted payee actually skipping per-tx SCA) is also not yet realized in the backend.
  • Reset status documents LIVENESS_PASSED / PENDING but no terminal failure value — infinite-poll risk (F10).
  • Distinct SCA 409 codes (SCA_NOT_REQUIRED vs NO_PENDING_SCA_CHALLENGE) are not yet modeled (F9).

Validation

npm run lint:openapi (redocly + spectral, fail-on-error) passes clean; additions are additive / non-breaking. (The OpenAPI build / breaking-changes / lint CI checks run on the base=main PR #558; they do not run here because this PR targets feat/striga-sca.)

Notes

  • Field shapes mirror the sparkcore implementation (the source of truth until the SDK regenerates). A few Striga response bodies (login-complete, reset-status/complete, passkey list/delete) are typed from the sparkcore to_dict() since Striga's docs only cover the start side — flagged for sandbox verification.

Stacking

main#558 (per-transaction) → #600 (this PR). Merges after #558; depends on the schemas #558 introduces.

🤖 Generated with Claude Code

@vercel

vercelBot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
ProjectDeploymentActionsUpdated (UTC)
grid-flow-builderIgnoredIgnoredPreviewJul 17, 2026 6:07pm
grid-wallet-demoIgnoredIgnoredPreviewJul 17, 2026 6:07pm

Request Review

@github-actions

github-actionsBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

✱ Stainless preview builds for grid

This PR will update the grid SDKs with the following commit messages.

cli

chore(internal): regenerate SDK with no functional changes

go

chore(internal): regenerate SDK with no functional changes

kotlin

chore(internal): regenerate SDK with no functional changes

openapi

feat(api): add SCA factor management, login, 2FA reset, beneficiary trust endpoints

php

chore(internal): regenerate SDK with no functional changes

python

chore(internal): regenerate SDK with no functional changes

ruby

chore(internal): regenerate SDK with no functional changes

typescript

chore(internal): regenerate SDK with no functional changes
⚠️grid-openapistudio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️

grid-rubystudio · code

Your SDK build had at least one "note" diagnostic.
generate ✅build ✅lint ✅test ✅

⚠️grid-gostudio · code

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

go get github.com/stainless-sdks/grid-go@1fe4485bac5384b80a875cf3663b8897ecde9a45
⚠️grid-kotlinstudio · code

Your SDK build had a failure in the test CI job, which is a regression from the base state.
generate ⚠️build ✅lint ✅test ❗

⚠️grid-pythonstudio · code

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

pip install https://pkg.stainless.com/s/grid-python/b17c66ee377f16f682424d9b3fa060201e16c7b4/grid-0.0.1-py3-none-any.whl
grid-typescriptstudio · conflict

Your SDK build had at least one note diagnostic.

grid-phpstudio · code

Your SDK build had at least one "note" diagnostic.
generate ✅lint ✅test ✅

⚠️grid-clistudio · code

Your SDK build had a failure in the build CI job, which is a regression from the base state.
generate ⚠️build ❗lint ❗test ❗


This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-07-17 23:15:33 UTC

@jklein24

Copy link
Copy Markdown
ContributorAuthor

@greptile review

@greptile-apps

greptile-appsBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

\m/ ⚡ RIFF OF THE SPEC DESCENDS ⚡ \m/
|===========================================|
| S C A M A N A G E M E N T |
| ~~ THRASH THE ENROLLMENT FLOWS ~~ |
|===========================================|
\m/ \m/

This PR adds the EU SCA management surface — factor enrollment (TOTP + passkey), the 180-day login/session flow, beneficiary trust/untrust, and 2FA liveness-gated reset — as 13 new OpenAPI paths and 22 new schemas, completing design PR3–PR7 that stacked on the per-transaction core in #558.

  • Factor enrollment & management:POST .../sca/factors/totp (+/confirm), .../passkey (+/confirm), GET .../sca/factors, DELETE .../passkey/{credentialId} — all returning 409 for non-SCA regions and mirroring sparkcore route shapes.
  • Login, security events, and 2FA reset:POST .../sca/login/start (+/complete), POST .../sca/record-event, and the POST .../sca/factors/reset → poll → /complete liveness flow with documented LIVENESS_PASSED sentinel after prior review iteration.
  • Beneficiary trust/untrust:POST .../trustPOST .../trust/confirm and POST .../untrust/confirm with a shared BeneficiaryTrustConfirmRequest; the whitelistedId field the PR description says is issued by the start and threaded on confirm is absent from both schemas, and anyOf in the confirm request (and ScaLoginCompleteRequest) should be oneOf to enforce mutual exclusion between OTP and passkey proof types.

Confidence Score: 4/5

Mergeable after resolving two spec gaps: the missing whitelistedId field in the trust start/confirm schemas and the anyOf-vs-oneOf discriminator ambiguity.

The trust-start response schema omits the whitelistedId the PR description says is issued and threaded back on confirm — if the sparkcore backend requires it, every trust-confirm call will fail silently. Separately, anyOf in both confirm-request schemas permits a caller to submit both an OTP code and a passkey assertion simultaneously, leaving the server to receive an ambiguous proof payload with no spec guidance on which to use.

BeneficiaryTrustStart.yaml, BeneficiaryTrustConfirmRequest.yaml, and ScaLoginCompleteRequest.yaml need attention before merge.

Important Files Changed

FilenameOverview
openapi/components/schemas/sca/BeneficiaryTrustStart.yamlTrust-start response schema is missing the whitelistedId field the PR description says it returns; the confirm request is also missing it as an input.
openapi/components/schemas/sca/ScaLoginCompleteRequest.yamlUses anyOf instead of oneOf for the OTP/passkey proof discriminator, allowing both proof types to be submitted in the same request; same issue in BeneficiaryTrustConfirmRequest.
openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yamlSame anyOf-vs-oneOf issue as ScaLoginCompleteRequest; also missing whitelistedId that the PR description says must be threaded back on confirm.
openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml2FA reset start endpoint correctly returns 201 and threads resetId through to status/complete; no issues found.
openapi/paths/customers/customers_{customerId}external-accounts{externalAccountId}_trust.yamlTrust-start endpoint has no request body (correctly inferred from path) but returns a BeneficiaryTrustStart that omits the whitelistedId the PR description says is issued here.
openapi/paths/customers/customers_{customerId}external-accounts{externalAccountId}_trust_confirm.yamlTrust-confirm endpoint structure is clean; underlying BeneficiaryTrustConfirmRequest schema has the anyOf/whitelistedId issues flagged separately.
openapi/openapi.yamlAll 13 new path refs wired correctly; ordering and grouping is consistent with existing paths.
.redocly.lint-ignore.yamlSuppressions added for anyOf branches referencing parent-level properties; comment explains the valid JSON Schema rationale.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant C as Caller
participant G as Grid API
participant S as Striga (SCA Provider)
Note over C,S: Factor Enrollment — TOTP
C->>G: "POST /customers/{id}/sca/factors/totp"
G->>S: start TOTP enrollment
S-->>G: secret + totpUri
G-->>C: "TotpEnrollmentStart {secret, secretBase32Encoded, totpUri}"
C->>G: "POST /sca/factors/totp/confirm {secret, code}"
G->>S: confirm TOTP
S-->>G: recovery codes
G-->>C: "TotpEnrollmentConfirmResponse {recoveryCodes}"
Note over C,S: SCA Login — SMS_OTP
C->>G: "POST /customers/{id}/sca/login/start {factor: SMS_OTP}"
G->>S: dispatch OTP
S-->>G: challengeId + expiresAt
G-->>C: "ScaLoginStart {factor, challengeId, expiresAt}"
C->>G: "POST /sca/login/complete {factor, challengeId, code}"
G->>S: verify code
S-->>G: status
G-->>C: "ScaLoginComplete {status: SUCCESS}"
Note over C,S: Beneficiary Trust
C->>G: "POST /customers/{id}/external-accounts/{extId}/trust"
G->>S: start trust
S-->>G: scaChallenge (optional)
G-->>C: "BeneficiaryTrustStart {scaChallenge?}"
C->>G: "POST .../trust/confirm {challengeId?, code|passkeyAssertion+origin}"
G->>S: confirm trust
S-->>G: "trusted=true"
G-->>C: "BeneficiaryTrustConfirm {trusted: true}"
Note over C,S: 2FA Reset
C->>G: "POST /customers/{id}/sca/factors/reset {factor}"
G->>S: start liveness
S-->>G: resetId + liveness handles
G-->>C: "TwoFactorResetStart {resetId, livenessAccessToken?, verificationLink?}"
loop Poll until LIVENESS_PASSED
C->>G: "GET /sca/factors/reset/{resetId}"
G->>S: check status
S-->>G: status
G-->>C: "TwoFactorResetStatus {status}"
end
C->>G: "POST /sca/factors/reset/{resetId}/complete"
G->>S: complete reset
G-->>C: 204 No Content
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant C as Caller
participant G as Grid API
participant S as Striga (SCA Provider)
Note over C,S: Factor Enrollment — TOTP
C->>G: "POST /customers/{id}/sca/factors/totp"
G->>S: start TOTP enrollment
S-->>G: secret + totpUri
G-->>C: "TotpEnrollmentStart {secret, secretBase32Encoded, totpUri}"
C->>G: "POST /sca/factors/totp/confirm {secret, code}"
G->>S: confirm TOTP
S-->>G: recovery codes
G-->>C: "TotpEnrollmentConfirmResponse {recoveryCodes}"
Note over C,S: SCA Login — SMS_OTP
C->>G: "POST /customers/{id}/sca/login/start {factor: SMS_OTP}"
G->>S: dispatch OTP
S-->>G: challengeId + expiresAt
G-->>C: "ScaLoginStart {factor, challengeId, expiresAt}"
C->>G: "POST /sca/login/complete {factor, challengeId, code}"
G->>S: verify code
S-->>G: status
G-->>C: "ScaLoginComplete {status: SUCCESS}"
Note over C,S: Beneficiary Trust
C->>G: "POST /customers/{id}/external-accounts/{extId}/trust"
G->>S: start trust
S-->>G: scaChallenge (optional)
G-->>C: "BeneficiaryTrustStart {scaChallenge?}"
C->>G: "POST .../trust/confirm {challengeId?, code|passkeyAssertion+origin}"
G->>S: confirm trust
S-->>G: "trusted=true"
G-->>C: "BeneficiaryTrustConfirm {trusted: true}"
Note over C,S: 2FA Reset
C->>G: "POST /customers/{id}/sca/factors/reset {factor}"
G->>S: start liveness
S-->>G: resetId + liveness handles
G-->>C: "TwoFactorResetStart {resetId, livenessAccessToken?, verificationLink?}"
loop Poll until LIVENESS_PASSED
C->>G: "GET /sca/factors/reset/{resetId}"
G->>S: check status
S-->>G: status
G-->>C: "TwoFactorResetStatus {status}"
end
C->>G: "POST /sca/factors/reset/{resetId}/complete"
G->>S: complete reset
G-->>C: 204 No Content
Loading

Reviews (7): Last reviewed commit: "docs(sca): enumerate record-event values..." | Re-trigger Greptile

Comment threadopenapi/components/schemas/sca/TwoFactorResetStatus.yaml
Comment threadopenapi/components/schemas/sca/TwoFactorResetStatus.yaml
@jklein24
jklein24force-pushed the feat/striga-sca-management branch from 9c598bf to 8b2b873CompareJune 17, 2026 22:35
@jklein24

Copy link
Copy Markdown
ContributorAuthor

@greptile review

1 similar comment
@jklein24

Copy link
Copy Markdown
ContributorAuthor

@greptile review

@jklein24Graphite App

jklein24 commented Jun 18, 2026

Copy link
Copy Markdown
ContributorAuthor

@jklein24
jklein24force-pushed the feat/striga-sca-management branch from 1bca871 to 9689a50CompareJuly 8, 2026 20:57
@jklein24
jklein24force-pushed the feat/striga-sca-management branch from 9689a50 to 3267da9CompareJuly 8, 2026 23:57
@lightsparkdevlightsparkdev deleted a comment from greptile-appsBotJul 9, 2026
@jklein24
jklein24 marked this pull request as draft July 9, 2026 06:50
@jklein24

Copy link
Copy Markdown
ContributorAuthor

@faraday review this

@ls-bolt

ls-boltBot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

📌 Bolt Status

2026-07-09 06:52:06 UTC — ⚡ Agent bright-palladium(#1) started


Feedback

Comment threadopenapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml Outdated
Comment threadopenapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml Outdated
Comment threadopenapi/components/schemas/sca/TwoFactorResetStart.yaml Outdated
@jklein24
jklein24force-pushed the feat/striga-sca-management branch from 3267da9 to 513676eCompareJuly 9, 2026 07:20
jklein24 added a commit that referenced this pull request Jul 9, 2026
…mt schemas
Address faraday review on #600:
- Add an anyOf (code XOR passkeyAssertion+origin) to BeneficiaryTrustConfirmRequest
and ScaLoginCompleteRequest so a proof-less body fails schema validation, not
just at runtime (matching lint-ignore precedent for required-on-shared-props).
- Rename sumsubAccessToken -> livenessAccessToken (TwoFactorResetStart + reset
start path) to keep the surface provider-neutral; the sparkcore response key
is renamed in lockstep.
- Document whitelistedId provenance (scoped to this account's own trust start,
not reusable across accounts or after expiry).
- Clarify eventType is a provider-defined closed vocabulary, not free-form input.
Co-Authored-By: Claude <noreply@anthropic.com>
jklein24 added a commit that referenced this pull request Jul 10, 2026
…mt schemas
Address faraday review on #600:
- Add an anyOf (code XOR passkeyAssertion+origin) to BeneficiaryTrustConfirmRequest
and ScaLoginCompleteRequest so a proof-less body fails schema validation, not
just at runtime (matching lint-ignore precedent for required-on-shared-props).
- Rename sumsubAccessToken -> livenessAccessToken (TwoFactorResetStart + reset
start path) to keep the surface provider-neutral; the sparkcore response key
is renamed in lockstep.
- Document whitelistedId provenance (scoped to this account's own trust start,
not reusable across accounts or after expiry).
- Clarify eventType is a provider-defined closed vocabulary, not free-form input.
Co-Authored-By: Claude <noreply@anthropic.com>
@jklein24
jklein24force-pushed the feat/striga-sca-management branch from 763b88b to af68ac7CompareJuly 10, 2026 07:52
Comment threadopenapi/components/schemas/sca/RecordSecurityEventRequest.yaml Outdated
Comment threadopenapi/components/schemas/sca/BeneficiaryTrustStart.yaml Outdated
jklein24 added a commit that referenced this pull request Jul 14, 2026
…mt schemas
Address faraday review on #600:
- Add an anyOf (code XOR passkeyAssertion+origin) to BeneficiaryTrustConfirmRequest
and ScaLoginCompleteRequest so a proof-less body fails schema validation, not
just at runtime (matching lint-ignore precedent for required-on-shared-props).
- Rename sumsubAccessToken -> livenessAccessToken (TwoFactorResetStart + reset
start path) to keep the surface provider-neutral; the sparkcore response key
is renamed in lockstep.
- Document whitelistedId provenance (scoped to this account's own trust start,
not reusable across accounts or after expiry).
- Clarify eventType is a provider-defined closed vocabulary, not free-form input.
Co-Authored-By: Claude <noreply@anthropic.com>
@jklein24
jklein24force-pushed the feat/striga-sca-management branch from af68ac7 to d71d44dCompareJuly 14, 2026 06:29
jklein24 added a commit that referenced this pull request Jul 14, 2026
Match the #558/#600 terminology fix in the embedded SCA docs section: 'customers
in a region where SCA is required (EU)' / 'customers outside SCA-regulated
regions' instead of 'whose payment provider requires SCA'.
Co-Authored-By: Claude <noreply@anthropic.com>
@jklein24
jklein24 changed the base branch from feat/striga-sca to graphite-base/600July 17, 2026 16:45
jklein24 added a commit that referenced this pull request Jul 17, 2026
## Summary
The **per-transaction SCA authorization surface** — everything needed to gate and authorize an EU (Striga) money-movement debit with an SMS OTP or passkey. This is the compliance-critical core of the Striga SCA work and the **base of the grid-api SCA stack**; the management surface (#600) stacks on top, and the sparkcore backend lands separately. Designed to stay **invisible to every non-SCA (non-EU) caller**.
## Covered by this PR
**Shared SCA vocabulary** (defined here, reused by the whole stack):
- `ScaFactor` (`SMS_OTP` / `TOTP` / `PASSKEY`)
- `ScaChallenge` (`id`, `expiresAt`, `factor`, `availableFactors`, `passkeyAssertionOptions`, `passkeyAllowedOrigins`)
- `ScaAuthorization` (exactly one of `code` / `passkeyAssertion`; `origin` required with a passkey assertion)
**Per-transaction authorization:**
- `PENDING_AUTHORIZATION` status + readOnly `scaChallenge` on `Quote` and `Transaction` (incl. the `OutgoingTransactionStatus` override) — omitted entirely for non-SCA providers.
- Resource-scoped authorize endpoints: `POST /quotes/{id}/authorize` → `Quote`, `POST /transactions/{id}/authorize` → `Transaction` (both `409` for non-SCA providers).
- SMS code re-send: `POST /quotes/{id}/authorize/resend`, `POST /transactions/{id}/authorize/resend`.
- Inline one-shot proof: optional `scaAuthorization` on `execute` / `transfer-out` (satisfy the challenge in the same call).
- Optional `scaFactor` on `execute` / `transfer-out` to pick `SMS_OTP` (default) or `PASSKEY` for the per-transaction challenge.
## NOT in this PR — see #600 (stacked on top)
The SCA **management** surface (customer-level setup/recovery, not per-payment authorization) lives in #600 and reuses the schemas defined here:
- Factor **enrollment** — TOTP + passkey register / list / delete
- **Login / 180-day session** — `sca/login/start|complete`
- **Beneficiary trust** whitelisting — `external-accounts/{id}/trust`, `.../untrust/confirm`
- **2FA reset / recovery** — liveness-gated
- Security-event recording — `sca/record-event`
Also out of scope: the **sparkcore backend** (separate stack) and any client-side WebAuthn ceremony logic (Grid relays opaque passkey blobs; it performs no crypto).
## Known caveat
- The realtime-funding **`202` + withheld-`paymentInstructions`** path on `POST /quotes` is specified here but **not yet reachable** — the backend defers it (Striga has no Grid realtime-funding path today; see design doc addendum item **F6** / open question 2). It's documented so the contract is stable; treat it as inert until that flow ships.
## Stacking
`main` → **#558 (this PR)** → #600 (management). #558 is self-contained and independently mergeable — SMS-OTP authorization needs no enrollment or login — so it can merge to `main` on its own. #600 depends on the `ScaFactor` / `ScaChallenge` / `ScaAuthorization` schemas introduced here.
## Invisibility guarantee
`scaChallenge`, `PENDING_AUTHORIZATION`, the authorize/resend endpoints, and `scaFactor` engage **only** for customers whose provider requires SCA. Non-EU callers see no new fields, no new statuses, and no behavioral change.
## Validation
`npm run lint:openapi` (redocly + spectral) passes; additions are additive / non-breaking ("Detect breaking changes" CI green).
## Design
webdev `docs/plans/2026-06-06-striga-sca-design.md`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
credentialId:
type:
- string
- 'null'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need explicit nulls here? I saw this in another PR but always thought that the field would be excluded vs explicitly null'd out

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good call — dropped them. These are genuinely omitted for TOTP/SMS_OTP, not sent as null, so plain-optional is the right contract and the explicit null was just wrong here.

fwiw the rule I'm going by: nullable and optional are separate axes. Leave a field out of required when the key can be absent; only add 'null' to the type when the server actually puts "field": null on the wire. A few of the newer sca/agents schemas started reaching for explicit null where the field is really just omitted — that's the inconsistency you're seeing. Fixing those as I hit them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait who dis?

jklein24and others added 8 commits July 17, 2026 10:21
Stacks on the per-transaction SCA spec (#558) and adds the remaining
Strong Customer Authentication surfaces an EU (Striga) partner needs, mirroring
the sparkcore handlers' exact routes + JSON shapes. All endpoints are EU-only
(409 for providers that don't require SCA).
- Factor enrollment: TOTP start/confirm, passkey register start/confirm,
list factors, delete passkey.
- SCA login / 180-day session: login start/complete, plus record-event.
- Beneficiary trust: trust start (issues the SCA challenge + whitelistedId),
trust/confirm and untrust/confirm (the whitelisting exemption that lets
recurring payees skip per-transaction SCA).
- 2FA reset: initiate (201) -> poll status -> complete.
Reuses ScaFactor/ScaChallenge/ScaAuthorization from #558. Confirm/login bodies
carry the code|passkeyAssertion+origin proof; beneficiary confirm threads the
whitelistedId back so confirm never re-whitelists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Name the provider-reported terminal sentinels (LIVENESS_PASSED for 2FA
reset, SUCCESS for login) in the status field descriptions so consumers
can write polling/branching logic without out-of-band knowledge, while
keeping the field a verbatim provider passthrough rather than a locked
enum.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mt schemas
Address faraday review on #600:
- Add an anyOf (code XOR passkeyAssertion+origin) to BeneficiaryTrustConfirmRequest
and ScaLoginCompleteRequest so a proof-less body fails schema validation, not
just at runtime (matching lint-ignore precedent for required-on-shared-props).
- Rename sumsubAccessToken -> livenessAccessToken (TwoFactorResetStart + reset
start path) to keep the surface provider-neutral; the sparkcore response key
is renamed in lockstep.
- Document whitelistedId provenance (scoped to this account's own trust start,
not reusable across accounts or after expiry).
- Clarify eventType is a provider-defined closed vocabulary, not free-form input.
Co-Authored-By: Claude <noreply@anthropic.com>
Match the #558 terminology fix: the enrollment / login / trust / reset endpoints
now say "customers in a region where SCA is required (e.g. EU)" / "customers
outside SCA-regulated regions" instead of "customers whose payment provider
requires SCA", and the 409 reads "SCA is not required for this customer".
Incidental references to the underlying provider as a system (provider-reported
status, risk engine, whitelist handle) are left as-is.
Co-Authored-By: Claude <noreply@anthropic.com>
…nt surface
To the integrator, Grid is the provider; the underlying provider must never leak
into the docs. Reworded provider-reported status -> the (reported) status, the
provider's risk engine -> Grid's risk engine, whitelist/liveness handles and
'when the provider does not return one' -> neutral phrasing.
Co-Authored-By: Claude <noreply@anthropic.com>
Per review on #600:
- RecordSecurityEventRequest.eventType is now an explicit enum
(RESET_PASSWORD_COMPLETED | FAILED_LOGIN_ATTEMPT) with each event's effect,
instead of a vague free-string "closed vocabulary".
- Drop whitelistedId from the beneficiary-trust surface: BeneficiaryTrustStart
returns just the scaChallenge, and confirm/untrust identify the beneficiary by
the {externalAccountId} already in the path. Requires the backend to persist
the externalAccountId -> whitelist handle mapping (tracked follow-up).
Co-Authored-By: Claude <noreply@anthropic.com>
Untrust previously had only a confirm step, so an SMS_OTP (default, no
enrollment) or PASSKEY customer had no way to obtain the SCA challenge that
untrust/confirm expects — only a self-served TOTP code worked. Add
POST /customers/{customerId}/external-accounts/{externalAccountId}/untrust
(startBeneficiaryUntrust), mirroring startBeneficiaryTrust: it issues the
scaChallenge that untrust/confirm satisfies. Returns BeneficiaryTrustStart.
…docs)
transfer-out has no associated quote and does not offer Strong Customer
Authentication — per-transaction SCA is authorized only on the quote
(POST /quotes/{quoteId}/authorize). Remove the vestigial scaFactor field from
TransferOutRequest and the SCA paragraph from the transfer-out 201 response
description, so the endpoint carries no SCA surface.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZsLXjVaJZbjjjoswBmhMu
jklein24 added a commit that referenced this pull request Jul 17, 2026
Match the #558/#600 terminology fix in the embedded SCA docs section: 'customers
in a region where SCA is required (EU)' / 'customers outside SCA-regulated
regions' instead of 'whose payment provider requires SCA'.
Co-Authored-By: Claude <noreply@anthropic.com>
@jklein24
jklein24force-pushed the feat/striga-sca-management branch from 94e5d2f to b7f7e5bCompareJuly 17, 2026 17:24
@jklein24
jklein24 changed the base branch from graphite-base/600 to mainJuly 17, 2026 17:24

@pengyingpengying left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 2 general questions
1. how does someone add sms otp
2. do we need -null as a type if the field is optional or are we explicitly setting it to null.

selects the factor — `TOTP` or `PASSKEY`. `SMS_OTP` is not enrollable (it uses
the customer's verified phone). Returns the factor-specific material needed to
finish via `POST /sca/factors/confirm`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is enrolling the concept of trusting a user device / auth method. What triggers the sms otp?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is. With striga, sms is enrolled by default since creating a customer requires sms and email verification via the contact verification API flows. I could definitely document this a bit better here, though. Striga does that at the top of this page: https://docs.striga.com/reference/enrollment. I'll clarify the SMS_OTP line here to indicate that all customers need a verified phone at customer creation in SCA-regulated regions. See the "verifications" routes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so then when the customer is created, they provide the phone number as part of customer creation. we auto send a sms otp?

What if they change phone numbers? is that a patch to customer.phone_number that triggers a sms otp? or do we make it explicit here?

…llable
These fields are omitted (not sent as null) for TOTP/SMS_OTP factors, so
explicit null was the wrong contract. Drop 'null' from the type; the fields
stay optional via absence from `required`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015635zZx4bhUJHNBs5WdB4c
jklein24 added a commit that referenced this pull request Jul 17, 2026
Match the #558/#600 terminology fix in the embedded SCA docs section: 'customers
in a region where SCA is required (EU)' / 'customers outside SCA-regulated
regions' instead of 'whose payment provider requires SCA'.
Co-Authored-By: Claude <noreply@anthropic.com>
@mintlify

mintlifyBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

ProjectStatusPreviewUpdated (UTC)
Grid🟢 ReadyView PreviewJul 17, 2026, 6:04 PM

@mintlify

mintlifyBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

ProjectStatusPreviewUpdated (UTC)
Grid🟡 BuildingJul 17, 2026, 6:03 PM

…lment
Expand the enrollment description to explain that SMS_OTP is not enrolled
through POST /sca/factors — every customer in an SCA-regulated region has a
verified phone from customer creation (Contact Verification flows), so SMS is
always available as a factor. TOTP/PASSKEY remain the explicit opt-in factors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015635zZx4bhUJHNBs5WdB4c
jklein24 added a commit that referenced this pull request Jul 17, 2026
Match the #558/#600 terminology fix in the embedded SCA docs section: 'customers
in a region where SCA is required (EU)' / 'customers outside SCA-regulated
regions' instead of 'whose payment provider requires SCA'.
Co-Authored-By: Claude <noreply@anthropic.com>
jklein24 added a commit that referenced this pull request Jul 17, 2026
The transfer-out SCA/deprecation language was removed from the endpoint spec in
#600, so drop the "being deprecated" claim from the per-transaction guide and
stop showing transfer-out as an SCA-challenge-producing path in the overview
sequence diagram. transfer-out has no associated quote and offers no SCA-gated
EU debits; the guide points EU callers to the quote + execute flow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZsLXjVaJZbjjjoswBmhMu
@jklein24
jklein24 requested a review from pengyingJuly 17, 2026 18:44
@jklein24
jklein24 merged commit 6c4497b into mainJul 17, 2026
12 checks passed
@jklein24
jklein24 deleted the feat/striga-sca-management branch July 17, 2026 23:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jklein24@pengying@prashb94@shreyav