> ## Documentation Index
> Fetch the complete documentation index at: https://developers.circle.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Before reading these docs, install the relevant Circle Skill to get pre-loaded context — skills are LLM-optimized and cover architecture decisions, correct flows, and common pitfalls. In Claude Code: run /plugin marketplace add circlefin/skills then /plugin install circle-skills@circle. With Vercel Skills CLI: run npx skills add circlefin/skills. Available skills: use-usdc https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-usdc/SKILL.md (balances, transfers, approvals on EVM and Solana); EURC has no skill — see https://developers.circle.com/stablecoins/what-is-eurc.md; bridge-stablecoin https://github.com/circlefin/skills/blob/master/plugins/circle/skills/bridge-stablecoin/SKILL.md (crosschain transfers via CCTP, USDC, EURC, and third-party assets); use-arc https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-arc/SKILL.md (Arc chain config, contract deployment, USDC bridging — Arc docs: https://docs.arc.io/llms.txt); use-circle-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-circle-wallets/SKILL.md (choose wallet type); use-developer-controlled-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-developer-controlled-wallets/SKILL.md (custodial wallets for payouts and treasury); use-user-controlled-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-user-controlled-wallets/SKILL.md (embedded wallets with Web2 login); use-modular-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-modular-wallets/SKILL.md (smart contract wallets with passkeys and ERC-4337); use-gateway https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-gateway/SKILL.md (unified USDC balance, nanopayments); use-smart-contract-platform https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-smart-contract-platform/SKILL.md (deploy, interact, monitor contracts). Pair any skill with the Circle MCP server for live SDK signatures, contract addresses, and chain IDs: {"mcpServers":{"circle":{"url":"https://api.circle.com/v1/codegen/mcp"}}}.
> Read the product overview first. Each product has an overview explaining when and why to use it. Read it before quickstarts.
> Pick the right wallet type. Developer-controlled (you hold keys), user-controlled (end user holds keys via PIN/social login), or modular (smart contract wallets with passkeys). See https://developers.circle.com/wallets.md.
> Use CCTP V2 for crosschain transfers. CCTP natively transfers USDC and EURC via burn-and-mint, and supports permissionless wrapping of third-party assets via lock-and-mint. V1 is legacy; always use V2 unless V1 is specified or the chain requires it (Noble, Sui, and Aptos still require V1). Migration guide: https://developers.circle.com/cctp/migration-from-v1-to-v2.md.
> Use Bridge Kit for frontend bridging. Wraps CCTP with a simpler SDK for user-facing apps. Use CCTP directly for backend transfers.
> Gas Station vs Paymaster. Gas Station sponsors gas for Circle Wallet transactions. Paymaster lets users pay gas in USDC. Different use cases — read both overviews.
> Gateway Nanopayments for sub-cent payments. Gasless USDC micropayments down to $0.000001 via x402 and batched settlement. For pay-per-request APIs, AI agent payments, streaming.
> Look up USDC addresses per chain. Never hardcode — use https://developers.circle.com/stablecoins/usdc-contract-addresses.md.
> Prefer SDKs over raw API calls. Node.js and Python SDKs handle auth, retries, and errors.
> API key required. Bearer token in Authorization header. Testnet and mainnet use separate keys and may use different base URLs depending on the product.
> Set up webhooks when available. Most operations are async. Webhooks deliver transaction confirmations and state changes.
> When calling list endpoints, paginate using pageSize and pageAfter until no nextPageAfter cursor is returned—stopping at the first page silently misses records.
> Building an AI agent? Start with the Agent Stack—Circle CLI, agent wallets, and nanopayments built for autonomous use cases: https://developers.circle.com/agent-stack.md.

# How-to: Implement Strong Customer Authentication

> Check whether an end user requires SCA, enroll their passkey, and complete SCA-protected operations using the challenge–assertion flow.

Check whether an end user requires SCA, enroll a passkey for them, then obtain a
signed approval for each sensitive operation and submit it alongside the API
request. For what an approval proves and why the ceremony runs in a
Circle-hosted iframe, see
[How Strong Customer Authentication works](/digital-asset-accounts/concepts/strong-customer-authentication).

