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

# Guide: Card + 3DS Checkout

> Implement card payments with 3D Secure using session-first or transaction-first flows.

Use this guide to run card payments that may require 3D Secure, using either the session-first or transaction-first architecture.

Both flows are officially supported. Choose the one that matches how your backend orchestrates transaction state and authentication.

## Choose a Flow

| Flow              | Sequence                                                                        | When to use                                                     |
| ----------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Session-first     | Create 3DS session -> complete 3DS -> create transaction                        | Use when your backend authenticates before transaction creation |
| Transaction-first | Create transaction (`AWAITING_3DS`) -> complete 3DS -> authenticate transaction | Use when your backend starts transaction orchestration first    |

<Info>
  Both flows are official in Rinne. There is no platform-level preferred flow.
</Info>

## Backend Endpoint Mapping

| Context                           | Create session                                 | Create transaction                             | Authenticate transaction                                                    |
| --------------------------------- | ---------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------- |
| Self (company)                    | `POST /v1/3ds-sessions`                        | `POST /v1/transactions`                        | `POST /v1/transactions/{transactionId}/authenticate`                        |
| Merchant (organization on behalf) | `POST /v1/merchants/{merchantId}/3ds-sessions` | `POST /v1/merchants/{merchantId}/transactions` | `POST /v1/merchants/{merchantId}/transactions/{transactionId}/authenticate` |

## Shared Frontend Setup

<Warning>
  Before processing card transactions, your merchant or organization must have an active Rinne card affiliation.
</Warning>

<Warning>
  Use encrypted values from `mountedCard.values` only. Do not collect raw PAN/CVC in custom inputs.
</Warning>

```typescript theme={null}
import { Rinne } from '@rinnebr/js'

const rinne = new Rinne({
  merchantId: 'your-merchant-id',
  environment: 'production'
})

const cardElement = await rinne.elements.card({
  theme: 'material',
  colorScheme: 'dark',
  locale: 'pt',
  fields: ['number', 'expiry', 'cvc'],
  onChange: (state) => {
    submitButton.disabled = !state.isComplete || !state.isValid
  }
})

const mountedCard = await cardElement.mount('#card-element')
```

## Session-First Flow

<Steps>
  <Step title="Create 3DS session on backend">
    Send the encrypted card number and plain expiry (month/year) from `mountedCard.values` to your backend. The Card Element encrypts the card number client-side; expiry values are not encrypted.
  </Step>

  <Step title="Run the 3DS challenge">
    Create `threeDSecure` once and call `mount(tdsSessionId)`.
  </Step>

  <Step title="Create transaction after authentication">
    On `onSuccess`, call your backend and include the authenticated session ID.
  </Step>
</Steps>

### Backend session request example

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST 'https://your-api.example.com/api/3ds-sessions' \
    -H 'Content-Type: application/json' \
    -d '{
      "amount": 1090,
      "currency": "BRL",
      "card": {
        "number": "ev:encrypted:...",
        "expiry": { "month": "04", "year": "2028" }
      }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json Example response theme={null}
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "tds_session_id": "tds_session_123",
    "auth_status": "ACTION_REQUIRED"
  }
  ```
</ResponseExample>

```typescript theme={null}
async function runThreeDSChallenge(sessionId: string): Promise<void> {
  let resolveAuth: () => void
  let rejectAuth: (error: Error) => void
  const completed = new Promise<void>((resolve, reject) => {
    resolveAuth = resolve
    rejectAuth = reject
  })

  const threeDS = await rinne.elements.threeDSecure({
    target: '#three-ds-container',
    colorScheme: 'dark',
    onReady: () => console.log('Challenge is ready'),
    onSuccess: () => resolveAuth(),
    onFailure: (error) => rejectAuth(new Error(error?.message ?? '3DS authentication failed')),
    onError: (error) => rejectAuth(new Error(error.message))
  })

  await threeDS.mount(sessionId)
  return completed
}

async function post(url: string, body: object) {
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body)
  })
  if (!res.ok) {
    const error = await res.json().catch(() => ({}))
    throw new Error(error.message ?? `Request failed with status ${res.status}`)
  }
  return res.json()
}

async function runSessionFirst(amountInCents: number) {
  if (!mountedCard.isComplete || !mountedCard.isValid) {
    throw new Error('Card form is not ready')
  }

  const cardValues = mountedCard.values

  const session = await post('/api/3ds-sessions', {
    amount: amountInCents,
    currency: 'BRL',
    card: {
      number: cardValues.card.number,
      expiry: { month: cardValues.card.expiry_month, year: cardValues.card.expiry_year }
    }
  })

  if (session.auth_status === 'FAILED') {
    throw new Error(session.failure_reason ?? '3DS authentication failed')
  }

  if (session.auth_status === 'ACTION_REQUIRED') {
    await runThreeDSChallenge(session.tds_session_id)
  }

  // AUTHENTICATED — authentication completed, either at creation time or after challenge. Continue to transaction.
  const payment = await post('/api/transactions', {
    amount: amountInCents,
    cardData: {
      number: cardValues.card.number,
      cvv: cardValues.card.cvv,
      expiry_month: cardValues.card.expiry_month,
      expiry_year: cardValues.card.expiry_year,
      cardholder_name: cardValues.card.cardholder_name,
      last_digits: cardValues.card.last_digits
    },
    three_d_secure_session_id: session.id
  })

  return payment
}
```

## Transaction-First Flow

<Info>
  In this flow, your backend transaction status determines whether frontend should run 3DS.
</Info>

<Steps>
  <Step title="Create transaction and branch by status">
    Backend creates the transaction and returns the initial status.

    Use `require_3ds: true` when you want deterministic `AWAITING_3DS` entry in transaction-first.
  </Step>

  <Step title="Start 3DS only for AWAITING_3DS">
    If transaction is `AWAITING_3DS`, create a 3DS session with the same amount/currency and encrypted card number/expiry.
  </Step>

  <Step title="Mount challenge when required and authenticate">
    Branch on `auth_status`: mount only for `ACTION_REQUIRED`, skip for `AUTHENTICATED`, abort for `FAILED`.
    After challenge success (or if already `AUTHENTICATED`), backend authenticates the pending transaction with session `id` using:

    * Self: `POST /v1/transactions/{transactionId}/authenticate`
    * Merchant: `POST /v1/merchants/{merchantId}/transactions/{transactionId}/authenticate`
  </Step>

  <Step title="Handle transaction result">
    Card transactions typically resolve synchronously — the `/authenticate` response already contains the final status (`APPROVED`, `AUTHORIZED`, or `REFUSED`). Use this status to drive your UI immediately.
  </Step>
</Steps>

### Challenge strategy flags (backend)

* `require_3ds: true`: create directly in `AWAITING_3DS`, so you can enforce 3DS while staying on one transaction-first backend flow.
* `refuse_on_challenge: true`: fail fast with `REFUSED` + `status_reason=CHALLENGE_NOT_ALLOWED` when a challenge-triggered path would occur.
* Use `refuse_on_challenge` when you intentionally do not support challenge UX for a merchant, channel, or transaction segment.
* The two flags cannot both be `true`.

### Transaction-first status branching

| Transaction status                  | Frontend/backend action                                          |
| ----------------------------------- | ---------------------------------------------------------------- |
| `AWAITING_3DS`                      | Run 3DS session/challenge and call authenticate                  |
| `PROCESSING`                        | Wait for webhook or status refresh; do not call authenticate yet |
| `AUTHORIZED` / `APPROVED`           | Payment already completed; continue normal success flow          |
| `REFUSED` + `CHALLENGE_NOT_ALLOWED` | Fail fast path triggered; no 3DS handling required               |
| `REFUSED` (other)                   | Regular decline flow                                             |

<Note>
  If a transaction is initially `PROCESSING` and later becomes `AWAITING_3DS` (for example, soft-decline recovery), run the same transaction-first 3DS steps at that point.
</Note>

### Transaction-first orchestration example

```typescript theme={null}
type TransactionResult = {
  id: string
  status: 'PROCESSING' | 'AWAITING_3DS' | 'AUTHORIZED' | 'APPROVED' | 'REFUSED'
  status_reason?: string
}

