# Cartão: como funciona

> Checkout de cartão embutido na sua página, comum às adquirentes Sterling, Imperium, NovaCorp, Eminence, Apex, ShadowBank e Vortex, com escolha da adquirente por chamada e venda em 48 moedas.

Fonte: https://app.thronecorporate.com/docs/cartao

## Visão geral

O cartão do Throne é um checkout **embutido na sua página**. O comprador digita o cartão dentro de um elemento do Throne (o Throne Elements), sem sair do seu site. O seu servidor nunca toca no número do cartão: não envie número, validade nem CVV em nenhuma chamada da API.

O checkout comum é o `POST /elements/checkout`, e o script é um só, o `throne-elements.js`. Para o comprador brasileiro, com a página em português e o CPF pedido na tela, existe também o [Checkout Brasil](/docs/cartao/brasil), com o mesmo script. Por trás, a cobrança passa por uma **adquirente de cartão habilitada na sua conta**: Sterling, Imperium, NovaCorp, Eminence, Apex ou ShadowBank. Com mais de uma habilitada, cada chamada escolhe a sua pelo campo `acquirer` (veja [escolher a adquirente](#escolher-a-adquirente)). Campos, respostas, erros e webhook são os mesmos nas seis. O que muda é o comportamento do checkout:

| Adquirente | Valor de `acquirer` | Verificação do banco | Navegador de app (Instagram, Facebook) | Tentativas por checkout | Guia |
|---|---|---|---|---|---|
| Sterling | `sterling` | Não tem: aprova ou recusa na hora | Tudo dentro do elemento | 5 | [Sterling](/docs/cartao/sterling) |
| Imperium | `imperium` | Não tem: o resultado sai na própria tentativa (uma recusa pode levar até 25 segundos) | Tudo dentro do elemento | 5 | [Imperium](/docs/cartao/imperium) |
| NovaCorp | `novacorp` | Abre uma janela de verificação; a tela do banco aparece nela quando ele pede | A própria aba vai para a página segura de pagamento, sem evento na sua página | 6 | [NovaCorp](/docs/cartao/novacorp) |
| Eminence | `eminence` | Pode pedir; a tela do banco aparece dentro do próprio elemento | Tudo dentro do elemento | 5 | [Eminence](/docs/cartao/eminence) |
| Apex | `apex` | Não tem: aprova ou recusa na hora | Tudo dentro do elemento | 5 | [Apex](/docs/cartao/apex) |
| ShadowBank | `shadowbank` | Não tem: aprova ou recusa na hora | Tudo dentro do elemento | 5 | [ShadowBank](/docs/cartao/shadowbank) |
| Vortex | `vortex` | Não tem: aprova ou recusa na hora | Tudo dentro do elemento | 5 | [Vortex](/docs/cartao/vortex) |

O máximo por venda depende da adquirente e varia com a moeda e com a cotação do dia. A comparação completa, item por item, está em [Comparar adquirentes](/docs/cartao/comparar).

## Escolher a adquirente

Com mais de uma adquirente habilitada, cada site (source) escolhe a dela com o campo `acquirer` no `POST /elements/checkout`:

```bash
curl -X POST https://app.thronecorporate.com/api/v1/elements/checkout \
  -H "Authorization: Bearer $THRONE_PK:$THRONE_SK" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "amount_cents": 4990, "currency": "chf", "acquirer": "novacorp" }'
```

- **Sem o campo**, vale a adquirente padrão da conta, como sempre foi (ou, com o [roteamento por valor](/docs/cartao/escolher#roteamento-por-valor) ligado, outra habilitada que aceite o valor).
- Os valores são `sterling`, `imperium`, `novacorp`, `eminence`, `apex`, `shadowbank` e `vortex`. Só vale uma adquirente habilitada na sua conta; outra volta `422 acquirer_not_enabled` e nada é criado.
- **Com o campo, o Throne nunca troca a adquirente que você pediu por outra.**
- A resposta traz `acquirer` com a adquirente que ficou com a venda. A venda fica nela até o fim.

As habilitadas aparecem no painel, em **Documentação da API**, e em `GET /elements/acquirers`, que também mostra o mínimo e o máximo por venda na moeda que você pedir. Tudo sobre a escolha, os limites, o roteamento por valor e a adquirente em pausa está em [Escolher a adquirente](/docs/cartao/escolher).

## Passo a passo

1. O **seu servidor** cria o checkout com `POST /elements/checkout`, na moeda do comprador, quando ele clica em comprar. Guarde o `transaction_id`.
2. A **sua página** coloca o container do Throne Elements com esse `transaction_id` e carrega o script do Throne.
3. O comprador paga dentro do elemento.
4. O Throne envia o webhook `transaction.paid`. Libere o pedido só nesse momento.

## 1. Criar o checkout

A criação fala com a adquirente. Use **60 segundos** de timeout no seu cliente HTTP e gere a `Idempotency-Key` antes de chamar, guardando junto do pedido.

```bash cURL
curl -X POST https://app.thronecorporate.com/api/v1/elements/checkout \
  --max-time 60 \
  -H "Authorization: Bearer $THRONE_PK:$THRONE_SK" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "amount_cents": 4990,
    "currency": "usd",
    "customer": { "name": "Emma Walker", "email": "emma@example.com" }
  }'
```
```php PHP
$chave = bin2hex(random_bytes(16)); // guarde junto do pedido antes de chamar

$ch = curl_init('https://app.thronecorporate.com/api/v1/elements/checkout');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 60,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('THRONE_PK') . ':' . getenv('THRONE_SK'),
        'Content-Type: application/json',
        'Accept: application/json',
        'Idempotency-Key: ' . $chave,
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'amount_cents' => 4990,   // US$ 49,90
        'currency'     => 'usd',
        'customer'     => ['name' => 'Emma Walker', 'email' => 'emma@example.com'],
    ]),
]);
$raw = curl_exec($ch);
if ($raw === false) {
    // sem resposta (conexão caiu ou timeout): repita com a MESMA $chave
}
$checkout = json_decode((string) $raw, true);
// entregue só o transaction_id para a sua página
echo json_encode(['transaction_id' => $checkout['transaction_id'] ?? null]);
```
```js Node.js
import { randomUUID } from 'node:crypto';

const chave = randomUUID(); // guarde junto do pedido antes de chamar

const res = await fetch('https://app.thronecorporate.com/api/v1/elements/checkout', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.THRONE_PK}:${process.env.THRONE_SK}`,
    'Content-Type': 'application/json',
    Accept: 'application/json',
    'Idempotency-Key': chave,
  },
  body: JSON.stringify({
    amount_cents: 4990,
    currency: 'usd',
    customer: { name: 'Emma Walker', email: 'emma@example.com' },
  }),
  signal: AbortSignal.timeout(60000), // num timeout, repita com a MESMA chave
});
const { transaction_id } = await res.json(); // res.status === 201
```
```python Python
import os, uuid, requests

chave = str(uuid.uuid4())  # guarde junto do pedido antes de chamar

r = requests.post(
    "https://app.thronecorporate.com/api/v1/elements/checkout",
    headers={
        "Authorization": f"Bearer {os.environ['THRONE_PK']}:{os.environ['THRONE_SK']}",
        "Accept": "application/json",
        "Idempotency-Key": chave,
    },
    json={"amount_cents": 4990, "currency": "usd",
          "customer": {"name": "Emma Walker", "email": "emma@example.com"}},
    timeout=60,  # num timeout, repita com a MESMA chave
)
transaction_id = r.json()["transaction_id"]  # r.status_code == 201
```

### Campos

| Campo | Obrigatório | Regra |
|---|---|---|
| `amount_cents` | Sim | Inteiro na **menor unidade da moeda** (US$ 49,90 = `4990`). Em moedas sem casa decimal, a unidade inteira (¥ 5000 = `5000`) |
| `currency` | Sim | Código de 3 letras, maiúsculas ou minúsculas. Veja a [lista](/docs/cartao/moeda#moedas-aceitas) |
| `acquirer` | Não | `sterling`, `imperium`, `novacorp`, `eminence`, `apex`, `shadowbank` ou `vortex`. Escolhe a adquirente desta venda entre as habilitadas na conta. Sem o campo, vale a padrão. Veja [escolher a adquirente](#escolher-a-adquirente) |
| `customer.name` | Não, salvo na ShadowBank e na Vortex | Até 255 caracteres (na ShadowBank e na Vortex, até 120). O Throne mantém só letras, espaços, apóstrofo e hífen e corta em 100. Vai pré-preenchido no campo "nome no cartão"; o comprador pode corrigir |
| `customer.email` | Não, salvo na ShadowBank e na Vortex | E-mail do comprador. Um e-mail inválido é descartado. Na ShadowBank, um e-mail inválido ou com mais de 180 caracteres volta `422 customer_required_for_shadowbank`; na Vortex, `422 customer_required_for_vortex` |
| `customer.document` | Não | CPF, CNPJ ou passaporte, de 5 a 20 caracteres, com ou sem pontos, traços e barra. `document_type` (`cpf`, `cnpj` ou `passport`) é opcional: sem ele, 11 dígitos valem como CPF, 14 como CNPJ, e um documento com letras ou com outra quantidade de dígitos, como passaporte |
| `customer.phone` | Não | Objeto `{ "country_code": "55", "area_code": "11", "number": "999998888" }`. Aceita espaço, hífen, `+`, parênteses, ponto e barra; o Throne guarda só os dígitos |
| `customer.address` | Não | `line_1`, `line_2`, `zip_code`, `city`, `state` e `country` (2 letras, ISO 3166; 3 letras é convertido) |

O checkout sempre pede ao comprador **número do cartão, validade, CVV e nome impresso no cartão**, e nada além disso. Os dados de `customer` ficam registrados na venda e voltam no webhook e na consulta já normalizados: documento só com dígitos (passaporte em maiúsculas), `document_type` em maiúsculas e telefone só com dígitos.

Fora da ShadowBank, nenhum campo de `customer` gera erro. Um campo fora do formato é ajustado ou descartado, a venda é criada e a resposta `201` avisa em `warnings`. Só `amount_cents`, `currency`, `acquirer`, `recurrence`, um `one_click_from` fora do formato (texto de até 64 caracteres) e, na ShadowBank, a falta de `customer.name` ou `customer.email` (`customer_required_for_shadowbank`, veja [Cartão com a ShadowBank](/docs/cartao/shadowbank)) podem dar `422`. Este endpoint não aceita `metadata` nem itens: guarde o `transaction_id` para ligar a venda ao seu pedido.

### Resposta

`201 Created`:

```json
{
  "transaction_id": "ae3bf1da-0c9b-4818-9140-c6432152c23e",
  "status": "pending",
  "checkout_url": "https://pay.example.com/checkout/0f1e2d3c4b5a69788796a5b4c3d2e1f0",
  "acquirer": "sterling",
  "display": { "currency": "USD", "amount_cents": 4990 },
  "expires_at": "2026-10-08T14:20:00Z"
}
```

- `acquirer` é a adquirente que ficou com a venda.
- `checkout_url` é o endereço do checkout hospedado. Use como veio: o host e o formato podem mudar. O recomendado é embutir com o Throne Elements.
- `display` repete a moeda e o valor que você mandou. Contas com integração antiga podem não receber esse campo.
- `expires_at` é o fim da validade do checkout, em UTC. Como `display`, pode faltar em contas com integração antiga.
- `warnings` aparece só quando há aviso. No checkout comum, o aviso é sobre um campo de `customer` que foi ajustado ou descartado: cada aviso traz `field`, `code` (`converted`, `inferred` ou `discarded`) e `message`. No [Checkout Brasil](/docs/cartao/brasil), `code` também pode ser `low_contrast`, sobre `appearance`. Serve para log: a venda foi criada.

```json
"warnings": [
  { "field": "customer.address.country", "code": "converted", "message": "País em 3 letras convertido para o código de 2 letras (ISO 3166)." }
]
```

O valor creditado no seu saldo e o `fx_rate` aparecem só quando a venda é paga.

## 2. Embutir na sua página

```html
<!-- onde o checkout deve aparecer -->
<div data-throne-elements
     data-id="ae3bf1da-0c9b-4818-9140-c6432152c23e"
     data-color="#7c3aed"
     data-theme="light"
     data-radius="10"></div>

<script src="https://app.thronecorporate.com/throne-elements.js" async></script>
```

| Atributo | O que faz |
|---|---|
| `data-id` | O `transaction_id` da criação. Precisa ser o UUID exato, senão nada aparece |
| `data-color` | Cor do botão e dos realces, no formato `#rrggbb` |
| `data-theme` | `light` ou `dark`. O elemento também detecta sozinho a cor de fundo do seu site e se ajusta a ela |
| `data-radius` | Arredondamento dos cantos, de `0` a `32` (px). Padrão `10` |

Boas práticas:

- Carregue o script **do endereço acima**. Não hospede uma cópia.
- Não fixe a altura do container nem esconda o overflow.
- Não coloque o elemento dentro de outro iframe com `sandbox`. Se não der para tirar, ele precisa incluir no mínimo `allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation`.
- Em página de uma tela só (React, Vue, etc.), depois de inserir o container chame `window.ThroneElements.mount()`. Para trocar de checkout, use um container novo: mudar o `data-id` de um container já montado não troca o checkout ([detalhes](/docs/cartao/elements#pagina-de-uma-tela-so-react-vue-e-outras)).
- Se o seu site usa Content-Security-Policy, libere `https://app.thronecorporate.com` em `script-src` e em `img-src`, e em `frame-src` libere esse domínio e o domínio do `checkout_url`.

A referência completa do elemento, com navegador de app e página de uma tela só, está em [Throne Elements](/docs/cartao/elements).

### Eventos na sua página

```js
window.addEventListener('throne-elements:ready', () => {
  // elemento montado, pronto para receber o cartão
});
window.addEventListener('throne-elements:paid', (e) => {
  // e.detail.transaction_id: o comprador concluiu. Mostre o "obrigado".
});
window.addEventListener('throne-elements:error', (e) => {
  const code = e.detail.code || e.detail.message; // declined, attempts_exhausted, network...
});
```

Os eventos são **sinais de tela**. A confirmação que vale para liberar o pedido é o webhook `transaction.paid` ou o `GET /transactions/{id}` com `status: "paid"`. Na NovaCorp, em navegador de app, nenhum evento chega à sua página. Os códigos de erro, por adquirente, estão em [Throne Elements](/docs/cartao/elements#codigos-de-erro).

### Idioma do checkout

O checkout sai no idioma do país do comprador, identificado pelo IP. Compradores no Brasil, em Portugal ou de país não identificado veem em inglês. O idioma não é configurado pela API. Para vender ao comprador brasileiro com a página em português e o CPF pedido na tela, use o [Checkout Brasil](/docs/cartao/brasil).

## Moeda e limites

Você manda o valor **na moeda do comprador** e ele paga nessa moeda; não mexa no valor no seu site. O saldo da sua conta é em reais, e a cotação que credita o seu saldo é **fixada na criação**: ela não muda depois, mesmo que o comprador pague horas mais tarde, e volta como `fx_rate` na venda paga.

- São 48 moedas. Em JPY, KRW, VND, CLP, HUF, TWD e PYG, `amount_cents` é a unidade inteira.
- **Mínimo**: varia com a moeda e com a cotação do dia, e algumas adquirentes têm um mínimo próprio, maior. Abaixo dele, `422 invalid_amount` com `min_amount_cents` na moeda que você mandou.
- **Máximo**: depende da adquirente e varia com a moeda e com a cotação do dia. Acima dele, `422 invalid_amount` com `max_amount_cents` na moeda que você mandou. `GET /elements/acquirers?currency=` mostra o máximo atual.
- Na fatura, a compra aparece sem o nome da sua loja. Na NovaCorp e na Eminence, o banco do comprador pode pedir uma confirmação da compra. Avise isso ao lado do botão de pagar.

A lista de moedas, o efeito da cotação no máximo e um exemplo em franco suíço estão em [Moedas e limites](/docs/cartao/moeda).

## 3. Confirmar a venda

### Webhook `transaction.paid`

```json
{
  "event_id": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
  "type": "transaction.paid",
  "version": "2026-05-01",
  "created_at": "2026-10-03T14:31:02Z",
  "data": {
    "transaction_id": "ae3bf1da-0c9b-4818-9140-c6432152c23e",
    "status": "paid",
    "payment_method": "credit_card",
    "amount_cents": 31237,
    "fee_cents": 4148,
    "net_amount_cents": 27089,
    "paid_at": "2026-10-03T14:30:58Z",
    "card_brand": "visa",
    "card_last4": "4242",
    "currency": "CHF",
    "native_amount_cents": 4990,
    "fx_rate": "6.26000000",
    "customer": { "name": "Luca Meier", "email": "luca@example.ch" },
    "shipping_address": null,
    "items": null,
    "metadata": {}
  }
}
```

- `currency` e `native_amount_cents` são a venda na moeda do comprador, como você criou.
- `amount_cents` é o valor **bruto** creditado no seu saldo, em reais (a moeda da sua conta); `net_amount_cents` é o que entra de fato, já descontada a taxa de cartão.
- `fx_rate` é a cotação usada para creditar o seu saldo, fixada na criação (reais por 1 unidade da moeda da venda). **No webhook ele chega como texto com 8 casas** (`"6.26000000"`); no `GET` chega como número. Trate como decimal antes de calcular. Em venda criada em BRL, `fx_rate` é `null`.
- `card_brand` vem em minúsculo (`visa`, `mastercard`, `elo`, `amex`, `hipercard`, entre outras). Bandeira e final do cartão vêm só no webhook.
- O formato é idêntico nas seis adquirentes. Valide a assinatura sobre o corpo cru, como em [Webhooks](/docs/webhooks).

**Conversão de campanha.** Envie a conversão de compra para a plataforma de anúncios pelo seu servidor, ao receber `transaction.paid`, e não só pelo evento `throne-elements:paid` da página. O evento de tela pode não chegar, como no navegador de app da NovaCorp. Veja [Webhooks](/docs/webhooks#conversao-de-campanha-pelo-servidor).

### Consulta

`GET /transactions/{id}` com o `transaction_id`:

- **Antes do pagamento**: `status: "pending"`, `amount_cents`, `fee_cents` e `net_amount_cents` iguais a 0, `fx_rate` e `paid_at` nulos. `currency` e `native_amount_cents` já vêm com o que você mandou.
- **Depois do pagamento**: `status: "paid"`, `amount_cents`, `fee_cents` e `net_amount_cents` preenchidos (o seu saldo, em reais) e `fx_rate` como número.
- `acquirer` traz a adquirente da venda.

## Ciclo da venda

| Status | Quando |
|---|---|
| `pending` | Checkout aberto. **Uma recusa com tentativas sobrando não muda o status**: o comprador pode tentar de novo |
| `paid` | Pagamento confirmado |
| `failed` | O checkout fechou por recusa. A regra muda por adquirente: veja [Status e ciclo de vida](/docs/cartao/status#quando-a-venda-vira-failed) |
| `expired` | O checkout passou da validade (24 horas) sem pagamento |
| `refunded` | Estornada |
| `chargeback` | Contestada pelo comprador |

- `failed` e `expired` não são definitivos: se o pagamento for confirmado depois, a venda passa a `paid` e o webhook é enviado. Libere o pedido **sempre** pelo webhook ou pela consulta, nunca pela falta de erro.
- Para vender de novo depois de `failed` ou `expired`, crie um checkout novo.
- No cartão, o único webhook é `transaction.paid`. Recusa, expiração, estorno e contestação aparecem na consulta.
- **Estorno** não é feito pela API. Fale com o suporte do Throne.

## Dois jeitos de cobrar mais de uma vez

Além da venda única, o mesmo endpoint de criação aceita dois campos opcionais:

- **[Upsell em um clique](/docs/cartao/oneclick)**: `one_click_from` cobra um segundo produto sem pedir o cartão de novo, depois de uma venda aprovada.
- **[Recorrência](/docs/cartao/recorrencias)**: `recurrence` cria a cobrança que se repete sozinha, no ciclo que você escolher.

Os dois dependem da adquirente: a recorrência funciona na Apex e na Vortex, e o upsell em um clique só na Apex. Sem esses campos, nada muda: o checkout é o mesmo de sempre.

## Testar

A resposta `201` só repete a moeda e o valor que você mandou (`display`); o valor creditado no seu saldo e o `fx_rate` aparecem quando a venda é paga. Como o cartão é processado de verdade, o teste de ponta a ponta (ver a venda `paid` com `currency`, `native_amount_cents` e `fx_rate`) usa um cartão de verdade com valor baixo. Os erros que dá para provocar sem custo estão em [Como testar](/docs/testar), e o roteiro de conferência em [Checklist de verificação](/docs/checklist).

## Erros

| Status | `error` | O que fazer | Repetir |
|---|---|---|---|
| `400` | `idempotency_key_required` | Envie o header `Idempotency-Key` | Sim, depois de corrigir |
| `403` | `scope_required` | A chave precisa da permissão `transactions:write` | Com chave nova, depois de corrigir |
| `422` | (validação) | Corrija o campo indicado em `errors` | Com chave nova, depois de corrigir |
| `422` | `currency_not_supported` | Use uma das 48 moedas | Com chave nova, depois de corrigir |
| `422` | `fx_unavailable` | Espere o `Retry-After` | Com chave nova |
| `422` | `invalid_amount` | Abaixo do mínimo ou acima do máximo da adquirente, na moeda enviada e pela cotação do dia. Acima do máximo, use `alternatives` ou divida o pedido | Não com o mesmo valor na mesma adquirente |
| `422` | `card_not_enabled` | A conta não tem cartão habilitado. Fale com o suporte | Não |
| `422` | `invalid_acquirer` | `acquirer` fora da lista. Use `sterling`, `imperium`, `novacorp`, `eminence`, `apex`, `shadowbank` ou `vortex` | Com chave nova, depois de corrigir |
| `422` | `acquirer_not_enabled` | A adquirente pedida não está habilitada na conta. A mensagem lista as habilitadas | Com chave nova, depois de corrigir |
| `422` | `customer_required_for_shadowbank` | Só na ShadowBank: a criação veio sem `customer.name` ou `customer.email`, com e-mail inválido ou fora dos tamanhos (nome até 120 caracteres, e-mail até 180). Nada é criado | Com chave nova, depois de corrigir |
| `422` | `idempotency_key_mismatch` | Mesma `Idempotency-Key` com corpo diferente | Com chave nova |
| `409` | `idempotency_key_processing` | A primeira chamada ainda está em andamento | A mesma chamada, igual. Veja [como repetir com segurança](/docs/comecar#como-repetir-uma-chamada-com-seguranca) |
| `429` | | Limite de requisições | Com chave nova, depois do `Retry-After` |
| `503` | `temporarily_unavailable` | Adquirente em pausa, instável ou sem capacidade. Nada foi criado | Com chave nova, depois do `Retry-After`, ou com outra adquirente |
| `502` | `checkout_failed` | O checkout não foi criado. Veja o campo `retryable` | Só com `"retryable": true`, com chave nova |
| `500` | | Erro inesperado no servidor | A mesma chamada, igual: se nada foi criado, ela roda de novo. Se vier `409 idempotency_key_processing` por mais de 2 minutos, confira no painel antes de usar uma chave nova |

Exemplo de `502`:

```json
{ "error": "checkout_failed", "message": "Não foi possível criar o checkout.", "retryable": false }
```
