Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
340 changes: 309 additions & 31 deletions mintlify/openapi.yaml

Large diffs are not rendered by default.

340 changes: 309 additions & 31 deletions openapi.yaml

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
type: object
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: ../sca/ScaFactor.yaml
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`.
16 changes: 15 additions & 1 deletion openapi/components/schemas/quotes/Quote.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ properties:
type: string
enum:
- PENDING
- PENDING_AUTHORIZATION
Comment thread
jklein24 marked this conversation as resolved.
- PROCESSING
- COMPLETED
- FAILED
- EXPIRED
description: Current status of the quote
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
Expand Down Expand Up @@ -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 outside SCA-regulated regions (non-EU).
9 changes: 9 additions & 0 deletions openapi/components/schemas/quotes/QuoteRequest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
Comment thread
jklein24 marked this conversation as resolved.
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
Expand Down
37 changes: 37 additions & 0 deletions openapi/components/schemas/sca/ScaAuthorization.yaml
Original file line number Diff line number Diff line change
@@ -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 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
88 changes: 88 additions & 0 deletions openapi/components/schemas/sca/ScaChallenge.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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
Comment thread
jklein24 marked this conversation as resolved.
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
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
13 changes: 13 additions & 0 deletions openapi/components/schemas/sca/ScaFactor.yaml
Original file line number Diff line number Diff line change
@@ -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. |
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
type: string
enum:
- PENDING
- PENDING_AUTHORIZATION
- EXPIRED
- PROCESSING
- COMPLETED
Expand All @@ -11,6 +12,7 @@ description: |
| Status | Description |
|--------|-------------|
| `PENDING` | Quote is pending confirmation |
| `PENDING_AUTHORIZATION` | Awaiting Strong Customer Authentication. Only occurs for customers in a region where SCA is required (e.g. EU); authorize the transaction's `scaChallenge` to proceed. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

do we need this represented in the transaciton as well or can a transaction be pending until the quote is executed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

IMO it's good to be explicit here that it's a different state that requires some action from the customer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should we also have a corresponding webhook

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think we get that for free with the existing transaction status changes, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm actually not sure it does since the status never switches to PENDING_AUTHORIZATION, it just starts there when created if needed, so there's no explicit time for a new webhook event here afaict.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hm we do send webhooks for PENDING so it might be nice for consistency

just to clarify my understanding
for realtime funding:

  • create quote -> PENDING_AUTHORIZATION
  • authorize quote -> PENDING (?)
  • fund quote -> PROCESSING

@jklein24 jklein24 Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah ok, if we do that already at creation, then I can add it! Yeah, that sequence looks correct for realtime-funded flows

| `EXPIRED` | Quote wasn't executed before expiry window |
| `PROCESSING` | Executing the quote after receiving funds |
| `COMPLETED` | Payout successfully reached the destination |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ type: string
enum:
- CREATED
- PENDING
- PENDING_AUTHORIZATION
- PROCESSING
- COMPLETED
- REJECTED
Expand All @@ -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 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 |
Expand Down
8 changes: 8 additions & 0 deletions openapi/components/schemas/transfers/TransferOutRequest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ properties:
remittanceInformation field, and for wires it populates the OBI
(Originator to Beneficiary Information) / beneficiary information.
example: '12345'
scaFactor:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess we're doing this until we deprecate it for quote? Or should we totally skip it since transfer out will be deprecated soon anyway?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Short term I think we need it even if things get routed through the quotes flows internally. @shreyav wdyt?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ahh sorry missed this comment! replied on the other thread, I don't have a strong preference if its not too much work, but we do plan to mark this deprecated very soon

$ref: ../sca/ScaFactor.yaml
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`.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ allOf:
type: string
enum:
- OUTGOING_PAYMENT.PENDING
- OUTGOING_PAYMENT.PENDING_AUTHORIZATION
- OUTGOING_PAYMENT.PROCESSING
- OUTGOING_PAYMENT.COMPLETED
- OUTGOING_PAYMENT.FAILED
Expand Down
8 changes: 8 additions & 0 deletions openapi/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions openapi/paths/quotes/quotes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ post:
exchangeRate: 0.92
feesIncluded: 10
transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000005
'202':
Comment thread
jklein24 marked this conversation as resolved.
Comment thread
jklein24 marked this conversation as resolved.
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:
Expand Down
87 changes: 87 additions & 0 deletions openapi/paths/quotes/quotes_{quoteId}_authorize.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what's the use case for both a /authorize end point and /execute with SCA object?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See the paragraph below. Specifically this is needed for realtime-funded quotes, which don't use the execute endpoint. IMO it's good to have these as separate explicit actions for that purpose to avoid confusion.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

damn is real time funding not enough of a secure customer authentication

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

EU gonna EU

@pengying pengying Jul 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just double checking to make sure that I understand the flow - lets say it's a withdrawal flow like Loot rush of external EURC account > EUR. All managed by Lootrush for their end customers.

In this case since
1. customer adds the beneficiary EUR bank account account
2. move the funds from the customer EURC wallet to fund the quote
3. use the real time funded quote mechanism to convert and offramp

And they need to SCA three times?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Realtime quotes combine steps 2 and 3 there. Currently that would just require 2 SCAs for the swap and then withdraw. We can get it down to just one though. See this discussion for the "swap and send" flow". https://lightsparkgroup.slack.com/archives/C09H6AEERDF/p1783576868751969

`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.

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 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
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: >-
SCA is not required for this customer, 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 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
Comment thread
jklein24 marked this conversation as resolved.
Loading
Loading