<Note>
  **Implement SCA if you serve end users in the European Economic Area (EEA).**
  Whether SCA applies depends on each end user, not on your platform. Check each
  end user, as described in step 1.
</Note>

The SCA ceremony runs in the browser. There is no iOS, Android, or React Native
SDK for it yet. Native applications must host the ceremony in a web view or the
system browser.

## Prerequisites

Before you begin, ensure that you've:

* Registered your application origin with Circle. Reach out to your support team
  with the exact origin (scheme + host, for example `https://app.example.com`).
  Registration is required separately for sandbox and production.
* Installed the [DAA web SDK](/sdks/daa/web-sdk).
* Obtained a `clientEntityId` for the end user you're enrolling. See
  [Onboard customers](/digital-asset-accounts/quickstarts/onboard-customers) for
  how to create one.
* Run a device check for the end user and stored the `deviceId` it returned. See
  [Collect device risk signals](/end-user-onboarding/howtos/collect-device-risk-signals).
  Every protected operation sends that `deviceId` in `riskSignals`.

<Warning>
  Reuse the `deviceId` returned by `checkDevice()`. Do not generate one. Circle
  resolves it against the completed device check, and an unrecognized value is
  **accepted and then declined**: the endpoint returns `201` and the transaction
  later settles as `failed`.
</Warning>

<Note>
  The Digital Asset Accounts API base URL is `https://api-sandbox.circle.com` for
  sandbox and `https://api.circle.com` for production. Set your API key in the
  `Authorization` header using the format `Bearer YOUR_API_KEY`. See
  [Sandbox environment](/digital-asset-accounts/references/testing-in-sandbox) and
  [Going to production](/digital-asset-accounts/references/going-to-production)
  for environment details.
</Note>

## Steps

### Step 1. Check whether the end user requires SCA

List the end user's passkeys with
[`GET /v1/accounts/passkeys`](/api-reference/digital-asset-accounts/all/list-passkeys).
The response carries both the SCA requirement and the passkeys already enrolled:

```bash theme={null}
curl --request GET \
  --url 'https://api-sandbox.circle.com/v1/accounts/passkeys?clientEntityId=${CLIENT_ENTITY_ID}' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer ${YOUR_API_KEY}'
```

**Response**

```json theme={null}
{
  "data": {
    "sca": {
      "required": true
    },
    "passkeys": [
      {
        "passkeyId": "a1b2c3d4-e5f6-7890-ab12-cdef34567890",
        "credentialId": "dGhpcyBpcyBhIGJhc2U2NHVybCBleGFtcGxl",
        "createDate": "2026-09-24T12:00:00Z",
        "revokedDate": null,
        "backupEligible": true,
        "backupState": true
      }
    ]
  }
}
```

The list includes revoked passkeys. An end user has an active passkey when at
least one entry has `revokedDate` set to `null`.

Use `sca.required` to decide what to do before a protected operation:

| `sca.required` | Before a protected operation |
| - | - |
| `true` | Approve the operation (step 3). If the end user has no active passkey, enroll one first (step 2). |
| `false` | Call the endpoint without SCA headers, or use SCA voluntarily. |

