feat(sca): add Strong Customer Authentication surface#558
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
|
@greptile review |
✱ Stainless preview builds for gridThis PR will update the cli go kotlin openapi php python ruby typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ grid-openapi studio · code · diff
✅ grid-ruby studio · code · diff
✅ grid-go studio · code · diff
✅ grid-kotlin studio · code · diff
✅ grid-python studio · code · diff
✅ grid-typescript studio · code · diff
✅ grid-php studio · code · diff
✅ grid-cli studio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
Greptile SummaryFORGE THE CHALLENGE; GUARD THE TRANSFER. This PR adds the Strong Customer Authentication API surface for protected money movement. The main changes are:
Confidence Score: 5/5This looks safe to merge.
|
| Filename | Overview |
|---|---|
| openapi/components/schemas/sca/ScaChallenge.yaml | Defines challenge metadata, factor options, expiration, and sequential authorization behavior. |
| openapi/components/schemas/sca/ScaAuthorization.yaml | Defines SMS, TOTP, and passkey authorization proof fields. |
| openapi/paths/quotes/quotes_{quoteId}_execute.yaml | Adds inline authorization and documents the transaction-scoped flow after execution enters pending authorization. |
| openapi/paths/transactions/transactions_{transactionId}_authorize.yaml | Adds transaction challenge authorization with support for sequential challenges. |
Reviews (8): Last reviewed commit: "docs(sca): drop last 'provider' mention ..." | Re-trigger Greptile
|
@greptile review |
|
@greptile review |
1 similar comment
|
@greptile review |
|
@greptile review |
ce99ce4 to
79c4671
Compare
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>
|
@greptile review |
1 similar comment
|
@greptile review |
This stack of pull requests is managed by Graphite. Learn more about stacking. |
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>
Adds the SCA surface required for EU (Striga) customers while keeping it
invisible to every other caller:
- New sca/ schemas: ScaFactor, ScaChallenge, ScaAuthorization.
- readOnly scaChallenge on Quote and Transaction, present only while status
is PENDING_AUTHORIZATION (new enum value on TransactionStatus and
Quote.status); omitted entirely for providers that don't require SCA.
- Resource-scoped authorize endpoints: POST /quotes/{quoteId}/authorize
(returns Quote) and POST /transactions/{transactionId}/authorize (returns
Transaction), plus an optional inline scaAuthorization on execute and
transfer-out for one-shot completion.
- Realtime-funding create-quote returns 202 and withholds paymentInstructions
until the challenge is authorized.
Design: webdev docs/plans/2026-06-06-striga-sca-design.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address Greptile review: the authorize endpoints are resource-scoped, so the server resolves the active challenge from the quote/transaction context. The id is informational, not a field the client passes back in ScaAuthorization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… only
Address Greptile review: after a quote returns PENDING_AUTHORIZATION, the only
resolution is POST /transactions/{transactionId}/authorize; re-calling execute
409s. scaAuthorization on ExecuteQuoteRequest applies only to the initial call
(to avoid the pending state entirely).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address Greptile review: mirror the execute/create-quote treatment so a
transfer-out client has a spec-level signal that the returned transaction may
be PENDING_AUTHORIZATION and require POST /transactions/{transactionId}/authorize.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… resend)
Addresses an independent audit of the SCA surface against Striga's API:
- ScaAuthorization gains `origin` — Striga's wallets/transaction/confirm
requires the WebAuthn origin alongside passkeyAssertion, and the assertion is
origin-bound. Without it a passkey-authorized debit could not be confirmed.
- ScaChallenge surfaces `passkeyAllowedOrigins` (and documents that the relying
party id lives in passkeyAssertionOptions) so the caller knows which origin to
echo back.
- New resend endpoints: POST /quotes/{quoteId}/authorize/resend and
/transactions/{transactionId}/authorize/resend, mapping to Striga's
resend-otp-for-transaction (SMS only; passkey 409). An SMS code that lapses
no longer forces abandoning the challenge.
- Cross-reference the realtime-funding quote authorize path and the resend
endpoint from the execute 200 description.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add PENDING_AUTHORIZATION to OutgoingTransactionStatus. SCA-gated debits are outgoing transactions, whose status is overridden by this enum; without the value, generated clients reject the very responses the feature produces. - Add the optional top-level scaFactor field to ExecuteQuoteRequest and TransferOutRequest. The backend already reads it to pick SMS_OTP (default) vs PASSKEY for the per-transaction challenge; it was undiscoverable in the spec. - Clarify where the passkey origin comes from for a per-transaction challenge: such challenges carry passkeyAssertionOptions but omit passkeyAllowedOrigins, so ScaAuthorization.origin and ScaChallenge.passkeyAllowedOrigins now explain the per-transaction case instead of pointing at an absent field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
|
@faraday review this |
|
📌 Bolt Status 2026-07-09 06:51:24 UTC — ⚡ Agent |
…e-target clarity Address faraday review on #558: - Add scaFactor to QuoteRequest so a challenge issued at quote creation (realtime funding) can be SMS_OTP or PASSKEY, matching execute/transfer-out — it was previously unspecified, implying SMS-only. - Reword the execute 200 description, which contradicted itself (claimed the transfer was initiated while also covering the PENDING_AUTHORIZATION case); it now splits the two outcomes and states which authorize endpoint to use. - Document a 429 (RATE_LIMITED) on the authorize and resend endpoints — brute -force protection on the OTP and anti-SMS-spam on resend, mirroring the existing auth/credentials OTP endpoints. - Clarify that resend reuses the existing challenge and does not extend expiresAt (post-expiry recovery tracked as design follow-up F8). - Fix nit: sandbox code wording SMS/TOTP -> SMS (TOTP is invalid per-transaction). Co-Authored-By: Claude <noreply@anthropic.com>
| | Status | Description | | ||
| |--------|-------------| | ||
| | `PENDING` | Quote is pending confirmation | | ||
| | `PENDING_AUTHORIZATION` | Awaiting Strong Customer Authentication. Only occurs for customers in a region where SCA is required (e.g. EU); authorize the transaction's `scaChallenge` to proceed. | |
There was a problem hiding this comment.
do we need this represented in the transaciton as well or can a transaction be pending until the quote is executed?
There was a problem hiding this comment.
IMO it's good to be explicit here that it's a different state that requires some action from the customer
There was a problem hiding this comment.
should we also have a corresponding webhook
There was a problem hiding this comment.
I think we get that for free with the existing transaction status changes, right?
There was a problem hiding this comment.
There was a problem hiding this comment.
I'm actually not sure it does since the status never switches to PENDING_AUTHORIZATION, it just starts there when created if needed, so there's no explicit time for a new webhook event here afaict.
There was a problem hiding this comment.
hm we do send webhooks for PENDING so it might be nice for consistency
just to clarify my understanding
for realtime funding:
- create quote -> PENDING_AUTHORIZATION
- authorize quote -> PENDING (?)
- fund quote -> PROCESSING
There was a problem hiding this comment.
Ah ok, if we do that already at creation, then I can add it! Yeah, that sequence looks correct for realtime-funded flows
| remittanceInformation field, and for wires it populates the OBI | ||
| (Originator to Beneficiary Information) / beneficiary information. | ||
| example: '12345' | ||
| scaFactor: |
There was a problem hiding this comment.
I guess we're doing this until we deprecate it for quote? Or should we totally skip it since transfer out will be deprecated soon anyway?
There was a problem hiding this comment.
Short term I think we need it even if things get routed through the quotes flows internally. @shreyav wdyt?
There was a problem hiding this comment.
ahh sorry missed this comment! replied on the other thread, I don't have a strong preference if its not too much work, but we do plan to mark this deprecated very soon
| post: | ||
| summary: Authorize a quote's SCA challenge | ||
| description: | | ||
| Satisfy the Strong Customer Authentication challenge carried by a quote in |
There was a problem hiding this comment.
what's the use case for both a /authorize end point and /execute with SCA object?
There was a problem hiding this comment.
See the paragraph below. Specifically this is needed for realtime-funded quotes, which don't use the execute endpoint. IMO it's good to have these as separate explicit actions for that purpose to avoid confusion.
There was a problem hiding this comment.
damn is real time funding not enough of a secure customer authentication
There was a problem hiding this comment.
Just double checking to make sure that I understand the flow - lets say it's a withdrawal flow like Loot rush of external EURC account > EUR. All managed by Lootrush for their end customers.
In this case since
1. customer adds the beneficiary EUR bank account account
2. move the funds from the customer EURC wallet to fund the quote
3. use the real time funded quote mechanism to convert and offramp
And they need to SCA three times?
There was a problem hiding this comment.
Realtime quotes combine steps 2 and 3 there. Currently that would just require 2 SCAs for the swap and then withdraw. We can get it down to just one though. See this discussion for the "swap and send" flow". https://lightsparkgroup.slack.com/archives/C09H6AEERDF/p1783576868751969
| schema: | ||
| type: string | ||
| example: Transaction:019542f5-b3e7-1d02-0000-000000000005 | ||
| post: |
There was a problem hiding this comment.
When would a user authorize a transaction instead of the quote?
I mean a user calls quotes then execute / authorize with SCA. This is the second SCA?
There was a problem hiding this comment.
This is a good question that's making me think a bit more about whether this one is needed longer term...
Right now, as designed, this one is used in 2 places - non-realtime quotes (triggered with /execute to send from an internal account) and transfer-out calls. The quotes one is only for real-time quotes.
Your question has made me realize that this is a bit confusing though... If we'd already gotten rid of transfer-out, this would be an easy call to just move auth to the quotes call always and start them in PENDING_AUTHORIZATION. However, with transfer-out existing, we still need this no matter what. @shreyav I'm curious your thoughts on the timeline and mechanics here. Should we just skip the transaction auth call and instead follow the same quotes auth flow always? It means transfer-out won't ever work for EU, but maybe that's ok if we're getting rid of it quickly.
There was a problem hiding this comment.
I think thats okay to handle the quotes path only, I plan to make transfers APIs deprecated in the next week, and will add a note about that!
There was a problem hiding this comment.
OK, I did some digging and reminded myself why I have it this way with 2 different quote paths... It's a Striga constraint, not something I explicitly picked. The SCA challenge isn't a standalone object we can mint up front, it's issued by the initiation call (wallets/send/initiate/*) and bound to that specific transfer, and confirm settles that exact challengeId. There's no "give me a challenge without initiating a transfer" primitive.
So it lands wherever initiation happens:
- Realtime quote: quote creation is the initiation, so the challenge's on the quote. Authorize the quote.
- Prefunded quote: the quote's just a price lock on our end, it never touches Striga. Initiation happens at /execute, so the challenge can't exist till then and lands on the transaction.
Putting it on a prefunded quote at creation time would mean initiating a real pending transfer for every quote, including all the ones nobody executes. Quotes are disposable and high-volume, so that's a not ideal.
Unifying on the quotes auth path is still doable though, for prefunded it just means surfacing the execute-time challenge on the quote object and authorizing there. Not super clean though :-/
There was a problem hiding this comment.
Do you want to do execute > quote authorize vs execute > transaction authorize? Also if I'm reading it correctly it looks like ExecuteQuoteRequest has auth evidence in it.
There was a problem hiding this comment.
Do you want to do execute > quote authorize vs execute > transaction authorize?
Yeah, exactly, that's where we'd need to go to make it consistent. Still a bit wonky, but at least it's always on the quote.
Also if I'm reading it correctly it looks like ExecuteQuoteRequest has auth evidence in it.
Ah yes, I left that there as optional to remove one step where possible, but I think that may just add more confusion than it solves. Let me just refactor to unify things a bit real quick and then I'll write out the clear API call steps here for each scenario.
There was a problem hiding this comment.
Refactored to unify, so here are the clear steps. Both scenarios authorize on the quote now; the only difference is where the challenge first surfaces.
Pre-funded (internal-account source, balance already there):
POST /quotes→ quote, no challengePOST /quotes/{quoteId}/execute→ quote returnsPENDING_AUTHORIZATION+scaChallengePOST /quotes/{quoteId}/authorizewith the proof → released
Realtime funding (funds arrive after):
POST /quotes→202, quotePENDING_AUTHORIZATION+scaChallenge,paymentInstructionswithheldPOST /quotes/{quoteId}/authorizewith the proof → instructions populate, then the customer funds
The transaction-scoped authorize endpoint is gone, so there's nothing to reconcile against transactionId anymore.
9d99644 to
19cf8b7
Compare
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>
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>
|
19cf8b7 to
8e8e208
Compare
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>
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>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
To the integrator, Grid is the provider; the underlying provider (Striga) must not leak into the API surface. Region-based framing here too. Co-Authored-By: Claude <noreply@anthropic.com>
8e8e208 to
3ab6065
Compare
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>
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>

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 ofcode/passkeyAssertion;originrequired with a passkey assertion)Per-transaction authorization:
PENDING_AUTHORIZATIONstatus + readOnlyscaChallengeonQuoteandTransaction(incl. theOutgoingTransactionStatusoverride) — omitted entirely for non-SCA providers.POST /quotes/{id}/authorize→Quote,POST /transactions/{id}/authorize→Transaction(both409for non-SCA providers).POST /quotes/{id}/authorize/resend,POST /transactions/{id}/authorize/resend.scaAuthorizationonexecute/transfer-out(satisfy the challenge in the same call).scaFactoronexecute/transfer-outto pickSMS_OTP(default) orPASSKEYfor 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:
sca/login/start|completeexternal-accounts/{id}/trust,.../untrust/confirmsca/record-eventAlso 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
202+ withheld-paymentInstructionspath onPOST /quotesis 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 tomainon its own. #600 depends on theScaFactor/ScaChallenge/ScaAuthorizationschemas introduced here.Invisibility guarantee
scaChallenge,PENDING_AUTHORIZATION, the authorize/resend endpoints, andscaFactorengage 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