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

# Card transactions

> Implement credit and debit card transaction flows, challenge strategy flags, and 3DS transitions.

Use this guide to implement `CREDIT_CARD` and `DEBIT_CARD` transactions with the Rinne Core API.

<Info>
  Rinne officially supports both 3DS strategies for card payments: session-first and transaction-first.
</Info>

## Transaction endpoints (self vs merchant)

| Context                            | Create transaction                                                                                                             | Authenticate pending transaction                                                                                                                 |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Self (company)                     | [`POST /v1/transactions`](/api-reference/company-transactions/create-a-new-transaction)                                        | [`POST /v1/transactions/{id}/authenticate`](/api-reference/3d-secure/authenticate-a-transaction-with-3d-secure)                                  |
| Organization on behalf of merchant | [`POST /v1/merchants/{merchantId}/transactions`](/api-reference/transactions/create-a-new-transaction-for-a-specific-merchant) | [`POST /v1/merchants/{merchantId}/transactions/{id}/authenticate`](/api-reference/3d-secure/authenticate-a-merchants-transaction-with-3d-secure) |

## Prerequisites

* Active affiliation for card payments.
* `ECOMMERCE` capture enabled for online transactions.
* One unique `request_id` per payment attempt.
* Encrypted card values from rinne-js secure components.

<Warning>
  Do not collect raw PAN/CVC in custom inputs. Forward encrypted values from [Card Element](/rinne-js/card-element) only.
</Warning>

## Sending card data

Rinne's API only accepts card credentials in encrypted form. The `card_data.number`, `card_data.cvv`, `card_data.network_token`, and `card_data.cryptogram` values must arrive encrypted — you can recognize encrypted values by their `ev:` prefix. Requests where one of these fields arrives in plain text are rejected with `400 VALIDATION_ERROR` before any processing happens.

These encrypted values come from the rinne-js secure components: the [Card Element](/rinne-js/card-element) encrypts the card number and CVV in the customer's browser, and the [wallet elements](/rinne-js/wallet-elements) emit `network_token` and `cryptogram` already encrypted. Raw card data never touches your servers.

## Required transaction shape

<ParamField body="provider" type="string" required>
  Payment provider configured for the affiliation.
</ParamField>

<ParamField body="request_id" type="string" required>
  Idempotency key for creation.
</ParamField>

<ParamField body="amount" type="integer" required>
  Amount in cents.
</ParamField>

<ParamField body="capture_method" type="string" required>
  Use `ECOMMERCE` for card-not-present checkout.
</ParamField>

<ParamField body="payment_method" type="string" required>
  `CREDIT_CARD` or `DEBIT_CARD`.
</ParamField>

<ParamField body="card_data" type="object" required>
  Card payload for card transactions.
</ParamField>

## Card data rules

