From f2d230ab2fee9b8d0859cf422df1714bbd1d8fbc Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Sun, 7 Jun 2026 23:52:31 -0700 Subject: [PATCH 01/10] feat(sca): add Strong Customer Authentication surface 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 --- mintlify/openapi.yaml | 258 +++++++++++++++++- openapi.yaml | 258 +++++++++++++++++- .../schemas/quotes/ExecuteQuoteRequest.yaml | 15 + openapi/components/schemas/quotes/Quote.yaml | 16 +- .../schemas/sca/ScaAuthorization.yaml | 21 ++ .../components/schemas/sca/ScaChallenge.yaml | 47 ++++ openapi/components/schemas/sca/ScaFactor.yaml | 13 + .../schemas/transactions/Transaction.yaml | 8 + .../transactions/TransactionStatus.yaml | 2 + .../schemas/transfers/TransferOutRequest.yaml | 9 + openapi/openapi.yaml | 10 + openapi/paths/quotes/quotes.yaml | 12 + .../quotes/quotes_{quoteId}_authorize.yaml | 74 +++++ .../quotes/quotes_{quoteId}_execute.yaml | 17 +- ...ransactions_{transactionId}_authorize.yaml | 76 ++++++ 15 files changed, 827 insertions(+), 9 deletions(-) create mode 100644 openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml create mode 100644 openapi/components/schemas/sca/ScaAuthorization.yaml create mode 100644 openapi/components/schemas/sca/ScaChallenge.yaml create mode 100644 openapi/components/schemas/sca/ScaFactor.yaml create mode 100644 openapi/paths/quotes/quotes_{quoteId}_authorize.yaml create mode 100644 openapi/paths/transactions/transactions_{transactionId}_authorize.yaml diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 5c510eb9c..4c182b0c1 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -23,6 +23,8 @@ tags: description: Customer management endpoints for creating and updating customer information - name: Contact Verification description: Endpoints for verifying a customer's email and phone via one-time codes. Required only for customers whose payment provider mandates contact verification (e.g. EU customers); other providers return 409. + - name: Strong Customer Authentication + description: Endpoints for authorizing money-movement operations that require Strong Customer Authentication. Relevant only for customers whose payment provider mandates SCA (e.g. EU customers); other providers never surface an SCA challenge and these endpoints return 409. - name: KYC/KYB Verifications description: Endpoints for Know Your Customer (KYC) and Know Your Business (KYB) verification, including managing beneficial owners and triggering verification for customers. - name: Documents @@ -2761,6 +2763,13 @@ paths: exchangeRate: 0.92 feesIncluded: 10 transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000005 + '202': + description: | + Quote created but awaiting Strong Customer Authentication. Returned only for customers whose provider requires SCA (e.g. EU) on a realtime-funding source: the quote has status `PENDING_AUTHORIZATION` and carries an `scaChallenge`, and `paymentInstructions` are **withheld** until the challenge is authorized via `POST /quotes/{quoteId}/authorize`. Once authorized, the quote's `paymentInstructions` are populated. + content: + application/json: + schema: + $ref: '#/components/schemas/Quote' '400': description: Bad request - Missing or invalid parameters content: @@ -2843,11 +2852,18 @@ paths: schema: type: string example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/ExecuteQuoteRequest' responses: '200': description: | - Quote confirmed successfully. The transfer has been initiated and - the quote status has been updated. + Quote confirmed successfully. The transfer has been initiated and the quote status has been updated. + + For customers whose provider requires Strong Customer Authentication (e.g. EU), the quote may instead come back with status `PENDING_AUTHORIZATION` and an `scaChallenge` that must be authorized — inline via `scaAuthorization`, or by calling `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` — before the transfer is released. content: application/json: schema: @@ -2882,6 +2898,79 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /quotes/{quoteId}/authorize: + parameters: + - name: quoteId + in: path + description: The unique identifier of the quote whose SCA challenge is being authorized. + required: true + schema: + type: string + example: Quote:019542f5-b3e7-1d02-0000-000000000006 + post: + summary: Authorize a quote's SCA challenge + description: | + Satisfy the Strong Customer Authentication challenge carried by a quote in + `PENDING_AUTHORIZATION` status by submitting an `ScaAuthorization` proof. + + This is used for realtime-funding quotes: the quote is returned with an + `scaChallenge` and **without** `paymentInstructions`; once authorized, the + quote advances and its `paymentInstructions` are populated so the customer + can fund the transfer. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: authorizeQuote + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ScaAuthorization' + responses: + '200': + description: Challenge authorized; the updated quote is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/Quote' + '400': + description: Invalid or expired authorization proof + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Quote not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA, or the quote has no pending challenge to authorize. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /transactions: get: summary: List transactions @@ -3229,6 +3318,81 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /transactions/{transactionId}/authorize: + parameters: + - name: transactionId + in: path + description: The unique identifier of the transaction whose SCA challenge is being authorized. + required: true + schema: + type: string + example: Transaction:019542f5-b3e7-1d02-0000-000000000005 + post: + summary: Authorize a transaction's SCA challenge + description: | + Satisfy the Strong Customer Authentication challenge carried by a + transaction in `PENDING_AUTHORIZATION` status by submitting an + `ScaAuthorization` proof. On success the transaction advances to + `PROCESSING` (or `COMPLETED`) and the underlying transfer is released. + + Transactions enter `PENDING_AUTHORIZATION` from `execute` (embedded-wallet + quotes) and `transfer-out` when the customer's provider requires SCA. The + same authorization can be supplied inline on those calls via their optional + `scaAuthorization` field instead of calling this endpoint. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: authorizeTransaction + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ScaAuthorization' + responses: + '200': + description: Challenge authorized; the updated transaction is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionOneOf' + '400': + description: Invalid or expired authorization proof + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Transaction not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA, or the transaction has no pending challenge to authorize. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /crypto/estimate-withdrawal-fee: post: summary: Estimate crypto withdrawal fee @@ -16800,6 +16964,7 @@ components: enum: - CREATED - PENDING + - PENDING_AUTHORIZATION - PROCESSING - COMPLETED - REJECTED @@ -16813,6 +16978,7 @@ components: |--------|-------------| | `CREATED` | Initial lookup has been created | | `PENDING` | Quote has been created | + | `PENDING_AUTHORIZATION` | Awaiting Strong Customer Authentication. Only occurs for customers whose provider requires SCA (e.g. EU); authorize the transaction's `scaChallenge` to proceed. | | `PROCESSING` | Funding has been received and payment initiated | | `COMPLETED` | Cross border payment has been received, converted and payment has been sent to the offramp network | | `REJECTED` | Receiving institution or wallet rejected payment, payment has been refunded | @@ -16896,6 +17062,57 @@ components: FULL_NAME: John Sender BIRTH_DATE: '1985-06-15' NATIONALITY: DE + ScaFactor: + type: string + enum: + - SMS_OTP + - TOTP + - PASSKEY + description: | + A Strong Customer Authentication factor. + + | Factor | Description | + |--------|-------------| + | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. | + | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). | + | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. | + ScaChallenge: + type: object + description: |- + A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer's payment provider requires SCA** (e.g. EU customers on an e-money provider); for customers whose provider has no such requirement it is omitted entirely and no action is needed. + + When present on a quote or transaction, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or `POST /transactions/{transactionId}/authorize`, or inline via the optional `scaAuthorization` field on the originating `execute` / `transfer-out` call. + required: + - id + - expiresAt + - factor + - availableFactors + properties: + id: + type: string + description: Unique identifier for this challenge, supplied back when authorizing. + example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007 + expiresAt: + type: string + format: date-time + description: Absolute UTC timestamp after which this challenge can no longer be authorized. + example: '2025-10-03T12:05:00Z' + factor: + $ref: '#/components/schemas/ScaFactor' + description: The factor this challenge was issued for. Defaults to `SMS_OTP`. + availableFactors: + type: array + description: The factors the customer may use to satisfy this challenge. + items: + $ref: '#/components/schemas/ScaFactor' + example: + - SMS_OTP + passkeyAssertionOptions: + type: + - object + - 'null' + additionalProperties: true + description: Opaque WebAuthn assertion request options, present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. Transaction: type: object required: @@ -16958,6 +17175,10 @@ components: example: 'Payment for invoice #1234' counterpartyInformation: $ref: '#/components/schemas/CounterpartyInformation' + scaChallenge: + $ref: '#/components/schemas/ScaChallenge' + readOnly: true + description: 'Present only while `status` is `PENDING_AUTHORIZATION`: the Strong Customer Authentication challenge to satisfy before this transaction can proceed. Omitted entirely for customers whose provider does not require SCA.' TransactionSourceType: type: string enum: @@ -17578,6 +17799,22 @@ components: description: The payment rail to use for the transfer. Must be one of the rails supported by the destination account. If not specified, the system will select a default rail. allOf: - $ref: '#/components/schemas/PaymentRail' + ScaAuthorization: + type: object + description: Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). + properties: + code: + type: + - string + - 'null' + description: The one-time code the customer received by SMS, or read from their authenticator app. In sandbox, the code is always `123456`. + example: '123456' + passkeyAssertion: + type: + - object + - 'null' + additionalProperties: true + description: Opaque WebAuthn assertion produced by the device from the challenge's `passkeyAssertionOptions`. Required when satisfying a `PASSKEY` challenge. TransferOutRequest: type: object required: @@ -17600,6 +17837,9 @@ components: maxLength: 80 description: 'Free-form information about the payment that travels with it to the recipient. The field this populates depends on the payment rail: for ACH it populates the Addenda record, for FedNow and RTP it populates the remittanceInformation field, and for wires it populates the OBI (Originator to Beneficiary Information) / beneficiary information.' example: '12345' + scaAuthorization: + $ref: '#/components/schemas/ScaAuthorization' + description: '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 on the first call to receive the transaction in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers whose provider does not require SCA.' CurrencyPreference: type: object required: @@ -17859,11 +18099,12 @@ components: type: string enum: - PENDING + - PENDING_AUTHORIZATION - PROCESSING - COMPLETED - FAILED - EXPIRED - description: Current status of the quote + description: 'Current status of the quote. `PENDING_AUTHORIZATION` occurs only for customers whose provider requires Strong Customer Authentication (e.g. EU): the quote carries an `scaChallenge` that must be authorized before execution, and for realtime-funding sources `paymentInstructions` are withheld until it is satisfied.' example: PENDING createdAt: type: string @@ -17938,6 +18179,10 @@ components: rateDetails: $ref: '#/components/schemas/OutgoingRateDetails' description: Details about the rate and fees for the transaction. + scaChallenge: + $ref: '#/components/schemas/ScaChallenge' + readOnly: true + description: 'Present only while `status` is `PENDING_AUTHORIZATION`: the Strong Customer Authentication challenge to satisfy before this quote can be executed (or, for realtime-funding sources, before `paymentInstructions` are issued). Omitted for customers whose provider does not require SCA.' QuoteLockSide: type: string enum: @@ -18011,6 +18256,13 @@ components: example: FULL_NAME: Jane Receiver NATIONALITY: FR + ExecuteQuoteRequest: + type: object + description: Optional body for executing a quote. Only needed to supply an inline Strong Customer Authentication proof; omit the body entirely for quotes that do not require SCA. + properties: + scaAuthorization: + $ref: '#/components/schemas/ScaAuthorization' + description: '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 quote in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers whose provider does not require SCA.' TransactionListResponse: type: object required: diff --git a/openapi.yaml b/openapi.yaml index 5c510eb9c..4c182b0c1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -23,6 +23,8 @@ tags: description: Customer management endpoints for creating and updating customer information - name: Contact Verification description: Endpoints for verifying a customer's email and phone via one-time codes. Required only for customers whose payment provider mandates contact verification (e.g. EU customers); other providers return 409. + - name: Strong Customer Authentication + description: Endpoints for authorizing money-movement operations that require Strong Customer Authentication. Relevant only for customers whose payment provider mandates SCA (e.g. EU customers); other providers never surface an SCA challenge and these endpoints return 409. - name: KYC/KYB Verifications description: Endpoints for Know Your Customer (KYC) and Know Your Business (KYB) verification, including managing beneficial owners and triggering verification for customers. - name: Documents @@ -2761,6 +2763,13 @@ paths: exchangeRate: 0.92 feesIncluded: 10 transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000005 + '202': + description: | + Quote created but awaiting Strong Customer Authentication. Returned only for customers whose provider requires SCA (e.g. EU) on a realtime-funding source: the quote has status `PENDING_AUTHORIZATION` and carries an `scaChallenge`, and `paymentInstructions` are **withheld** until the challenge is authorized via `POST /quotes/{quoteId}/authorize`. Once authorized, the quote's `paymentInstructions` are populated. + content: + application/json: + schema: + $ref: '#/components/schemas/Quote' '400': description: Bad request - Missing or invalid parameters content: @@ -2843,11 +2852,18 @@ paths: schema: type: string example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/ExecuteQuoteRequest' responses: '200': description: | - Quote confirmed successfully. The transfer has been initiated and - the quote status has been updated. + Quote confirmed successfully. The transfer has been initiated and the quote status has been updated. + + For customers whose provider requires Strong Customer Authentication (e.g. EU), the quote may instead come back with status `PENDING_AUTHORIZATION` and an `scaChallenge` that must be authorized — inline via `scaAuthorization`, or by calling `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` — before the transfer is released. content: application/json: schema: @@ -2882,6 +2898,79 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /quotes/{quoteId}/authorize: + parameters: + - name: quoteId + in: path + description: The unique identifier of the quote whose SCA challenge is being authorized. + required: true + schema: + type: string + example: Quote:019542f5-b3e7-1d02-0000-000000000006 + post: + summary: Authorize a quote's SCA challenge + description: | + Satisfy the Strong Customer Authentication challenge carried by a quote in + `PENDING_AUTHORIZATION` status by submitting an `ScaAuthorization` proof. + + This is used for realtime-funding quotes: the quote is returned with an + `scaChallenge` and **without** `paymentInstructions`; once authorized, the + quote advances and its `paymentInstructions` are populated so the customer + can fund the transfer. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: authorizeQuote + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ScaAuthorization' + responses: + '200': + description: Challenge authorized; the updated quote is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/Quote' + '400': + description: Invalid or expired authorization proof + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Quote not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA, or the quote has no pending challenge to authorize. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /transactions: get: summary: List transactions @@ -3229,6 +3318,81 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /transactions/{transactionId}/authorize: + parameters: + - name: transactionId + in: path + description: The unique identifier of the transaction whose SCA challenge is being authorized. + required: true + schema: + type: string + example: Transaction:019542f5-b3e7-1d02-0000-000000000005 + post: + summary: Authorize a transaction's SCA challenge + description: | + Satisfy the Strong Customer Authentication challenge carried by a + transaction in `PENDING_AUTHORIZATION` status by submitting an + `ScaAuthorization` proof. On success the transaction advances to + `PROCESSING` (or `COMPLETED`) and the underlying transfer is released. + + Transactions enter `PENDING_AUTHORIZATION` from `execute` (embedded-wallet + quotes) and `transfer-out` when the customer's provider requires SCA. The + same authorization can be supplied inline on those calls via their optional + `scaAuthorization` field instead of calling this endpoint. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: authorizeTransaction + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ScaAuthorization' + responses: + '200': + description: Challenge authorized; the updated transaction is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionOneOf' + '400': + description: Invalid or expired authorization proof + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Transaction not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA, or the transaction has no pending challenge to authorize. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /crypto/estimate-withdrawal-fee: post: summary: Estimate crypto withdrawal fee @@ -16800,6 +16964,7 @@ components: enum: - CREATED - PENDING + - PENDING_AUTHORIZATION - PROCESSING - COMPLETED - REJECTED @@ -16813,6 +16978,7 @@ components: |--------|-------------| | `CREATED` | Initial lookup has been created | | `PENDING` | Quote has been created | + | `PENDING_AUTHORIZATION` | Awaiting Strong Customer Authentication. Only occurs for customers whose provider requires SCA (e.g. EU); authorize the transaction's `scaChallenge` to proceed. | | `PROCESSING` | Funding has been received and payment initiated | | `COMPLETED` | Cross border payment has been received, converted and payment has been sent to the offramp network | | `REJECTED` | Receiving institution or wallet rejected payment, payment has been refunded | @@ -16896,6 +17062,57 @@ components: FULL_NAME: John Sender BIRTH_DATE: '1985-06-15' NATIONALITY: DE + ScaFactor: + type: string + enum: + - SMS_OTP + - TOTP + - PASSKEY + description: | + A Strong Customer Authentication factor. + + | Factor | Description | + |--------|-------------| + | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. | + | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). | + | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. | + ScaChallenge: + type: object + description: |- + A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer's payment provider requires SCA** (e.g. EU customers on an e-money provider); for customers whose provider has no such requirement it is omitted entirely and no action is needed. + + When present on a quote or transaction, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or `POST /transactions/{transactionId}/authorize`, or inline via the optional `scaAuthorization` field on the originating `execute` / `transfer-out` call. + required: + - id + - expiresAt + - factor + - availableFactors + properties: + id: + type: string + description: Unique identifier for this challenge, supplied back when authorizing. + example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007 + expiresAt: + type: string + format: date-time + description: Absolute UTC timestamp after which this challenge can no longer be authorized. + example: '2025-10-03T12:05:00Z' + factor: + $ref: '#/components/schemas/ScaFactor' + description: The factor this challenge was issued for. Defaults to `SMS_OTP`. + availableFactors: + type: array + description: The factors the customer may use to satisfy this challenge. + items: + $ref: '#/components/schemas/ScaFactor' + example: + - SMS_OTP + passkeyAssertionOptions: + type: + - object + - 'null' + additionalProperties: true + description: Opaque WebAuthn assertion request options, present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. Transaction: type: object required: @@ -16958,6 +17175,10 @@ components: example: 'Payment for invoice #1234' counterpartyInformation: $ref: '#/components/schemas/CounterpartyInformation' + scaChallenge: + $ref: '#/components/schemas/ScaChallenge' + readOnly: true + description: 'Present only while `status` is `PENDING_AUTHORIZATION`: the Strong Customer Authentication challenge to satisfy before this transaction can proceed. Omitted entirely for customers whose provider does not require SCA.' TransactionSourceType: type: string enum: @@ -17578,6 +17799,22 @@ components: description: The payment rail to use for the transfer. Must be one of the rails supported by the destination account. If not specified, the system will select a default rail. allOf: - $ref: '#/components/schemas/PaymentRail' + ScaAuthorization: + type: object + description: Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). + properties: + code: + type: + - string + - 'null' + description: The one-time code the customer received by SMS, or read from their authenticator app. In sandbox, the code is always `123456`. + example: '123456' + passkeyAssertion: + type: + - object + - 'null' + additionalProperties: true + description: Opaque WebAuthn assertion produced by the device from the challenge's `passkeyAssertionOptions`. Required when satisfying a `PASSKEY` challenge. TransferOutRequest: type: object required: @@ -17600,6 +17837,9 @@ components: maxLength: 80 description: 'Free-form information about the payment that travels with it to the recipient. The field this populates depends on the payment rail: for ACH it populates the Addenda record, for FedNow and RTP it populates the remittanceInformation field, and for wires it populates the OBI (Originator to Beneficiary Information) / beneficiary information.' example: '12345' + scaAuthorization: + $ref: '#/components/schemas/ScaAuthorization' + description: '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 on the first call to receive the transaction in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers whose provider does not require SCA.' CurrencyPreference: type: object required: @@ -17859,11 +18099,12 @@ components: type: string enum: - PENDING + - PENDING_AUTHORIZATION - PROCESSING - COMPLETED - FAILED - EXPIRED - description: Current status of the quote + description: 'Current status of the quote. `PENDING_AUTHORIZATION` occurs only for customers whose provider requires Strong Customer Authentication (e.g. EU): the quote carries an `scaChallenge` that must be authorized before execution, and for realtime-funding sources `paymentInstructions` are withheld until it is satisfied.' example: PENDING createdAt: type: string @@ -17938,6 +18179,10 @@ components: rateDetails: $ref: '#/components/schemas/OutgoingRateDetails' description: Details about the rate and fees for the transaction. + scaChallenge: + $ref: '#/components/schemas/ScaChallenge' + readOnly: true + description: 'Present only while `status` is `PENDING_AUTHORIZATION`: the Strong Customer Authentication challenge to satisfy before this quote can be executed (or, for realtime-funding sources, before `paymentInstructions` are issued). Omitted for customers whose provider does not require SCA.' QuoteLockSide: type: string enum: @@ -18011,6 +18256,13 @@ components: example: FULL_NAME: Jane Receiver NATIONALITY: FR + ExecuteQuoteRequest: + type: object + description: Optional body for executing a quote. Only needed to supply an inline Strong Customer Authentication proof; omit the body entirely for quotes that do not require SCA. + properties: + scaAuthorization: + $ref: '#/components/schemas/ScaAuthorization' + description: '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 quote in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers whose provider does not require SCA.' TransactionListResponse: type: object required: diff --git a/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml b/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml new file mode 100644 index 000000000..b21335027 --- /dev/null +++ b/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml @@ -0,0 +1,15 @@ +type: object +description: >- + Optional body for executing a quote. Only needed to supply an inline Strong + Customer Authentication proof; omit the body entirely for quotes that do not + require SCA. +properties: + scaAuthorization: + $ref: ../sca/ScaAuthorization.yaml + description: >- + 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 quote in `PENDING_AUTHORIZATION` with an + `scaChallenge`, then authorize separately. Ignored for customers whose + provider does not require SCA. diff --git a/openapi/components/schemas/quotes/Quote.yaml b/openapi/components/schemas/quotes/Quote.yaml index aee147bbc..23b52f374 100644 --- a/openapi/components/schemas/quotes/Quote.yaml +++ b/openapi/components/schemas/quotes/Quote.yaml @@ -22,11 +22,17 @@ properties: type: string enum: - PENDING + - PENDING_AUTHORIZATION - PROCESSING - COMPLETED - FAILED - EXPIRED - description: Current status of the quote + description: >- + Current status of the quote. `PENDING_AUTHORIZATION` occurs only for + customers whose provider requires Strong Customer Authentication (e.g. + EU): the quote carries an `scaChallenge` that must be authorized before + execution, and for realtime-funding sources `paymentInstructions` are + withheld until it is satisfied. example: PENDING createdAt: type: string @@ -120,3 +126,11 @@ properties: rateDetails: $ref: ../transactions/OutgoingRateDetails.yaml description: Details about the rate and fees for the transaction. + scaChallenge: + $ref: ../sca/ScaChallenge.yaml + readOnly: true + description: >- + Present only while `status` is `PENDING_AUTHORIZATION`: the Strong + Customer Authentication challenge to satisfy before this quote can be + executed (or, for realtime-funding sources, before `paymentInstructions` + are issued). Omitted for customers whose provider does not require SCA. diff --git a/openapi/components/schemas/sca/ScaAuthorization.yaml b/openapi/components/schemas/sca/ScaAuthorization.yaml new file mode 100644 index 000000000..e51dbc1bd --- /dev/null +++ b/openapi/components/schemas/sca/ScaAuthorization.yaml @@ -0,0 +1,21 @@ +type: object +description: >- + Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for + `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). +properties: + code: + type: + - string + - 'null' + description: >- + The one-time code the customer received by SMS, or read from their + authenticator app. In sandbox, the code is always `123456`. + example: '123456' + passkeyAssertion: + type: + - object + - 'null' + additionalProperties: true + description: >- + Opaque WebAuthn assertion produced by the device from the challenge's + `passkeyAssertionOptions`. Required when satisfying a `PASSKEY` challenge. diff --git a/openapi/components/schemas/sca/ScaChallenge.yaml b/openapi/components/schemas/sca/ScaChallenge.yaml new file mode 100644 index 000000000..dc822cd6f --- /dev/null +++ b/openapi/components/schemas/sca/ScaChallenge.yaml @@ -0,0 +1,47 @@ +type: object +description: >- + A Strong Customer Authentication challenge that must be satisfied before a + money-movement operation can complete. This object is **only present when the + customer's payment provider requires SCA** (e.g. EU customers on an e-money + provider); for customers whose provider has no such requirement it is omitted + entirely and no action is needed. + + + When present on a quote or transaction, authorize it by submitting an + `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or + `POST /transactions/{transactionId}/authorize`, or inline via the optional + `scaAuthorization` field on the originating `execute` / `transfer-out` call. +required: + - id + - expiresAt + - factor + - availableFactors +properties: + id: + type: string + description: Unique identifier for this challenge, supplied back when authorizing. + example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007 + expiresAt: + type: string + format: date-time + description: Absolute UTC timestamp after which this challenge can no longer be authorized. + example: '2025-10-03T12:05:00Z' + factor: + $ref: ./ScaFactor.yaml + description: The factor this challenge was issued for. Defaults to `SMS_OTP`. + availableFactors: + type: array + description: The factors the customer may use to satisfy this challenge. + items: + $ref: ./ScaFactor.yaml + example: + - SMS_OTP + passkeyAssertionOptions: + type: + - object + - 'null' + additionalProperties: true + description: >- + Opaque WebAuthn assertion request options, present only when `factor` is + `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion + submitted back in `ScaAuthorization.passkeyAssertion`. diff --git a/openapi/components/schemas/sca/ScaFactor.yaml b/openapi/components/schemas/sca/ScaFactor.yaml new file mode 100644 index 000000000..2e48b92bf --- /dev/null +++ b/openapi/components/schemas/sca/ScaFactor.yaml @@ -0,0 +1,13 @@ +type: string +enum: + - SMS_OTP + - TOTP + - PASSKEY +description: | + A Strong Customer Authentication factor. + + | Factor | Description | + |--------|-------------| + | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. | + | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). | + | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. | diff --git a/openapi/components/schemas/transactions/Transaction.yaml b/openapi/components/schemas/transactions/Transaction.yaml index ebc3ce889..9ace65c0c 100644 --- a/openapi/components/schemas/transactions/Transaction.yaml +++ b/openapi/components/schemas/transactions/Transaction.yaml @@ -65,3 +65,11 @@ properties: example: 'Payment for invoice #1234' counterpartyInformation: $ref: ./CounterpartyInformation.yaml + scaChallenge: + $ref: ../sca/ScaChallenge.yaml + readOnly: true + description: >- + Present only while `status` is `PENDING_AUTHORIZATION`: the Strong + Customer Authentication challenge to satisfy before this transaction can + proceed. Omitted entirely for customers whose provider does not require + SCA. diff --git a/openapi/components/schemas/transactions/TransactionStatus.yaml b/openapi/components/schemas/transactions/TransactionStatus.yaml index e13b4bdb0..19377b812 100644 --- a/openapi/components/schemas/transactions/TransactionStatus.yaml +++ b/openapi/components/schemas/transactions/TransactionStatus.yaml @@ -2,6 +2,7 @@ type: string enum: - CREATED - PENDING + - PENDING_AUTHORIZATION - PROCESSING - COMPLETED - REJECTED @@ -15,6 +16,7 @@ description: | |--------|-------------| | `CREATED` | Initial lookup has been created | | `PENDING` | Quote has been created | + | `PENDING_AUTHORIZATION` | Awaiting Strong Customer Authentication. Only occurs for customers whose provider requires SCA (e.g. EU); authorize the transaction's `scaChallenge` to proceed. | | `PROCESSING` | Funding has been received and payment initiated | | `COMPLETED` | Cross border payment has been received, converted and payment has been sent to the offramp network | | `REJECTED` | Receiving institution or wallet rejected payment, payment has been refunded | diff --git a/openapi/components/schemas/transfers/TransferOutRequest.yaml b/openapi/components/schemas/transfers/TransferOutRequest.yaml index 1678b4070..eb5bb2f1a 100644 --- a/openapi/components/schemas/transfers/TransferOutRequest.yaml +++ b/openapi/components/schemas/transfers/TransferOutRequest.yaml @@ -26,3 +26,12 @@ properties: remittanceInformation field, and for wires it populates the OBI (Originator to Beneficiary Information) / beneficiary information. example: '12345' + scaAuthorization: + $ref: ../sca/ScaAuthorization.yaml + description: >- + 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 on the first call to receive the transaction in + `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. + Ignored for customers whose provider does not require SCA. diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 6f3cc6181..300544efc 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -23,6 +23,12 @@ tags: Endpoints for verifying a customer's email and phone via one-time codes. Required only for customers whose payment provider mandates contact verification (e.g. EU customers); other providers return 409. + - name: Strong Customer Authentication + description: >- + Endpoints for authorizing money-movement operations that require Strong + Customer Authentication. Relevant only for customers whose payment + provider mandates SCA (e.g. EU customers); other providers never surface + an SCA challenge and these endpoints return 409. - name: KYC/KYB Verifications description: >- Endpoints for Know Your Customer (KYC) and Know Your Business (KYB) @@ -187,6 +193,8 @@ paths: $ref: paths/quotes/quotes.yaml /quotes/{quoteId}/execute: $ref: paths/quotes/quotes_{quoteId}_execute.yaml + /quotes/{quoteId}/authorize: + $ref: paths/quotes/quotes_{quoteId}_authorize.yaml /transactions: $ref: paths/transactions/transactions.yaml /transactions/{transactionId}: @@ -197,6 +205,8 @@ paths: $ref: paths/transactions/transactions_{transactionId}_approve.yaml /transactions/{transactionId}/reject: $ref: paths/transactions/transactions_{transactionId}_reject.yaml + /transactions/{transactionId}/authorize: + $ref: paths/transactions/transactions_{transactionId}_authorize.yaml /crypto/estimate-withdrawal-fee: $ref: paths/crypto/crypto_estimate-withdrawal-fee.yaml /sandbox/webhooks/test: diff --git a/openapi/paths/quotes/quotes.yaml b/openapi/paths/quotes/quotes.yaml index 75322de32..651e5573e 100644 --- a/openapi/paths/quotes/quotes.yaml +++ b/openapi/paths/quotes/quotes.yaml @@ -115,6 +115,18 @@ post: exchangeRate: 0.92 feesIncluded: 10 transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000005 + '202': + description: > + Quote created but awaiting Strong Customer Authentication. Returned only + for customers whose provider requires SCA (e.g. EU) on a realtime-funding + source: the quote has status `PENDING_AUTHORIZATION` and carries an + `scaChallenge`, and `paymentInstructions` are **withheld** until the + challenge is authorized via `POST /quotes/{quoteId}/authorize`. Once + authorized, the quote's `paymentInstructions` are populated. + content: + application/json: + schema: + $ref: ../../components/schemas/quotes/Quote.yaml '400': description: Bad request - Missing or invalid parameters content: diff --git a/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml b/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml new file mode 100644 index 000000000..d868f43c0 --- /dev/null +++ b/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml @@ -0,0 +1,74 @@ +parameters: + - name: quoteId + in: path + description: The unique identifier of the quote whose SCA challenge is being authorized. + required: true + schema: + type: string + example: Quote:019542f5-b3e7-1d02-0000-000000000006 +post: + summary: Authorize a quote's SCA challenge + description: | + Satisfy the Strong Customer Authentication challenge carried by a quote in + `PENDING_AUTHORIZATION` status by submitting an `ScaAuthorization` proof. + + This is used for realtime-funding quotes: the quote is returned with an + `scaChallenge` and **without** `paymentInstructions`; once authorized, the + quote advances and its `paymentInstructions` are populated so the customer + can fund the transfer. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: authorizeQuote + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/sca/ScaAuthorization.yaml + responses: + '200': + description: Challenge authorized; the updated quote is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/quotes/Quote.yaml + '400': + description: Invalid or expired authorization proof + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Quote not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: >- + The customer's payment provider does not require SCA, or the quote has + no pending challenge to authorize. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error409.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml diff --git a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml index 27d1bb7a8..dfd714459 100644 --- a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml +++ b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml @@ -50,12 +50,25 @@ post: schema: type: string example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + requestBody: + required: false + content: + application/json: + schema: + $ref: ../../components/schemas/quotes/ExecuteQuoteRequest.yaml responses: '200': description: > - Quote confirmed successfully. The transfer has been initiated and + Quote confirmed successfully. The transfer has been initiated and the + quote status has been updated. - the quote status has been updated. + + For customers whose provider requires Strong Customer Authentication + (e.g. EU), the quote may instead come back with status + `PENDING_AUTHORIZATION` and an `scaChallenge` that must be authorized — + inline via `scaAuthorization`, or by calling + `POST /transactions/{transactionId}/authorize` for the quote's + `transactionId` — before the transfer is released. content: application/json: schema: diff --git a/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml b/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml new file mode 100644 index 000000000..fa7ea394b --- /dev/null +++ b/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml @@ -0,0 +1,76 @@ +parameters: + - name: transactionId + in: path + description: The unique identifier of the transaction whose SCA challenge is being authorized. + required: true + schema: + type: string + example: Transaction:019542f5-b3e7-1d02-0000-000000000005 +post: + summary: Authorize a transaction's SCA challenge + description: | + Satisfy the Strong Customer Authentication challenge carried by a + transaction in `PENDING_AUTHORIZATION` status by submitting an + `ScaAuthorization` proof. On success the transaction advances to + `PROCESSING` (or `COMPLETED`) and the underlying transfer is released. + + Transactions enter `PENDING_AUTHORIZATION` from `execute` (embedded-wallet + quotes) and `transfer-out` when the customer's provider requires SCA. The + same authorization can be supplied inline on those calls via their optional + `scaAuthorization` field instead of calling this endpoint. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: authorizeTransaction + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/sca/ScaAuthorization.yaml + responses: + '200': + description: Challenge authorized; the updated transaction is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/transactions/TransactionOneOf.yaml + '400': + description: Invalid or expired authorization proof + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Transaction not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: >- + The customer's payment provider does not require SCA, or the + transaction has no pending challenge to authorize. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error409.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml From a181371c6c20210115ab94e6b86b6952f0a82314 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 8 Jun 2026 00:06:25 -0700 Subject: [PATCH 02/10] docs(sca): clarify ScaChallenge.id is informational, not supplied back 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 --- mintlify/openapi.yaml | 2 +- openapi.yaml | 2 +- openapi/components/schemas/sca/ScaChallenge.yaml | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 4c182b0c1..53b7bca6d 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -17090,7 +17090,7 @@ components: properties: id: type: string - description: Unique identifier for this challenge, supplied back when authorizing. + description: Unique identifier for this challenge. The server resolves the active challenge from the quote or transaction being authorized, so this field need not be supplied back; it is informational (e.g. for logging or correlation). example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007 expiresAt: type: string diff --git a/openapi.yaml b/openapi.yaml index 4c182b0c1..53b7bca6d 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -17090,7 +17090,7 @@ components: properties: id: type: string - description: Unique identifier for this challenge, supplied back when authorizing. + description: Unique identifier for this challenge. The server resolves the active challenge from the quote or transaction being authorized, so this field need not be supplied back; it is informational (e.g. for logging or correlation). example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007 expiresAt: type: string diff --git a/openapi/components/schemas/sca/ScaChallenge.yaml b/openapi/components/schemas/sca/ScaChallenge.yaml index dc822cd6f..a2529ad04 100644 --- a/openapi/components/schemas/sca/ScaChallenge.yaml +++ b/openapi/components/schemas/sca/ScaChallenge.yaml @@ -19,7 +19,11 @@ required: properties: id: type: string - description: Unique identifier for this challenge, supplied back when authorizing. + description: >- + Unique identifier for this challenge. The server resolves the active + challenge from the quote or transaction being authorized, so this field + need not be supplied back; it is informational (e.g. for logging or + correlation). example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007 expiresAt: type: string From b72ee9d75feed97ff7e90fc82e86bbe35e0d536c Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 8 Jun 2026 00:19:57 -0700 Subject: [PATCH 03/10] docs(sca): clarify inline scaAuthorization is for the initial execute 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 --- mintlify/openapi.yaml | 2 +- openapi.yaml | 2 +- openapi/paths/quotes/quotes_{quoteId}_execute.yaml | 12 +++++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 53b7bca6d..060ca3d73 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2863,7 +2863,7 @@ paths: description: | Quote confirmed successfully. The transfer has been initiated and the quote status has been updated. - For customers whose provider requires Strong Customer Authentication (e.g. EU), the quote may instead come back with status `PENDING_AUTHORIZATION` and an `scaChallenge` that must be authorized — inline via `scaAuthorization`, or by calling `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` — before the transfer is released. + For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` **upfront on this call** satisfies the challenge in one shot and avoids the pending state. Without it, the quote comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; from that point the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). The inline `scaAuthorization` field is therefore for the initial call only. content: application/json: schema: diff --git a/openapi.yaml b/openapi.yaml index 53b7bca6d..060ca3d73 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2863,7 +2863,7 @@ paths: description: | Quote confirmed successfully. The transfer has been initiated and the quote status has been updated. - For customers whose provider requires Strong Customer Authentication (e.g. EU), the quote may instead come back with status `PENDING_AUTHORIZATION` and an `scaChallenge` that must be authorized — inline via `scaAuthorization`, or by calling `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` — before the transfer is released. + For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` **upfront on this call** satisfies the challenge in one shot and avoids the pending state. Without it, the quote comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; from that point the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). The inline `scaAuthorization` field is therefore for the initial call only. content: application/json: schema: diff --git a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml index dfd714459..dd47e7bc6 100644 --- a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml +++ b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml @@ -64,11 +64,13 @@ post: For customers whose provider requires Strong Customer Authentication - (e.g. EU), the quote may instead come back with status - `PENDING_AUTHORIZATION` and an `scaChallenge` that must be authorized — - inline via `scaAuthorization`, or by calling - `POST /transactions/{transactionId}/authorize` for the quote's - `transactionId` — before the transfer is released. + (e.g. EU): supplying `scaAuthorization` **upfront on this call** satisfies + the challenge in one shot and avoids the pending state. Without it, the + quote comes back with status `PENDING_AUTHORIZATION` and an + `scaChallenge`; from that point the **only** way to release the transfer + is `POST /transactions/{transactionId}/authorize` for the quote's + `transactionId` (re-calling `execute` returns 409). The inline + `scaAuthorization` field is therefore for the initial call only. content: application/json: schema: From 736ce4b43627ccd1030edbe95f88c7ca4a05bf2e Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 8 Jun 2026 00:26:30 -0700 Subject: [PATCH 04/10] docs(sca): document PENDING_AUTHORIZATION outcome on transfer-out 201 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 --- mintlify/openapi.yaml | 5 ++++- openapi.yaml | 5 ++++- openapi/paths/transfers/transfer_out.yaml | 11 ++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 060ca3d73..5d984ed05 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2426,7 +2426,10 @@ paths: remittanceInformation: '12345' responses: '201': - description: Transfer-out request created successfully + description: | + Transfer-out request created successfully. + + For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies the challenge in one shot. Without it, the returned transaction comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by calling `POST /transactions/{transactionId}/authorize`. For providers that don't require SCA the transaction proceeds as usual. content: application/json: schema: diff --git a/openapi.yaml b/openapi.yaml index 060ca3d73..5d984ed05 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2426,7 +2426,10 @@ paths: remittanceInformation: '12345' responses: '201': - description: Transfer-out request created successfully + description: | + Transfer-out request created successfully. + + For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies the challenge in one shot. Without it, the returned transaction comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by calling `POST /transactions/{transactionId}/authorize`. For providers that don't require SCA the transaction proceeds as usual. content: application/json: schema: diff --git a/openapi/paths/transfers/transfer_out.yaml b/openapi/paths/transfers/transfer_out.yaml index 60b5b36c8..b72a39c36 100644 --- a/openapi/paths/transfers/transfer_out.yaml +++ b/openapi/paths/transfers/transfer_out.yaml @@ -36,7 +36,16 @@ post: remittanceInformation: '12345' responses: '201': - description: Transfer-out request created successfully + description: > + Transfer-out request created successfully. + + + For customers whose provider requires Strong Customer Authentication + (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies + the challenge in one shot. Without it, the returned transaction comes + back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release + the transfer by calling `POST /transactions/{transactionId}/authorize`. + For providers that don't require SCA the transaction proceeds as usual. content: application/json: schema: From 0e2f4f168c0ccfe3d9ab4b7bbf08b39802460c73 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Wed, 17 Jun 2026 14:06:28 -0700 Subject: [PATCH 05/10] feat(sca): complete the per-transaction SCA surface (passkey origin + resend) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- mintlify/openapi.yaml | 127 +++++++++++++++++- openapi.yaml | 127 +++++++++++++++++- .../schemas/sca/ScaAuthorization.yaml | 14 +- .../components/schemas/sca/ScaChallenge.yaml | 15 ++- openapi/openapi.yaml | 4 + .../quotes_{quoteId}_authorize_resend.yaml | 54 ++++++++ .../quotes/quotes_{quoteId}_execute.yaml | 6 +- ...ions_{transactionId}_authorize_resend.yaml | 56 ++++++++ 8 files changed, 394 insertions(+), 9 deletions(-) create mode 100644 openapi/paths/quotes/quotes_{quoteId}_authorize_resend.yaml create mode 100644 openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 5d984ed05..1ec645947 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2866,7 +2866,7 @@ paths: description: | Quote confirmed successfully. The transfer has been initiated and the quote status has been updated. - For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` **upfront on this call** satisfies the challenge in one shot and avoids the pending state. Without it, the quote comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; from that point the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). The inline `scaAuthorization` field is therefore for the initial call only. + For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` **upfront on this call** satisfies the challenge in one shot and avoids the pending state. Without it, the quote comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; from that point the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). The inline `scaAuthorization` field is therefore for the initial call only. (For a realtime-funding quote that returned 202 from `POST /quotes`, authorize the quote itself via `POST /quotes/{quoteId}/authorize` instead.) If an SMS code lapses, re-send it via `POST /transactions/{transactionId}/authorize/resend`. content: application/json: schema: @@ -2974,6 +2974,58 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /quotes/{quoteId}/authorize/resend: + parameters: + - name: quoteId + in: path + description: The unique identifier of the quote whose SCA challenge code should be re-sent. + required: true + schema: + type: string + example: Quote:019542f5-b3e7-1d02-0000-000000000006 + post: + summary: Resend a quote's SCA challenge code + description: | + Re-send the one-time code for a realtime-funding quote in + `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP`. The + existing challenge is reused — no new challenge is issued. + + Only meaningful for customers whose provider requires SCA (e.g. EU); other + providers return 409. `PASSKEY` challenges cannot be re-sent and return 409. + + In sandbox, the code is always `123456`. + operationId: resendQuoteScaCode + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '204': + description: Code re-sent. + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Quote not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The customer's provider does not require SCA, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /transactions: get: summary: List transactions @@ -3396,6 +3448,60 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /transactions/{transactionId}/authorize/resend: + parameters: + - name: transactionId + in: path + description: The unique identifier of the transaction whose SCA challenge code should be re-sent. + required: true + schema: + type: string + example: Transaction:019542f5-b3e7-1d02-0000-000000000005 + post: + summary: Resend a transaction's SCA challenge code + description: | + Re-send the one-time code for a transaction in `PENDING_AUTHORIZATION` + status whose `scaChallenge.factor` is `SMS_OTP` (e.g. the customer didn't + receive it, or it expired). The existing challenge is reused — no new + challenge is issued. + + Only meaningful for customers whose provider requires SCA (e.g. EU); other + providers return 409. `PASSKEY` challenges cannot be re-sent (there is no + code to deliver) and return 409. + + In sandbox, the code is always `123456`. + operationId: resendTransactionScaCode + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '204': + description: Code re-sent. + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Transaction not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The customer's provider does not require SCA, the transaction has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /crypto/estimate-withdrawal-fee: post: summary: Estimate crypto withdrawal fee @@ -17115,7 +17221,16 @@ components: - object - 'null' additionalProperties: true - description: Opaque WebAuthn assertion request options, present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. + description: Opaque WebAuthn assertion request options (including the relying-party id, challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. + passkeyAllowedOrigins: + type: + - array + - 'null' + items: + type: string + description: The origins the WebAuthn ceremony may run against, present only when `factor` is `PASSKEY`. The origin the assertion is produced against must be one of these and must be echoed back as `ScaAuthorization.origin`. + example: + - https://app.example.com Transaction: type: object required: @@ -17804,7 +17919,7 @@ components: - $ref: '#/components/schemas/PaymentRail' ScaAuthorization: type: object - description: Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). + description: Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). When supplying a `passkeyAssertion`, `origin` is **required** — the WebAuthn assertion is bound to the origin it was produced against, and the provider rejects a passkey confirmation without it. properties: code: type: @@ -17818,6 +17933,12 @@ components: - 'null' additionalProperties: true description: Opaque WebAuthn assertion produced by the device from the challenge's `passkeyAssertionOptions`. Required when satisfying a `PASSKEY` challenge. + origin: + type: + - string + - 'null' + description: The WebAuthn origin the `passkeyAssertion` was produced against (one of the challenge's `passkeyAllowedOrigins`). **Required** alongside `passkeyAssertion`; omit it for the `code` path. + example: https://app.example.com TransferOutRequest: type: object required: diff --git a/openapi.yaml b/openapi.yaml index 5d984ed05..1ec645947 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2866,7 +2866,7 @@ paths: description: | Quote confirmed successfully. The transfer has been initiated and the quote status has been updated. - For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` **upfront on this call** satisfies the challenge in one shot and avoids the pending state. Without it, the quote comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; from that point the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). The inline `scaAuthorization` field is therefore for the initial call only. + For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` **upfront on this call** satisfies the challenge in one shot and avoids the pending state. Without it, the quote comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; from that point the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). The inline `scaAuthorization` field is therefore for the initial call only. (For a realtime-funding quote that returned 202 from `POST /quotes`, authorize the quote itself via `POST /quotes/{quoteId}/authorize` instead.) If an SMS code lapses, re-send it via `POST /transactions/{transactionId}/authorize/resend`. content: application/json: schema: @@ -2974,6 +2974,58 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /quotes/{quoteId}/authorize/resend: + parameters: + - name: quoteId + in: path + description: The unique identifier of the quote whose SCA challenge code should be re-sent. + required: true + schema: + type: string + example: Quote:019542f5-b3e7-1d02-0000-000000000006 + post: + summary: Resend a quote's SCA challenge code + description: | + Re-send the one-time code for a realtime-funding quote in + `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP`. The + existing challenge is reused — no new challenge is issued. + + Only meaningful for customers whose provider requires SCA (e.g. EU); other + providers return 409. `PASSKEY` challenges cannot be re-sent and return 409. + + In sandbox, the code is always `123456`. + operationId: resendQuoteScaCode + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '204': + description: Code re-sent. + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Quote not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The customer's provider does not require SCA, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /transactions: get: summary: List transactions @@ -3396,6 +3448,60 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /transactions/{transactionId}/authorize/resend: + parameters: + - name: transactionId + in: path + description: The unique identifier of the transaction whose SCA challenge code should be re-sent. + required: true + schema: + type: string + example: Transaction:019542f5-b3e7-1d02-0000-000000000005 + post: + summary: Resend a transaction's SCA challenge code + description: | + Re-send the one-time code for a transaction in `PENDING_AUTHORIZATION` + status whose `scaChallenge.factor` is `SMS_OTP` (e.g. the customer didn't + receive it, or it expired). The existing challenge is reused — no new + challenge is issued. + + Only meaningful for customers whose provider requires SCA (e.g. EU); other + providers return 409. `PASSKEY` challenges cannot be re-sent (there is no + code to deliver) and return 409. + + In sandbox, the code is always `123456`. + operationId: resendTransactionScaCode + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '204': + description: Code re-sent. + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Transaction not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The customer's provider does not require SCA, the transaction has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /crypto/estimate-withdrawal-fee: post: summary: Estimate crypto withdrawal fee @@ -17115,7 +17221,16 @@ components: - object - 'null' additionalProperties: true - description: Opaque WebAuthn assertion request options, present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. + description: Opaque WebAuthn assertion request options (including the relying-party id, challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. + passkeyAllowedOrigins: + type: + - array + - 'null' + items: + type: string + description: The origins the WebAuthn ceremony may run against, present only when `factor` is `PASSKEY`. The origin the assertion is produced against must be one of these and must be echoed back as `ScaAuthorization.origin`. + example: + - https://app.example.com Transaction: type: object required: @@ -17804,7 +17919,7 @@ components: - $ref: '#/components/schemas/PaymentRail' ScaAuthorization: type: object - description: Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). + description: Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). When supplying a `passkeyAssertion`, `origin` is **required** — the WebAuthn assertion is bound to the origin it was produced against, and the provider rejects a passkey confirmation without it. properties: code: type: @@ -17818,6 +17933,12 @@ components: - 'null' additionalProperties: true description: Opaque WebAuthn assertion produced by the device from the challenge's `passkeyAssertionOptions`. Required when satisfying a `PASSKEY` challenge. + origin: + type: + - string + - 'null' + description: The WebAuthn origin the `passkeyAssertion` was produced against (one of the challenge's `passkeyAllowedOrigins`). **Required** alongside `passkeyAssertion`; omit it for the `code` path. + example: https://app.example.com TransferOutRequest: type: object required: diff --git a/openapi/components/schemas/sca/ScaAuthorization.yaml b/openapi/components/schemas/sca/ScaAuthorization.yaml index e51dbc1bd..ecf052865 100644 --- a/openapi/components/schemas/sca/ScaAuthorization.yaml +++ b/openapi/components/schemas/sca/ScaAuthorization.yaml @@ -1,7 +1,10 @@ type: object description: >- Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for - `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). + `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). When supplying a + `passkeyAssertion`, `origin` is **required** — the WebAuthn assertion is bound + to the origin it was produced against, and the provider rejects a passkey + confirmation without it. properties: code: type: @@ -19,3 +22,12 @@ properties: description: >- Opaque WebAuthn assertion produced by the device from the challenge's `passkeyAssertionOptions`. Required when satisfying a `PASSKEY` challenge. + origin: + type: + - string + - 'null' + description: >- + The WebAuthn origin the `passkeyAssertion` was produced against (one of + the challenge's `passkeyAllowedOrigins`). **Required** alongside + `passkeyAssertion`; omit it for the `code` path. + example: https://app.example.com diff --git a/openapi/components/schemas/sca/ScaChallenge.yaml b/openapi/components/schemas/sca/ScaChallenge.yaml index a2529ad04..023e03a41 100644 --- a/openapi/components/schemas/sca/ScaChallenge.yaml +++ b/openapi/components/schemas/sca/ScaChallenge.yaml @@ -46,6 +46,19 @@ properties: - 'null' additionalProperties: true description: >- - Opaque WebAuthn assertion request options, present only when `factor` is + Opaque WebAuthn assertion request options (including the relying-party id, + challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. + passkeyAllowedOrigins: + type: + - array + - 'null' + items: + type: string + description: >- + The origins the WebAuthn ceremony may run against, present only when + `factor` is `PASSKEY`. The origin the assertion is produced against must + be one of these and must be echoed back as `ScaAuthorization.origin`. + example: + - https://app.example.com diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 300544efc..95a61c7e8 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -195,6 +195,8 @@ paths: $ref: paths/quotes/quotes_{quoteId}_execute.yaml /quotes/{quoteId}/authorize: $ref: paths/quotes/quotes_{quoteId}_authorize.yaml + /quotes/{quoteId}/authorize/resend: + $ref: paths/quotes/quotes_{quoteId}_authorize_resend.yaml /transactions: $ref: paths/transactions/transactions.yaml /transactions/{transactionId}: @@ -207,6 +209,8 @@ paths: $ref: paths/transactions/transactions_{transactionId}_reject.yaml /transactions/{transactionId}/authorize: $ref: paths/transactions/transactions_{transactionId}_authorize.yaml + /transactions/{transactionId}/authorize/resend: + $ref: paths/transactions/transactions_{transactionId}_authorize_resend.yaml /crypto/estimate-withdrawal-fee: $ref: paths/crypto/crypto_estimate-withdrawal-fee.yaml /sandbox/webhooks/test: diff --git a/openapi/paths/quotes/quotes_{quoteId}_authorize_resend.yaml b/openapi/paths/quotes/quotes_{quoteId}_authorize_resend.yaml new file mode 100644 index 000000000..ffd4dc564 --- /dev/null +++ b/openapi/paths/quotes/quotes_{quoteId}_authorize_resend.yaml @@ -0,0 +1,54 @@ +parameters: + - name: quoteId + in: path + description: The unique identifier of the quote whose SCA challenge code should be re-sent. + required: true + schema: + type: string + example: Quote:019542f5-b3e7-1d02-0000-000000000006 +post: + summary: Resend a quote's SCA challenge code + description: | + Re-send the one-time code for a realtime-funding quote in + `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP`. The + existing challenge is reused — no new challenge is issued. + + Only meaningful for customers whose provider requires SCA (e.g. EU); other + providers return 409. `PASSKEY` challenges cannot be re-sent and return 409. + + In sandbox, the code is always `123456`. + operationId: resendQuoteScaCode + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '204': + description: Code re-sent. + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Quote not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: >- + The customer's provider does not require SCA, the quote has no pending + SMS challenge, or the challenge uses a factor whose code cannot be + re-sent (e.g. passkey). + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error409.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml diff --git a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml index dd47e7bc6..2c04ab7f1 100644 --- a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml +++ b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml @@ -70,7 +70,11 @@ post: `scaChallenge`; from that point the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). The inline - `scaAuthorization` field is therefore for the initial call only. + `scaAuthorization` field is therefore for the initial call only. (For a + realtime-funding quote that returned 202 from `POST /quotes`, authorize + the quote itself via `POST /quotes/{quoteId}/authorize` instead.) If an + SMS code lapses, re-send it via + `POST /transactions/{transactionId}/authorize/resend`. content: application/json: schema: diff --git a/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml b/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml new file mode 100644 index 000000000..a97c9ce2e --- /dev/null +++ b/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml @@ -0,0 +1,56 @@ +parameters: + - name: transactionId + in: path + description: The unique identifier of the transaction whose SCA challenge code should be re-sent. + required: true + schema: + type: string + example: Transaction:019542f5-b3e7-1d02-0000-000000000005 +post: + summary: Resend a transaction's SCA challenge code + description: | + Re-send the one-time code for a transaction in `PENDING_AUTHORIZATION` + status whose `scaChallenge.factor` is `SMS_OTP` (e.g. the customer didn't + receive it, or it expired). The existing challenge is reused — no new + challenge is issued. + + Only meaningful for customers whose provider requires SCA (e.g. EU); other + providers return 409. `PASSKEY` challenges cannot be re-sent (there is no + code to deliver) and return 409. + + In sandbox, the code is always `123456`. + operationId: resendTransactionScaCode + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '204': + description: Code re-sent. + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Transaction not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: >- + The customer's provider does not require SCA, the transaction has no + pending SMS challenge, or the challenge uses a factor whose code cannot + be re-sent (e.g. passkey). + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error409.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml From 3603269558bb32a47f5f6d39d02afb0758e01449 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Wed, 8 Jul 2026 13:51:38 -0700 Subject: [PATCH 06/10] fix(sca): close design-review gaps in the per-transaction surface - 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 --- mintlify/openapi.yaml | 12 ++++++++++-- openapi.yaml | 12 ++++++++++-- .../schemas/quotes/ExecuteQuoteRequest.yaml | 8 ++++++++ openapi/components/schemas/sca/ScaAuthorization.yaml | 10 +++++++--- openapi/components/schemas/sca/ScaChallenge.yaml | 9 ++++++--- .../transactions/OutgoingTransactionStatus.yaml | 2 ++ .../schemas/transfers/TransferOutRequest.yaml | 8 ++++++++ 7 files changed, 51 insertions(+), 10 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 1ec645947..d269878cb 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -17228,7 +17228,7 @@ components: - 'null' items: type: string - description: The origins the WebAuthn ceremony may run against, present only when `factor` is `PASSKEY`. The origin the assertion is produced against must be one of these and must be echoed back as `ScaAuthorization.origin`. + description: The origins the WebAuthn ceremony may run against. Populated for enrollment and login passkey challenges; the origin the assertion is produced against must be one of these and echoed back as `ScaAuthorization.origin`. Per-transaction passkey challenges omit this (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` for how to source the origin in that case. example: - https://app.example.com Transaction: @@ -17511,6 +17511,7 @@ components: type: string enum: - PENDING + - PENDING_AUTHORIZATION - EXPIRED - PROCESSING - COMPLETED @@ -17521,6 +17522,7 @@ components: | Status | Description | |--------|-------------| | `PENDING` | Quote is pending confirmation | + | `PENDING_AUTHORIZATION` | Awaiting Strong Customer Authentication. Only occurs for customers whose provider requires SCA (e.g. EU); authorize the transaction's `scaChallenge` to proceed. | | `EXPIRED` | Quote wasn't executed before expiry window | | `PROCESSING` | Executing the quote after receiving funds | | `COMPLETED` | Payout successfully reached the destination | @@ -17937,7 +17939,7 @@ components: type: - string - 'null' - description: The WebAuthn origin the `passkeyAssertion` was produced against (one of the challenge's `passkeyAllowedOrigins`). **Required** alongside `passkeyAssertion`; omit it for the `code` path. + description: The WebAuthn origin the `passkeyAssertion` was produced against. **Required** alongside `passkeyAssertion`; omit it for the `code` path. When the challenge lists `passkeyAllowedOrigins` (enrollment / login challenges), it must be one of those. A per-transaction passkey challenge carries `passkeyAssertionOptions` but may omit `passkeyAllowedOrigins`; in that case supply the origin your app invoked the WebAuthn API from, which must match the relying party in `passkeyAssertionOptions`. example: https://app.example.com TransferOutRequest: type: object @@ -17961,6 +17963,9 @@ components: maxLength: 80 description: 'Free-form information about the payment that travels with it to the recipient. The field this populates depends on the payment rail: for ACH it populates the Addenda record, for FedNow and RTP it populates the remittanceInformation field, and for wires it populates the OBI (Originator to Beneficiary Information) / beneficiary information.' example: '12345' + scaFactor: + $ref: '#/components/schemas/ScaFactor' + description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers whose provider requires SCA (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. scaAuthorization: $ref: '#/components/schemas/ScaAuthorization' description: '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 on the first call to receive the transaction in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers whose provider does not require SCA.' @@ -18384,6 +18389,9 @@ components: type: object description: Optional body for executing a quote. Only needed to supply an inline Strong Customer Authentication proof; omit the body entirely for quotes that do not require SCA. properties: + scaFactor: + $ref: '#/components/schemas/ScaFactor' + description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers whose provider requires SCA (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. scaAuthorization: $ref: '#/components/schemas/ScaAuthorization' description: '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 quote in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers whose provider does not require SCA.' diff --git a/openapi.yaml b/openapi.yaml index 1ec645947..d269878cb 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -17228,7 +17228,7 @@ components: - 'null' items: type: string - description: The origins the WebAuthn ceremony may run against, present only when `factor` is `PASSKEY`. The origin the assertion is produced against must be one of these and must be echoed back as `ScaAuthorization.origin`. + description: The origins the WebAuthn ceremony may run against. Populated for enrollment and login passkey challenges; the origin the assertion is produced against must be one of these and echoed back as `ScaAuthorization.origin`. Per-transaction passkey challenges omit this (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` for how to source the origin in that case. example: - https://app.example.com Transaction: @@ -17511,6 +17511,7 @@ components: type: string enum: - PENDING + - PENDING_AUTHORIZATION - EXPIRED - PROCESSING - COMPLETED @@ -17521,6 +17522,7 @@ components: | Status | Description | |--------|-------------| | `PENDING` | Quote is pending confirmation | + | `PENDING_AUTHORIZATION` | Awaiting Strong Customer Authentication. Only occurs for customers whose provider requires SCA (e.g. EU); authorize the transaction's `scaChallenge` to proceed. | | `EXPIRED` | Quote wasn't executed before expiry window | | `PROCESSING` | Executing the quote after receiving funds | | `COMPLETED` | Payout successfully reached the destination | @@ -17937,7 +17939,7 @@ components: type: - string - 'null' - description: The WebAuthn origin the `passkeyAssertion` was produced against (one of the challenge's `passkeyAllowedOrigins`). **Required** alongside `passkeyAssertion`; omit it for the `code` path. + description: The WebAuthn origin the `passkeyAssertion` was produced against. **Required** alongside `passkeyAssertion`; omit it for the `code` path. When the challenge lists `passkeyAllowedOrigins` (enrollment / login challenges), it must be one of those. A per-transaction passkey challenge carries `passkeyAssertionOptions` but may omit `passkeyAllowedOrigins`; in that case supply the origin your app invoked the WebAuthn API from, which must match the relying party in `passkeyAssertionOptions`. example: https://app.example.com TransferOutRequest: type: object @@ -17961,6 +17963,9 @@ components: maxLength: 80 description: 'Free-form information about the payment that travels with it to the recipient. The field this populates depends on the payment rail: for ACH it populates the Addenda record, for FedNow and RTP it populates the remittanceInformation field, and for wires it populates the OBI (Originator to Beneficiary Information) / beneficiary information.' example: '12345' + scaFactor: + $ref: '#/components/schemas/ScaFactor' + description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers whose provider requires SCA (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. scaAuthorization: $ref: '#/components/schemas/ScaAuthorization' description: '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 on the first call to receive the transaction in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers whose provider does not require SCA.' @@ -18384,6 +18389,9 @@ components: type: object description: Optional body for executing a quote. Only needed to supply an inline Strong Customer Authentication proof; omit the body entirely for quotes that do not require SCA. properties: + scaFactor: + $ref: '#/components/schemas/ScaFactor' + description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers whose provider requires SCA (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. scaAuthorization: $ref: '#/components/schemas/ScaAuthorization' description: '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 quote in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers whose provider does not require SCA.' diff --git a/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml b/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml index b21335027..6f4564763 100644 --- a/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml +++ b/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml @@ -4,6 +4,14 @@ description: >- Customer Authentication proof; omit the body entirely for quotes that do not require SCA. properties: + scaFactor: + $ref: ../sca/ScaFactor.yaml + description: >- + Optional preferred factor for the Strong Customer Authentication challenge + this call issues. Only relevant for customers whose provider requires SCA + (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge + are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required + dynamic linking and is rejected here. Omit to default to `SMS_OTP`. scaAuthorization: $ref: ../sca/ScaAuthorization.yaml description: >- diff --git a/openapi/components/schemas/sca/ScaAuthorization.yaml b/openapi/components/schemas/sca/ScaAuthorization.yaml index ecf052865..0b70f75d5 100644 --- a/openapi/components/schemas/sca/ScaAuthorization.yaml +++ b/openapi/components/schemas/sca/ScaAuthorization.yaml @@ -27,7 +27,11 @@ properties: - string - 'null' description: >- - The WebAuthn origin the `passkeyAssertion` was produced against (one of - the challenge's `passkeyAllowedOrigins`). **Required** alongside - `passkeyAssertion`; omit it for the `code` path. + The WebAuthn origin the `passkeyAssertion` was produced against. + **Required** alongside `passkeyAssertion`; omit it for the `code` path. + When the challenge lists `passkeyAllowedOrigins` (enrollment / login + challenges), it must be one of those. A per-transaction passkey challenge + carries `passkeyAssertionOptions` but may omit `passkeyAllowedOrigins`; in + that case supply the origin your app invoked the WebAuthn API from, which + must match the relying party in `passkeyAssertionOptions`. example: https://app.example.com diff --git a/openapi/components/schemas/sca/ScaChallenge.yaml b/openapi/components/schemas/sca/ScaChallenge.yaml index 023e03a41..083d4f2f1 100644 --- a/openapi/components/schemas/sca/ScaChallenge.yaml +++ b/openapi/components/schemas/sca/ScaChallenge.yaml @@ -57,8 +57,11 @@ properties: items: type: string description: >- - The origins the WebAuthn ceremony may run against, present only when - `factor` is `PASSKEY`. The origin the assertion is produced against must - be one of these and must be echoed back as `ScaAuthorization.origin`. + The origins the WebAuthn ceremony may run against. Populated for + enrollment and login passkey challenges; the origin the assertion is + produced against must be one of these and echoed back as + `ScaAuthorization.origin`. Per-transaction passkey challenges omit this + (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` + for how to source the origin in that case. example: - https://app.example.com diff --git a/openapi/components/schemas/transactions/OutgoingTransactionStatus.yaml b/openapi/components/schemas/transactions/OutgoingTransactionStatus.yaml index 426d67153..099d674c6 100644 --- a/openapi/components/schemas/transactions/OutgoingTransactionStatus.yaml +++ b/openapi/components/schemas/transactions/OutgoingTransactionStatus.yaml @@ -1,6 +1,7 @@ type: string enum: - PENDING + - PENDING_AUTHORIZATION - EXPIRED - PROCESSING - COMPLETED @@ -11,6 +12,7 @@ description: | | Status | Description | |--------|-------------| | `PENDING` | Quote is pending confirmation | + | `PENDING_AUTHORIZATION` | Awaiting Strong Customer Authentication. Only occurs for customers whose provider requires SCA (e.g. EU); authorize the transaction's `scaChallenge` to proceed. | | `EXPIRED` | Quote wasn't executed before expiry window | | `PROCESSING` | Executing the quote after receiving funds | | `COMPLETED` | Payout successfully reached the destination | diff --git a/openapi/components/schemas/transfers/TransferOutRequest.yaml b/openapi/components/schemas/transfers/TransferOutRequest.yaml index eb5bb2f1a..6cc777c0e 100644 --- a/openapi/components/schemas/transfers/TransferOutRequest.yaml +++ b/openapi/components/schemas/transfers/TransferOutRequest.yaml @@ -26,6 +26,14 @@ properties: remittanceInformation field, and for wires it populates the OBI (Originator to Beneficiary Information) / beneficiary information. example: '12345' + scaFactor: + $ref: ../sca/ScaFactor.yaml + description: >- + Optional preferred factor for the Strong Customer Authentication challenge + this call issues. Only relevant for customers whose provider requires SCA + (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge + are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required + dynamic linking and is rejected here. Omit to default to `SMS_OTP`. scaAuthorization: $ref: ../sca/ScaAuthorization.yaml description: >- From 1bf0bc39a20e549b57ceb161f5d0c88f8acc3aec Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Thu, 9 Jul 2026 00:19:53 -0700 Subject: [PATCH 07/10] docs(sca): full-fidelity quote SCA + rate-limit, expiry, and authorize-target clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- mintlify/openapi.yaml | 100 ++++++++++++------ openapi.yaml | 100 ++++++++++++------ .../schemas/quotes/QuoteRequest.yaml | 9 ++ .../quotes/quotes_{quoteId}_authorize.yaml | 13 ++- .../quotes_{quoteId}_authorize_resend.yaml | 14 ++- .../quotes/quotes_{quoteId}_execute.yaml | 29 ++--- ...ransactions_{transactionId}_authorize.yaml | 13 ++- ...ions_{transactionId}_authorize_resend.yaml | 15 ++- 8 files changed, 208 insertions(+), 85 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index d269878cb..1c2a90288 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2864,9 +2864,11 @@ paths: responses: '200': description: | - Quote confirmed successfully. The transfer has been initiated and the quote status has been updated. + Quote processed. The outcome depends on whether the customer's provider requires Strong Customer Authentication (SCA): - For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` **upfront on this call** satisfies the challenge in one shot and avoids the pending state. Without it, the quote comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; from that point the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). The inline `scaAuthorization` field is therefore for the initial call only. (For a realtime-funding quote that returned 202 from `POST /quotes`, authorize the quote itself via `POST /quotes/{quoteId}/authorize` instead.) If an SMS code lapses, re-send it via `POST /transactions/{transactionId}/authorize/resend`. + - **No SCA required, or a valid `scaAuthorization` is supplied inline on this call:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`). + + - **SCA required and no inline proof:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`, and the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). The inline `scaAuthorization` field is therefore for the initial call only. A client holding a `PENDING_AUTHORIZATION` quote from `execute` always authorizes via the **transaction**-scoped endpoint; the quote-scoped `POST /quotes/{quoteId}/authorize` is only for a realtime-funding quote whose challenge was issued at quote creation (the 202 from `POST /quotes`). If an SMS code lapses, re-send it via `POST /transactions/{transactionId}/authorize/resend`. content: application/json: schema: @@ -2925,7 +2927,7 @@ paths: requires SCA (e.g. EU customers). For customers whose provider has no such requirement, this returns `409`. - In sandbox, the SMS/TOTP code is always `123456`. + In sandbox, the SMS code is always `123456`. operationId: authorizeQuote tags: - Strong Customer Authentication @@ -2968,6 +2970,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Error409' + '429': + description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The provider may invalidate the challenge after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + content: + application/json: + schema: + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: @@ -2988,7 +2996,9 @@ paths: description: | Re-send the one-time code for a realtime-funding quote in `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP`. The - existing challenge is reused — no new challenge is issued. + existing challenge is reused — no new challenge is issued, and its + `scaChallenge.expiresAt` is **not** extended; once the challenge is past + `expiresAt` it can no longer be authorized. Only meaningful for customers whose provider requires SCA (e.g. EU); other providers return 409. `PASSKEY` challenges cannot be re-sent and return 409. @@ -3020,6 +3030,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Error409' + '429': + description: Too many requests. Returned with `RATE_LIMITED` when codes are re-sent too frequently, to avoid spamming the customer's phone. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + content: + application/json: + schema: + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: @@ -3399,7 +3415,7 @@ paths: requires SCA (e.g. EU customers). For customers whose provider has no such requirement, this returns `409`. - In sandbox, the SMS/TOTP code is always `123456`. + In sandbox, the SMS code is always `123456`. operationId: authorizeTransaction tags: - Strong Customer Authentication @@ -3442,6 +3458,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Error409' + '429': + description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The provider may invalidate the challenge after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + content: + application/json: + schema: + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: @@ -3462,8 +3484,9 @@ paths: description: | Re-send the one-time code for a transaction in `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP` (e.g. the customer didn't - receive it, or it expired). The existing challenge is reused — no new - challenge is issued. + receive it). The existing challenge is reused — no new challenge is issued, + and its `scaChallenge.expiresAt` is **not** extended; once the challenge is + past `expiresAt` it can no longer be authorized. Only meaningful for customers whose provider requires SCA (e.g. EU); other providers return 409. `PASSKEY` challenges cannot be re-sent (there is no @@ -3496,6 +3519,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Error409' + '429': + description: Too many requests. Returned with `RATE_LIMITED` when codes are re-sent too frequently, to avoid spamming the customer's phone. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + content: + application/json: + schema: + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: @@ -18376,6 +18405,9 @@ components: example: 'Invoice #1234 payment' purposeOfPayment: $ref: '#/components/schemas/PurposeOfPayment' + scaFactor: + $ref: '#/components/schemas/ScaFactor' + description: Optional preferred factor for a Strong Customer Authentication challenge issued at quote creation. Only relevant for a realtime-funding source whose provider requires SCA (e.g. EU); ignored otherwise. Valid values are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected. When the quote is returned in `PENDING_AUTHORIZATION`, authorize it via `POST /quotes/{quoteId}/authorize`. senderCustomerInfo: type: object additionalProperties: true @@ -18395,6 +18427,33 @@ components: scaAuthorization: $ref: '#/components/schemas/ScaAuthorization' description: '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 quote in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers whose provider does not require SCA.' + Error429: + type: object + required: + - message + - status + - code + properties: + status: + type: integer + enum: + - 429 + description: HTTP status code + code: + type: string + description: | + | Error Code | Description | + |------------|-------------| + | RATE_LIMITED | Too many requests in a short window; retry after the interval indicated by the `Retry-After` response header | + enum: + - RATE_LIMITED + message: + type: string + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true TransactionListResponse: type: object required: @@ -19367,33 +19426,6 @@ components: format: date-time description: Timestamp after which the session is no longer valid and the `encryptedSessionSigningKey` must not be used to sign further requests. example: '2026-04-09T15:30:01Z' - Error429: - type: object - required: - - message - - status - - code - properties: - status: - type: integer - enum: - - 429 - description: HTTP status code - code: - type: string - description: | - | Error Code | Description | - |------------|-------------| - | RATE_LIMITED | Too many requests in a short window; retry after the interval indicated by the `Retry-After` response header | - enum: - - RATE_LIMITED - message: - type: string - description: Error message - details: - type: object - description: Additional error details - additionalProperties: true AuthCredentialChallengeRequest: title: Auth Credential Challenge Request description: Request body for `POST /auth/credentials/{id}/challenge`. Required when re-challenging a `PASSKEY` credential — must carry `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from. Ignored for `EMAIL_OTP` and `SMS_OTP`, where the credential type alone is sufficient because the OTP is delivered out-of-band. OAuth credentials do not use this endpoint; authenticate or reauthenticate them with `POST /auth/credentials/{id}/verify`. diff --git a/openapi.yaml b/openapi.yaml index d269878cb..1c2a90288 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2864,9 +2864,11 @@ paths: responses: '200': description: | - Quote confirmed successfully. The transfer has been initiated and the quote status has been updated. + Quote processed. The outcome depends on whether the customer's provider requires Strong Customer Authentication (SCA): - For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` **upfront on this call** satisfies the challenge in one shot and avoids the pending state. Without it, the quote comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; from that point the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). The inline `scaAuthorization` field is therefore for the initial call only. (For a realtime-funding quote that returned 202 from `POST /quotes`, authorize the quote itself via `POST /quotes/{quoteId}/authorize` instead.) If an SMS code lapses, re-send it via `POST /transactions/{transactionId}/authorize/resend`. + - **No SCA required, or a valid `scaAuthorization` is supplied inline on this call:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`). + + - **SCA required and no inline proof:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`, and the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). The inline `scaAuthorization` field is therefore for the initial call only. A client holding a `PENDING_AUTHORIZATION` quote from `execute` always authorizes via the **transaction**-scoped endpoint; the quote-scoped `POST /quotes/{quoteId}/authorize` is only for a realtime-funding quote whose challenge was issued at quote creation (the 202 from `POST /quotes`). If an SMS code lapses, re-send it via `POST /transactions/{transactionId}/authorize/resend`. content: application/json: schema: @@ -2925,7 +2927,7 @@ paths: requires SCA (e.g. EU customers). For customers whose provider has no such requirement, this returns `409`. - In sandbox, the SMS/TOTP code is always `123456`. + In sandbox, the SMS code is always `123456`. operationId: authorizeQuote tags: - Strong Customer Authentication @@ -2968,6 +2970,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Error409' + '429': + description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The provider may invalidate the challenge after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + content: + application/json: + schema: + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: @@ -2988,7 +2996,9 @@ paths: description: | Re-send the one-time code for a realtime-funding quote in `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP`. The - existing challenge is reused — no new challenge is issued. + existing challenge is reused — no new challenge is issued, and its + `scaChallenge.expiresAt` is **not** extended; once the challenge is past + `expiresAt` it can no longer be authorized. Only meaningful for customers whose provider requires SCA (e.g. EU); other providers return 409. `PASSKEY` challenges cannot be re-sent and return 409. @@ -3020,6 +3030,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Error409' + '429': + description: Too many requests. Returned with `RATE_LIMITED` when codes are re-sent too frequently, to avoid spamming the customer's phone. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + content: + application/json: + schema: + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: @@ -3399,7 +3415,7 @@ paths: requires SCA (e.g. EU customers). For customers whose provider has no such requirement, this returns `409`. - In sandbox, the SMS/TOTP code is always `123456`. + In sandbox, the SMS code is always `123456`. operationId: authorizeTransaction tags: - Strong Customer Authentication @@ -3442,6 +3458,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Error409' + '429': + description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The provider may invalidate the challenge after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + content: + application/json: + schema: + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: @@ -3462,8 +3484,9 @@ paths: description: | Re-send the one-time code for a transaction in `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP` (e.g. the customer didn't - receive it, or it expired). The existing challenge is reused — no new - challenge is issued. + receive it). The existing challenge is reused — no new challenge is issued, + and its `scaChallenge.expiresAt` is **not** extended; once the challenge is + past `expiresAt` it can no longer be authorized. Only meaningful for customers whose provider requires SCA (e.g. EU); other providers return 409. `PASSKEY` challenges cannot be re-sent (there is no @@ -3496,6 +3519,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Error409' + '429': + description: Too many requests. Returned with `RATE_LIMITED` when codes are re-sent too frequently, to avoid spamming the customer's phone. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + content: + application/json: + schema: + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: @@ -18376,6 +18405,9 @@ components: example: 'Invoice #1234 payment' purposeOfPayment: $ref: '#/components/schemas/PurposeOfPayment' + scaFactor: + $ref: '#/components/schemas/ScaFactor' + description: Optional preferred factor for a Strong Customer Authentication challenge issued at quote creation. Only relevant for a realtime-funding source whose provider requires SCA (e.g. EU); ignored otherwise. Valid values are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected. When the quote is returned in `PENDING_AUTHORIZATION`, authorize it via `POST /quotes/{quoteId}/authorize`. senderCustomerInfo: type: object additionalProperties: true @@ -18395,6 +18427,33 @@ components: scaAuthorization: $ref: '#/components/schemas/ScaAuthorization' description: '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 quote in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers whose provider does not require SCA.' + Error429: + type: object + required: + - message + - status + - code + properties: + status: + type: integer + enum: + - 429 + description: HTTP status code + code: + type: string + description: | + | Error Code | Description | + |------------|-------------| + | RATE_LIMITED | Too many requests in a short window; retry after the interval indicated by the `Retry-After` response header | + enum: + - RATE_LIMITED + message: + type: string + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true TransactionListResponse: type: object required: @@ -19367,33 +19426,6 @@ components: format: date-time description: Timestamp after which the session is no longer valid and the `encryptedSessionSigningKey` must not be used to sign further requests. example: '2026-04-09T15:30:01Z' - Error429: - type: object - required: - - message - - status - - code - properties: - status: - type: integer - enum: - - 429 - description: HTTP status code - code: - type: string - description: | - | Error Code | Description | - |------------|-------------| - | RATE_LIMITED | Too many requests in a short window; retry after the interval indicated by the `Retry-After` response header | - enum: - - RATE_LIMITED - message: - type: string - description: Error message - details: - type: object - description: Additional error details - additionalProperties: true AuthCredentialChallengeRequest: title: Auth Credential Challenge Request description: Request body for `POST /auth/credentials/{id}/challenge`. Required when re-challenging a `PASSKEY` credential — must carry `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from. Ignored for `EMAIL_OTP` and `SMS_OTP`, where the credential type alone is sufficient because the OTP is delivered out-of-band. OAuth credentials do not use this endpoint; authenticate or reauthenticate them with `POST /auth/credentials/{id}/verify`. diff --git a/openapi/components/schemas/quotes/QuoteRequest.yaml b/openapi/components/schemas/quotes/QuoteRequest.yaml index f8a98c921..6faa0ef98 100644 --- a/openapi/components/schemas/quotes/QuoteRequest.yaml +++ b/openapi/components/schemas/quotes/QuoteRequest.yaml @@ -57,6 +57,15 @@ properties: example: 'Invoice #1234 payment' purposeOfPayment: $ref: ./PurposeOfPayment.yaml + scaFactor: + $ref: ../sca/ScaFactor.yaml + description: >- + Optional preferred factor for a Strong Customer Authentication challenge + issued at quote creation. Only relevant for a realtime-funding source + whose provider requires SCA (e.g. EU); ignored otherwise. Valid values are + `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required + dynamic linking and is rejected. When the quote is returned in + `PENDING_AUTHORIZATION`, authorize it via `POST /quotes/{quoteId}/authorize`. senderCustomerInfo: type: object additionalProperties: true diff --git a/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml b/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml index d868f43c0..3a1241f9c 100644 --- a/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml +++ b/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml @@ -21,7 +21,7 @@ post: requires SCA (e.g. EU customers). For customers whose provider has no such requirement, this returns `409`. - In sandbox, the SMS/TOTP code is always `123456`. + In sandbox, the SMS code is always `123456`. operationId: authorizeQuote tags: - Strong Customer Authentication @@ -66,6 +66,17 @@ post: application/json: schema: $ref: ../../components/schemas/errors/Error409.yaml + '429': + description: >- + Too many requests. Returned with `RATE_LIMITED` when authorization + attempts for this challenge happen too frequently (for example, repeated + bad codes brute-forcing the OTP). The provider may invalidate the + challenge after too many failed attempts. Clients should back off and + retry after the interval indicated by the `Retry-After` response header. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error429.yaml '500': description: Internal service error content: diff --git a/openapi/paths/quotes/quotes_{quoteId}_authorize_resend.yaml b/openapi/paths/quotes/quotes_{quoteId}_authorize_resend.yaml index ffd4dc564..96e5edbdf 100644 --- a/openapi/paths/quotes/quotes_{quoteId}_authorize_resend.yaml +++ b/openapi/paths/quotes/quotes_{quoteId}_authorize_resend.yaml @@ -11,7 +11,9 @@ post: description: | Re-send the one-time code for a realtime-funding quote in `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP`. The - existing challenge is reused — no new challenge is issued. + existing challenge is reused — no new challenge is issued, and its + `scaChallenge.expiresAt` is **not** extended; once the challenge is past + `expiresAt` it can no longer be authorized. Only meaningful for customers whose provider requires SCA (e.g. EU); other providers return 409. `PASSKEY` challenges cannot be re-sent and return 409. @@ -46,6 +48,16 @@ post: application/json: schema: $ref: ../../components/schemas/errors/Error409.yaml + '429': + description: >- + Too many requests. Returned with `RATE_LIMITED` when codes are re-sent + too frequently, to avoid spamming the customer's phone. Clients should + back off and retry after the interval indicated by the `Retry-After` + response header. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error429.yaml '500': description: Internal service error content: diff --git a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml index 2c04ab7f1..4b53517e1 100644 --- a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml +++ b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml @@ -59,21 +59,26 @@ post: responses: '200': description: > - Quote confirmed successfully. The transfer has been initiated and the - quote status has been updated. + Quote processed. The outcome depends on whether the customer's provider + requires Strong Customer Authentication (SCA): - For customers whose provider requires Strong Customer Authentication - (e.g. EU): supplying `scaAuthorization` **upfront on this call** satisfies - the challenge in one shot and avoids the pending state. Without it, the - quote comes back with status `PENDING_AUTHORIZATION` and an - `scaChallenge`; from that point the **only** way to release the transfer - is `POST /transactions/{transactionId}/authorize` for the quote's + - **No SCA required, or a valid `scaAuthorization` is supplied inline on + this call:** the transfer is initiated and the quote status advances + (`PROCESSING` / `COMPLETED`). + + + - **SCA required and no inline proof:** the transfer is **not** initiated + yet. The quote is returned with status `PENDING_AUTHORIZATION` and an + `scaChallenge`, and the **only** way to release the transfer is + `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). The inline - `scaAuthorization` field is therefore for the initial call only. (For a - realtime-funding quote that returned 202 from `POST /quotes`, authorize - the quote itself via `POST /quotes/{quoteId}/authorize` instead.) If an - SMS code lapses, re-send it via + `scaAuthorization` field is therefore for the initial call only. A + client holding a `PENDING_AUTHORIZATION` quote from `execute` always + authorizes via the **transaction**-scoped endpoint; the quote-scoped + `POST /quotes/{quoteId}/authorize` is only for a realtime-funding quote + whose challenge was issued at quote creation (the 202 from `POST + /quotes`). If an SMS code lapses, re-send it via `POST /transactions/{transactionId}/authorize/resend`. content: application/json: diff --git a/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml b/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml index fa7ea394b..0e4949da3 100644 --- a/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml +++ b/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml @@ -23,7 +23,7 @@ post: requires SCA (e.g. EU customers). For customers whose provider has no such requirement, this returns `409`. - In sandbox, the SMS/TOTP code is always `123456`. + In sandbox, the SMS code is always `123456`. operationId: authorizeTransaction tags: - Strong Customer Authentication @@ -68,6 +68,17 @@ post: application/json: schema: $ref: ../../components/schemas/errors/Error409.yaml + '429': + description: >- + Too many requests. Returned with `RATE_LIMITED` when authorization + attempts for this challenge happen too frequently (for example, repeated + bad codes brute-forcing the OTP). The provider may invalidate the + challenge after too many failed attempts. Clients should back off and + retry after the interval indicated by the `Retry-After` response header. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error429.yaml '500': description: Internal service error content: diff --git a/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml b/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml index a97c9ce2e..c3d566f81 100644 --- a/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml +++ b/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml @@ -11,8 +11,9 @@ post: description: | Re-send the one-time code for a transaction in `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP` (e.g. the customer didn't - receive it, or it expired). The existing challenge is reused — no new - challenge is issued. + receive it). The existing challenge is reused — no new challenge is issued, + and its `scaChallenge.expiresAt` is **not** extended; once the challenge is + past `expiresAt` it can no longer be authorized. Only meaningful for customers whose provider requires SCA (e.g. EU); other providers return 409. `PASSKEY` challenges cannot be re-sent (there is no @@ -48,6 +49,16 @@ post: application/json: schema: $ref: ../../components/schemas/errors/Error409.yaml + '429': + description: >- + Too many requests. Returned with `RATE_LIMITED` when codes are re-sent + too frequently, to avoid spamming the customer's phone. Clients should + back off and retry after the interval indicated by the `Retry-After` + response header. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error429.yaml '500': description: Internal service error content: From 37bf5cd0c58c9cdce28cf7252a2fc6707037179f Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Fri, 10 Jul 2026 00:51:53 -0700 Subject: [PATCH 08/10] docs(sca): make multi-authorization forward-compatible (loop contract) A money-movement operation may require more than one SCA authorization in sequence (today a cross-currency send authorizes the currency conversion and then the payout). Model authorization as a status-driven loop rather than a fixed single step: scaChallenge is "the challenge to satisfy now", and clients re-inspect the returned quote/transaction and keep authorizing while it stays PENDING_AUTHORIZATION. This makes the future collapse to a single authorization (pending Striga's new API) a non-breaking reduction in loop iterations rather than a contract change. Adds an optional, non-exhaustive scaChallenge.purpose hint for step UX. Co-Authored-By: Claude --- mintlify/openapi.yaml | 36 +++++++++++++++---- openapi.yaml | 36 +++++++++++++++---- .../components/schemas/sca/ScaChallenge.yaml | 26 ++++++++++++++ .../quotes/quotes_{quoteId}_authorize.yaml | 5 +++ .../quotes/quotes_{quoteId}_execute.yaml | 7 ++-- ...ransactions_{transactionId}_authorize.yaml | 22 +++++++++--- openapi/paths/transfers/transfer_out.yaml | 12 ++++--- 7 files changed, 119 insertions(+), 25 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 1c2a90288..885e9130c 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2429,7 +2429,7 @@ paths: description: | Transfer-out request created successfully. - For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies the challenge in one shot. Without it, the returned transaction comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by calling `POST /transactions/{transactionId}/authorize`. For providers that don't require SCA the transaction proceeds as usual. + For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies the **first** challenge in one shot. Without it, the returned transaction comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by calling `POST /transactions/{transactionId}/authorize`. This may be required **more than once** — the transaction can surface a further `scaChallenge` after each authorization (see `ScaChallenge`); keep authorizing until it leaves `PENDING_AUTHORIZATION`. For providers that don't require SCA the transaction proceeds as usual. content: application/json: schema: @@ -2868,7 +2868,7 @@ paths: - **No SCA required, or a valid `scaAuthorization` is supplied inline on this call:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`). - - **SCA required and no inline proof:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`, and the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). The inline `scaAuthorization` field is therefore for the initial call only. A client holding a `PENDING_AUTHORIZATION` quote from `execute` always authorizes via the **transaction**-scoped endpoint; the quote-scoped `POST /quotes/{quoteId}/authorize` is only for a realtime-funding quote whose challenge was issued at quote creation (the 202 from `POST /quotes`). If an SMS code lapses, re-send it via `POST /transactions/{transactionId}/authorize/resend`. + - **SCA required and no inline proof:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`, and the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). That may need to be done **more than once** — the transaction can surface a further `scaChallenge` after each authorization (see `ScaChallenge`); keep authorizing until it leaves `PENDING_AUTHORIZATION`. The inline `scaAuthorization` field satisfies only the **first** challenge. A client holding a `PENDING_AUTHORIZATION` quote from `execute` always authorizes via the **transaction**-scoped endpoint; the quote-scoped `POST /quotes/{quoteId}/authorize` is only for a realtime-funding quote whose challenge was issued at quote creation (the 202 from `POST /quotes`). If an SMS code lapses, re-send it via `POST /transactions/{transactionId}/authorize/resend`. content: application/json: schema: @@ -2923,6 +2923,11 @@ paths: quote advances and its `paymentInstructions` are populated so the customer can fund the transfer. + As with all SCA, a quote may require more than one authorization: after + authorizing, if the quote is still `PENDING_AUTHORIZATION` it carries the + next `scaChallenge` — authorize that too, repeating until it advances (see + `ScaChallenge`). + This endpoint is only meaningful for customers whose payment provider requires SCA (e.g. EU customers). For customers whose provider has no such requirement, this returns `409`. @@ -3403,13 +3408,22 @@ paths: description: | Satisfy the Strong Customer Authentication challenge carried by a transaction in `PENDING_AUTHORIZATION` status by submitting an - `ScaAuthorization` proof. On success the transaction advances to - `PROCESSING` (or `COMPLETED`) and the underlying transfer is released. + `ScaAuthorization` proof for the transaction's **current** `scaChallenge`. + + A transaction may require **more than one** authorization in sequence (see + `ScaChallenge`). On success, inspect the returned transaction: if it has + advanced to `PROCESSING` / `COMPLETED`, the transfer is released; if it is + **still** `PENDING_AUTHORIZATION`, it now carries the next `scaChallenge` + (a new `id`) — call this endpoint again for that one. Repeat until the + transaction leaves `PENDING_AUTHORIZATION`. Do not assume a single call + releases the transfer; the number of authorizations is provider-dependent + and may change over time. Transactions enter `PENDING_AUTHORIZATION` from `execute` (embedded-wallet quotes) and `transfer-out` when the customer's provider requires SCA. The - same authorization can be supplied inline on those calls via their optional - `scaAuthorization` field instead of calling this endpoint. + first authorization can be supplied inline on those calls via their optional + `scaAuthorization` field instead of calling this endpoint; any subsequent + authorizations in the sequence use this endpoint. This endpoint is only meaningful for customers whose payment provider requires SCA (e.g. EU customers). For customers whose provider has no such @@ -3429,7 +3443,7 @@ paths: $ref: '#/components/schemas/ScaAuthorization' responses: '200': - description: Challenge authorized; the updated transaction is returned. + description: Challenge authorized; the updated transaction is returned. It has either advanced (`PROCESSING` / `COMPLETED`) or, if another authorization is required, remains `PENDING_AUTHORIZATION` with the next `scaChallenge`. content: application/json: schema: @@ -17220,6 +17234,8 @@ components: A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer's payment provider requires SCA** (e.g. EU customers on an e-money provider); for customers whose provider has no such requirement it is omitted entirely and no action is needed. When present on a quote or transaction, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or `POST /transactions/{transactionId}/authorize`, or inline via the optional `scaAuthorization` field on the originating `execute` / `transfer-out` call. + + **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow- and provider-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future provider update may collapse them into one. A client written to loop on status handles any count unchanged. required: - id - expiresAt @@ -17245,6 +17261,12 @@ components: $ref: '#/components/schemas/ScaFactor' example: - SMS_OTP + purpose: + type: + - string + - 'null' + description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when the provider does not distinguish steps (e.g. a single-authorization flow). + example: PAYOUT passkeyAssertionOptions: type: - object diff --git a/openapi.yaml b/openapi.yaml index 1c2a90288..885e9130c 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2429,7 +2429,7 @@ paths: description: | Transfer-out request created successfully. - For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies the challenge in one shot. Without it, the returned transaction comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by calling `POST /transactions/{transactionId}/authorize`. For providers that don't require SCA the transaction proceeds as usual. + For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies the **first** challenge in one shot. Without it, the returned transaction comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by calling `POST /transactions/{transactionId}/authorize`. This may be required **more than once** — the transaction can surface a further `scaChallenge` after each authorization (see `ScaChallenge`); keep authorizing until it leaves `PENDING_AUTHORIZATION`. For providers that don't require SCA the transaction proceeds as usual. content: application/json: schema: @@ -2868,7 +2868,7 @@ paths: - **No SCA required, or a valid `scaAuthorization` is supplied inline on this call:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`). - - **SCA required and no inline proof:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`, and the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). The inline `scaAuthorization` field is therefore for the initial call only. A client holding a `PENDING_AUTHORIZATION` quote from `execute` always authorizes via the **transaction**-scoped endpoint; the quote-scoped `POST /quotes/{quoteId}/authorize` is only for a realtime-funding quote whose challenge was issued at quote creation (the 202 from `POST /quotes`). If an SMS code lapses, re-send it via `POST /transactions/{transactionId}/authorize/resend`. + - **SCA required and no inline proof:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`, and the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). That may need to be done **more than once** — the transaction can surface a further `scaChallenge` after each authorization (see `ScaChallenge`); keep authorizing until it leaves `PENDING_AUTHORIZATION`. The inline `scaAuthorization` field satisfies only the **first** challenge. A client holding a `PENDING_AUTHORIZATION` quote from `execute` always authorizes via the **transaction**-scoped endpoint; the quote-scoped `POST /quotes/{quoteId}/authorize` is only for a realtime-funding quote whose challenge was issued at quote creation (the 202 from `POST /quotes`). If an SMS code lapses, re-send it via `POST /transactions/{transactionId}/authorize/resend`. content: application/json: schema: @@ -2923,6 +2923,11 @@ paths: quote advances and its `paymentInstructions` are populated so the customer can fund the transfer. + As with all SCA, a quote may require more than one authorization: after + authorizing, if the quote is still `PENDING_AUTHORIZATION` it carries the + next `scaChallenge` — authorize that too, repeating until it advances (see + `ScaChallenge`). + This endpoint is only meaningful for customers whose payment provider requires SCA (e.g. EU customers). For customers whose provider has no such requirement, this returns `409`. @@ -3403,13 +3408,22 @@ paths: description: | Satisfy the Strong Customer Authentication challenge carried by a transaction in `PENDING_AUTHORIZATION` status by submitting an - `ScaAuthorization` proof. On success the transaction advances to - `PROCESSING` (or `COMPLETED`) and the underlying transfer is released. + `ScaAuthorization` proof for the transaction's **current** `scaChallenge`. + + A transaction may require **more than one** authorization in sequence (see + `ScaChallenge`). On success, inspect the returned transaction: if it has + advanced to `PROCESSING` / `COMPLETED`, the transfer is released; if it is + **still** `PENDING_AUTHORIZATION`, it now carries the next `scaChallenge` + (a new `id`) — call this endpoint again for that one. Repeat until the + transaction leaves `PENDING_AUTHORIZATION`. Do not assume a single call + releases the transfer; the number of authorizations is provider-dependent + and may change over time. Transactions enter `PENDING_AUTHORIZATION` from `execute` (embedded-wallet quotes) and `transfer-out` when the customer's provider requires SCA. The - same authorization can be supplied inline on those calls via their optional - `scaAuthorization` field instead of calling this endpoint. + first authorization can be supplied inline on those calls via their optional + `scaAuthorization` field instead of calling this endpoint; any subsequent + authorizations in the sequence use this endpoint. This endpoint is only meaningful for customers whose payment provider requires SCA (e.g. EU customers). For customers whose provider has no such @@ -3429,7 +3443,7 @@ paths: $ref: '#/components/schemas/ScaAuthorization' responses: '200': - description: Challenge authorized; the updated transaction is returned. + description: Challenge authorized; the updated transaction is returned. It has either advanced (`PROCESSING` / `COMPLETED`) or, if another authorization is required, remains `PENDING_AUTHORIZATION` with the next `scaChallenge`. content: application/json: schema: @@ -17220,6 +17234,8 @@ components: A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer's payment provider requires SCA** (e.g. EU customers on an e-money provider); for customers whose provider has no such requirement it is omitted entirely and no action is needed. When present on a quote or transaction, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or `POST /transactions/{transactionId}/authorize`, or inline via the optional `scaAuthorization` field on the originating `execute` / `transfer-out` call. + + **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow- and provider-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future provider update may collapse them into one. A client written to loop on status handles any count unchanged. required: - id - expiresAt @@ -17245,6 +17261,12 @@ components: $ref: '#/components/schemas/ScaFactor' example: - SMS_OTP + purpose: + type: + - string + - 'null' + description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when the provider does not distinguish steps (e.g. a single-authorization flow). + example: PAYOUT passkeyAssertionOptions: type: - object diff --git a/openapi/components/schemas/sca/ScaChallenge.yaml b/openapi/components/schemas/sca/ScaChallenge.yaml index 083d4f2f1..73e971715 100644 --- a/openapi/components/schemas/sca/ScaChallenge.yaml +++ b/openapi/components/schemas/sca/ScaChallenge.yaml @@ -11,6 +11,19 @@ description: >- `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or `POST /transactions/{transactionId}/authorize`, or inline via the optional `scaAuthorization` field on the originating `execute` / `transfer-out` call. + + + **A single operation may require more than one authorization, in sequence.** + Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". + After each authorize, re-inspect the returned quote/transaction: if it is + still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new + `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. + Do not assume one authorization releases the transfer. The number of + authorizations is flow- and provider-dependent and **may decrease in future**: + for example, a cross-currency send today authorizes the currency conversion + and the payout as two separate challenges; a future provider update may + collapse them into one. A client written to loop on status handles any count + unchanged. required: - id - expiresAt @@ -40,6 +53,19 @@ properties: $ref: ./ScaFactor.yaml example: - SMS_OTP + purpose: + type: + - string + - 'null' + description: >- + Optional, informational label for what this particular challenge in the + sequence authorizes — useful for step UX (e.g. "Authorize the currency + conversion" vs "Authorize the payout"). Known values include + `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is + **non-exhaustive and may grow** — treat unrecognized values as a generic + authorization step and do not branch program logic on it. Omitted when the + provider does not distinguish steps (e.g. a single-authorization flow). + example: PAYOUT passkeyAssertionOptions: type: - object diff --git a/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml b/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml index 3a1241f9c..e448f4a04 100644 --- a/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml +++ b/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml @@ -17,6 +17,11 @@ post: quote advances and its `paymentInstructions` are populated so the customer can fund the transfer. + As with all SCA, a quote may require more than one authorization: after + authorizing, if the quote is still `PENDING_AUTHORIZATION` it carries the + next `scaChallenge` — authorize that too, repeating until it advances (see + `ScaChallenge`). + This endpoint is only meaningful for customers whose payment provider requires SCA (e.g. EU customers). For customers whose provider has no such requirement, this returns `409`. diff --git a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml index 4b53517e1..c486df66a 100644 --- a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml +++ b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml @@ -72,8 +72,11 @@ post: yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`, and the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's - `transactionId` (re-calling `execute` returns 409). The inline - `scaAuthorization` field is therefore for the initial call only. A + `transactionId` (re-calling `execute` returns 409). That may need to be + done **more than once** — the transaction can surface a further + `scaChallenge` after each authorization (see `ScaChallenge`); keep + authorizing until it leaves `PENDING_AUTHORIZATION`. The inline + `scaAuthorization` field satisfies only the **first** challenge. A client holding a `PENDING_AUTHORIZATION` quote from `execute` always authorizes via the **transaction**-scoped endpoint; the quote-scoped `POST /quotes/{quoteId}/authorize` is only for a realtime-funding quote diff --git a/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml b/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml index 0e4949da3..99b7f8b6c 100644 --- a/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml +++ b/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml @@ -11,13 +11,22 @@ post: description: | Satisfy the Strong Customer Authentication challenge carried by a transaction in `PENDING_AUTHORIZATION` status by submitting an - `ScaAuthorization` proof. On success the transaction advances to - `PROCESSING` (or `COMPLETED`) and the underlying transfer is released. + `ScaAuthorization` proof for the transaction's **current** `scaChallenge`. + + A transaction may require **more than one** authorization in sequence (see + `ScaChallenge`). On success, inspect the returned transaction: if it has + advanced to `PROCESSING` / `COMPLETED`, the transfer is released; if it is + **still** `PENDING_AUTHORIZATION`, it now carries the next `scaChallenge` + (a new `id`) — call this endpoint again for that one. Repeat until the + transaction leaves `PENDING_AUTHORIZATION`. Do not assume a single call + releases the transfer; the number of authorizations is provider-dependent + and may change over time. Transactions enter `PENDING_AUTHORIZATION` from `execute` (embedded-wallet quotes) and `transfer-out` when the customer's provider requires SCA. The - same authorization can be supplied inline on those calls via their optional - `scaAuthorization` field instead of calling this endpoint. + first authorization can be supplied inline on those calls via their optional + `scaAuthorization` field instead of calling this endpoint; any subsequent + authorizations in the sequence use this endpoint. This endpoint is only meaningful for customers whose payment provider requires SCA (e.g. EU customers). For customers whose provider has no such @@ -37,7 +46,10 @@ post: $ref: ../../components/schemas/sca/ScaAuthorization.yaml responses: '200': - description: Challenge authorized; the updated transaction is returned. + description: >- + Challenge authorized; the updated transaction is returned. It has either + advanced (`PROCESSING` / `COMPLETED`) or, if another authorization is + required, remains `PENDING_AUTHORIZATION` with the next `scaChallenge`. content: application/json: schema: diff --git a/openapi/paths/transfers/transfer_out.yaml b/openapi/paths/transfers/transfer_out.yaml index b72a39c36..8f7f16d55 100644 --- a/openapi/paths/transfers/transfer_out.yaml +++ b/openapi/paths/transfers/transfer_out.yaml @@ -42,10 +42,14 @@ post: For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies - the challenge in one shot. Without it, the returned transaction comes - back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release - the transfer by calling `POST /transactions/{transactionId}/authorize`. - For providers that don't require SCA the transaction proceeds as usual. + the **first** challenge in one shot. Without it, the returned transaction + comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; + release the transfer by calling + `POST /transactions/{transactionId}/authorize`. This may be required + **more than once** — the transaction can surface a further `scaChallenge` + after each authorization (see `ScaChallenge`); keep authorizing until it + leaves `PENDING_AUTHORIZATION`. For providers that don't require SCA the + transaction proceeds as usual. content: application/json: schema: From 338d604d29700b410cbf7ef3c754610e953eb1b2 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 13 Jul 2026 23:24:24 -0700 Subject: [PATCH 09/10] docs(sca): reframe SCA requirement by regulatory region, not "provider" Per review on #558: the provider is always Lightspark; it's the regulatory region (currently the EU) that mandates SCA. Reword all SCA descriptions from "customers whose provider requires SCA" to region-based phrasing ("customers in a region where SCA is required (e.g. EU)" / "customers outside SCA-regulated regions"), and drop incidental "provider" references (challenge invalidation, auth-count, passkey rejection). Contact Verification tag left untouched. Co-Authored-By: Claude --- mintlify/openapi.yaml | 66 +++++++++---------- openapi.yaml | 66 +++++++++---------- .../schemas/quotes/ExecuteQuoteRequest.yaml | 7 +- openapi/components/schemas/quotes/Quote.yaml | 4 +- .../schemas/quotes/QuoteRequest.yaml | 2 +- .../schemas/sca/ScaAuthorization.yaml | 4 +- .../components/schemas/sca/ScaChallenge.yaml | 11 ++-- .../OutgoingTransactionStatus.yaml | 2 +- .../transactions/TransactionStatus.yaml | 2 +- .../schemas/transfers/TransferOutRequest.yaml | 6 +- openapi/openapi.yaml | 4 +- .../quotes/quotes_{quoteId}_authorize.yaml | 9 +-- .../quotes_{quoteId}_authorize_resend.yaml | 6 +- .../quotes/quotes_{quoteId}_execute.yaml | 3 +- ...ransactions_{transactionId}_authorize.yaml | 13 ++-- ...ions_{transactionId}_authorize_resend.yaml | 8 +-- openapi/paths/transfers/transfer_out.yaml | 4 +- 17 files changed, 98 insertions(+), 119 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 885e9130c..39599156f 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -24,7 +24,7 @@ tags: - name: Contact Verification description: Endpoints for verifying a customer's email and phone via one-time codes. Required only for customers whose payment provider mandates contact verification (e.g. EU customers); other providers return 409. - name: Strong Customer Authentication - description: Endpoints for authorizing money-movement operations that require Strong Customer Authentication. Relevant only for customers whose payment provider mandates SCA (e.g. EU customers); other providers never surface an SCA challenge and these endpoints return 409. + description: Endpoints for authorizing money-movement operations that require Strong Customer Authentication. Relevant only for customers in a region where SCA is required (e.g. EU); customers outside SCA-regulated regions never see an SCA challenge and these endpoints return 409. - name: KYC/KYB Verifications description: Endpoints for Know Your Customer (KYC) and Know Your Business (KYB) verification, including managing beneficial owners and triggering verification for customers. - name: Documents @@ -2429,7 +2429,7 @@ paths: description: | Transfer-out request created successfully. - For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies the **first** challenge in one shot. Without it, the returned transaction comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by calling `POST /transactions/{transactionId}/authorize`. This may be required **more than once** — the transaction can surface a further `scaChallenge` after each authorization (see `ScaChallenge`); keep authorizing until it leaves `PENDING_AUTHORIZATION`. For providers that don't require SCA the transaction proceeds as usual. + For customers in a region where Strong Customer Authentication is required (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies the **first** challenge in one shot. Without it, the returned transaction comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by calling `POST /transactions/{transactionId}/authorize`. This may be required **more than once** — the transaction can surface a further `scaChallenge` after each authorization (see `ScaChallenge`); keep authorizing until it leaves `PENDING_AUTHORIZATION`. For customers outside SCA-regulated regions the transaction proceeds as usual. content: application/json: schema: @@ -2864,7 +2864,7 @@ paths: responses: '200': description: | - Quote processed. The outcome depends on whether the customer's provider requires Strong Customer Authentication (SCA): + Quote processed. The outcome depends on whether SCA applies to the customer (currently EU customers): - **No SCA required, or a valid `scaAuthorization` is supplied inline on this call:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`). @@ -2928,9 +2928,7 @@ paths: next `scaChallenge` — authorize that too, repeating until it advances (see `ScaChallenge`). - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. In sandbox, the SMS code is always `123456`. operationId: authorizeQuote @@ -2970,13 +2968,13 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA, or the quote has no pending challenge to authorize. + description: SCA is not required for this customer, or the quote has no pending challenge to authorize. content: application/json: schema: $ref: '#/components/schemas/Error409' '429': - description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The provider may invalidate the challenge after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. content: application/json: schema: @@ -3005,8 +3003,8 @@ paths: `scaChallenge.expiresAt` is **not** extended; once the challenge is past `expiresAt` it can no longer be authorized. - Only meaningful for customers whose provider requires SCA (e.g. EU); other - providers return 409. `PASSKEY` challenges cannot be re-sent and return 409. + Only meaningful for customers in a region where SCA is required (e.g. EU); + a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent and return 409. In sandbox, the code is always `123456`. operationId: resendQuoteScaCode @@ -3030,7 +3028,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's provider does not require SCA, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). + description: SCA is not required for this customer, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). content: application/json: schema: @@ -3416,18 +3414,16 @@ paths: **still** `PENDING_AUTHORIZATION`, it now carries the next `scaChallenge` (a new `id`) — call this endpoint again for that one. Repeat until the transaction leaves `PENDING_AUTHORIZATION`. Do not assume a single call - releases the transfer; the number of authorizations is provider-dependent + releases the transfer; the number of authorizations is flow-dependent and may change over time. Transactions enter `PENDING_AUTHORIZATION` from `execute` (embedded-wallet - quotes) and `transfer-out` when the customer's provider requires SCA. The + quotes) and `transfer-out` when the customer is in a region where SCA is required. The first authorization can be supplied inline on those calls via their optional `scaAuthorization` field instead of calling this endpoint; any subsequent authorizations in the sequence use this endpoint. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. In sandbox, the SMS code is always `123456`. operationId: authorizeTransaction @@ -3467,13 +3463,13 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA, or the transaction has no pending challenge to authorize. + description: SCA is not required for this customer, or the transaction has no pending challenge to authorize. content: application/json: schema: $ref: '#/components/schemas/Error409' '429': - description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The provider may invalidate the challenge after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. content: application/json: schema: @@ -3502,9 +3498,9 @@ paths: and its `scaChallenge.expiresAt` is **not** extended; once the challenge is past `expiresAt` it can no longer be authorized. - Only meaningful for customers whose provider requires SCA (e.g. EU); other - providers return 409. `PASSKEY` challenges cannot be re-sent (there is no - code to deliver) and return 409. + Only meaningful for customers in a region where SCA is required (e.g. EU); + a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent (there is + no code to deliver) and return 409. In sandbox, the code is always `123456`. operationId: resendTransactionScaCode @@ -3528,7 +3524,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's provider does not require SCA, the transaction has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). + description: SCA is not required for this customer, the transaction has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). content: application/json: schema: @@ -17130,7 +17126,7 @@ components: |--------|-------------| | `CREATED` | Initial lookup has been created | | `PENDING` | Quote has been created | - | `PENDING_AUTHORIZATION` | Awaiting Strong Customer Authentication. Only occurs for customers whose provider requires SCA (e.g. EU); authorize the transaction's `scaChallenge` to proceed. | + | `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. | | `PROCESSING` | Funding has been received and payment initiated | | `COMPLETED` | Cross border payment has been received, converted and payment has been sent to the offramp network | | `REJECTED` | Receiving institution or wallet rejected payment, payment has been refunded | @@ -17231,11 +17227,11 @@ components: ScaChallenge: type: object description: |- - A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer's payment provider requires SCA** (e.g. EU customers on an e-money provider); for customers whose provider has no such requirement it is omitted entirely and no action is needed. + A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed. When present on a quote or transaction, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or `POST /transactions/{transactionId}/authorize`, or inline via the optional `scaAuthorization` field on the originating `execute` / `transfer-out` call. - **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow- and provider-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future provider update may collapse them into one. A client written to loop on status handles any count unchanged. + **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged. required: - id - expiresAt @@ -17265,7 +17261,7 @@ components: type: - string - 'null' - description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when the provider does not distinguish steps (e.g. a single-authorization flow). + description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow). example: PAYOUT passkeyAssertionOptions: type: @@ -17573,7 +17569,7 @@ components: | Status | Description | |--------|-------------| | `PENDING` | Quote is pending confirmation | - | `PENDING_AUTHORIZATION` | Awaiting Strong Customer Authentication. Only occurs for customers whose provider requires SCA (e.g. EU); authorize the transaction's `scaChallenge` to proceed. | + | `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. | | `EXPIRED` | Quote wasn't executed before expiry window | | `PROCESSING` | Executing the quote after receiving funds | | `COMPLETED` | Payout successfully reached the destination | @@ -17972,7 +17968,7 @@ components: - $ref: '#/components/schemas/PaymentRail' ScaAuthorization: type: object - description: Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). When supplying a `passkeyAssertion`, `origin` is **required** — the WebAuthn assertion is bound to the origin it was produced against, and the provider rejects a passkey confirmation without it. + description: Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). When supplying a `passkeyAssertion`, `origin` is **required** — the WebAuthn assertion is bound to the origin it was produced against, and a passkey confirmation is rejected without it. properties: code: type: @@ -18016,10 +18012,10 @@ components: example: '12345' scaFactor: $ref: '#/components/schemas/ScaFactor' - description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers whose provider requires SCA (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. + description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers in a region where SCA is required (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. scaAuthorization: $ref: '#/components/schemas/ScaAuthorization' - description: '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 on the first call to receive the transaction in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers whose provider does not require SCA.' + description: 'Optional inline Strong Customer Authentication proof. Only relevant for customers in a region where SCA is required (e.g. EU): supply this to satisfy the challenge in the same call once the customer has the code/assertion. Omit it on the first call to receive the transaction in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers outside SCA-regulated regions (non-EU).' CurrencyPreference: type: object required: @@ -18284,7 +18280,7 @@ components: - COMPLETED - FAILED - EXPIRED - description: 'Current status of the quote. `PENDING_AUTHORIZATION` occurs only for customers whose provider requires Strong Customer Authentication (e.g. EU): the quote carries an `scaChallenge` that must be authorized before execution, and for realtime-funding sources `paymentInstructions` are withheld until it is satisfied.' + description: 'Current status of the quote. `PENDING_AUTHORIZATION` occurs only for customers in a region where Strong Customer Authentication is required (e.g. EU): the quote carries an `scaChallenge` that must be authorized before execution, and for realtime-funding sources `paymentInstructions` are withheld until it is satisfied.' example: PENDING createdAt: type: string @@ -18362,7 +18358,7 @@ components: scaChallenge: $ref: '#/components/schemas/ScaChallenge' readOnly: true - description: 'Present only while `status` is `PENDING_AUTHORIZATION`: the Strong Customer Authentication challenge to satisfy before this quote can be executed (or, for realtime-funding sources, before `paymentInstructions` are issued). Omitted for customers whose provider does not require SCA.' + description: 'Present only while `status` is `PENDING_AUTHORIZATION`: the Strong Customer Authentication challenge to satisfy before this quote can be executed (or, for realtime-funding sources, before `paymentInstructions` are issued). Omitted for customers outside SCA-regulated regions (non-EU).' QuoteLockSide: type: string enum: @@ -18429,7 +18425,7 @@ components: $ref: '#/components/schemas/PurposeOfPayment' scaFactor: $ref: '#/components/schemas/ScaFactor' - description: Optional preferred factor for a Strong Customer Authentication challenge issued at quote creation. Only relevant for a realtime-funding source whose provider requires SCA (e.g. EU); ignored otherwise. Valid values are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected. When the quote is returned in `PENDING_AUTHORIZATION`, authorize it via `POST /quotes/{quoteId}/authorize`. + description: Optional preferred factor for a Strong Customer Authentication challenge issued at quote creation. Only relevant for a realtime-funding source in a region where SCA is required (e.g. EU); ignored otherwise. Valid values are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected. When the quote is returned in `PENDING_AUTHORIZATION`, authorize it via `POST /quotes/{quoteId}/authorize`. senderCustomerInfo: type: object additionalProperties: true @@ -18445,10 +18441,10 @@ components: properties: scaFactor: $ref: '#/components/schemas/ScaFactor' - description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers whose provider requires SCA (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. + description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers in a region where SCA is required (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. scaAuthorization: $ref: '#/components/schemas/ScaAuthorization' - description: '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 quote in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers whose provider does not require SCA.' + description: 'Optional inline Strong Customer Authentication proof. Only relevant for customers in a region where SCA is required (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 quote in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers outside SCA-regulated regions (non-EU).' Error429: type: object required: diff --git a/openapi.yaml b/openapi.yaml index 885e9130c..39599156f 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -24,7 +24,7 @@ tags: - name: Contact Verification description: Endpoints for verifying a customer's email and phone via one-time codes. Required only for customers whose payment provider mandates contact verification (e.g. EU customers); other providers return 409. - name: Strong Customer Authentication - description: Endpoints for authorizing money-movement operations that require Strong Customer Authentication. Relevant only for customers whose payment provider mandates SCA (e.g. EU customers); other providers never surface an SCA challenge and these endpoints return 409. + description: Endpoints for authorizing money-movement operations that require Strong Customer Authentication. Relevant only for customers in a region where SCA is required (e.g. EU); customers outside SCA-regulated regions never see an SCA challenge and these endpoints return 409. - name: KYC/KYB Verifications description: Endpoints for Know Your Customer (KYC) and Know Your Business (KYB) verification, including managing beneficial owners and triggering verification for customers. - name: Documents @@ -2429,7 +2429,7 @@ paths: description: | Transfer-out request created successfully. - For customers whose provider requires Strong Customer Authentication (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies the **first** challenge in one shot. Without it, the returned transaction comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by calling `POST /transactions/{transactionId}/authorize`. This may be required **more than once** — the transaction can surface a further `scaChallenge` after each authorization (see `ScaChallenge`); keep authorizing until it leaves `PENDING_AUTHORIZATION`. For providers that don't require SCA the transaction proceeds as usual. + For customers in a region where Strong Customer Authentication is required (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies the **first** challenge in one shot. Without it, the returned transaction comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by calling `POST /transactions/{transactionId}/authorize`. This may be required **more than once** — the transaction can surface a further `scaChallenge` after each authorization (see `ScaChallenge`); keep authorizing until it leaves `PENDING_AUTHORIZATION`. For customers outside SCA-regulated regions the transaction proceeds as usual. content: application/json: schema: @@ -2864,7 +2864,7 @@ paths: responses: '200': description: | - Quote processed. The outcome depends on whether the customer's provider requires Strong Customer Authentication (SCA): + Quote processed. The outcome depends on whether SCA applies to the customer (currently EU customers): - **No SCA required, or a valid `scaAuthorization` is supplied inline on this call:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`). @@ -2928,9 +2928,7 @@ paths: next `scaChallenge` — authorize that too, repeating until it advances (see `ScaChallenge`). - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. In sandbox, the SMS code is always `123456`. operationId: authorizeQuote @@ -2970,13 +2968,13 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA, or the quote has no pending challenge to authorize. + description: SCA is not required for this customer, or the quote has no pending challenge to authorize. content: application/json: schema: $ref: '#/components/schemas/Error409' '429': - description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The provider may invalidate the challenge after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. content: application/json: schema: @@ -3005,8 +3003,8 @@ paths: `scaChallenge.expiresAt` is **not** extended; once the challenge is past `expiresAt` it can no longer be authorized. - Only meaningful for customers whose provider requires SCA (e.g. EU); other - providers return 409. `PASSKEY` challenges cannot be re-sent and return 409. + Only meaningful for customers in a region where SCA is required (e.g. EU); + a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent and return 409. In sandbox, the code is always `123456`. operationId: resendQuoteScaCode @@ -3030,7 +3028,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's provider does not require SCA, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). + description: SCA is not required for this customer, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). content: application/json: schema: @@ -3416,18 +3414,16 @@ paths: **still** `PENDING_AUTHORIZATION`, it now carries the next `scaChallenge` (a new `id`) — call this endpoint again for that one. Repeat until the transaction leaves `PENDING_AUTHORIZATION`. Do not assume a single call - releases the transfer; the number of authorizations is provider-dependent + releases the transfer; the number of authorizations is flow-dependent and may change over time. Transactions enter `PENDING_AUTHORIZATION` from `execute` (embedded-wallet - quotes) and `transfer-out` when the customer's provider requires SCA. The + quotes) and `transfer-out` when the customer is in a region where SCA is required. The first authorization can be supplied inline on those calls via their optional `scaAuthorization` field instead of calling this endpoint; any subsequent authorizations in the sequence use this endpoint. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. In sandbox, the SMS code is always `123456`. operationId: authorizeTransaction @@ -3467,13 +3463,13 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA, or the transaction has no pending challenge to authorize. + description: SCA is not required for this customer, or the transaction has no pending challenge to authorize. content: application/json: schema: $ref: '#/components/schemas/Error409' '429': - description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The provider may invalidate the challenge after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. content: application/json: schema: @@ -3502,9 +3498,9 @@ paths: and its `scaChallenge.expiresAt` is **not** extended; once the challenge is past `expiresAt` it can no longer be authorized. - Only meaningful for customers whose provider requires SCA (e.g. EU); other - providers return 409. `PASSKEY` challenges cannot be re-sent (there is no - code to deliver) and return 409. + Only meaningful for customers in a region where SCA is required (e.g. EU); + a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent (there is + no code to deliver) and return 409. In sandbox, the code is always `123456`. operationId: resendTransactionScaCode @@ -3528,7 +3524,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's provider does not require SCA, the transaction has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). + description: SCA is not required for this customer, the transaction has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). content: application/json: schema: @@ -17130,7 +17126,7 @@ components: |--------|-------------| | `CREATED` | Initial lookup has been created | | `PENDING` | Quote has been created | - | `PENDING_AUTHORIZATION` | Awaiting Strong Customer Authentication. Only occurs for customers whose provider requires SCA (e.g. EU); authorize the transaction's `scaChallenge` to proceed. | + | `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. | | `PROCESSING` | Funding has been received and payment initiated | | `COMPLETED` | Cross border payment has been received, converted and payment has been sent to the offramp network | | `REJECTED` | Receiving institution or wallet rejected payment, payment has been refunded | @@ -17231,11 +17227,11 @@ components: ScaChallenge: type: object description: |- - A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer's payment provider requires SCA** (e.g. EU customers on an e-money provider); for customers whose provider has no such requirement it is omitted entirely and no action is needed. + A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed. When present on a quote or transaction, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or `POST /transactions/{transactionId}/authorize`, or inline via the optional `scaAuthorization` field on the originating `execute` / `transfer-out` call. - **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow- and provider-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future provider update may collapse them into one. A client written to loop on status handles any count unchanged. + **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged. required: - id - expiresAt @@ -17265,7 +17261,7 @@ components: type: - string - 'null' - description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when the provider does not distinguish steps (e.g. a single-authorization flow). + description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow). example: PAYOUT passkeyAssertionOptions: type: @@ -17573,7 +17569,7 @@ components: | Status | Description | |--------|-------------| | `PENDING` | Quote is pending confirmation | - | `PENDING_AUTHORIZATION` | Awaiting Strong Customer Authentication. Only occurs for customers whose provider requires SCA (e.g. EU); authorize the transaction's `scaChallenge` to proceed. | + | `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. | | `EXPIRED` | Quote wasn't executed before expiry window | | `PROCESSING` | Executing the quote after receiving funds | | `COMPLETED` | Payout successfully reached the destination | @@ -17972,7 +17968,7 @@ components: - $ref: '#/components/schemas/PaymentRail' ScaAuthorization: type: object - description: Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). When supplying a `passkeyAssertion`, `origin` is **required** — the WebAuthn assertion is bound to the origin it was produced against, and the provider rejects a passkey confirmation without it. + description: Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). When supplying a `passkeyAssertion`, `origin` is **required** — the WebAuthn assertion is bound to the origin it was produced against, and a passkey confirmation is rejected without it. properties: code: type: @@ -18016,10 +18012,10 @@ components: example: '12345' scaFactor: $ref: '#/components/schemas/ScaFactor' - description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers whose provider requires SCA (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. + description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers in a region where SCA is required (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. scaAuthorization: $ref: '#/components/schemas/ScaAuthorization' - description: '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 on the first call to receive the transaction in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers whose provider does not require SCA.' + description: 'Optional inline Strong Customer Authentication proof. Only relevant for customers in a region where SCA is required (e.g. EU): supply this to satisfy the challenge in the same call once the customer has the code/assertion. Omit it on the first call to receive the transaction in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers outside SCA-regulated regions (non-EU).' CurrencyPreference: type: object required: @@ -18284,7 +18280,7 @@ components: - COMPLETED - FAILED - EXPIRED - description: 'Current status of the quote. `PENDING_AUTHORIZATION` occurs only for customers whose provider requires Strong Customer Authentication (e.g. EU): the quote carries an `scaChallenge` that must be authorized before execution, and for realtime-funding sources `paymentInstructions` are withheld until it is satisfied.' + description: 'Current status of the quote. `PENDING_AUTHORIZATION` occurs only for customers in a region where Strong Customer Authentication is required (e.g. EU): the quote carries an `scaChallenge` that must be authorized before execution, and for realtime-funding sources `paymentInstructions` are withheld until it is satisfied.' example: PENDING createdAt: type: string @@ -18362,7 +18358,7 @@ components: scaChallenge: $ref: '#/components/schemas/ScaChallenge' readOnly: true - description: 'Present only while `status` is `PENDING_AUTHORIZATION`: the Strong Customer Authentication challenge to satisfy before this quote can be executed (or, for realtime-funding sources, before `paymentInstructions` are issued). Omitted for customers whose provider does not require SCA.' + description: 'Present only while `status` is `PENDING_AUTHORIZATION`: the Strong Customer Authentication challenge to satisfy before this quote can be executed (or, for realtime-funding sources, before `paymentInstructions` are issued). Omitted for customers outside SCA-regulated regions (non-EU).' QuoteLockSide: type: string enum: @@ -18429,7 +18425,7 @@ components: $ref: '#/components/schemas/PurposeOfPayment' scaFactor: $ref: '#/components/schemas/ScaFactor' - description: Optional preferred factor for a Strong Customer Authentication challenge issued at quote creation. Only relevant for a realtime-funding source whose provider requires SCA (e.g. EU); ignored otherwise. Valid values are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected. When the quote is returned in `PENDING_AUTHORIZATION`, authorize it via `POST /quotes/{quoteId}/authorize`. + description: Optional preferred factor for a Strong Customer Authentication challenge issued at quote creation. Only relevant for a realtime-funding source in a region where SCA is required (e.g. EU); ignored otherwise. Valid values are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected. When the quote is returned in `PENDING_AUTHORIZATION`, authorize it via `POST /quotes/{quoteId}/authorize`. senderCustomerInfo: type: object additionalProperties: true @@ -18445,10 +18441,10 @@ components: properties: scaFactor: $ref: '#/components/schemas/ScaFactor' - description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers whose provider requires SCA (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. + description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers in a region where SCA is required (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. scaAuthorization: $ref: '#/components/schemas/ScaAuthorization' - description: '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 quote in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers whose provider does not require SCA.' + description: 'Optional inline Strong Customer Authentication proof. Only relevant for customers in a region where SCA is required (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 quote in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers outside SCA-regulated regions (non-EU).' Error429: type: object required: diff --git a/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml b/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml index 6f4564763..354a82fa3 100644 --- a/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml +++ b/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml @@ -8,7 +8,7 @@ properties: $ref: ../sca/ScaFactor.yaml description: >- Optional preferred factor for the Strong Customer Authentication challenge - this call issues. Only relevant for customers whose provider requires SCA + this call issues. Only relevant for customers in a region where SCA is required (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. @@ -16,8 +16,7 @@ properties: $ref: ../sca/ScaAuthorization.yaml description: >- Optional inline Strong Customer Authentication proof. Only relevant for - customers whose provider requires SCA (e.g. EU): supply this to satisfy + customers in a region where SCA is required (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 quote in `PENDING_AUTHORIZATION` with an - `scaChallenge`, then authorize separately. Ignored for customers whose - provider does not require SCA. + `scaChallenge`, then authorize separately. Ignored for customers outside SCA-regulated regions (non-EU). diff --git a/openapi/components/schemas/quotes/Quote.yaml b/openapi/components/schemas/quotes/Quote.yaml index 23b52f374..76f1279be 100644 --- a/openapi/components/schemas/quotes/Quote.yaml +++ b/openapi/components/schemas/quotes/Quote.yaml @@ -29,7 +29,7 @@ properties: - EXPIRED description: >- Current status of the quote. `PENDING_AUTHORIZATION` occurs only for - customers whose provider requires Strong Customer Authentication (e.g. + customers in a region where Strong Customer Authentication is required (e.g. EU): the quote carries an `scaChallenge` that must be authorized before execution, and for realtime-funding sources `paymentInstructions` are withheld until it is satisfied. @@ -133,4 +133,4 @@ properties: Present only while `status` is `PENDING_AUTHORIZATION`: the Strong Customer Authentication challenge to satisfy before this quote can be executed (or, for realtime-funding sources, before `paymentInstructions` - are issued). Omitted for customers whose provider does not require SCA. + are issued). Omitted for customers outside SCA-regulated regions (non-EU). diff --git a/openapi/components/schemas/quotes/QuoteRequest.yaml b/openapi/components/schemas/quotes/QuoteRequest.yaml index 6faa0ef98..651e498d5 100644 --- a/openapi/components/schemas/quotes/QuoteRequest.yaml +++ b/openapi/components/schemas/quotes/QuoteRequest.yaml @@ -62,7 +62,7 @@ properties: description: >- Optional preferred factor for a Strong Customer Authentication challenge issued at quote creation. Only relevant for a realtime-funding source - whose provider requires SCA (e.g. EU); ignored otherwise. Valid values are + in a region where SCA is required (e.g. EU); ignored otherwise. Valid values are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected. When the quote is returned in `PENDING_AUTHORIZATION`, authorize it via `POST /quotes/{quoteId}/authorize`. diff --git a/openapi/components/schemas/sca/ScaAuthorization.yaml b/openapi/components/schemas/sca/ScaAuthorization.yaml index 0b70f75d5..b1798e97b 100644 --- a/openapi/components/schemas/sca/ScaAuthorization.yaml +++ b/openapi/components/schemas/sca/ScaAuthorization.yaml @@ -3,8 +3,8 @@ description: >- Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). When supplying a `passkeyAssertion`, `origin` is **required** — the WebAuthn assertion is bound - to the origin it was produced against, and the provider rejects a passkey - confirmation without it. + to the origin it was produced against, and a passkey confirmation is rejected + without it. properties: code: type: diff --git a/openapi/components/schemas/sca/ScaChallenge.yaml b/openapi/components/schemas/sca/ScaChallenge.yaml index 73e971715..47523de67 100644 --- a/openapi/components/schemas/sca/ScaChallenge.yaml +++ b/openapi/components/schemas/sca/ScaChallenge.yaml @@ -1,9 +1,7 @@ type: object description: >- A Strong Customer Authentication challenge that must be satisfied before a - money-movement operation can complete. This object is **only present when the - customer's payment provider requires SCA** (e.g. EU customers on an e-money - provider); for customers whose provider has no such requirement it is omitted + money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed. @@ -19,9 +17,9 @@ description: >- still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of - authorizations is flow- and provider-dependent and **may decrease in future**: + authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion - and the payout as two separate challenges; a future provider update may + and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged. required: @@ -63,8 +61,7 @@ properties: conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic - authorization step and do not branch program logic on it. Omitted when the - provider does not distinguish steps (e.g. a single-authorization flow). + authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow). example: PAYOUT passkeyAssertionOptions: type: diff --git a/openapi/components/schemas/transactions/OutgoingTransactionStatus.yaml b/openapi/components/schemas/transactions/OutgoingTransactionStatus.yaml index 099d674c6..d49ffd374 100644 --- a/openapi/components/schemas/transactions/OutgoingTransactionStatus.yaml +++ b/openapi/components/schemas/transactions/OutgoingTransactionStatus.yaml @@ -12,7 +12,7 @@ description: | | Status | Description | |--------|-------------| | `PENDING` | Quote is pending confirmation | - | `PENDING_AUTHORIZATION` | Awaiting Strong Customer Authentication. Only occurs for customers whose provider requires SCA (e.g. EU); authorize the transaction's `scaChallenge` to proceed. | + | `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. | | `EXPIRED` | Quote wasn't executed before expiry window | | `PROCESSING` | Executing the quote after receiving funds | | `COMPLETED` | Payout successfully reached the destination | diff --git a/openapi/components/schemas/transactions/TransactionStatus.yaml b/openapi/components/schemas/transactions/TransactionStatus.yaml index 19377b812..dc5524c0f 100644 --- a/openapi/components/schemas/transactions/TransactionStatus.yaml +++ b/openapi/components/schemas/transactions/TransactionStatus.yaml @@ -16,7 +16,7 @@ description: | |--------|-------------| | `CREATED` | Initial lookup has been created | | `PENDING` | Quote has been created | - | `PENDING_AUTHORIZATION` | Awaiting Strong Customer Authentication. Only occurs for customers whose provider requires SCA (e.g. EU); authorize the transaction's `scaChallenge` to proceed. | + | `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. | | `PROCESSING` | Funding has been received and payment initiated | | `COMPLETED` | Cross border payment has been received, converted and payment has been sent to the offramp network | | `REJECTED` | Receiving institution or wallet rejected payment, payment has been refunded | diff --git a/openapi/components/schemas/transfers/TransferOutRequest.yaml b/openapi/components/schemas/transfers/TransferOutRequest.yaml index 6cc777c0e..39fb48f1a 100644 --- a/openapi/components/schemas/transfers/TransferOutRequest.yaml +++ b/openapi/components/schemas/transfers/TransferOutRequest.yaml @@ -30,7 +30,7 @@ properties: $ref: ../sca/ScaFactor.yaml description: >- Optional preferred factor for the Strong Customer Authentication challenge - this call issues. Only relevant for customers whose provider requires SCA + this call issues. Only relevant for customers in a region where SCA is required (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. @@ -38,8 +38,8 @@ properties: $ref: ../sca/ScaAuthorization.yaml description: >- Optional inline Strong Customer Authentication proof. Only relevant for - customers whose provider requires SCA (e.g. EU): supply this to satisfy + customers in a region where SCA is required (e.g. EU): supply this to satisfy the challenge in the same call once the customer has the code/assertion. Omit it on the first call to receive the transaction in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. - Ignored for customers whose provider does not require SCA. + Ignored for customers outside SCA-regulated regions (non-EU). diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 95a61c7e8..1e903f9aa 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -26,9 +26,7 @@ tags: - name: Strong Customer Authentication description: >- Endpoints for authorizing money-movement operations that require Strong - Customer Authentication. Relevant only for customers whose payment - provider mandates SCA (e.g. EU customers); other providers never surface - an SCA challenge and these endpoints return 409. + Customer Authentication. Relevant only for customers in a region where SCA is required (e.g. EU); customers outside SCA-regulated regions never see an SCA challenge and these endpoints return 409. - name: KYC/KYB Verifications description: >- Endpoints for Know Your Customer (KYC) and Know Your Business (KYB) diff --git a/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml b/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml index e448f4a04..13c6bfd8a 100644 --- a/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml +++ b/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml @@ -22,9 +22,7 @@ post: next `scaChallenge` — authorize that too, repeating until it advances (see `ScaChallenge`). - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. In sandbox, the SMS code is always `123456`. operationId: authorizeQuote @@ -65,7 +63,7 @@ post: $ref: ../../components/schemas/errors/Error404.yaml '409': description: >- - The customer's payment provider does not require SCA, or the quote has + SCA is not required for this customer, or the quote has no pending challenge to authorize. content: application/json: @@ -75,8 +73,7 @@ post: description: >- Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated - bad codes brute-forcing the OTP). The provider may invalidate the - challenge after too many failed attempts. Clients should back off and + bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. content: application/json: diff --git a/openapi/paths/quotes/quotes_{quoteId}_authorize_resend.yaml b/openapi/paths/quotes/quotes_{quoteId}_authorize_resend.yaml index 96e5edbdf..c2957faf1 100644 --- a/openapi/paths/quotes/quotes_{quoteId}_authorize_resend.yaml +++ b/openapi/paths/quotes/quotes_{quoteId}_authorize_resend.yaml @@ -15,8 +15,8 @@ post: `scaChallenge.expiresAt` is **not** extended; once the challenge is past `expiresAt` it can no longer be authorized. - Only meaningful for customers whose provider requires SCA (e.g. EU); other - providers return 409. `PASSKEY` challenges cannot be re-sent and return 409. + Only meaningful for customers in a region where SCA is required (e.g. EU); + a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent and return 409. In sandbox, the code is always `123456`. operationId: resendQuoteScaCode @@ -41,7 +41,7 @@ post: $ref: ../../components/schemas/errors/Error404.yaml '409': description: >- - The customer's provider does not require SCA, the quote has no pending + SCA is not required for this customer, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). content: diff --git a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml index c486df66a..86aa84e45 100644 --- a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml +++ b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml @@ -59,8 +59,7 @@ post: responses: '200': description: > - Quote processed. The outcome depends on whether the customer's provider - requires Strong Customer Authentication (SCA): + Quote processed. The outcome depends on whether SCA applies to the customer (currently EU customers): - **No SCA required, or a valid `scaAuthorization` is supplied inline on diff --git a/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml b/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml index 99b7f8b6c..aba178bea 100644 --- a/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml +++ b/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml @@ -19,18 +19,16 @@ post: **still** `PENDING_AUTHORIZATION`, it now carries the next `scaChallenge` (a new `id`) — call this endpoint again for that one. Repeat until the transaction leaves `PENDING_AUTHORIZATION`. Do not assume a single call - releases the transfer; the number of authorizations is provider-dependent + releases the transfer; the number of authorizations is flow-dependent and may change over time. Transactions enter `PENDING_AUTHORIZATION` from `execute` (embedded-wallet - quotes) and `transfer-out` when the customer's provider requires SCA. The + quotes) and `transfer-out` when the customer is in a region where SCA is required. The first authorization can be supplied inline on those calls via their optional `scaAuthorization` field instead of calling this endpoint; any subsequent authorizations in the sequence use this endpoint. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. In sandbox, the SMS code is always `123456`. operationId: authorizeTransaction @@ -74,7 +72,7 @@ post: $ref: ../../components/schemas/errors/Error404.yaml '409': description: >- - The customer's payment provider does not require SCA, or the + SCA is not required for this customer, or the transaction has no pending challenge to authorize. content: application/json: @@ -84,8 +82,7 @@ post: description: >- Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated - bad codes brute-forcing the OTP). The provider may invalidate the - challenge after too many failed attempts. Clients should back off and + bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. content: application/json: diff --git a/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml b/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml index c3d566f81..024fa1d43 100644 --- a/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml +++ b/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml @@ -15,9 +15,9 @@ post: and its `scaChallenge.expiresAt` is **not** extended; once the challenge is past `expiresAt` it can no longer be authorized. - Only meaningful for customers whose provider requires SCA (e.g. EU); other - providers return 409. `PASSKEY` challenges cannot be re-sent (there is no - code to deliver) and return 409. + Only meaningful for customers in a region where SCA is required (e.g. EU); + a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent (there is + no code to deliver) and return 409. In sandbox, the code is always `123456`. operationId: resendTransactionScaCode @@ -42,7 +42,7 @@ post: $ref: ../../components/schemas/errors/Error404.yaml '409': description: >- - The customer's provider does not require SCA, the transaction has no + SCA is not required for this customer, the transaction has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). content: diff --git a/openapi/paths/transfers/transfer_out.yaml b/openapi/paths/transfers/transfer_out.yaml index 8f7f16d55..bad24c505 100644 --- a/openapi/paths/transfers/transfer_out.yaml +++ b/openapi/paths/transfers/transfer_out.yaml @@ -40,7 +40,7 @@ post: Transfer-out request created successfully. - For customers whose provider requires Strong Customer Authentication + For customers in a region where Strong Customer Authentication is required (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies the **first** challenge in one shot. Without it, the returned transaction comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; @@ -48,7 +48,7 @@ post: `POST /transactions/{transactionId}/authorize`. This may be required **more than once** — the transaction can surface a further `scaChallenge` after each authorization (see `ScaChallenge`); keep authorizing until it - leaves `PENDING_AUTHORIZATION`. For providers that don't require SCA the + leaves `PENDING_AUTHORIZATION`. For customers outside SCA-regulated regions the transaction proceeds as usual. content: application/json: From 3ab60659203a214e783d72a4964a51014c83a7ed Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 13 Jul 2026 23:34:34 -0700 Subject: [PATCH 10/10] docs(sca): drop last 'provider' mention on Transaction.scaChallenge 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 --- mintlify/openapi.yaml | 350 +++++------------- openapi.yaml | 350 +++++------------- .../schemas/quotes/ExecuteQuoteRequest.yaml | 14 +- .../components/schemas/sca/ScaChallenge.yaml | 6 +- .../schemas/transactions/Transaction.yaml | 8 - .../schemas/transfers/TransferOutRequest.yaml | 9 - .../webhooks/OutgoingPaymentWebhook.yaml | 1 + openapi/openapi.yaml | 4 - .../quotes/quotes_{quoteId}_execute.yaml | 29 +- ...ransactions_{transactionId}_authorize.yaml | 96 ----- ...ions_{transactionId}_authorize_resend.yaml | 67 ---- openapi/paths/transfers/transfer_out.yaml | 19 +- 12 files changed, 220 insertions(+), 733 deletions(-) delete mode 100644 openapi/paths/transactions/transactions_{transactionId}_authorize.yaml delete mode 100644 openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 39599156f..23554bda8 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2429,7 +2429,9 @@ paths: description: | Transfer-out request created successfully. - For customers in a region where Strong Customer Authentication is required (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies the **first** challenge in one shot. Without it, the returned transaction comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by calling `POST /transactions/{transactionId}/authorize`. This may be required **more than once** — the transaction can surface a further `scaChallenge` after each authorization (see `ScaChallenge`); keep authorizing until it leaves `PENDING_AUTHORIZATION`. For customers outside SCA-regulated regions the transaction proceeds as usual. + For customers outside SCA-regulated regions the transaction proceeds as usual. + + **Strong Customer Authentication (EU):** per-transaction SCA is authorized only on the quote resource (`POST /quotes/{quoteId}/authorize`), and `transfer-out` has no associated quote. `transfer-out` is being deprecated, so SCA-gated EU debits are not offered on this endpoint — use the quote + `execute` flow instead. content: application/json: schema: @@ -2866,9 +2868,9 @@ paths: description: | Quote processed. The outcome depends on whether SCA applies to the customer (currently EU customers): - - **No SCA required, or a valid `scaAuthorization` is supplied inline on this call:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`). + - **No SCA required:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`). - - **SCA required and no inline proof:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`, and the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). That may need to be done **more than once** — the transaction can surface a further `scaChallenge` after each authorization (see `ScaChallenge`); keep authorizing until it leaves `PENDING_AUTHORIZATION`. The inline `scaAuthorization` field satisfies only the **first** challenge. A client holding a `PENDING_AUTHORIZATION` quote from `execute` always authorizes via the **transaction**-scoped endpoint; the quote-scoped `POST /quotes/{quoteId}/authorize` is only for a realtime-funding quote whose challenge was issued at quote creation (the 202 from `POST /quotes`). If an SMS code lapses, re-send it via `POST /transactions/{transactionId}/authorize/resend`. + - **SCA required:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by authorizing the quote you already hold — `POST /quotes/{quoteId}/authorize` (re-calling `execute` returns 409). A pre-funded send is a single challenge, so one authorization releases it. The challenge (and its SMS code / passkey assertion) only comes into existence once this call initiates the transfer, so the proof is always supplied on the follow-up authorize, never inline on this call. If an SMS code lapses, re-send it via `POST /quotes/{quoteId}/authorize/resend`. content: application/json: schema: @@ -3392,155 +3394,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /transactions/{transactionId}/authorize: - parameters: - - name: transactionId - in: path - description: The unique identifier of the transaction whose SCA challenge is being authorized. - required: true - schema: - type: string - example: Transaction:019542f5-b3e7-1d02-0000-000000000005 - post: - summary: Authorize a transaction's SCA challenge - description: | - Satisfy the Strong Customer Authentication challenge carried by a - transaction in `PENDING_AUTHORIZATION` status by submitting an - `ScaAuthorization` proof for the transaction's **current** `scaChallenge`. - - A transaction may require **more than one** authorization in sequence (see - `ScaChallenge`). On success, inspect the returned transaction: if it has - advanced to `PROCESSING` / `COMPLETED`, the transfer is released; if it is - **still** `PENDING_AUTHORIZATION`, it now carries the next `scaChallenge` - (a new `id`) — call this endpoint again for that one. Repeat until the - transaction leaves `PENDING_AUTHORIZATION`. Do not assume a single call - releases the transfer; the number of authorizations is flow-dependent - and may change over time. - - Transactions enter `PENDING_AUTHORIZATION` from `execute` (embedded-wallet - quotes) and `transfer-out` when the customer is in a region where SCA is required. The - first authorization can be supplied inline on those calls via their optional - `scaAuthorization` field instead of calling this endpoint; any subsequent - authorizations in the sequence use this endpoint. - - This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. - - In sandbox, the SMS code is always `123456`. - operationId: authorizeTransaction - tags: - - Strong Customer Authentication - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ScaAuthorization' - responses: - '200': - description: Challenge authorized; the updated transaction is returned. It has either advanced (`PROCESSING` / `COMPLETED`) or, if another authorization is required, remains `PENDING_AUTHORIZATION` with the next `scaChallenge`. - content: - application/json: - schema: - $ref: '#/components/schemas/TransactionOneOf' - '400': - description: Invalid or expired authorization proof - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Transaction not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '409': - description: SCA is not required for this customer, or the transaction has no pending challenge to authorize. - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' - '429': - description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. - content: - application/json: - schema: - $ref: '#/components/schemas/Error429' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /transactions/{transactionId}/authorize/resend: - parameters: - - name: transactionId - in: path - description: The unique identifier of the transaction whose SCA challenge code should be re-sent. - required: true - schema: - type: string - example: Transaction:019542f5-b3e7-1d02-0000-000000000005 - post: - summary: Resend a transaction's SCA challenge code - description: | - Re-send the one-time code for a transaction in `PENDING_AUTHORIZATION` - status whose `scaChallenge.factor` is `SMS_OTP` (e.g. the customer didn't - receive it). The existing challenge is reused — no new challenge is issued, - and its `scaChallenge.expiresAt` is **not** extended; once the challenge is - past `expiresAt` it can no longer be authorized. - - Only meaningful for customers in a region where SCA is required (e.g. EU); - a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent (there is - no code to deliver) and return 409. - - In sandbox, the code is always `123456`. - operationId: resendTransactionScaCode - tags: - - Strong Customer Authentication - security: - - BasicAuth: [] - responses: - '204': - description: Code re-sent. - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Transaction not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '409': - description: SCA is not required for this customer, the transaction has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' - '429': - description: Too many requests. Returned with `RATE_LIMITED` when codes are re-sent too frequently, to avoid spamming the customer's phone. Clients should back off and retry after the interval indicated by the `Retry-After` response header. - content: - application/json: - schema: - $ref: '#/components/schemas/Error429' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' /crypto/estimate-withdrawal-fee: post: summary: Estimate crypto withdrawal fee @@ -17210,74 +17063,6 @@ components: FULL_NAME: John Sender BIRTH_DATE: '1985-06-15' NATIONALITY: DE - ScaFactor: - type: string - enum: - - SMS_OTP - - TOTP - - PASSKEY - description: | - A Strong Customer Authentication factor. - - | Factor | Description | - |--------|-------------| - | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. | - | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). | - | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. | - ScaChallenge: - type: object - description: |- - A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed. - - When present on a quote or transaction, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or `POST /transactions/{transactionId}/authorize`, or inline via the optional `scaAuthorization` field on the originating `execute` / `transfer-out` call. - - **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged. - required: - - id - - expiresAt - - factor - - availableFactors - properties: - id: - type: string - description: Unique identifier for this challenge. The server resolves the active challenge from the quote or transaction being authorized, so this field need not be supplied back; it is informational (e.g. for logging or correlation). - example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007 - expiresAt: - type: string - format: date-time - description: Absolute UTC timestamp after which this challenge can no longer be authorized. - example: '2025-10-03T12:05:00Z' - factor: - $ref: '#/components/schemas/ScaFactor' - description: The factor this challenge was issued for. Defaults to `SMS_OTP`. - availableFactors: - type: array - description: The factors the customer may use to satisfy this challenge. - items: - $ref: '#/components/schemas/ScaFactor' - example: - - SMS_OTP - purpose: - type: - - string - - 'null' - description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow). - example: PAYOUT - passkeyAssertionOptions: - type: - - object - - 'null' - additionalProperties: true - description: Opaque WebAuthn assertion request options (including the relying-party id, challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. - passkeyAllowedOrigins: - type: - - array - - 'null' - items: - type: string - description: The origins the WebAuthn ceremony may run against. Populated for enrollment and login passkey challenges; the origin the assertion is produced against must be one of these and echoed back as `ScaAuthorization.origin`. Per-transaction passkey challenges omit this (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` for how to source the origin in that case. - example: - - https://app.example.com Transaction: type: object required: @@ -17340,10 +17125,6 @@ components: example: 'Payment for invoice #1234' counterpartyInformation: $ref: '#/components/schemas/CounterpartyInformation' - scaChallenge: - $ref: '#/components/schemas/ScaChallenge' - readOnly: true - description: 'Present only while `status` is `PENDING_AUTHORIZATION`: the Strong Customer Authentication challenge to satisfy before this transaction can proceed. Omitted entirely for customers whose provider does not require SCA.' TransactionSourceType: type: string enum: @@ -17966,28 +17747,20 @@ components: description: The payment rail to use for the transfer. Must be one of the rails supported by the destination account. If not specified, the system will select a default rail. allOf: - $ref: '#/components/schemas/PaymentRail' - ScaAuthorization: - type: object - description: Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). When supplying a `passkeyAssertion`, `origin` is **required** — the WebAuthn assertion is bound to the origin it was produced against, and a passkey confirmation is rejected without it. - properties: - code: - type: - - string - - 'null' - description: The one-time code the customer received by SMS, or read from their authenticator app. In sandbox, the code is always `123456`. - example: '123456' - passkeyAssertion: - type: - - object - - 'null' - additionalProperties: true - description: Opaque WebAuthn assertion produced by the device from the challenge's `passkeyAssertionOptions`. Required when satisfying a `PASSKEY` challenge. - origin: - type: - - string - - 'null' - description: The WebAuthn origin the `passkeyAssertion` was produced against. **Required** alongside `passkeyAssertion`; omit it for the `code` path. When the challenge lists `passkeyAllowedOrigins` (enrollment / login challenges), it must be one of those. A per-transaction passkey challenge carries `passkeyAssertionOptions` but may omit `passkeyAllowedOrigins`; in that case supply the origin your app invoked the WebAuthn API from, which must match the relying party in `passkeyAssertionOptions`. - example: https://app.example.com + ScaFactor: + type: string + enum: + - SMS_OTP + - TOTP + - PASSKEY + description: | + A Strong Customer Authentication factor. + + | Factor | Description | + |--------|-------------| + | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. | + | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). | + | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. | TransferOutRequest: type: object required: @@ -18013,9 +17786,6 @@ components: scaFactor: $ref: '#/components/schemas/ScaFactor' description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers in a region where SCA is required (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. - scaAuthorization: - $ref: '#/components/schemas/ScaAuthorization' - description: 'Optional inline Strong Customer Authentication proof. Only relevant for customers in a region where SCA is required (e.g. EU): supply this to satisfy the challenge in the same call once the customer has the code/assertion. Omit it on the first call to receive the transaction in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers outside SCA-regulated regions (non-EU).' CurrencyPreference: type: object required: @@ -18250,6 +18020,60 @@ components: mapping: ACCOUNT: '#/components/schemas/AccountDestination' UMA_ADDRESS: '#/components/schemas/UmaAddressDestination' + ScaChallenge: + type: object + description: |- + A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed. + + When present on a quote, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize`. + + **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged. + required: + - id + - expiresAt + - factor + - availableFactors + properties: + id: + type: string + description: Unique identifier for this challenge. The server resolves the active challenge from the quote or transaction being authorized, so this field need not be supplied back; it is informational (e.g. for logging or correlation). + example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007 + expiresAt: + type: string + format: date-time + description: Absolute UTC timestamp after which this challenge can no longer be authorized. + example: '2025-10-03T12:05:00Z' + factor: + $ref: '#/components/schemas/ScaFactor' + description: The factor this challenge was issued for. Defaults to `SMS_OTP`. + availableFactors: + type: array + description: The factors the customer may use to satisfy this challenge. + items: + $ref: '#/components/schemas/ScaFactor' + example: + - SMS_OTP + purpose: + type: + - string + - 'null' + description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow). + example: PAYOUT + passkeyAssertionOptions: + type: + - object + - 'null' + additionalProperties: true + description: Opaque WebAuthn assertion request options (including the relying-party id, challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. + passkeyAllowedOrigins: + type: + - array + - 'null' + items: + type: string + description: The origins the WebAuthn ceremony may run against. Populated for enrollment and login passkey challenges; the origin the assertion is produced against must be one of these and echoed back as `ScaAuthorization.origin`. Per-transaction passkey challenges omit this (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` for how to source the origin in that case. + example: + - https://app.example.com Quote: type: object required: @@ -18437,14 +18261,33 @@ components: NATIONALITY: FR ExecuteQuoteRequest: type: object - description: Optional body for executing a quote. Only needed to supply an inline Strong Customer Authentication proof; omit the body entirely for quotes that do not require SCA. + description: Optional body for executing a quote. Only needed to request a specific Strong Customer Authentication factor (`scaFactor`) for the challenge this call issues; omit the body entirely otherwise. properties: scaFactor: $ref: '#/components/schemas/ScaFactor' description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers in a region where SCA is required (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. - scaAuthorization: - $ref: '#/components/schemas/ScaAuthorization' - description: 'Optional inline Strong Customer Authentication proof. Only relevant for customers in a region where SCA is required (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 quote in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers outside SCA-regulated regions (non-EU).' + ScaAuthorization: + type: object + description: Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). When supplying a `passkeyAssertion`, `origin` is **required** — the WebAuthn assertion is bound to the origin it was produced against, and a passkey confirmation is rejected without it. + properties: + code: + type: + - string + - 'null' + description: The one-time code the customer received by SMS, or read from their authenticator app. In sandbox, the code is always `123456`. + example: '123456' + passkeyAssertion: + type: + - object + - 'null' + additionalProperties: true + description: Opaque WebAuthn assertion produced by the device from the challenge's `passkeyAssertionOptions`. Required when satisfying a `PASSKEY` challenge. + origin: + type: + - string + - 'null' + description: The WebAuthn origin the `passkeyAssertion` was produced against. **Required** alongside `passkeyAssertion`; omit it for the `code` path. When the challenge lists `passkeyAllowedOrigins` (enrollment / login challenges), it must be one of those. A per-transaction passkey challenge carries `passkeyAssertionOptions` but may omit `passkeyAllowedOrigins`; in that case supply the origin your app invoked the WebAuthn API from, which must match the relying party in `passkeyAssertionOptions`. + example: https://app.example.com Error429: type: object required: @@ -20624,6 +20467,7 @@ components: type: string enum: - OUTGOING_PAYMENT.PENDING + - OUTGOING_PAYMENT.PENDING_AUTHORIZATION - OUTGOING_PAYMENT.PROCESSING - OUTGOING_PAYMENT.COMPLETED - OUTGOING_PAYMENT.FAILED diff --git a/openapi.yaml b/openapi.yaml index 39599156f..23554bda8 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2429,7 +2429,9 @@ paths: description: | Transfer-out request created successfully. - For customers in a region where Strong Customer Authentication is required (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies the **first** challenge in one shot. Without it, the returned transaction comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by calling `POST /transactions/{transactionId}/authorize`. This may be required **more than once** — the transaction can surface a further `scaChallenge` after each authorization (see `ScaChallenge`); keep authorizing until it leaves `PENDING_AUTHORIZATION`. For customers outside SCA-regulated regions the transaction proceeds as usual. + For customers outside SCA-regulated regions the transaction proceeds as usual. + + **Strong Customer Authentication (EU):** per-transaction SCA is authorized only on the quote resource (`POST /quotes/{quoteId}/authorize`), and `transfer-out` has no associated quote. `transfer-out` is being deprecated, so SCA-gated EU debits are not offered on this endpoint — use the quote + `execute` flow instead. content: application/json: schema: @@ -2866,9 +2868,9 @@ paths: description: | Quote processed. The outcome depends on whether SCA applies to the customer (currently EU customers): - - **No SCA required, or a valid `scaAuthorization` is supplied inline on this call:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`). + - **No SCA required:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`). - - **SCA required and no inline proof:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`, and the **only** way to release the transfer is `POST /transactions/{transactionId}/authorize` for the quote's `transactionId` (re-calling `execute` returns 409). That may need to be done **more than once** — the transaction can surface a further `scaChallenge` after each authorization (see `ScaChallenge`); keep authorizing until it leaves `PENDING_AUTHORIZATION`. The inline `scaAuthorization` field satisfies only the **first** challenge. A client holding a `PENDING_AUTHORIZATION` quote from `execute` always authorizes via the **transaction**-scoped endpoint; the quote-scoped `POST /quotes/{quoteId}/authorize` is only for a realtime-funding quote whose challenge was issued at quote creation (the 202 from `POST /quotes`). If an SMS code lapses, re-send it via `POST /transactions/{transactionId}/authorize/resend`. + - **SCA required:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by authorizing the quote you already hold — `POST /quotes/{quoteId}/authorize` (re-calling `execute` returns 409). A pre-funded send is a single challenge, so one authorization releases it. The challenge (and its SMS code / passkey assertion) only comes into existence once this call initiates the transfer, so the proof is always supplied on the follow-up authorize, never inline on this call. If an SMS code lapses, re-send it via `POST /quotes/{quoteId}/authorize/resend`. content: application/json: schema: @@ -3392,155 +3394,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /transactions/{transactionId}/authorize: - parameters: - - name: transactionId - in: path - description: The unique identifier of the transaction whose SCA challenge is being authorized. - required: true - schema: - type: string - example: Transaction:019542f5-b3e7-1d02-0000-000000000005 - post: - summary: Authorize a transaction's SCA challenge - description: | - Satisfy the Strong Customer Authentication challenge carried by a - transaction in `PENDING_AUTHORIZATION` status by submitting an - `ScaAuthorization` proof for the transaction's **current** `scaChallenge`. - - A transaction may require **more than one** authorization in sequence (see - `ScaChallenge`). On success, inspect the returned transaction: if it has - advanced to `PROCESSING` / `COMPLETED`, the transfer is released; if it is - **still** `PENDING_AUTHORIZATION`, it now carries the next `scaChallenge` - (a new `id`) — call this endpoint again for that one. Repeat until the - transaction leaves `PENDING_AUTHORIZATION`. Do not assume a single call - releases the transfer; the number of authorizations is flow-dependent - and may change over time. - - Transactions enter `PENDING_AUTHORIZATION` from `execute` (embedded-wallet - quotes) and `transfer-out` when the customer is in a region where SCA is required. The - first authorization can be supplied inline on those calls via their optional - `scaAuthorization` field instead of calling this endpoint; any subsequent - authorizations in the sequence use this endpoint. - - This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. - - In sandbox, the SMS code is always `123456`. - operationId: authorizeTransaction - tags: - - Strong Customer Authentication - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ScaAuthorization' - responses: - '200': - description: Challenge authorized; the updated transaction is returned. It has either advanced (`PROCESSING` / `COMPLETED`) or, if another authorization is required, remains `PENDING_AUTHORIZATION` with the next `scaChallenge`. - content: - application/json: - schema: - $ref: '#/components/schemas/TransactionOneOf' - '400': - description: Invalid or expired authorization proof - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Transaction not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '409': - description: SCA is not required for this customer, or the transaction has no pending challenge to authorize. - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' - '429': - description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. - content: - application/json: - schema: - $ref: '#/components/schemas/Error429' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /transactions/{transactionId}/authorize/resend: - parameters: - - name: transactionId - in: path - description: The unique identifier of the transaction whose SCA challenge code should be re-sent. - required: true - schema: - type: string - example: Transaction:019542f5-b3e7-1d02-0000-000000000005 - post: - summary: Resend a transaction's SCA challenge code - description: | - Re-send the one-time code for a transaction in `PENDING_AUTHORIZATION` - status whose `scaChallenge.factor` is `SMS_OTP` (e.g. the customer didn't - receive it). The existing challenge is reused — no new challenge is issued, - and its `scaChallenge.expiresAt` is **not** extended; once the challenge is - past `expiresAt` it can no longer be authorized. - - Only meaningful for customers in a region where SCA is required (e.g. EU); - a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent (there is - no code to deliver) and return 409. - - In sandbox, the code is always `123456`. - operationId: resendTransactionScaCode - tags: - - Strong Customer Authentication - security: - - BasicAuth: [] - responses: - '204': - description: Code re-sent. - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Transaction not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '409': - description: SCA is not required for this customer, the transaction has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' - '429': - description: Too many requests. Returned with `RATE_LIMITED` when codes are re-sent too frequently, to avoid spamming the customer's phone. Clients should back off and retry after the interval indicated by the `Retry-After` response header. - content: - application/json: - schema: - $ref: '#/components/schemas/Error429' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' /crypto/estimate-withdrawal-fee: post: summary: Estimate crypto withdrawal fee @@ -17210,74 +17063,6 @@ components: FULL_NAME: John Sender BIRTH_DATE: '1985-06-15' NATIONALITY: DE - ScaFactor: - type: string - enum: - - SMS_OTP - - TOTP - - PASSKEY - description: | - A Strong Customer Authentication factor. - - | Factor | Description | - |--------|-------------| - | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. | - | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). | - | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. | - ScaChallenge: - type: object - description: |- - A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed. - - When present on a quote or transaction, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or `POST /transactions/{transactionId}/authorize`, or inline via the optional `scaAuthorization` field on the originating `execute` / `transfer-out` call. - - **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged. - required: - - id - - expiresAt - - factor - - availableFactors - properties: - id: - type: string - description: Unique identifier for this challenge. The server resolves the active challenge from the quote or transaction being authorized, so this field need not be supplied back; it is informational (e.g. for logging or correlation). - example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007 - expiresAt: - type: string - format: date-time - description: Absolute UTC timestamp after which this challenge can no longer be authorized. - example: '2025-10-03T12:05:00Z' - factor: - $ref: '#/components/schemas/ScaFactor' - description: The factor this challenge was issued for. Defaults to `SMS_OTP`. - availableFactors: - type: array - description: The factors the customer may use to satisfy this challenge. - items: - $ref: '#/components/schemas/ScaFactor' - example: - - SMS_OTP - purpose: - type: - - string - - 'null' - description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow). - example: PAYOUT - passkeyAssertionOptions: - type: - - object - - 'null' - additionalProperties: true - description: Opaque WebAuthn assertion request options (including the relying-party id, challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. - passkeyAllowedOrigins: - type: - - array - - 'null' - items: - type: string - description: The origins the WebAuthn ceremony may run against. Populated for enrollment and login passkey challenges; the origin the assertion is produced against must be one of these and echoed back as `ScaAuthorization.origin`. Per-transaction passkey challenges omit this (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` for how to source the origin in that case. - example: - - https://app.example.com Transaction: type: object required: @@ -17340,10 +17125,6 @@ components: example: 'Payment for invoice #1234' counterpartyInformation: $ref: '#/components/schemas/CounterpartyInformation' - scaChallenge: - $ref: '#/components/schemas/ScaChallenge' - readOnly: true - description: 'Present only while `status` is `PENDING_AUTHORIZATION`: the Strong Customer Authentication challenge to satisfy before this transaction can proceed. Omitted entirely for customers whose provider does not require SCA.' TransactionSourceType: type: string enum: @@ -17966,28 +17747,20 @@ components: description: The payment rail to use for the transfer. Must be one of the rails supported by the destination account. If not specified, the system will select a default rail. allOf: - $ref: '#/components/schemas/PaymentRail' - ScaAuthorization: - type: object - description: Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). When supplying a `passkeyAssertion`, `origin` is **required** — the WebAuthn assertion is bound to the origin it was produced against, and a passkey confirmation is rejected without it. - properties: - code: - type: - - string - - 'null' - description: The one-time code the customer received by SMS, or read from their authenticator app. In sandbox, the code is always `123456`. - example: '123456' - passkeyAssertion: - type: - - object - - 'null' - additionalProperties: true - description: Opaque WebAuthn assertion produced by the device from the challenge's `passkeyAssertionOptions`. Required when satisfying a `PASSKEY` challenge. - origin: - type: - - string - - 'null' - description: The WebAuthn origin the `passkeyAssertion` was produced against. **Required** alongside `passkeyAssertion`; omit it for the `code` path. When the challenge lists `passkeyAllowedOrigins` (enrollment / login challenges), it must be one of those. A per-transaction passkey challenge carries `passkeyAssertionOptions` but may omit `passkeyAllowedOrigins`; in that case supply the origin your app invoked the WebAuthn API from, which must match the relying party in `passkeyAssertionOptions`. - example: https://app.example.com + ScaFactor: + type: string + enum: + - SMS_OTP + - TOTP + - PASSKEY + description: | + A Strong Customer Authentication factor. + + | Factor | Description | + |--------|-------------| + | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. | + | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). | + | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. | TransferOutRequest: type: object required: @@ -18013,9 +17786,6 @@ components: scaFactor: $ref: '#/components/schemas/ScaFactor' description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers in a region where SCA is required (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. - scaAuthorization: - $ref: '#/components/schemas/ScaAuthorization' - description: 'Optional inline Strong Customer Authentication proof. Only relevant for customers in a region where SCA is required (e.g. EU): supply this to satisfy the challenge in the same call once the customer has the code/assertion. Omit it on the first call to receive the transaction in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers outside SCA-regulated regions (non-EU).' CurrencyPreference: type: object required: @@ -18250,6 +18020,60 @@ components: mapping: ACCOUNT: '#/components/schemas/AccountDestination' UMA_ADDRESS: '#/components/schemas/UmaAddressDestination' + ScaChallenge: + type: object + description: |- + A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed. + + When present on a quote, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize`. + + **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged. + required: + - id + - expiresAt + - factor + - availableFactors + properties: + id: + type: string + description: Unique identifier for this challenge. The server resolves the active challenge from the quote or transaction being authorized, so this field need not be supplied back; it is informational (e.g. for logging or correlation). + example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007 + expiresAt: + type: string + format: date-time + description: Absolute UTC timestamp after which this challenge can no longer be authorized. + example: '2025-10-03T12:05:00Z' + factor: + $ref: '#/components/schemas/ScaFactor' + description: The factor this challenge was issued for. Defaults to `SMS_OTP`. + availableFactors: + type: array + description: The factors the customer may use to satisfy this challenge. + items: + $ref: '#/components/schemas/ScaFactor' + example: + - SMS_OTP + purpose: + type: + - string + - 'null' + description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow). + example: PAYOUT + passkeyAssertionOptions: + type: + - object + - 'null' + additionalProperties: true + description: Opaque WebAuthn assertion request options (including the relying-party id, challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. + passkeyAllowedOrigins: + type: + - array + - 'null' + items: + type: string + description: The origins the WebAuthn ceremony may run against. Populated for enrollment and login passkey challenges; the origin the assertion is produced against must be one of these and echoed back as `ScaAuthorization.origin`. Per-transaction passkey challenges omit this (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` for how to source the origin in that case. + example: + - https://app.example.com Quote: type: object required: @@ -18437,14 +18261,33 @@ components: NATIONALITY: FR ExecuteQuoteRequest: type: object - description: Optional body for executing a quote. Only needed to supply an inline Strong Customer Authentication proof; omit the body entirely for quotes that do not require SCA. + description: Optional body for executing a quote. Only needed to request a specific Strong Customer Authentication factor (`scaFactor`) for the challenge this call issues; omit the body entirely otherwise. properties: scaFactor: $ref: '#/components/schemas/ScaFactor' description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers in a region where SCA is required (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. - scaAuthorization: - $ref: '#/components/schemas/ScaAuthorization' - description: 'Optional inline Strong Customer Authentication proof. Only relevant for customers in a region where SCA is required (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 quote in `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. Ignored for customers outside SCA-regulated regions (non-EU).' + ScaAuthorization: + type: object + description: Proof that satisfies an `ScaChallenge`. Provide exactly one of `code` (for `SMS_OTP` / `TOTP`) or `passkeyAssertion` (for `PASSKEY`). When supplying a `passkeyAssertion`, `origin` is **required** — the WebAuthn assertion is bound to the origin it was produced against, and a passkey confirmation is rejected without it. + properties: + code: + type: + - string + - 'null' + description: The one-time code the customer received by SMS, or read from their authenticator app. In sandbox, the code is always `123456`. + example: '123456' + passkeyAssertion: + type: + - object + - 'null' + additionalProperties: true + description: Opaque WebAuthn assertion produced by the device from the challenge's `passkeyAssertionOptions`. Required when satisfying a `PASSKEY` challenge. + origin: + type: + - string + - 'null' + description: The WebAuthn origin the `passkeyAssertion` was produced against. **Required** alongside `passkeyAssertion`; omit it for the `code` path. When the challenge lists `passkeyAllowedOrigins` (enrollment / login challenges), it must be one of those. A per-transaction passkey challenge carries `passkeyAssertionOptions` but may omit `passkeyAllowedOrigins`; in that case supply the origin your app invoked the WebAuthn API from, which must match the relying party in `passkeyAssertionOptions`. + example: https://app.example.com Error429: type: object required: @@ -20624,6 +20467,7 @@ components: type: string enum: - OUTGOING_PAYMENT.PENDING + - OUTGOING_PAYMENT.PENDING_AUTHORIZATION - OUTGOING_PAYMENT.PROCESSING - OUTGOING_PAYMENT.COMPLETED - OUTGOING_PAYMENT.FAILED diff --git a/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml b/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml index 354a82fa3..f83b77c63 100644 --- a/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml +++ b/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml @@ -1,8 +1,8 @@ type: object description: >- - Optional body for executing a quote. Only needed to supply an inline Strong - Customer Authentication proof; omit the body entirely for quotes that do not - require SCA. + Optional body for executing a quote. Only needed to request a specific Strong + Customer Authentication factor (`scaFactor`) for the challenge this call + issues; omit the body entirely otherwise. properties: scaFactor: $ref: ../sca/ScaFactor.yaml @@ -12,11 +12,3 @@ properties: (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. - scaAuthorization: - $ref: ../sca/ScaAuthorization.yaml - description: >- - Optional inline Strong Customer Authentication proof. Only relevant for - customers in a region where SCA is required (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 quote in `PENDING_AUTHORIZATION` with an - `scaChallenge`, then authorize separately. Ignored for customers outside SCA-regulated regions (non-EU). diff --git a/openapi/components/schemas/sca/ScaChallenge.yaml b/openapi/components/schemas/sca/ScaChallenge.yaml index 47523de67..4835d6777 100644 --- a/openapi/components/schemas/sca/ScaChallenge.yaml +++ b/openapi/components/schemas/sca/ScaChallenge.yaml @@ -5,10 +5,8 @@ description: >- entirely and no action is needed. - When present on a quote or transaction, authorize it by submitting an - `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or - `POST /transactions/{transactionId}/authorize`, or inline via the optional - `scaAuthorization` field on the originating `execute` / `transfer-out` call. + When present on a quote, authorize it by submitting an `ScaAuthorization` proof + to `POST /quotes/{quoteId}/authorize`. **A single operation may require more than one authorization, in sequence.** diff --git a/openapi/components/schemas/transactions/Transaction.yaml b/openapi/components/schemas/transactions/Transaction.yaml index 9ace65c0c..ebc3ce889 100644 --- a/openapi/components/schemas/transactions/Transaction.yaml +++ b/openapi/components/schemas/transactions/Transaction.yaml @@ -65,11 +65,3 @@ properties: example: 'Payment for invoice #1234' counterpartyInformation: $ref: ./CounterpartyInformation.yaml - scaChallenge: - $ref: ../sca/ScaChallenge.yaml - readOnly: true - description: >- - Present only while `status` is `PENDING_AUTHORIZATION`: the Strong - Customer Authentication challenge to satisfy before this transaction can - proceed. Omitted entirely for customers whose provider does not require - SCA. diff --git a/openapi/components/schemas/transfers/TransferOutRequest.yaml b/openapi/components/schemas/transfers/TransferOutRequest.yaml index 39fb48f1a..865360b34 100644 --- a/openapi/components/schemas/transfers/TransferOutRequest.yaml +++ b/openapi/components/schemas/transfers/TransferOutRequest.yaml @@ -34,12 +34,3 @@ properties: (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. - scaAuthorization: - $ref: ../sca/ScaAuthorization.yaml - description: >- - Optional inline Strong Customer Authentication proof. Only relevant for - customers in a region where SCA is required (e.g. EU): supply this to satisfy - the challenge in the same call once the customer has the code/assertion. - Omit it on the first call to receive the transaction in - `PENDING_AUTHORIZATION` with an `scaChallenge`, then authorize separately. - Ignored for customers outside SCA-regulated regions (non-EU). diff --git a/openapi/components/schemas/webhooks/OutgoingPaymentWebhook.yaml b/openapi/components/schemas/webhooks/OutgoingPaymentWebhook.yaml index bfc7cc46f..4163eb87d 100644 --- a/openapi/components/schemas/webhooks/OutgoingPaymentWebhook.yaml +++ b/openapi/components/schemas/webhooks/OutgoingPaymentWebhook.yaml @@ -10,6 +10,7 @@ allOf: type: string enum: - OUTGOING_PAYMENT.PENDING + - OUTGOING_PAYMENT.PENDING_AUTHORIZATION - OUTGOING_PAYMENT.PROCESSING - OUTGOING_PAYMENT.COMPLETED - OUTGOING_PAYMENT.FAILED diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 1e903f9aa..1e2a31ba0 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -205,10 +205,6 @@ paths: $ref: paths/transactions/transactions_{transactionId}_approve.yaml /transactions/{transactionId}/reject: $ref: paths/transactions/transactions_{transactionId}_reject.yaml - /transactions/{transactionId}/authorize: - $ref: paths/transactions/transactions_{transactionId}_authorize.yaml - /transactions/{transactionId}/authorize/resend: - $ref: paths/transactions/transactions_{transactionId}_authorize_resend.yaml /crypto/estimate-withdrawal-fee: $ref: paths/crypto/crypto_estimate-withdrawal-fee.yaml /sandbox/webhooks/test: diff --git a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml index 86aa84e45..47c822812 100644 --- a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml +++ b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml @@ -62,26 +62,19 @@ post: Quote processed. The outcome depends on whether SCA applies to the customer (currently EU customers): - - **No SCA required, or a valid `scaAuthorization` is supplied inline on - this call:** the transfer is initiated and the quote status advances - (`PROCESSING` / `COMPLETED`). + - **No SCA required:** the transfer is initiated and the quote status + advances (`PROCESSING` / `COMPLETED`). - - **SCA required and no inline proof:** the transfer is **not** initiated - yet. The quote is returned with status `PENDING_AUTHORIZATION` and an - `scaChallenge`, and the **only** way to release the transfer is - `POST /transactions/{transactionId}/authorize` for the quote's - `transactionId` (re-calling `execute` returns 409). That may need to be - done **more than once** — the transaction can surface a further - `scaChallenge` after each authorization (see `ScaChallenge`); keep - authorizing until it leaves `PENDING_AUTHORIZATION`. The inline - `scaAuthorization` field satisfies only the **first** challenge. A - client holding a `PENDING_AUTHORIZATION` quote from `execute` always - authorizes via the **transaction**-scoped endpoint; the quote-scoped - `POST /quotes/{quoteId}/authorize` is only for a realtime-funding quote - whose challenge was issued at quote creation (the 202 from `POST - /quotes`). If an SMS code lapses, re-send it via - `POST /transactions/{transactionId}/authorize/resend`. + - **SCA required:** the transfer is **not** initiated yet. The quote is + returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`; + release the transfer by authorizing the quote you already hold — + `POST /quotes/{quoteId}/authorize` (re-calling `execute` returns 409). A + pre-funded send is a single challenge, so one authorization releases it. + The challenge (and its SMS code / passkey assertion) only comes into + existence once this call initiates the transfer, so the proof is always + supplied on the follow-up authorize, never inline on this call. If an SMS + code lapses, re-send it via `POST /quotes/{quoteId}/authorize/resend`. content: application/json: schema: diff --git a/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml b/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml deleted file mode 100644 index aba178bea..000000000 --- a/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml +++ /dev/null @@ -1,96 +0,0 @@ -parameters: - - name: transactionId - in: path - description: The unique identifier of the transaction whose SCA challenge is being authorized. - required: true - schema: - type: string - example: Transaction:019542f5-b3e7-1d02-0000-000000000005 -post: - summary: Authorize a transaction's SCA challenge - description: | - Satisfy the Strong Customer Authentication challenge carried by a - transaction in `PENDING_AUTHORIZATION` status by submitting an - `ScaAuthorization` proof for the transaction's **current** `scaChallenge`. - - A transaction may require **more than one** authorization in sequence (see - `ScaChallenge`). On success, inspect the returned transaction: if it has - advanced to `PROCESSING` / `COMPLETED`, the transfer is released; if it is - **still** `PENDING_AUTHORIZATION`, it now carries the next `scaChallenge` - (a new `id`) — call this endpoint again for that one. Repeat until the - transaction leaves `PENDING_AUTHORIZATION`. Do not assume a single call - releases the transfer; the number of authorizations is flow-dependent - and may change over time. - - Transactions enter `PENDING_AUTHORIZATION` from `execute` (embedded-wallet - quotes) and `transfer-out` when the customer is in a region where SCA is required. The - first authorization can be supplied inline on those calls via their optional - `scaAuthorization` field instead of calling this endpoint; any subsequent - authorizations in the sequence use this endpoint. - - This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. - - In sandbox, the SMS code is always `123456`. - operationId: authorizeTransaction - tags: - - Strong Customer Authentication - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: ../../components/schemas/sca/ScaAuthorization.yaml - responses: - '200': - description: >- - Challenge authorized; the updated transaction is returned. It has either - advanced (`PROCESSING` / `COMPLETED`) or, if another authorization is - required, remains `PENDING_AUTHORIZATION` with the next `scaChallenge`. - content: - application/json: - schema: - $ref: ../../components/schemas/transactions/TransactionOneOf.yaml - '400': - description: Invalid or expired authorization proof - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error400.yaml - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error401.yaml - '404': - description: Transaction not found - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error404.yaml - '409': - description: >- - SCA is not required for this customer, or the - transaction has no pending challenge to authorize. - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error409.yaml - '429': - description: >- - Too many requests. Returned with `RATE_LIMITED` when authorization - attempts for this challenge happen too frequently (for example, repeated - bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and - retry after the interval indicated by the `Retry-After` response header. - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error429.yaml - '500': - description: Internal service error - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error500.yaml diff --git a/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml b/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml deleted file mode 100644 index 024fa1d43..000000000 --- a/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml +++ /dev/null @@ -1,67 +0,0 @@ -parameters: - - name: transactionId - in: path - description: The unique identifier of the transaction whose SCA challenge code should be re-sent. - required: true - schema: - type: string - example: Transaction:019542f5-b3e7-1d02-0000-000000000005 -post: - summary: Resend a transaction's SCA challenge code - description: | - Re-send the one-time code for a transaction in `PENDING_AUTHORIZATION` - status whose `scaChallenge.factor` is `SMS_OTP` (e.g. the customer didn't - receive it). The existing challenge is reused — no new challenge is issued, - and its `scaChallenge.expiresAt` is **not** extended; once the challenge is - past `expiresAt` it can no longer be authorized. - - Only meaningful for customers in a region where SCA is required (e.g. EU); - a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent (there is - no code to deliver) and return 409. - - In sandbox, the code is always `123456`. - operationId: resendTransactionScaCode - tags: - - Strong Customer Authentication - security: - - BasicAuth: [] - responses: - '204': - description: Code re-sent. - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error401.yaml - '404': - description: Transaction not found - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error404.yaml - '409': - description: >- - SCA is not required for this customer, the transaction has no - pending SMS challenge, or the challenge uses a factor whose code cannot - be re-sent (e.g. passkey). - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error409.yaml - '429': - description: >- - Too many requests. Returned with `RATE_LIMITED` when codes are re-sent - too frequently, to avoid spamming the customer's phone. Clients should - back off and retry after the interval indicated by the `Retry-After` - response header. - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error429.yaml - '500': - description: Internal service error - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error500.yaml diff --git a/openapi/paths/transfers/transfer_out.yaml b/openapi/paths/transfers/transfer_out.yaml index bad24c505..85831695f 100644 --- a/openapi/paths/transfers/transfer_out.yaml +++ b/openapi/paths/transfers/transfer_out.yaml @@ -40,16 +40,15 @@ post: Transfer-out request created successfully. - For customers in a region where Strong Customer Authentication is required - (e.g. EU): supplying `scaAuthorization` upfront in the request satisfies - the **first** challenge in one shot. Without it, the returned transaction - comes back with status `PENDING_AUTHORIZATION` and an `scaChallenge`; - release the transfer by calling - `POST /transactions/{transactionId}/authorize`. This may be required - **more than once** — the transaction can surface a further `scaChallenge` - after each authorization (see `ScaChallenge`); keep authorizing until it - leaves `PENDING_AUTHORIZATION`. For customers outside SCA-regulated regions the - transaction proceeds as usual. + For customers outside SCA-regulated regions the transaction proceeds as + usual. + + + **Strong Customer Authentication (EU):** per-transaction SCA is authorized + only on the quote resource (`POST /quotes/{quoteId}/authorize`), and + `transfer-out` has no associated quote. `transfer-out` is being + deprecated, so SCA-gated EU debits are not offered on this endpoint — use + the quote + `execute` flow instead. content: application/json: schema: