diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 5c510eb9c..1c2a90288 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 @@ -2424,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: @@ -2761,6 +2766,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 +2855,20 @@ 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 processed. The outcome depends on whether the customer's provider requires Strong Customer Authentication (SCA): + + - **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: @@ -2882,6 +2903,145 @@ 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 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' + '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: + 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, 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. + + 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' + '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' /transactions: get: summary: List transactions @@ -3229,6 +3389,148 @@ 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 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' + '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: + 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 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' + '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 @@ -16800,6 +17102,7 @@ components: enum: - CREATED - PENDING + - PENDING_AUTHORIZATION - PROCESSING - COMPLETED - REJECTED @@ -16813,6 +17116,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 +17200,66 @@ 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. 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 + 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: @@ -16958,6 +17322,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: @@ -17172,6 +17540,7 @@ components: type: string enum: - PENDING + - PENDING_AUTHORIZATION - EXPIRED - PROCESSING - COMPLETED @@ -17182,6 +17551,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 | @@ -17578,6 +17948,28 @@ 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 the provider rejects a passkey confirmation 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 TransferOutRequest: type: object required: @@ -17600,6 +17992,12 @@ 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.' CurrencyPreference: type: object required: @@ -17859,11 +18257,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 +18337,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: @@ -18002,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 @@ -18011,6 +18417,43 @@ 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: + 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.' + 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: @@ -18983,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 5c510eb9c..1c2a90288 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 @@ -2424,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: @@ -2761,6 +2766,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 +2855,20 @@ 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 processed. The outcome depends on whether the customer's provider requires Strong Customer Authentication (SCA): + + - **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: @@ -2882,6 +2903,145 @@ 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 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' + '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: + 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, 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. + + 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' + '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' /transactions: get: summary: List transactions @@ -3229,6 +3389,148 @@ 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 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' + '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: + 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 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' + '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 @@ -16800,6 +17102,7 @@ components: enum: - CREATED - PENDING + - PENDING_AUTHORIZATION - PROCESSING - COMPLETED - REJECTED @@ -16813,6 +17116,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 +17200,66 @@ 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. 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 + 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: @@ -16958,6 +17322,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: @@ -17172,6 +17540,7 @@ components: type: string enum: - PENDING + - PENDING_AUTHORIZATION - EXPIRED - PROCESSING - COMPLETED @@ -17182,6 +17551,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 | @@ -17578,6 +17948,28 @@ 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 the provider rejects a passkey confirmation 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 TransferOutRequest: type: object required: @@ -17600,6 +17992,12 @@ 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.' CurrencyPreference: type: object required: @@ -17859,11 +18257,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 +18337,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: @@ -18002,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 @@ -18011,6 +18417,43 @@ 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: + 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.' + 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: @@ -18983,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/ExecuteQuoteRequest.yaml b/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml new file mode 100644 index 000000000..6f4564763 --- /dev/null +++ b/openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml @@ -0,0 +1,23 @@ +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: ../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: >- + 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/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/components/schemas/sca/ScaAuthorization.yaml b/openapi/components/schemas/sca/ScaAuthorization.yaml new file mode 100644 index 000000000..0b70f75d5 --- /dev/null +++ b/openapi/components/schemas/sca/ScaAuthorization.yaml @@ -0,0 +1,37 @@ +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. +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 diff --git a/openapi/components/schemas/sca/ScaChallenge.yaml b/openapi/components/schemas/sca/ScaChallenge.yaml new file mode 100644 index 000000000..083d4f2f1 --- /dev/null +++ b/openapi/components/schemas/sca/ScaChallenge.yaml @@ -0,0 +1,67 @@ +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. 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: ./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 (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 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/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/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..6cc777c0e 100644 --- a/openapi/components/schemas/transfers/TransferOutRequest.yaml +++ b/openapi/components/schemas/transfers/TransferOutRequest.yaml @@ -26,3 +26,20 @@ 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: >- + 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..95a61c7e8 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,10 @@ 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 + /quotes/{quoteId}/authorize/resend: + $ref: paths/quotes/quotes_{quoteId}_authorize_resend.yaml /transactions: $ref: paths/transactions/transactions.yaml /transactions/{transactionId}: @@ -197,6 +207,10 @@ 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.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..3a1241f9c --- /dev/null +++ b/openapi/paths/quotes/quotes_{quoteId}_authorize.yaml @@ -0,0 +1,85 @@ +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 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 + '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: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml 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..96e5edbdf --- /dev/null +++ b/openapi/paths/quotes/quotes_{quoteId}_authorize_resend.yaml @@ -0,0 +1,66 @@ +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, 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. + + 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 + '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/quotes/quotes_{quoteId}_execute.yaml b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml index 27d1bb7a8..4b53517e1 100644 --- a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml +++ b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml @@ -50,12 +50,36 @@ 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 processed. The outcome depends on whether the customer's provider + requires Strong Customer Authentication (SCA): - the quote status has been updated. + + - **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: diff --git a/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml b/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml new file mode 100644 index 000000000..0e4949da3 --- /dev/null +++ b/openapi/paths/transactions/transactions_{transactionId}_authorize.yaml @@ -0,0 +1,87 @@ +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 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 + '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: + 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 new file mode 100644 index 000000000..c3d566f81 --- /dev/null +++ b/openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml @@ -0,0 +1,67 @@ +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 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 + '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 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: