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

# Digital Asset Accounts Node.js SDK

> Reference for the server-side SDK that opens passkey Strong Customer Authentication ceremonies

The Node.js SDK runs on your backend and calls the Digital Asset Accounts
passkey API with your API key.

## What you can do

* **Open passkey ceremonies**: check whether an end customer requires
  [Strong Customer Authentication (SCA)](https://developers.circle.com/digital-asset-accounts/concepts/strong-customer-authentication.md),
  open passkey enrollments and approval challenges, and complete enrollment.
  Pair it with the [web SDK](https://developers.circle.com/sdks/daa/web-sdk.md), which runs the ceremony in the
  end customer's browser. See
  [How-to: Implement Strong Customer Authentication](https://developers.circle.com/digital-asset-accounts/howtos/strong-customer-authentication.md).
* **Manage passkeys**: list and revoke an end customer's passkeys.

The SDK wraps the passkey routes only. It doesn't send the operation that SCA
protects. Your backend sends that request itself, with the SCA headers.

For a ceremony in a native application, send `redirectUri` instead of
`embedOrigin`, and pass the frame token to the [iOS](https://developers.circle.com/sdks/daa/ios-sdk.md),
[Android](https://developers.circle.com/sdks/daa/android-sdk.md), or [React Native](https://developers.circle.com/sdks/daa/react-native-sdk.md)
SDK. `redirectUri`, `username`, and `displayName` need version 0.2.5 or later.

## Install

The SDK needs Node.js 22 or later. The package is ECMAScript modules (ESM) only,
so use `import`, not `require`.

```bash theme={null}
npm install @circle-fin/daa-node-sdk
```

## Initialize

```typescript theme={null}
import { createDaaClient, type DaaClient } from "@circle-fin/daa-node-sdk";

const apiKey: string | undefined = process.env.CIRCLE_API_KEY;
if (apiKey === undefined) throw new Error("CIRCLE_API_KEY is not set");

const daa: DaaClient = createDaaClient({ environment: "sandbox", apiKey });
```

| Option | Description |
| - | - |
| `environment` | `sandbox` or `production`. The web SDK uses the same value |
| `apiKey` | Your Circle API key. Keep it on your server |
| `euRestricted` | Set to `true` only if Circle tells you that your API key is EU-restricted. The SDK then calls `api-sandbox-eu.circle.com` or `api-eu.circle.com`. An EU-restricted key also needs a `transport` |
| `timeoutMs` | Request timeout for the built-in transport. Defaults to 30 seconds. If you pass a `transport`, set your own timeout in it |
| `transport` | Optional. Your own HTTP function. Use it to present a client certificate for mutual TLS |

An EU-restricted API key requires mutual TLS. The default transport uses Node's
`fetch`, which can't present a client certificate, so every call fails with a
`403`. Supply a `transport` that sends your certificate. This example uses
`undici`:

```bash theme={null}
npm install undici
```

Use the `fetch` that `undici` exports, not Node's built-in `fetch`. The built-in
`fetch` can reject an `Agent` from a different `undici` version.

```typescript theme={null}
import { readFileSync } from "node:fs";
import {
  DaaTransportError,
  type DaaRequest,
  type DaaResponse,
} from "@circle-fin/daa-node-sdk";
import { Agent, fetch } from "undici";

// Your client certificate and private key for mutual TLS.
const agent: Agent = new Agent({
  connect: {
    cert: readFileSync("client.crt"),
    key: readFileSync("client.key"),
  },
});

const euDaa: DaaClient = createDaaClient({
  environment: "sandbox",
  apiKey,
  euRestricted: true,
  transport: async ({
    method,
    url,
    headers,
    body,
  }: DaaRequest): Promise<DaaResponse> => {
    try {
      const res = await fetch(url, {
        method,
        headers,
        body,
        dispatcher: agent,
      });
      return { status: res.status, body: await res.text() };
    } catch (cause: unknown) {
      // Report network and TLS failures the way the built-in transport does.
      throw new DaaTransportError(`Request to ${url} failed`, { cause });
    }
  },
});
```

The transport receives your API key in its headers. Don't log them.

## Methods

`createDaaClient` returns a `DaaClient`. Its `passkeys` property is a
`PasskeysApi`, which has these methods:

| Method | Returns | Endpoint |
| - | - | - |
| `PasskeysApi.createRegistration(request)` | `RegistrationCreated` | [`POST /v1/accounts/passkeys/registrations`](https://developers.circle.com/api-reference/digital-asset-accounts/all/open-passkey-registration) |
| `PasskeysApi.complete(request)` | `PasskeyCreated` | [`POST /v1/accounts/passkeys`](https://developers.circle.com/api-reference/digital-asset-accounts/all/complete-passkey-registration) |
| `PasskeysApi.list(query)` | `PasskeyView[]` | [`GET /v1/accounts/passkeys`](https://developers.circle.com/api-reference/digital-asset-accounts/all/list-passkeys) |
| `PasskeysApi.status(query)` | `PasskeyStatus` | [`GET /v1/accounts/passkeys`](https://developers.circle.com/api-reference/digital-asset-accounts/all/list-passkeys) |
| `PasskeysApi.revoke(passkeyId, query)` | Nothing | [`DELETE /v1/accounts/passkeys/{passkeyId}`](https://developers.circle.com/api-reference/digital-asset-accounts/all/revoke-passkey) |
| `PasskeysApi.openChallenge(request)` | `ChallengeOpened` | [`POST /v1/accounts/passkeys/challenges`](https://developers.circle.com/api-reference/digital-asset-accounts/all/create-sca-challenge) |

## `daa.passkeys.createRegistration`

Opens a passkey enrollment ceremony for an end customer. Calls
[`POST /v1/accounts/passkeys/registrations`](https://developers.circle.com/api-reference/digital-asset-accounts/all/open-passkey-registration).

```typescript theme={null}
import { randomUUID } from "node:crypto";
import type { RegistrationCreated } from "@circle-fin/daa-node-sdk";

const { registrationId, frameToken }: RegistrationCreated =
  await daa.passkeys.createRegistration({
    clientEntityId,
    idempotencyKey: randomUUID(),
    embedOrigin: "https://app.example.com",
  });
```

### Parameters

| Parameter | Type | Required | Description |
| - | - | - | - |
| `clientEntityId` | `string` | Yes | The end customer. Circle checks that the end customer belongs to your entity |
| `idempotencyKey` | `string` | Yes | A UUID v4. A replay returns the same `registrationId`, but it issues a new `frameToken` and invalidates the earlier one |
| `embedOrigin` | `string` | No | The origin of the page that runs the ceremony, such as `https://app.example.com`. It must match an origin that you registered in Circle Hub. Send it on every web call. Leave it out when you send `redirectUri` |
| `redirectUri` | `string` | No | For a native application, your registered `https` app callback URI, exactly as registered. Circle returns 420068 for any other value |
| `username` | `string` | No | A name for the passkey that the end customer's device shows and keeps, and can sync across their devices. Use a masked identifier, not a full email address or phone number. Up to 64 bytes. If you leave it out, Circle uses the end customer's full email address on file, or their `clientEntityId`. A replay with the same `idempotencyKey` must send the same `username` and `displayName` |
| `displayName` | `string` | No | A friendlier name for the passkey, such as `Jo at Acme`. Defaults to `username`. Same rules as `username` |
| `spcCapable` | `boolean` | No | Requests the WebAuthn `payment` extension for Secure Payment Confirmation. You can set it only at enrollment: a passkey enrolled without it can't be used with Secure Payment Confirmation later. Secure Payment Confirmation isn't available yet |

### Returns

| Field | Type | Description |
| - | - | - |
| `registrationId` | `string` | Pass it to `complete` |
| `frameToken` | `string` | Send only this value to the page. The web SDK uses it to run the enrollment ceremony |
| `expiresAt` | `string` | RFC 3339 time when the ceremony expires, 10 minutes after it opens |

> [!WARNING]
>
> Don't retry `createRegistration` with the same idempotency key after you send
> the `frameToken` to the page. The retry issues a new token and breaks the
> ceremony in progress. Retrying a call whose response you never received is
> safe.

***

## `daa.passkeys.complete`

Completes enrollment with the result of a client SDK's `enroll`. Calls
[`POST /v1/accounts/passkeys`](https://developers.circle.com/api-reference/digital-asset-accounts/all/complete-passkey-registration).

```typescript theme={null}
import type { PasskeyCreated } from "@circle-fin/daa-node-sdk";

const { passkeyId }: PasskeyCreated = await daa.passkeys.complete({
  registrationId,
  attestationResponse,
});
```

### Parameters

| Parameter | Type | Required | Description |
| - | - | - | - |
| `registrationId` | `string` | Yes | From `createRegistration` |
| `attestationResponse` | `object` | Yes | The result of `enroll`, with no fields changed. The web SDK returns an object. The React Native, iOS, and Android SDKs return JSON text: parse it once, and pass the parsed object |

### Returns

| Field | Type | Description |
| - | - | - |
| `passkeyId` | `string` | The new passkey |
| `credentialId` | `string` | The base64url WebAuthn credential ID |

***

## `daa.passkeys.list`

Lists an end customer's passkeys. Calls
[`GET /v1/accounts/passkeys`](https://developers.circle.com/api-reference/digital-asset-accounts/all/list-passkeys).

```typescript theme={null}
import type { PasskeyView } from "@circle-fin/daa-node-sdk";

const passkeys: readonly PasskeyView[] = await daa.passkeys.list({
  clientEntityId,
});
```

### Parameters

| Parameter | Type | Required | Description |
| - | - | - | - |
| `clientEntityId` | `string` | Yes | The end customer |

### Returns

An array of passkeys. Revoked passkeys are included, with `revokedDate` set.
Each passkey includes `passkeyId`, `credentialId`, `algorithm`, `aaguid`,
`attestationFormat`, `transports`, `backupEligible`, `backupState`, `signCount`,
`rpId`, `paymentEligible`, `createDate`, and `updateDate`.

***

## `daa.passkeys.status`

Returns whether an end customer requires SCA, together with their passkeys.
Calls the same route as `list`,
[`GET /v1/accounts/passkeys`](https://developers.circle.com/api-reference/digital-asset-accounts/all/list-passkeys).

```typescript theme={null}
import type { PasskeyStatus } from "@circle-fin/daa-node-sdk";

const { sca, passkeys }: PasskeyStatus = await daa.passkeys.status({
  clientEntityId,
});
```

### Parameters

| Parameter | Type | Required | Description |
| - | - | - | - |
| `clientEntityId` | `string` | Yes | The end customer |

### Returns

| Field | Type | Description |
| - | - | - |
| `sca` | `{ required: boolean }` or `null` | Whether SCA applies to the end customer. If it's `null`, don't store it: call the protected endpoint and handle HTTP 428. See [Which end customers require SCA](https://developers.circle.com/digital-asset-accounts/concepts/strong-customer-authentication.md#which-end-customers-require-sca) |
| `passkeys` | `PasskeyView[]` | The same array that `list` returns |

***

## `daa.passkeys.revoke`

Revokes a passkey. The passkey isn't deleted, and `list` still returns it. Calls
[`DELETE /v1/accounts/passkeys/{passkeyId}`](https://developers.circle.com/api-reference/digital-asset-accounts/all/revoke-passkey).

```typescript theme={null}
await daa.passkeys.revoke(passkeyId, { clientEntityId });
```

### Parameters

| Parameter | Type | Required | Description |
| - | - | - | - |
| `passkeyId` | `string` | Yes | The passkey to revoke |
| `clientEntityId` | `string` | Yes | The end customer |

### Returns

Nothing.

***

## `daa.passkeys.openChallenge`

Opens an approval ceremony for one protected request. Calls
[`POST /v1/accounts/passkeys/challenges`](https://developers.circle.com/api-reference/digital-asset-accounts/all/create-sca-challenge).

```typescript theme={null}
import type { ChallengeOpened } from "@circle-fin/daa-node-sdk";

const { challengeId, frameToken }: ChallengeOpened =
  await daa.passkeys.openChallenge({
    clientEntityId,
    operation: "TRANSFER",
    intent: transferBody,
    embedOrigin: "https://app.example.com",
  });
```

### Parameters

| Parameter | Type | Required | Description |
| - | - | - | - |
| `clientEntityId` | `string` | Yes | The end customer |
| `operation` | `string` | Yes | The uppercase operation name: `TRANSFER`, `WITHDRAWAL`, `WIRE_ACCOUNT_CREATE`, `ADDRESS_BOOK_ADD`, or `ADDRESS_BOOK_DELETE` |
| `intent` | `object` | Yes | The protected request's body, exactly as you will send it, including its `idempotencyKey`. Use `{}` for `ADDRESS_BOOK_DELETE`, which has no body |
| `pathParameters` | `object` | No | Path parameters for an operation without a body. For `ADDRESS_BOOK_DELETE`, pass `{ id }` with the recipient ID from the request URL |
| `embedOrigin` | `string` | No | The origin of the page that runs the ceremony. It must match an origin that you registered in Circle Hub. Send it on every web call. Leave it out when you send `redirectUri` |
| `redirectUri` | `string` | No | For a native application, your registered `https` app callback URI, exactly as registered |

### Returns

| Field | Type | Description |
| - | - | - |
| `challengeId` | `string` | Send it on the protected request in the `X-Sca-Challenge-Id` header |
| `frameToken` | `string` | Send only this value to the page. The web SDK uses it to run the approval ceremony |
| `expiresAt` | `string` | RFC 3339 time when the challenge expires, 5 minutes after it opens |
| `summary` | `object` | Display fields for your records. Don't render it. The ceremony shows its own summary |

After the page returns the assertion, send the protected request with the same
body plus two headers. The SDK exports their names as `SCA_HEADERS`:

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

const headers: Record<string, string> = {
  Authorization: `Bearer ${apiKey}`,
  "Content-Type": "application/json",
  [SCA_HEADERS.challengeId]: challengeId,
  [SCA_HEADERS.assertion]: assertion,
};
```

A retry under a new idempotency key is a different intent and needs a new
challenge. For the full enrollment and approval steps, see
[How-to: Implement SCA](https://developers.circle.com/digital-asset-accounts/howtos/strong-customer-authentication.md).

***

## Errors

A non-2xx response throws `DaaApiError`. With the built-in transport, a network
failure, such as a DNS error, a TLS error, or a timeout, throws
`DaaTransportError`. The SDK passes errors from your own `transport` through
unchanged, so throw `DaaTransportError` from it for those failures. After a
transport error, the request might or might not have been applied.

```typescript theme={null}
import { DAA_ERROR_CODES, isDaaApiError } from "@circle-fin/daa-node-sdk";

try {
  await daa.passkeys.openChallenge(request);
} catch (err: unknown) {
  if (
    isDaaApiError(err) &&
    err.code === DAA_ERROR_CODES.SCA_ORIGIN_NOT_CONFIGURED
  ) {
    // Register your page's origin in Circle Hub.
  }
}
```

`DaaApiError` has these fields:

| Field | Type | Description |
| - | - | - |
| `status` | `number` | The HTTP status |
| `code` | `number \| undefined` | Circle's numeric error code. `-1` means an unrecognized upstream error, and the message includes an `errId` for Circle support |
| `rawBody` | `string` | The response body as received |

`DAA_ERROR_CODES` maps names to the numeric codes. Common codes:

| Name | Code | Cause |
| - | - | - |
| `CLIENT_ENTITY_NOT_OWNED` | 420001 | The `clientEntityId` isn't one of your end customers |
| `IDEMPOTENCY_KEY_REUSED` | 420034 | The idempotency key was reused with different parameters |
| `PASSKEY_REGISTRATION_EXPIRED` | 420051 | The enrollment ceremony expired. Start a new one |
| `SCA_CHALLENGE_EXPIRED` | 420054 | The challenge expired. Open a new one |
| `SCA_INTENT_MISMATCH` | 420047 | The protected request doesn't match the approved intent |
| `SCA_OPERATION_NOT_IMPLEMENTED` | 420062 | SCA covers the operation, but approval isn't available yet |
| `SCA_ORIGIN_NOT_CONFIGURED` | 420064 | Circle hasn't approved any origin for you |
| `SCA_REDIRECT_URI_NOT_ALLOWED` | 420068 | The `redirectUri` isn't one of your registered app callback URLs. The constant is in version 0.2.5 or later |

## Usage notes

* Don't log `frameToken`. It grants access to one pending ceremony.
* Send only `frameToken` to the page. Keep the rest of each response on your
  server.
* Circle decides whether SCA applies to an end customer. A request that Circle
  doesn't gate succeeds without SCA headers.

## Versions

The SDK follows semantic versioning. Before version 1.0.0, a minor version can
include breaking changes, so pin an exact version. The source is at
[`circlefin/daa-node-sdk`](https://github.com/circlefin/daa-node-sdk), in
`packages/node`, with a changelog for each release.
