feat(sca): add Strong Customer Authentication surface#558
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
@greptile review |
✱ Stainless preview builds for gridThis PR will update the cli csharp go kotlin openapi php python ruby typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ grid-typescript studio · code · diff
✅ 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-csharp 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 SummaryThis PR introduces a Strong Customer Authentication (SCA) surface for EU/Striga customers, designed so that non-EU callers see no behavioral change. It adds three new schemas (
Confidence Score: 5/5Additive, non-breaking changes that are invisible to non-EU callers; safe to merge. All changes are purely additive to the OpenAPI spec — new optional fields, new enum values, and new endpoints that return 409 for existing customers. No existing contract is modified. The SCA challenge flow is well-documented, and the authorize endpoints correctly mirror the conventions used elsewhere in the spec. openapi/components/schemas/sca/ScaAuthorization.yaml warrants a second look for discriminated-union enforcement between the code and passkeyAssertion fields.
|
| Filename | Overview |
|---|---|
| openapi/components/schemas/sca/ScaAuthorization.yaml | New schema describing the authorization proof; description says "exactly one of" code or passkeyAssertion, but no oneOf/anyOf discriminator is present in the schema — an empty body or both fields simultaneously are spec-valid. |
| openapi/components/schemas/sca/ScaChallenge.yaml | New challenge schema; all required fields declared, id description correctly updated to clarify it is informational and not sent back, passkeyAssertionOptions correctly optional. |
| openapi/components/schemas/sca/ScaFactor.yaml | New enum schema for SMS_OTP/TOTP/PASSKEY; includes useful per-factor notes including the TOTP dynamic-linking limitation. |
| openapi/components/schemas/transfers/TransferOutRequest.yaml | Adds optional scaAuthorization field; "Omit it on the first call" phrasing could mislead developers into thinking the inline path involves re-calling transfer-out, when the correct resolution after PENDING_AUTHORIZATION is the separate authorize endpoint. |
| openapi/paths/quotes/quotes_{quoteId}_authorize.yaml | New authorize endpoint for quotes; 400/401/404/409/500 responses correct, 409 usage for non-SCA providers documented, consistent with existing endpoint conventions. |
| openapi/paths/transactions/transactions_{transactionId}_authorize.yaml | New authorize endpoint for transactions; returns TransactionOneOf on success, all response codes consistent with quote authorize endpoint. |
| openapi/paths/quotes/quotes_{quoteId}_execute.yaml | Adds optional ExecuteQuoteRequest body; 200 description clearly documents upfront inline scaAuthorization vs. the PENDING_AUTHORIZATION/authorize-endpoint resolution path. |
| openapi/components/schemas/quotes/Quote.yaml | Adds PENDING_AUTHORIZATION to the status enum and optional readOnly scaChallenge field; paymentInstructions remains optional (not required), correctly allowing it to be withheld in the 202 case. |
| openapi/components/schemas/transactions/TransactionStatus.yaml | Adds PENDING_AUTHORIZATION value with clear SCA-scoped description and table row; consistent with other status values. |
| openapi/components/schemas/transactions/Transaction.yaml | Adds optional readOnly scaChallenge field matching the pattern on Quote.yaml. |
| openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml | New optional request body schema for execute; scaAuthorization description clearly states upfront on the initial call without the misleading first call language found in TransferOutRequest. |
Sequence Diagram
sequenceDiagram
participant C as Client
participant G as grid-api
participant P as SCA Provider (Striga)
rect rgb(240, 248, 255)
Note over C,P: Realtime-funding quote flow (202)
C->>G: POST /quotes
G->>P: Create quote + SCA challenge
P-->>G: Challenge issued (SMS sent)
G-->>C: "202 Quote{status:PENDING_AUTHORIZATION, scaChallenge}"
Note over C: Customer enters OTP
C->>G: "POST /quotes/{quoteId}/authorize {code}"
G->>P: Verify code
P-->>G: OK
G-->>C: "200 Quote{status:PENDING, paymentInstructions}"
end
rect rgb(255, 248, 240)
Note over C,P: Execute (embedded-wallet) flow
C->>G: "POST /quotes/{quoteId}/execute"
G->>P: Execute + create SCA challenge
P-->>G: Challenge issued (SMS sent)
G-->>C: "200 Quote{status:PENDING_AUTHORIZATION, scaChallenge, transactionId}"
Note over C: Customer enters OTP
C->>G: "POST /transactions/{transactionId}/authorize {code}"
G->>P: Verify code
P-->>G: OK
G-->>C: "200 Transaction{status:PROCESSING}"
end
rect rgb(240, 255, 248)
Note over C,P: Transfer-out flow
C->>G: POST /transfer-out
G->>P: Create transaction + SCA challenge
P-->>G: Challenge issued (SMS sent)
G-->>C: "201 Transaction{status:PENDING_AUTHORIZATION, scaChallenge}"
Note over C: Customer enters OTP
C->>G: "POST /transactions/{transactionId}/authorize {code}"
G->>P: Verify code
P-->>G: OK
G-->>C: "200 Transaction{status:PROCESSING}"
end
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
openapi/components/schemas/transfers/TransferOutRequest.yaml:22-27
The phrase "Omit it on the first call" implies there is a subsequent second call to `transfer-out` that uses the inline path — but after a `PENDING_AUTHORIZATION` response the resource is already created and re-calling `transfer-out` would create a duplicate transaction. The correct resolution path is `POST /transactions/{transactionId}/authorize`, not a second `transfer-out`. Compare the parallel field in `ExecuteQuoteRequest`, which uses "Omit it to receive…" without the "first call" framing and is unambiguous.
```suggestion
Optional inline Strong Customer Authentication proof. Only relevant for
customers whose provider requires SCA (e.g. EU): supply this to satisfy
the challenge in the same call once the customer has the code/assertion.
Omit it to receive the transaction in `PENDING_AUTHORIZATION` with an
`scaChallenge`, then authorize it via
`POST /transactions/{transactionId}/authorize`. Ignored for customers
whose provider does not require SCA.
```
Reviews (4): Last reviewed commit: "docs(sca): document PENDING_AUTHORIZATIO..." | Re-trigger Greptile
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>
|
@greptile review |
… 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>
|
@greptile review |
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>
|
@greptile review |
Summary
Adds the Strong Customer Authentication (SCA) surface required for EU (Striga) customers, designed to stay invisible to every other caller. This is the grid-api half of the Striga SCA stack (sparkcore implementation lands separately).
What's added
sca/schemas:ScaFactor(SMS_OTP/TOTP/PASSKEY),ScaChallenge(id, expiresAt, factor, availableFactors, passkeyAssertionOptions),ScaAuthorization(code | passkeyAssertion).scaChallenge(readOnly) onQuoteandTransaction, present only while status isPENDING_AUTHORIZATION(new enum value onTransactionStatusandQuote.status); omitted entirely for providers that don't require SCA.POST /quotes/{quoteId}/authorize→Quote,POST /transactions/{transactionId}/authorize→Transaction. Both 409 for non-SCA providers.scaAuthorizationonexecute(newExecuteQuoteRequest) andtransfer-out.paymentInstructionsuntil the challenge is authorized.Invisibility guarantee
scaChallenge,PENDING_AUTHORIZATION, the 202, and the authorize endpoints only engage for customers whose provider requires SCA. Non-EU callers see no behavioral change.Validation
npm run lint:openapi(redocly + spectral) passes; additions are additive/non-breaking.Design
webdev
docs/plans/2026-06-06-striga-sca-design.md🤖 Generated with Claude Code