* Send exactly one of `card_data.number` (raw card entry), `card_data.network_token` (wallet), or `card_data.card_id` ([stored card](/guides/cards-on-file)).
* Send `number` and `cvv` as encrypted values from rinne-js. Values that reach the API unencrypted are rejected with `400 VALIDATION_ERROR`.
* With `number` or `network_token`, send `expiry_month` (`MM`), `expiry_year` (`YYYY`), `last_digits` (the last 4 digits of the card number, exactly 4 digits), and `cardholder_name` as it appears on the card.
* With `card_id`, Rinne resolves those fields from the stored card — see [Example: charge a stored card](#example-charge-a-stored-card).
* For authenticated/tokenized flows, provide cryptogram directly or via `three_d_secure_session_id` injection.
* For wallet tokenized flows, include `wallet_type` (`APPLE_PAY` or `GOOGLE_PAY`).

<Warning>
  Do not send more than one of `number`, `network_token`, and `card_id` in the same request.
</Warning>

## 3DS challenge strategy flags

`TransactionCreateRequest` supports two optional card-only flags:

<ParamField body="require_3ds" type="boolean">
  When `true`, card transaction creation always goes directly to `AWAITING_3DS`. Provider authorization is deferred until `/authenticate` is called with an authenticated 3DS session.
</ParamField>

<ParamField body="refuse_on_challenge" type="boolean">
  When `true`, any path that would require a challenge (risk-triggered or soft-decline-triggered) is refused immediately (`REFUSED`) instead of transitioning to `AWAITING_3DS`.
</ParamField>

### Why these flags are useful

| Flag                  | Immediate status behavior                                                                     | What your backend still handles                                                | What this avoids                                                                       |
| --------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
| `require_3ds`         | Always creates card transactions in `AWAITING_3DS`                                            | Create 3DS session, complete challenge when required, and call `/authenticate` | Implementing a second session-first architecture only to enforce 3DS policy            |
| `refuse_on_challenge` | Returns `REFUSED` with `status_reason=CHALLENGE_NOT_ALLOWED` when challenge would be required | Handle refusal/retry paths only                                                | Implementing challenge UX and pending `AWAITING_3DS` operational queues for that scope |

With `require_3ds`, organizations that already run transaction-first can enforce 3DS for all or selected transactions without adding a separate session-first policy path.
The transaction enters `AWAITING_3DS` immediately, which is the status you already handle in this flow.

With `refuse_on_challenge`, challenge-triggered flows do not enter `AWAITING_3DS`; they stop immediately as `REFUSED`, so your operation fails fast and carries no challenge-handling workload.

<Note>
  `refuse_on_challenge` changes only challenge-triggered paths. Transactions that do not require challenge continue normal provider processing.
</Note>

### `CHALLENGE_NOT_ALLOWED` status reason

When `refuse_on_challenge` is `true` and a challenge would be required (risk-triggered or soft-decline-triggered), transaction response uses:

* `status = REFUSED`
* `status_reason = CHALLENGE_NOT_ALLOWED`

<Warning>
  `require_3ds` and `refuse_on_challenge` are mutually exclusive. Requests with both set to `true` must be rejected.
</Warning>

### Policy granularity

Because these flags are part of each transaction request, you can apply policies with fine control:

* Per merchant (choose behavior by `merchantId` in your backend routing).
* Per payment flow (for example, mobile app vs web checkout).
* Per transaction segment (risk tier, issuer group, customer cohort, high-value purchases).

## Example: standard card transaction

```bash cURL theme={null}
curl -X POST 'https://api-sandbox.rinne.com.br/core/v1/transactions' \
  -H 'x-api-key: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "provider": "RINNE",
    "request_id": "order-card-0001",
    "amount": 25990,
    "currency": "BRL",
    "capture_method": "ECOMMERCE",
    "payment_method": "CREDIT_CARD",
    "installments": 1,
    "card_data": {
      "number": "ev:encrypted:card_number",
      "cvv": "ev:encrypted:cvc",
      "expiry_month": "12",
      "expiry_year": "2028",
      "cardholder_name": "Maria Santos",
      "last_digits": "1111"
    }
  }'
```

## Example: charge a stored card

To charge a card you [stored on file](/guides/cards-on-file), send its `id` as `card_data.card_id` in place of the card credentials. Rinne resolves the encrypted number, `expiry_month`, `expiry_year`, `last_digits`, `brand`, and `cardholder_name` from the stored card.

```bash cURL theme={null}
curl -X POST 'https://api-sandbox.rinne.com.br/core/v1/transactions' \
  -H 'x-api-key: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "provider": "RINNE",
    "request_id": "order-stored-card-0001",
    "amount": 25990,
    "currency": "BRL",
    "capture_method": "ECOMMERCE",
    "payment_method": "CREDIT_CARD",
    "installments": 1,
    "card_data": {
      "card_id": "9c1f2e3a-4b5c-4d6e-8f90-a1b2c3d4e5f6",
      "cvv": "ev:encrypted:cvc"
    }
  }'
```

Include `cvv` for customer-present payments that re-collect it, and omit it for merchant-initiated payments such as recurring charges. Everything outside `card_data` — installments, the 3DS challenge strategy flags, and the merchant route — behaves exactly as it does with inline card data.

<Warning>
  Do not send `number`, `network_token`, `expiry_month`, `expiry_year`, `last_digits`, `brand`, or `cardholder_name` alongside `card_id`. The stored card supplies these fields, so sending them inline is ambiguous and is rejected with `400 VALIDATION_ERROR`.
</Warning>

A `card_id` that Rinne cannot charge is rejected with `400 VALIDATION_ERROR` on the `card_data.card_id` field, in three cases:

* **No such card in scope** — the card doesn't exist, was deleted, or belongs to another company. A card is only chargeable by the company that owns it.
* **The card isn't active** — its `status` is not `ACTIVE`.
* **The card has no `brand`** — `brand` is optional to store, but Rinne's providers currently require it to process a transaction. Store cards with a `brand` if you intend to charge them.

## Example: force transaction-first 3DS

```bash cURL theme={null}
curl -X POST 'https://api-sandbox.rinne.com.br/core/v1/transactions' \
  -H 'x-api-key: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "provider": "RINNE",
    "request_id": "order-card-0002",
    "amount": 25990,
    "currency": "BRL",
    "capture_method": "ECOMMERCE",
    "payment_method": "CREDIT_CARD",
    "card_data": {
      "number": "ev:encrypted:card_number",
      "cvv": "ev:encrypted:cvc",
      "expiry_month": "12",
      "expiry_year": "2028",
      "cardholder_name": "Maria Santos",
      "last_digits": "1111"
    },
    "require_3ds": true
  }'
```

```json Example response theme={null}
{
  "id": "tx_123456789",
  "status": "AWAITING_3DS",
  "amount": 25990,
  "currency": "BRL"
}
```

## Example: fail fast when challenge is not allowed

```bash cURL theme={null}
curl -X POST 'https://api-sandbox.rinne.com.br/core/v1/transactions' \
  -H 'x-api-key: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "provider": "RINNE",
    "request_id": "order-card-0003",
    "amount": 25990,
    "currency": "BRL",
    "capture_method": "ECOMMERCE",
    "payment_method": "CREDIT_CARD",
    "card_data": {
      "number": "ev:encrypted:card_number",
      "cvv": "ev:encrypted:cvc",
      "expiry_month": "12",
      "expiry_year": "2028",
      "cardholder_name": "Maria Santos",
      "last_digits": "1111"
    },
    "refuse_on_challenge": true
  }'
```

```json Example response theme={null}
{
  "id": "tx_123456789",
  "status": "REFUSED",
  "status_reason": "CHALLENGE_NOT_ALLOWED"
}
```

## Card transaction lifecycle highlights

| Status         | Meaning                     | Typical next action                   |
| -------------- | --------------------------- | ------------------------------------- |
| `PROCESSING`   | Authorization in progress   | Wait for webhook/result fetch         |
| `AWAITING_3DS` | 3DS authentication required | Complete 3DS and call `/authenticate` |
| `AUTHORIZED`   | Authorized but not captured | Follow capture policy                 |
| `APPROVED`     | Payment accepted            | Fulfill order                         |
| `REFUSED`      | Payment declined            | Retry with new attempt/payment method |

<Tip>
  Reproduce approvals, declines, and provider errors on demand in the sandbox with deterministic [test scenarios](/guides/testing) — the transaction amount selects the final payment outcome.
</Tip>

## Retry and idempotency

1. Generate one `request_id` per attempt.
2. Reuse same `request_id` only for safe retries of the same attempt.
3. Use a new `request_id` for a new customer attempt.

## Refunding card transactions

Send `refund_amount` equal to the remaining refundable amount for a full refund, or any smaller amount for a partial refund (subject to the rules below).

```bash cURL theme={null}
curl -X POST 'https://api-sandbox.rinne.com.br/core/v1/merchants/MERCHANT_ID/transactions/TRANSACTION_ID/refunds' \
  -H 'x-api-key: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "refund_amount": 25990,
    "refund_reason": "CUSTOMER_REQUEST",
    "description": "Full refund - same day"
  }'
```

### Card refund rules

Two independent acquirer rules apply to every card refund. Both are pre-validated server-side and surface as `HTTP 400` `VALIDATION_ERROR` before any provider round-trip.

<Warning>
  **One successful refund per card transaction.** Once a `CREDIT_CARD` or `DEBIT_CARD` transaction has a refund in `COMPLETED` status, the acquirer treats it as terminally refunded. Any additional refund request — partial or full — is rejected with `HTTP 400`.

  Plan the refund amount up front: if you need to return funds in pieces, the first successful refund must cover the full remaining amount you intend to return. Refunds in `FAILED` or `CANCELLED` status do not count toward this rule, so you can submit a new attempt after a failed one.
</Warning>

<Warning>
  **Same-day partial refunds are not allowed.** When a refund request lands on the same calendar day as the transaction's `approved_at` (D0), only a refund for the **full remaining refundable amount** is accepted. Partial refunds are allowed only from the next calendar day (D+1) onward.
</Warning>

The acceptance matrix on a transaction approved with `approved_amount = 10000` and **no prior `COMPLETED` refund**:

| Day           | Refund amount  | Result                                                                                                                                                                      |
| ------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| D0 (same day) | 10000 (full)   | Accepted (outcome resolved per response semantics below)                                                                                                                    |
| D0 (same day) | 5000 (partial) | Rejected — `400` `VALIDATION_ERROR`: `Partial refund on the same day as the transaction is not allowed by the acquirer; submit the full remaining amount or wait until D+1` |
| D+1 or later  | 5000 (partial) | Accepted (outcome resolved per response semantics below)                                                                                                                    |
| D+1 or later  | 10000 (full)   | Accepted (outcome resolved per response semantics below)                                                                                                                    |

Once any refund on the transaction reaches `COMPLETED`, every row above is rejected with `HTTP 400` (`Card transaction has already been refunded; the acquirer does not allow additional refunds`), regardless of the day or whether the new request is partial or full.

### Card refund response semantics

Card refunds resolve synchronously whenever the acquirer returns a definitive answer:

* A successful cancel returns `refund.status = COMPLETED` and the transaction status becomes `REFUNDED` (full) or `PARTIALLY_REFUNDED` (partial).
* A definitive provider rejection (for example, an acquirer business decline) returns `refund.status = FAILED`. **Failed card refunds are terminal and are not retried** — submit a new refund request to try again.
* An uncertain outcome (acquirer 5xx or network timeout) triggers an inline status query against the acquirer. If that resolves the outcome, you still get `COMPLETED` or `FAILED` on the same call. Otherwise the refund moves to `TIMEOUT` and is reconciled asynchronously.
* A `PENDING_REFUND` transaction status on a card transaction means the request was accepted but did not produce a definitive answer; we will retry the (idempotent) cancel.

For the full set of refund rules, statuses, and the cancel-pending-refund endpoint, see [Refunds in the Transactions concept](/concepts/transactions#refunds).

## Integration with rinne-js

* Use [Card Element](/rinne-js/card-element) for encrypted card capture.
* Use [Guide: Card + 3DS Checkout](/rinne-js/guides/card-3ds-checkout) for frontend 3DS orchestration.
* Use [3D Secure authentication](/guides/three-d-secure-authentication) for backend 3DS API contracts.

<Check>
  Your card integration is production-ready when you correctly handle `PROCESSING`, `AWAITING_3DS`, `APPROVED`, `REFUSED`, and the `CHALLENGE_NOT_ALLOWED` status reason.
</Check>
