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

# Cards.js Integration Guide

> The fetchSession contract, the backend glue, session behaviour, the reveal window, errors, and multiple cards

<Warning>
  **Cards is available in sandbox only while we finish development.** Dakota enables Cards per
  account. The Cards endpoints are in the [API reference](/api-reference/introduction), marked
  **Sandbox only**. Endpoints, fields, and flows can still change before release.
</Warning>

This guide covers the one piece of backend you have to write — the reveal-session endpoint — plus how sessions behave, how to handle errors, and how to show more than one card on a page.

This page is the reference for the **SDK side**. The Dakota REST call that your backend makes — its request, response, headers, and rules — is documented once, on [Secure card data](/documentation/cards/secure-card-data#mint-a-reveal-session).

## How a reveal works

1. Your page creates a `DakotaCards` client and mounts a card. Mounting only paints the masked face. It makes no network calls.
2. When the cardholder asks to see the details, the SDK calls your `fetchSession` function with a request context.
3. `fetchSession` forwards that context to **your** backend. Your backend calls Dakota with your secret API key and returns a short-lived session token to the browser.
4. The SDK hands the token to the card-data processor, which renders the real values inside frames your code cannot read.

The API key lives only on your server. The browser only ever holds a single-use session token that is worthless once consumed or expired.

## The `fetchSession` contract

`fetchSession` is the only integration point you implement on the client. Its signature:

```ts theme={null}
type FetchSession = (ctx: SessionRequestContext) => Promise<RevealSession>;

interface SessionRequestContext {
  sessionType: 'card_details'; // reserved for future session types
  origin: string;              // window.location.origin at reveal time
}

interface RevealSession {
  session: string;    // the single-use token
  expiresAt: string;  // RFC 3339 timestamp
  provider?: string;  // reserved; omit it
}
```

The SDK builds the `ctx` for you. Your job is to POST it to your backend and return the `{ session, expiresAt }` your backend gives back. **Forward `sessionType` and `origin` unchanged** — the `sessionType` names the flow requesting the session, and the `origin` is what the session is bound to.

<Note>
  `provider` is a reserved routing discriminator for future card-data providers. Leave it unset. When it is absent, the SDK uses its default provider.
</Note>

## The backend endpoint

Your backend is the trust boundary: it holds the API key and mints sessions on the page's behalf. It forwards the SDK's `{ sessionType, origin }` to Dakota's reveal-session endpoint for the card you are displaying.

<Warning>
  **Authenticate and authorize before you mint.** A reveal session unlocks a specific card's details in the browser, so minting one is a privileged action. Before your endpoint mints, it must authenticate the cardholder's session and confirm that the requested card belongs to that cardholder. Skip either check, and anyone who can reach the endpoint can reveal the card it is bound to.
</Warning>

Mint against the API environment that matches the `environment` you give `DakotaCards`: `https://api.platform.dakota.xyz` for `production`, `https://api.platform.sandbox.dakota.xyz` for `sandbox`. The request itself — headers, body, and response — is on [Secure card data](/documentation/cards/secure-card-data#mint-a-reveal-session).

The Dakota API speaks snake\_case and the SDK speaks camelCase, so your backend renames the two fields that differ — one on the way out, one on the way back. On the request, rename `sessionType` to `session_type`. On the response, rename `expires_at` to the `expiresAt` that the SDK expects:

```js theme={null}
// The glue every integration writes, in each direction:
body: JSON.stringify({ session_type: ctx.sessionType, origin: ctx.origin });
res.json({ session: upstream.session, expiresAt: upstream.expires_at });
```

A minimal Node handler, in Express style:

```js Illustrative draft — subject to change theme={null}
app.post('/api/reveal-session', requireCardholderAuth, async (req, res) => {
  const cardId = cardIdForAuthenticatedUser(req); // your ownership check
  const upstream = await fetch(`${DAKOTA_API_BASE}/cards/${cardId}/reveal_session`, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'x-api-key': DAKOTA_API_KEY,
    },
    body: JSON.stringify({ session_type: req.body.sessionType, origin: req.body.origin }),
  });
  const data = await upstream.json();
  if (!upstream.ok) return res.status(upstream.status).json(data);
  res.json({ session: data.session, expiresAt: data.expires_at });
});
```

Return an error status (4xx or 5xx) if the upstream call fails. The SDK surfaces a thrown `fetchSession` as a retryable [`session_fetch_failed`](#error-codes).

## Copy to clipboard

The values never reach your JavaScript, so your code cannot copy them. The value frames are click-to-copy instead: the cardholder clicks a value, the frame performs the copy, and the SDK reports it through the `copied` event (`{ field, success }`).

To offer a copy *button*, render your icon underneath a frame's transparent tail — size the frame a little wider than the text — so that a click on the icon lands on the frame. Then flip the icon to a confirmation on the `copied` event.

## Session semantics

* **Single-use.** Each session token is consumed by exactly one reveal. There is no caching or reuse: **every `reveal()` mints a fresh session** by calling `fetchSession` again. The only sharing is deduplication of *concurrent* calls that are already in flight.
* **Short-lived.** The token is valid until `expiresAt`, capped at **10 minutes** by the SDK regardless of what you send.

## The reveal window & auto-mask

A successful reveal is always time-boxed. When the window ends, the SDK tears down the value frames itself and shows the masked face again — you do not have to.

The window length is `min(autoMaskMs, remaining session TTL)`, and the session TTL is itself capped at 10 minutes. So even if you set no `autoMaskMs`, a reveal auto-masks when the session expires. When the window ends, the handle fires an `expired` event — versus a `masked` event for a reveal you dismissed yourself with `mask()`:

```ts theme={null}
const card = cards.cardDetails({ last4: '4242', autoMaskMs: 30_000 });
card.on('revealed', () => {/* details are visible */});
card.on('expired', () => {/* window ended; face is masked again */});
card.on('masked', () => {/* you called mask() */});
card.on('copied', ({ field, success }) => {/* cardholder copied a field */});
```

The card-details handle emits `ready`, `revealing`, `revealed`, `masked`, `expired`, and `copied`. Errors are **not** delivered as events. They surface as rejections of the `reveal()` and `mount()` promises, so wrap those calls in `try/catch`. The one synchronous throw is `invalid_theme`, raised by `cardDetails()` itself when the handle is created.

A reserved `error` key does appear in the `CardDetailsEvents` type map, but the SDK never emits it in this version — see the [API reference](/documentation/cards/cards-js/api-reference#events).

## Error handling

Every error the SDK throws is a `DakotaCardsError`. Branch on `error.retryable` to decide whether a retry makes sense, and read `error.code` for the specific case. When the failure originated with the card-data processor, `error.diagnosticId` carries an upstream request ID that you can quote to Dakota support.

```ts theme={null}
import { isDakotaCardsError } from '@dakota-xyz/cards-js';

try {
  await card.reveal();
} catch (err) {
  if (isDakotaCardsError(err)) {
    console.error(err.code, err.retryable, err.diagnosticId);
    if (err.retryable) {/* offer the cardholder a retry */}
  }
}
```

### Error codes

| `code` | `retryable` | When it happens |
| - | - | - |
| `session_fetch_failed` | yes | Your `fetchSession` threw, or returned something other than `{ session, expiresAt }`. |
| `session_rejected` | yes | The card-data processor rejected the token — for example, it was already consumed or expired. Mint a fresh session and retry. |
| `origin_mismatch` | no | **Reserved** — kept in the `ErrorCode` union for future use. Not currently thrown. |
| `mount_target_not_found` | no | A mount selector or element could not be resolved in the DOM. |
| `invalid_theme` | no | A `theme` value contains a CSS `var()` reference. Thrown synchronously by `cardDetails()` when the handle is created. Pass concrete values — see [theming](/documentation/cards/cards-js/theming#the-theme-option). |
| `invalid_state` | no | A method was called in the wrong lifecycle state — for example, `reveal()` before `mount()`, calling `mount()` twice, or using a destroyed handle. |
| `reveal_failed` | usually no | The value frames failed to load for a reason unrelated to the session. Exception: a transient module-load failure inside the library surfaces as a retryable `reveal_failed`. Always branch on `err.retryable` rather than assuming per-code behaviour. |

<Note>
  **Troubleshooting origin problems.** `origin_mismatch` is reserved and not thrown today. Origin problems currently surface as `reveal_failed` or `session_rejected`. If reveals consistently fail with either code, first verify that the origin your backend forwards to the reveal-session endpoint exactly matches the page's origin — scheme, host, and port, with no path. A mismatch there is a common cause of both codes.
</Note>

## Showing more than one card on a page

A `DakotaCards` client can drive any number of cards. A reveal session is minted for a **specific** card (`/cards/{card_id}/reveal_session`), so give each handle its own `fetchSession` that targets the right card. Each handle keeps its own session broker, so their reveals never interfere.

```ts theme={null}
import { DakotaCards, type FetchSession } from '@dakota-xyz/cards-js';

function mintFor(cardId: string): FetchSession {
  return async (ctx) => {
    const res = await fetch(`/api/cards/${cardId}/reveal-session`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(ctx),
    });
    if (!res.ok) throw new Error(`session mint failed: ${res.status}`);
    return res.json();
  };
}

const cards = new DakotaCards({ environment: 'production', fetchSession: mintFor(defaultCardId) });

const cardA = cards.cardDetails({ last4: '4242', fetchSession: mintFor(cardIdA) });
const cardB = cards.cardDetails({ last4: '1881', fetchSession: mintFor(cardIdB) });
await cardA.mount({ pan: '#pan-a', cvv: '#cvv-a', expiryMonth: '#mm-a', expiryYear: '#yy-a' });
await cardB.mount({ pan: '#pan-b', cvv: '#cvv-b', expiryMonth: '#mm-b', expiryYear: '#yy-b' });
```

In React, pass the per-card `fetchSession` as a prop to each `<CardDetails>` — or to `useCardDetails`. It overrides the client-level one for that handle.

## Driving a loading treatment

`reveal()` emits `revealing` the moment an attempt starts. At the same moment, the face swaps to the frame side, where empty frames show the stylesheet's loading state. The hook's `state` reports the same value, so a custom UI can derive its in-flight treatment directly: disable the trigger, show a spinner, or animate a transition. A failed attempt emits `masked` on the way back, so event-driven UIs always see the attempt end.

## Known sharp edge: `mask()` during an in-flight reveal

`mask()` only acts on an already-revealed card. If you call it while a `reveal()` is still in flight — the handle is in the `revealing` state — it is a **no-op**. It does not cancel the pending reveal. If you need a "cancel" affordance, disable it until the `revealed` event fires, or `destroy()` the handle to tear everything down.