If `sca` is `null`, don't store it. Call the endpoint as usual and handle HTTP
428 if it's returned. Handle a 428 even when `sca.required` is `false`. For what
each value means, whether you can store it, and how voluntary SCA works, see
[Which end users require SCA](/digital-asset-accounts/concepts/strong-customer-authentication#which-end-users-require-sca).

### Step 2. Enroll a passkey

Enroll a passkey for each end user who requires SCA, before they run any
protected operation. Run this flow once per end user, and again if they revoke
their last active passkey or can't access it on a new device.

<Warning>
  If your application origin isn't registered, the request to create a
  registration session fails with HTTP 400 and error code 420064. If your page
  runs on an origin other than the one you registered, such as `localhost`, the
  ceremony fails. Register each origin, for sandbox and production, before
  running this flow.
</Warning>

#### Step 2.1. Create a registration session

```bash theme={null}
curl --request POST \
  --url https://api-sandbox.circle.com/v1/accounts/passkeys/registrations \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer ${YOUR_API_KEY}' \
  --header 'Content-Type: application/json' \
  --data '
{
  "clientEntityId": "${CLIENT_ENTITY_ID}",
  "idempotencyKey": "${RANDOM_UUID}"
}
'
```

**Response**

```json theme={null}
{
  "data": {
    "registrationId": "a1b2c3d4-e5f6-7890-ab12-cdef34567890",
    "frameToken": "k74ia-AcnTzXtBdxnbVqn1IBpVUXBbhGiGxQkGD386A",
    "expiresAt": "2026-09-24T12:10:00Z"
  }
}
```

<Note>
  The frame token expires after 10 minutes. Pass it to your frontend right away.
  Do not re-send the request with the same `idempotencyKey` after delivering the
  token. This rotates the token and invalidates the copy you already sent. Start
  a fresh session with a new `idempotencyKey` if the token expires.
</Note>

#### Step 2.2. Run the enrollment ceremony

Initialize the SDK on your frontend and call `sca.enroll` with the frame token
from step 2.1.

```typescript theme={null}
import { createScaClient } from "@circle-fin/daa-web-sdk";

const sca = createScaClient({ environment: "sandbox" }); // or 'production'

const attestationResponse = await sca.enroll(frameToken, {
  mode: "modal", // or 'inline' with a container element
});

// Forward attestationResponse to your backend unchanged; do not re-serialize
```

<Note>
  The SDK renders the passkey prompt in a Circle-hosted iframe. Pass
  `attestationResponse` to your backend as-is. Re-serializing it causes a
  verification error.
</Note>

#### Step 2.3. Complete the registration

Send the `attestationResponse` from the SDK to your backend. Then forward it to
Circle to complete enrollment.

```bash theme={null}
curl --request POST \
  --url https://api-sandbox.circle.com/v1/accounts/passkeys \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer ${YOUR_API_KEY}' \
  --header 'Content-Type: application/json' \
  --data '
{
  "registrationId": "${REGISTRATION_ID}",
  "attestationResponse": ${ATTESTATION_RESPONSE}
}
'
```

**Response**

```json theme={null}
{
  "data": {
    "passkeyId": "a1b2c3d4-e5f6-7890-ab12-cdef34567890",
    "credentialId": "dGhpcyBpcyBhIGJhc2U2NHVybCBleGFtcGxl"
  }
}
```

### Step 3. Approve a protected operation

For every protected operation, obtain a signed challenge and include it in the
API request.

#### Step 3.1. Create a challenge

```bash theme={null}
curl --request POST \
  --url https://api-sandbox.circle.com/v1/accounts/passkeys/challenges \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer ${YOUR_API_KEY}' \
  --header 'Content-Type: application/json' \
  --data '
{
  "clientEntityId": "${CLIENT_ENTITY_ID}",
  "operation": "TRANSFER",
  "intent": {
    "idempotencyKey": "${RANDOM_UUID}",
    "source": {
      "type": "account",
      "id": "${ACCOUNT_ID}"
    },
    "destination": {
      "type": "verified_blockchain",
      "addressId": "${RECIPIENT_ADDRESS_ID}"
    },
    "amount": {
      "amount": "50.00",
      "currency": "USD"
    },
    "riskSignals": {
      "ipAddress": "${END_USER_IP}",
      "sessionId": "${SESSION_ID}",
      "deviceId": "${DEVICE_ID}"
    }
  }
}
'
```

**Response**

```json theme={null}
{
  "data": {
    "challengeId": "b2c3d4e5-f6a7-8901-bc23-def456789012",
    "frameToken": "k74ia-AcnTzXtBdxnbVqn1IBpVUXBbhGiGxQkGD386A",
    "expiresAt": "2026-09-24T12:05:00Z",
    "summary": {
      "type": "transfer",
      "amount": { "amount": "50.00", "currency": "USD" },
      "destination": "1001587127",
      "sourceAccountLabel": "My DAA account"
    }
  }
}
```

The `summary` is a server-generated record of the operation. Don't use it to
render a pre-confirmation to the end user, because the Circle iframe shows its
own description to the user.

<Note>
  The frame token expires after 5 minutes. Pass it to your frontend right away.
  Re-sending with the same `idempotencyKey` rotates the token and invalidates
  the copy you already sent. Create a new challenge with a fresh
  `idempotencyKey` if the token expires.
</Note>

The intent must be identical to the operation request body: same fields, same
values. An extra or missing field causes an intent mismatch error (420047). For
the full list of intent shapes by operation, see the
[Create a challenge API reference](/api-reference/digital-asset-accounts/all/create-sca-challenge).

Set `operation` to match the endpoint you are about to call: `TRANSFER`,
`WITHDRAWAL`, `ADDRESS_BOOK_ADD`, `ADDRESS_BOOK_DELETE`, or
`WIRE_ACCOUNT_CREATE`. `ADDRESS_BOOK_DELETE` is the one exception. That endpoint
takes no request body, so send an empty `intent` and put the address ID in
`pathParameters`:

```json theme={null}
{
  "clientEntityId": "${CLIENT_ENTITY_ID}",
  "operation": "ADDRESS_BOOK_DELETE",
  "intent": {},
  "pathParameters": { "id": "${RECIPIENT_ADDRESS_ID}" }
}
```

<Warning>
  The `idempotencyKey` in the intent must match the `idempotencyKey` in the
  operation request body. A mismatch causes an intent mismatch error.
</Warning>

#### Step 3.2. Run the approval ceremony

Pass the `frameToken` from the challenge response to the SDK's `approve` method.
The SDK renders the passkey prompt and returns a signed assertion string.

```typescript theme={null}
import { createScaClient } from "@circle-fin/daa-web-sdk";

const sca = createScaClient({ environment: "sandbox" }); // or 'production'

const assertion = await sca.approve(frameToken, { mode: "modal" });

// Forward assertion to your backend as a string; do not re-serialize
```

#### Step 3.3. Submit the protected operation

Include the `challengeId` and `assertion` as request headers. Use the same field
values you set in the challenge intent.

```bash theme={null}
curl --request POST \
  --url https://api-sandbox.circle.com/v1/accounts/transfers \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer ${YOUR_API_KEY}' \
  --header 'Content-Type: application/json' \
  --header 'X-Sca-Challenge-Id: ${CHALLENGE_ID}' \
  --header 'X-Sca-Assertion: ${ASSERTION}' \
  --data '
{
  "idempotencyKey": "${RANDOM_UUID}",
  "source": {
    "type": "account",
    "id": "${ACCOUNT_ID}"
  },
  "destination": {
    "type": "verified_blockchain",
    "addressId": "${RECIPIENT_ADDRESS_ID}"
  },
  "amount": {
    "amount": "50.00",
    "currency": "USD"
  },
  "riskSignals": {
    "ipAddress": "${END_USER_IP}",
    "sessionId": "${SESSION_ID}",
    "deviceId": "${DEVICE_ID}"
  }
}
'
```

**Response**

```json theme={null}
{
  "data": {
    "id": "e1f2a3b4-c5d6-7890-ef12-3456789abcde",
    "status": "pending",
    "amount": {
      "amount": "50.00",
      "currency": "USD"
    },
    "source": {
      "type": "account",
      "id": "1017381855"
    },
    "destination": {
      "type": "verified_blockchain",
      "id": "c7d8e9f0-a1b2-3456-7890-abcdef123456"
    },
    "createDate": "2026-09-24T12:00:00Z",
    "updateDate": "2026-09-24T12:00:00Z"
  }
}
```

<Warning>
  `201` means the operation was accepted, not that it succeeded. A transfer is
  screened after it is created and can settle as `failed`. Poll `GET
      /v1/accounts/transfers/{id}` until `status` is terminal, and read `errorCode`
  and `riskEvaluation` on that response to see why, for example `errorCode:
      transfer_denied` with `riskEvaluation.decision: denied`. The list endpoint
  omits `riskEvaluation`, so fetch the transfer by id. A declined device check
  or an unrecognized `deviceId` surfaces here, not on the create call.
</Warning>

If the request was rejected before the transfer was created, the endpoint
returns an error instead of 201.

<Note>
  If a protected endpoint returns HTTP 428 (error code 420058), the
  `X-Sca-Challenge-Id` or `X-Sca-Assertion` header was omitted from the request.
  Circle requires SCA for this end user. If they have no active passkey, enroll
  one (step 2) first. Then run the approval ceremony and retry with both headers
  present. For other SCA errors, see the [Digital Asset Accounts API
  reference](/api-reference/digital-asset-accounts).
</Note>
