diff --git a/mintlify/docs.json b/mintlify/docs.json
index f74c8476f..ab8eec45b 100644
--- a/mintlify/docs.json
+++ b/mintlify/docs.json
@@ -58,6 +58,17 @@
"platform-overview/core-concepts/currencies-and-rails",
"platform-overview/configuration"
]
+ },
+ {
+ "group": "Strong Customer Authentication",
+ "pages": [
+ "platform-overview/sca/overview",
+ "platform-overview/sca/per-transaction-authorization",
+ "platform-overview/sca/factor-enrollment",
+ "platform-overview/sca/login-and-sessions",
+ "platform-overview/sca/trusted-beneficiaries",
+ "platform-overview/sca/two-factor-reset"
+ ]
}
]
},
diff --git a/mintlify/global-p2p/sending-receiving-payments/sending-payments.mdx b/mintlify/global-p2p/sending-receiving-payments/sending-payments.mdx
index d8e1b6b9d..3197247e0 100644
--- a/mintlify/global-p2p/sending-receiving-payments/sending-payments.mdx
+++ b/mintlify/global-p2p/sending-receiving-payments/sending-payments.mdx
@@ -27,4 +27,13 @@ import SendUMA from '/snippets/sending/uma.mdx'
+## Strong Customer Authentication (EU customers)
+
+Customers in SCA-regulated regions (in practice the EU: EUR / USDC) must confirm
+payments with Strong Customer Authentication. When it applies, the payment comes
+back `PENDING_AUTHORIZATION` with an `scaChallenge` that you authorize before the
+transfer is released; for every other customer nothing changes. See
+[Per-transaction authorization](/platform-overview/sca/per-transaction-authorization)
+for the full walkthrough.
+
diff --git a/mintlify/payouts-and-b2b/payment-flow/send-payment.mdx b/mintlify/payouts-and-b2b/payment-flow/send-payment.mdx
index 548095f84..689b824f8 100644
--- a/mintlify/payouts-and-b2b/payment-flow/send-payment.mdx
+++ b/mintlify/payouts-and-b2b/payment-flow/send-payment.mdx
@@ -362,6 +362,15 @@ If a transaction fails, Grid initiates a refund automatically. You'll receive `O
| `FAILED` | Transfer failed — refund initiated automatically (track via `refund` object) |
| `EXPIRED` | Quote expired without execution |
+## Strong Customer Authentication (EU customers)
+
+Customers in SCA-regulated regions (in practice the EU: EUR / USDC) must confirm
+payments with Strong Customer Authentication. When it applies, the payment comes
+back `PENDING_AUTHORIZATION` with an `scaChallenge` that you authorize before the
+transfer is released; for every other customer nothing changes. See
+[Per-transaction authorization](/platform-overview/sca/per-transaction-authorization)
+for the full walkthrough.
+
## Checking Payment Status
Configure a webhook endpoint to receive real-time notifications when payment status changes:
diff --git a/mintlify/platform-overview/sca/factor-enrollment.mdx b/mintlify/platform-overview/sca/factor-enrollment.mdx
new file mode 100644
index 000000000..a890e0395
--- /dev/null
+++ b/mintlify/platform-overview/sca/factor-enrollment.mdx
@@ -0,0 +1,145 @@
+---
+icon: "/images/icons/key2.svg"
+title: "Factor enrollment"
+description: "Enroll and manage a customer's TOTP and passkey factors"
+"og:image": "/images/og/og-get-started.png"
+---
+
+
+Applies only to customers in an SCA-required region (EU). Every endpoint here
+returns **`409`** for other customers.
+
+
+`SMS_OTP` needs no enrollment; a code is sent to the customer's verified phone.
+`TOTP` and `PASSKEY` must be enrolled before a customer can authenticate with
+them. Enrolled factors then appear in `scaChallenge.availableFactors` and can be
+requested per transaction (see
+[per-transaction authorization](/platform-overview/sca/per-transaction-authorization)).
+
+Enrollment is two calls, both discriminated by a `type` field (`TOTP` or
+`PASSKEY`) — the same shape the [login and session](/platform-overview/sca/login-and-sessions)
+endpoints use:
+
+- `POST /sca/factors` — start enrollment; returns the factor-specific material.
+- `POST /sca/factors/confirm` — finish enrollment with the factor-specific proof.
+
+All paths below are relative to `https://api.lightspark.com/grid/2025-10-13`.
+
+## Enroll a TOTP authenticator
+
+
+
+
+```bash
+POST /sca/factors?customerId={customerId}
+
+{ "type": "TOTP" }
+```
+
+Returns the shared secret and an `otpauth://` provisioning URI. Render `totpUri`
+as a QR code (or show `secretBase32Encoded` for manual entry) so the customer can
+add it to their authenticator app.
+
+```json
+{
+ "type": "TOTP",
+ "secret": "…",
+ "secretBase32Encoded": "ABC123…",
+ "totpUri": "otpauth://totp/Grid:customer@example.com?secret=ABC123&issuer=Grid"
+}
+```
+
+
+
+Submit the `secret` from the start call plus the first code the app produces.
+Grid returns one-time **recovery codes**; surface them to the customer once and
+don't store them server-side.
+
+```bash
+POST /sca/factors/confirm?customerId={customerId}
+
+{ "type": "TOTP", "secret": "…", "code": "123456" }
+```
+
+```json
+{ "type": "TOTP", "recoveryCodes": ["ABCD-EFGH-IJKL", "MNOP-QRST-UVWX"] }
+```
+
+A wrong or expired code returns `400`. **In sandbox, the code is always
+`123456`.**
+
+
+
+## Enroll a passkey
+
+Passkey enrollment is a standard WebAuthn registration ceremony. Grid issues the
+options, the customer's device produces the credential, and you hand it back.
+
+
+A customer may have **only one passkey**. If one is already enrolled, starting
+another returns `409` (`PASSKEY_ALREADY_ENROLLED`) — delete the existing passkey
+first (see below). `GET /sca/factors` therefore lists at most one passkey.
+
+
+
+
+
+```bash
+POST /sca/factors?customerId={customerId}
+
+{ "type": "PASSKEY" }
+```
+
+```json
+{
+ "type": "PASSKEY",
+ "options": { "…": "opaque WebAuthn PublicKeyCredentialCreationOptions" },
+ "allowedOrigins": ["https://app.example.com"],
+ "relyingPartyId": "app.example.com"
+}
+```
+
+Pass `options` unmodified to the device's WebAuthn API
+(`navigator.credentials.create`). The ceremony must run against one of
+`allowedOrigins`.
+
+
+Submit the credential the device produced and the `origin` it ran against.
+
+```bash
+POST /sca/factors/confirm?customerId={customerId}
+
+{ "type": "PASSKEY", "origin": "https://app.example.com", "credential": { "…": "opaque WebAuthn credential" } }
+```
+
+Returns the enrolled `factor` (an `ScaFactorView`, including the `credentialId`
+you'll use to delete it later). An invalid credential or origin returns `400`.
+
+
+
+## List enrolled factors
+
+```bash
+GET /sca/factors?customerId={customerId}
+```
+
+```json
+{
+ "factors": [
+ { "factor": "TOTP", "name": "Authenticator app" },
+ { "factor": "PASSKEY", "credentialId": "…", "name": "iPhone" }
+ ]
+}
+```
+
+`credentialId` is populated only for `PASSKEY` factors.
+
+## Delete a factor
+
+```bash
+DELETE /sca/factors/{credentialId}?customerId={customerId}
+```
+
+Returns `204`. Use the `credentialId` from the factor list (or the confirm
+response). Today only passkeys carry a `credentialId`, so this is how you remove
+an enrolled passkey.
diff --git a/mintlify/platform-overview/sca/login-and-sessions.mdx b/mintlify/platform-overview/sca/login-and-sessions.mdx
new file mode 100644
index 000000000..b786f531c
--- /dev/null
+++ b/mintlify/platform-overview/sca/login-and-sessions.mdx
@@ -0,0 +1,103 @@
+---
+icon: "/images/icons/shield.svg"
+title: "Login & sessions"
+description: "The end-user SCA login and the session it grants"
+"og:image": "/images/og/og-get-started.png"
+---
+
+
+Applies only to customers in an SCA-required region (EU). Every endpoint here
+returns **`409`** for other customers.
+
+
+Per-transaction authorization gates individual debits. The **SCA login** is
+separate: it authenticates the end user to open a longer-lived session that
+covers reads and account access beyond the per-transaction window. Grid provides
+the login plumbing; your application decides when to drive it (for example, when
+a customer opens their account and the previous session has lapsed).
+
+
+**A customer's EUR / USDC accounts aren't provisioned until their first SCA
+login after KYC approval.** Provisioning is deferred from KYC-approval time to
+the first login that opens a valid SCA session, so a freshly KYC-approved
+customer's EUR / USDC accounts won't appear in `GET /customers/internal-accounts`
+until then. Expect those accounts to be unavailable, and drive the SCA login
+once KYC is approved, before relying on them.
+
+
+All paths below are relative to `https://api.lightspark.com/grid/2025-10-13`.
+
+## Logging in
+
+
+
+
+```bash
+POST /sca/login/start?customerId={customerId}
+
+{ "factor": "SMS_OTP" }
+```
+
+The response carries only what the chosen factor needs:
+
+- **`SMS_OTP`**: a code is dispatched; you get back `challengeId` and `expiresAt`.
+- **`TOTP`**: nothing extra; the customer reads the code from their app.
+- **`PASSKEY`**: WebAuthn `passkeyOptions` (with `allowedOrigins` and `relyingPartyId`) to pass to the device.
+
+The factor must already be enrolled (or, for `SMS_OTP`, the phone verified). See
+[factor enrollment](/platform-overview/sca/factor-enrollment).
+
+
+Submit the proof for the factor you started with: `code` for `SMS_OTP` / `TOTP`
+(echoing `challengeId` for `SMS_OTP`), or `passkeyAssertion` + `origin` for
+`PASSKEY`.
+
+```bash
+POST /sca/login/complete?customerId={customerId}
+
+{ "factor": "SMS_OTP", "challengeId": "…", "code": "123456" }
+```
+
+```json
+{ "status": "SUCCESS" }
+```
+
+A `status` of `SUCCESS` means the session is open. Any other value means the
+login did not complete; the field is passed through verbatim, so treat only
+`SUCCESS` as success. An invalid or expired proof returns `400`. **In sandbox,
+the code is always `123456`.**
+
+
+
+## Account-security signals
+
+Grid runs an adaptive-authentication risk engine that maintains each customer's
+login-security state. Because your application owns the customer's login, you
+report the security-relevant events it sees so the engine can act on them:
+
+```bash
+POST /sca/record-event?customerId={customerId}
+
+{ "eventType": "FAILED_LOGIN_ATTEMPT" }
+```
+
+Returns the customer's resulting login-security state so you can surface a
+lockout — `{ eventType, suspended, lockedUntil, failedAttempts }`. When the
+customer is locked out, this (and `POST /sca/login/complete`) returns `423` with
+`details.lockedUntil` (when they may retry) and `details.failedAttempts`.
+`eventType` must be one of:
+
+| `eventType` | Effect |
+|---|---|
+| `FAILED_LOGIN_ATTEMPT` | Increments the failed-login counter and escalates a lockout: **5 → 15 min, 6 → 30 min, 7 → 1 hour, 8 → 24 hours, 9 or more → suspension.** |
+| `RESET_PASSWORD_COMPLETED` | Revokes every active SCA session for the customer and clears the failed-login counter. |
+
+Report `FAILED_LOGIN_ATTEMPT` on each failed sign-in and `RESET_PASSWORD_COMPLETED`
+once a password recovery finishes. Any other value returns `400`.
+
+
+The failed-login counter is cumulative and is **not** reset by a successful
+login; only `RESET_PASSWORD_COMPLETED` clears it. Record that event after a
+password recovery to zero the counter and lift a lockout, rather than relying on
+the customer simply logging in again.
+
diff --git a/mintlify/platform-overview/sca/overview.mdx b/mintlify/platform-overview/sca/overview.mdx
new file mode 100644
index 000000000..eaa8f30fd
--- /dev/null
+++ b/mintlify/platform-overview/sca/overview.mdx
@@ -0,0 +1,131 @@
+---
+icon: "/images/icons/shield.svg"
+title: "Strong Customer Authentication"
+description: "How Grid satisfies PSD2 SCA for EU customers"
+"og:image": "/images/og/og-get-started.png"
+---
+
+
+**This applies only to customers in a region where Strong Customer Authentication
+is required, in practice customers in the EU (EUR / USDC).** For every other
+customer none of this appears: money-movement calls complete as usual, no
+`scaChallenge` is returned, the authentication endpoints return `409`, and you
+can skip this section.
+
+
+Under PSD2, EU e-money and e-money-token (EUR / USDC) money movement must be
+confirmed by the end user with Strong Customer Authentication (SCA). Grid wraps
+SCA so you satisfy it through the same resources you already use. There is no
+separate product to integrate, and the same request shapes work for every
+customer whether or not SCA applies.
+
+This section covers the whole surface:
+
+
+
+ Authorize a money movement that came back `PENDING_AUTHORIZATION`. This is the flow you hit most often.
+
+
+ Enroll and manage a customer's TOTP and passkey factors.
+
+
+ The end-user SCA login and the session it grants for reads.
+
+
+ Whitelist a payee once so future sends to it skip the per-transaction ceremony.
+
+
+ Recover a customer who has lost their factors, gated by an identity (liveness) check.
+
+
+
+## What SCA covers
+
+SCA gates **debits** on EU-regulated balances. Reads and non-EUR/USDC accounts
+are never gated. "Dynamic linking" means the authorization is cryptographically
+bound to the transaction's amount and payee (PSD2 Article 97(2)); it forces a
+fresh, transaction-specific challenge, and it's the reason some flows can't use
+TOTP (see [Authentication factors](#authentication-factors)).
+
+| Operation | SCA required? | Dynamically linked? |
+|---|---|---|
+| Send EUR / USDC (SEPA + intra-ledger) | Yes | Yes |
+| Convert **from** EUR / USDC (the swap leg) | Yes | Yes |
+| On-chain / Lightning withdrawal | Yes | No |
+| Trust / untrust a beneficiary | Yes | No |
+| Send to an **already-trusted** beneficiary | Lighter, no dynamic linking | No |
+| Reading balances / history | Covered by the login session | n/a |
+| Non-EUR/USDC accounts (e.g. USD) | No | n/a |
+
+## Authentication factors
+
+The `scaChallenge.availableFactors` field tells you which factors a customer may
+use; `scaChallenge.factor` is the one in use (default `SMS_OTP`).
+
+| Factor | Enrollment | Per-transaction debit |
+|--------|-----------|-----------------------|
+| `SMS_OTP` | None; a code is sent to the customer's verified phone | ✅ Default |
+| `PASSKEY` | Required (WebAuthn credential) | ✅ |
+| `TOTP` | Required (authenticator app) | ❌ Not permitted (a TOTP code can't be dynamically linked to the amount and payee) |
+
+TOTP is barred from dynamically-linked debits because an authenticator code is
+derived only from a clock and a shared secret, so it can't be bound to *this*
+amount and payee. It stays valid for flows that don't require dynamic linking:
+login, trusting a beneficiary, and sends to an already-trusted beneficiary.
+
+## The authorization flow
+
+For an SCA-required customer, a money-movement call that would otherwise complete
+instead returns the resource in status **`PENDING_AUTHORIZATION`** carrying an
+**`scaChallenge`**, and the transfer is not released until the challenge is
+satisfied. A single money movement can require **more than one** challenge in
+sequence, so loop on status rather than assuming one authorization releases the
+transfer.
+
+```mermaid
+sequenceDiagram
+ participant P as Your platform
+ participant G as Grid
+ participant U as End user
+
+ P->>G: Initiate money movement (execute / transfer-out)
+ G-->>P: PENDING_AUTHORIZATION + scaChallenge
+ G->>U: Deliver challenge (e.g. SMS OTP)
+ loop while status == PENDING_AUTHORIZATION
+ U-->>P: Provide proof (code / passkey assertion)
+ P->>G: POST .../authorize (scaChallenge.id + proof)
+ G-->>P: Updated resource + next scaChallenge (if any)
+ end
+ G-->>P: Resource leaves PENDING_AUTHORIZATION, transfer released
+```
+
+[Per-transaction authorization](/platform-overview/sca/per-transaction-authorization)
+covers the mechanics: authorizing the quote, the multi-step loop, resending an
+expired code, and the realtime-funding-quote nuance.
+
+## Lifetimes & limits
+
+| Aspect | Behavior |
+|---|---|
+| Challenge expiry | Each challenge carries an absolute `scaChallenge.expiresAt` (UTC). After it, the challenge can no longer be authorized. |
+| Resend | `SMS_OTP` only. Resending reuses the existing challenge and does not extend `expiresAt`; `PASSKEY` (and `TOTP`) codes can't be resent. |
+| Repeated failures | Too many failed authorizations may invalidate the challenge and return `429 RATE_LIMITED`, so honor `Retry-After`. |
+| Login session | A completed [SCA login](/platform-overview/sca/login-and-sessions) grants a session that covers reads and account access beyond the per-transaction window; when it lapses the customer logs in again. |
+| Account lockout | Repeated `FAILED_LOGIN_ATTEMPT` signals escalate a lockout (5 → 15 min, 6 → 30 min, 7 → 1 hour, 8 → 24 hours, 9+ → suspension). See [account-security signals](/platform-overview/sca/login-and-sessions#account-security-signals). |
+| 2FA reset window | A started reset carries its own `expiresAt`; complete it before then. |
+
+## Errors you'll encounter
+
+| Status | Meaning | What to do |
+|---|---|---|
+| `400` | Invalid or expired proof: wrong code, expired challenge, or a factor that can't satisfy this challenge (e.g. `TOTP` on a dynamically-linked debit). | Re-collect the proof. If the code lapsed, resend (`SMS_OTP`) or start over. |
+| `409` | SCA isn't required for this customer (non-EU), there's no pending challenge, or the factor's code can't be resent (e.g. `PASSKEY`). | Don't retry the same call. Treat a non-EU `409` as nothing to authorize. |
+| `429` | `RATE_LIMITED`: too many attempts or resends, and the challenge may now be invalidated. | Honor `Retry-After`; you may need to restart the flow. |
+| `404` | The customer, transaction, quote, external account, or reset wasn't found. | Check the id. |
+
+## Calling a customer outside SCA-regulated regions
+
+Every authentication endpoint returns **`409`** for customers outside
+SCA-regulated regions (non-EU), and no `scaChallenge` is ever attached to their
+transactions. You don't need to branch on region. Handle `scaChallenge` when it's
+present and treat its absence as nothing to do.
diff --git a/mintlify/platform-overview/sca/per-transaction-authorization.mdx b/mintlify/platform-overview/sca/per-transaction-authorization.mdx
new file mode 100644
index 000000000..665cd4045
--- /dev/null
+++ b/mintlify/platform-overview/sca/per-transaction-authorization.mdx
@@ -0,0 +1,74 @@
+---
+icon: "/images/icons/lock.svg"
+title: "Per-transaction authorization"
+description: "Authorize an SCA-gated money movement"
+"og:image": "/images/og/og-get-started.png"
+---
+
+This is the flow you hit most often: an SCA-required customer initiates a money
+movement, it comes back `PENDING_AUTHORIZATION` with an `scaChallenge`, and you
+authorize it before the transfer is released. For where this sits in the wider
+SCA surface, see the [overview](/platform-overview/sca/overview).
+
+import StrongCustomerAuthentication from '/snippets/sca/strong-customer-authentication.mdx';
+
+
+
+## Walkthrough by flow
+
+The mechanics above are the same everywhere: inspect for an `scaChallenge`,
+submit an `ScaAuthorization`, and repeat until the resource leaves
+`PENDING_AUTHORIZATION`. What differs between flows is *which* call first returns
+the challenge and *which* resource you authorize. Here is each one end to end.
+
+
+
+The common case — lock a quote, execute it, authorize the quote.
+
+
+
+`POST /quotes` returns a quote as usual. A standard (prefunded) send carries no
+challenge at quote time.
+
+
+`POST /quotes/{quoteId}/execute` returns the **quote** in `PENDING_AUTHORIZATION`
+with an `scaChallenge`.
+
+
+`POST /quotes/{quoteId}/authorize` with the proof. A pre-funded send is a single
+challenge — one authorization releases it (the currency-conversion leg of a
+cross-currency send is not separately gated).
+
+
+
+
+
+Here the challenge is issued at *quote* time and `paymentInstructions` are
+withheld until you clear it — you authorize before you fund.
+
+
+
+`POST /quotes` for a realtime-funded send returns `202` /
+`PENDING_AUTHORIZATION` with an `scaChallenge`. `paymentInstructions` are
+**omitted** from this response.
+
+
+`POST /quotes/{quoteId}/authorize` with the proof — the challenge is carried by
+the quote, and a single authorization clears it.
+
+
+Read `paymentInstructions` from the returned (advanced) quote and fund the
+transfer. Reading them off the initial pending response would show the customer
+nothing to fund.
+
+
+
+
+
+`transfer-out` has no associated quote, and per-transaction SCA is authorized
+only on the quote resource. `transfer-out` is being deprecated, so SCA-gated EU
+debits are **not** offered on this endpoint — use the quote + `execute` flow
+above for EU customers. For customers outside SCA-regulated regions,
+`transfer-out` proceeds as usual.
+
+
diff --git a/mintlify/platform-overview/sca/trusted-beneficiaries.mdx b/mintlify/platform-overview/sca/trusted-beneficiaries.mdx
new file mode 100644
index 000000000..514956cfe
--- /dev/null
+++ b/mintlify/platform-overview/sca/trusted-beneficiaries.mdx
@@ -0,0 +1,92 @@
+---
+icon: "/images/icons/checkmark1.svg"
+title: "Trusted beneficiaries"
+description: "Whitelist a payee so future sends skip the per-transaction ceremony"
+"og:image": "/images/og/og-get-started.png"
+---
+
+
+Applies only to customers in an SCA-required region (EU). Every endpoint here
+returns **`409`** for other customers.
+
+
+Trusting a beneficiary is a one-time, SCA-gated step that whitelists an external
+account. Only **USDC addresses** can be trusted today. Once trusted, future sends
+to that payee are no longer dynamically linked; they drop to a lighter
+authentication instead of a full per-transaction challenge. Use it for recurring
+payouts to known destinations.
+
+The beneficiary is identified end-to-end by its **`externalAccountId`** in the
+path, so there is no separate whitelist handle to track. All paths below are
+relative to `https://api.lightspark.com/grid/2025-10-13`.
+
+## Trusting a beneficiary
+
+
+
+```bash
+POST /customers/external-accounts/{externalAccountId}/trust
+```
+
+Returns the `scaChallenge` to satisfy:
+
+```json
+{ "scaChallenge": { "id": "…", "factor": "SMS_OTP", "expiresAt": "2025-10-03T12:05:00Z", "availableFactors": ["SMS_OTP"] } }
+```
+
+
+`scaChallenge` may be **omitted** when no challenge is issued. In that case,
+confirm directly without a `challengeId`.
+
+
+
+Submit the SCA proof (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` +
+`origin` for `PASSKEY`), echoing `challengeId` when start issued one.
+
+```bash
+POST /customers/external-accounts/{externalAccountId}/trust/confirm
+
+{ "challengeId": "…", "code": "123456" }
+```
+
+```json
+{ "trusted": true }
+```
+
+An invalid or expired proof returns `400`. **In sandbox, the code is always
+`123456`.**
+
+
+
+## Untrusting a beneficiary
+
+Untrusting mirrors trusting: a start call issues the challenge, then confirm
+submits the proof.
+
+
+
+
+```bash
+POST /customers/external-accounts/{externalAccountId}/untrust
+```
+
+Returns the `scaChallenge` to satisfy, omitted when no challenge is issued (the
+caller then confirms without a `challengeId`).
+
+
+
+Submit the SCA proof (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` +
+`origin` for `PASSKEY`), echoing `challengeId` when start issued one. Returns
+`trusted: false`.
+
+```bash
+POST /customers/external-accounts/{externalAccountId}/untrust/confirm
+
+{ "challengeId": "…", "code": "123456" }
+```
+
+
+
+
+Once untrusted, sends to that beneficiary are dynamically linked again and each
+one requires a full per-transaction challenge.
diff --git a/mintlify/platform-overview/sca/two-factor-reset.mdx b/mintlify/platform-overview/sca/two-factor-reset.mdx
new file mode 100644
index 000000000..0a7be1861
--- /dev/null
+++ b/mintlify/platform-overview/sca/two-factor-reset.mdx
@@ -0,0 +1,82 @@
+---
+icon: "/images/icons/key2.svg"
+title: "Two-factor reset"
+description: "Recover a customer who has lost their SCA factor"
+"og:image": "/images/og/og-get-started.png"
+---
+
+
+Applies only to customers in an SCA-required region (EU). Every endpoint here
+returns **`409`** for other customers.
+
+
+When a customer loses an enrolled factor (a new phone, a deleted authenticator),
+they recover it with a **2FA reset**: an identity (liveness) check that, once
+passed, clears the lost factor so they can re-enroll it. It's a poll-based flow:
+start, poll until liveness passes, then complete.
+
+All paths below are relative to `https://api.lightspark.com/grid/2025-10-13`.
+
+
+
+
+```bash
+POST /sca/factors/reset?customerId={customerId}
+
+{ "factor": "TOTP" }
+```
+
+Returns **`201`** with a `resetId` and opaque liveness handles. Embed
+`livenessAccessToken` in the verification SDK, or send the customer to
+`verificationLink`. `expiresAt` bounds the reset window. Reset initiation is
+rate-limited to **5 per 24 hours** per customer; beyond that this returns `429`.
+
+```json
+{
+ "resetId": "…",
+ "livenessAccessToken": "…",
+ "verificationLink": "https://…",
+ "expiresAt": "2025-10-03T12:30:00Z"
+}
+```
+
+
+
+```bash
+GET /sca/factors/reset/{resetId}?customerId={customerId}
+```
+
+```json
+{ "status": "INITIATED" }
+```
+
+Poll with a short backoff. `status` is one of `INITIATED`, `PENDING_REVIEW`,
+`LIVENESS_PASSED`, `COMPLETED`, `REJECTED`, or `EXPIRED`:
+
+- `INITIATED` — reset started, liveness not yet submitted; keep polling.
+- `PENDING_REVIEW` — liveness submitted and under review; keep polling.
+- `LIVENESS_PASSED` — proceed to complete.
+- `COMPLETED` (reset finished, factor cleared), `REJECTED` (liveness failed), and
+ `EXPIRED` (window closed) are **terminal** — stop polling. On `REJECTED` or
+ `EXPIRED`, start a new reset.
+
+The response also carries `factor`, `enrollmentStatus` (`PENDING` until the
+replacement factor is re-enrolled, then `COMPLETED`; `null` for an `SMS_OTP`
+reset), `expiresAt` (the window bound), and `completedAt`. Stop at any terminal
+status or once `expiresAt` passes; never poll indefinitely.
+
+
+
+```bash
+POST /sca/factors/reset/{resetId}/complete?customerId={customerId}
+
+{ "mobile": { "countryCode": "+1", "number": "4155550123" } }
+```
+
+Returns `204` and clears the lost factor. For an `SMS_OTP` reset, include the new
+`mobile` number in the body — it's enrolled as the customer completes the reset;
+other factors need no body. Calling it before liveness has passed returns `400`.
+The customer can then re-enroll via
+[factor enrollment](/platform-overview/sca/factor-enrollment).
+
+
diff --git a/mintlify/ramps/conversion-flows/fiat-crypto-conversion.mdx b/mintlify/ramps/conversion-flows/fiat-crypto-conversion.mdx
index d4a4c56b0..fd3b8ed79 100644
--- a/mintlify/ramps/conversion-flows/fiat-crypto-conversion.mdx
+++ b/mintlify/ramps/conversion-flows/fiat-crypto-conversion.mdx
@@ -112,6 +112,14 @@ curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
```javascript
function displayPaymentInstructions(quote) {
+ // EU customers only: an SCA-gated realtime-funding quote comes back
+ // PENDING_AUTHORIZATION with paymentInstructions withheld until its
+ // scaChallenge is authorized (see "Strong Customer Authentication" below).
+ // Authorize first, then display — don't read paymentInstructions while pending.
+ if (!quote.paymentInstructions?.length) {
+ return { pendingAuthorization: true, scaChallenge: quote.scaChallenge };
+ }
+
const instructions = quote.paymentInstructions[0];
return {
@@ -233,6 +241,16 @@ curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
`immediatelyExecute` can only be used with sources that are either internal accounts or external accounts with direct pull functionality (e.g., ACH pull).
+## Strong Customer Authentication (EU customers)
+
+Customers in SCA-regulated regions (in practice the EU: EUR / USDC) must confirm
+money movements with Strong Customer Authentication, and a conversion out of
+EUR / USDC is gated on its swap leg. When it applies, the transaction comes back
+`PENDING_AUTHORIZATION` with an `scaChallenge` that you authorize before the
+conversion is released; for every other customer nothing changes. See
+[Per-transaction authorization](/platform-overview/sca/per-transaction-authorization)
+for the full walkthrough.
+
## Best practices
diff --git a/mintlify/snippets/sca/strong-customer-authentication.mdx b/mintlify/snippets/sca/strong-customer-authentication.mdx
new file mode 100644
index 000000000..4ec734d80
--- /dev/null
+++ b/mintlify/snippets/sca/strong-customer-authentication.mdx
@@ -0,0 +1,104 @@
+
+**This applies only to customers in a region where Strong Customer Authentication
+is required, in practice customers in the EU (EUR / USDC).** For every other
+customer, none of this appears: money-movement calls complete as usual, no
+`scaChallenge` is returned, and the authorization endpoints are not used. If you
+don't serve EU customers you can skip this section.
+
+
+Under PSD2, EU e-money and e-money-token (EUR / USDC) money movement must be
+confirmed by the end user with Strong Customer Authentication (SCA). Grid wraps
+SCA so you satisfy it through the same resources you already use. There is no
+separate SCA product to integrate.
+
+### When you'll encounter it
+
+For an SCA-required customer, a money-movement call that would otherwise complete
+instead returns the transaction (or quote) in status **`PENDING_AUTHORIZATION`**
+with an **`scaChallenge`** object, and the transfer is **not** released until the
+challenge is satisfied. This affects debits such as:
+
+- Sending EUR / USDC (SEPA and intra-ledger transfers)
+- Cross-currency conversions from EUR / USDC (the swap leg)
+- On-chain and Lightning withdrawals
+
+Reads and non-EUR/USDC accounts are not gated this way.
+
+### Authentication factors
+
+The `scaChallenge.availableFactors` field tells you which factors the customer
+may use. `scaChallenge.factor` is the one in use (default `SMS_OTP`).
+
+| Factor | Enrollment | Per-transaction debit |
+|--------|-----------|-----------------------|
+| `SMS_OTP` | None; a code is sent to the customer's verified phone | ✅ Default |
+| `PASSKEY` | Required (WebAuthn credential) | ✅ |
+| `TOTP` | Required (authenticator app) | ❌ Not permitted (a TOTP code can't be dynamically linked to the amount and payee) |
+
+Request a specific factor per transaction with the optional top-level `scaFactor`
+field on `execute` / `transfer-out` (`SMS_OTP` default, or `PASSKEY`).
+
+### Satisfying a challenge
+
+Submit an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` for the
+quote that carries the challenge. Provide exactly one of `code` (for `SMS_OTP`)
+or `passkeyAssertion` + `origin` (for `PASSKEY`):
+
+```bash
+curl -X POST https://api.lightspark.com/grid/2025-10-13/quotes/{quoteId}/authorize \
+ -u "$GRID_API_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{ "code": "123456" }'
+```
+
+
+**A pre-funded send is a single authorization today — but write your client to
+loop.** One `POST /quotes/{quoteId}/authorize` releases a pre-funded send; the
+currency-conversion leg of a cross-currency send is not separately gated. Even
+so, treat `scaChallenge` as the challenge to satisfy *now*, not necessarily the
+only one: after authorizing, re-inspect the returned quote, and if it is still
+`PENDING_AUTHORIZATION` it carries the **next** `scaChallenge` (a new `id`) —
+authorize that one too and repeat until it leaves `PENDING_AUTHORIZATION`. A
+client written to loop on status stays correct if a future flow adds steps.
+
+
+Once the quote is in `PENDING_AUTHORIZATION`, authorize it:
+`POST /quotes/{quoteId}/authorize`. This is the single authorize path for both
+`execute` (pre-funded) and realtime-funding quotes. The challenge — and the SMS
+code or passkey assertion that satisfies it — only exists after the challenge is
+issued, so the proof is always supplied on this follow-up call, never on the
+originating request.
+
+
+For a **realtime-funding quote**, the `202` / `PENDING_AUTHORIZATION` response
+**withholds `paymentInstructions`** until the challenge is authorized. Authorize
+first, then read `paymentInstructions` from the returned (advanced) quote. If you
+read them off the initial pending response you'll show the customer nothing to
+fund.
+
+
+If an SMS code lapses before it's used, re-send it. The existing challenge is
+reused, and its `expiresAt` is not extended. Use the quote resend endpoint:
+`POST /quotes/{quoteId}/authorize/resend`.
+
+```bash
+curl -X POST https://api.lightspark.com/grid/2025-10-13/quotes/{quoteId}/authorize/resend \
+ -u "$GRID_API_TOKEN"
+```
+
+
+In **sandbox**, the SMS code is always `123456`.
+
+
+### Reducing prompts for repeat payees
+
+Trusting a beneficiary (a one-time SCA-gated whitelisting step) lets subsequent
+sends to that payee skip the per-transaction challenge. Use this for recurring
+payouts to known destinations rather than authorizing every send.
+
+### Calling a customer outside SCA-regulated regions
+
+The authorization endpoints return **`409`** for customers outside SCA-regulated
+regions (non-EU), and no `scaChallenge` is ever attached to their transactions.
+You don't need to branch on region. Handle `scaChallenge` when it's present and
+treat its absence as nothing to do.