async function runTransactionFirst(amountInCents: number): Promise<TransactionResult> {
  if (!mountedCard.isComplete || !mountedCard.isValid) {
    throw new Error('Card form is not ready')
  }

  const cardValues = mountedCard.values

  const created: TransactionResult = await post('/api/transactions', {
    amount: amountInCents,
    currency: 'BRL',
    cardData: {
      number: cardValues.card.number,
      cvv: cardValues.card.cvv,
      expiry_month: cardValues.card.expiry_month,
      expiry_year: cardValues.card.expiry_year,
      cardholder_name: cardValues.card.cardholder_name,
      last_digits: cardValues.card.last_digits
    },
    require_3ds: true
  })

  // If you set refuse_on_challenge instead of require_3ds, you will need the following branch:
  // if (created.status === 'REFUSED' && created.status_reason === 'CHALLENGE_NOT_ALLOWED') {
  //   throw new Error('3DS challenge is disabled by transaction policy')
  // }

  if (created.status !== 'AWAITING_3DS') {
    return created
  }

  const session = await post('/api/3ds-sessions', {
    amount: amountInCents,
    currency: 'BRL',
    card: {
      number: cardValues.card.number,
      expiry: { month: cardValues.card.expiry_month, year: cardValues.card.expiry_year }
    }
  })

  if (session.auth_status === 'FAILED') {
    throw new Error(session.failure_reason ?? '3DS authentication failed')
  }

  if (session.auth_status === 'ACTION_REQUIRED') {
    await runThreeDSChallenge(session.tds_session_id)
  }

  const authenticated: TransactionResult = await post(
    `/api/transactions/${created.id}/authenticate`,
    { three_d_secure_session_id: session.id }
  )

  return authenticated
}
```

<Warning>
  Authenticate requests must use session `id` as `three_d_secure_session_id`. Do not send `tds_session_id` to your backend authenticate route.
</Warning>

## Session ID Mapping

* `tds_session_id`: use in `threeDSecure.mount(tds_session_id)` on frontend.
* `id`: use as `three_d_secure_session_id` in backend transaction create/authenticate calls.

## Handle 3DS Statuses

In practice, sessions come back as `ACTION_REQUIRED` or `FAILED` at creation — even frictionless authentication completes through the SDK element. The API contract allows `AUTHENTICATED` as a creation-time response for forward compatibility, so always branch on all three statuses.

| `auth_status` at creation | Meaning                                                       | Frontend action                                                               |
| ------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `ACTION_REQUIRED`         | Authentication required — mount the 3DS element               | Mount 3DS element; the SDK handles frictionless and challenge internally      |
| `FAILED`                  | Card was rejected before the flow could start                 | Surface the error; let the user retry with a different card or payment method |
| `AUTHENTICATED`           | Frictionless authentication completed at creation time (rare) | Skip mounting; proceed directly to transaction creation or `/authenticate`    |

### Frictionless vs challenge

When the element is mounted, the issuer decides whether the transaction can be authenticated silently (frictionless) or needs cardholder interaction (challenge):

* **Frictionless**: authentication completes in the background with no visible UI. `onSuccess` fires automatically, often within seconds.
* **Challenge**: an issuer-hosted verification step appears inside the element (OTP, app notification, biometric). `onSuccess` fires after the user completes it.

Your frontend code handles both identically — `onSuccess` is the signal to proceed in either case. You can inspect `authentication_flow` on the backend after the session is authenticated to see which path was taken.

## Retry Strategy

When a session expires or fails, request a new session and mount again.

```typescript theme={null}
await threeDSecure.mount('tds_session_new')
```

<Info>
  The SDK automatically unmounts previous 3DS instances when you remount with a new session ID.
</Info>
