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

# Idempotência na NyxPag API

> Use Idempotency-Key ou externalId para que retentativas de cobrança e transferência Pix nunca virem operações duplicadas, e saiba quando usar uma chave nova.

Rede cai. Timeout acontece. Você manda uma cobrança, não recebe resposta e não sabe se ela foi criada. Sem idempotência, reenviar pode cobrar duas vezes, ou pior, **transferir duas vezes**.

Com idempotência, você reenvia **a mesma requisição com a mesma chave** e a NyxPag devolve a operação original, sem duplicar.

## Onde é obrigatória

Em toda requisição que cria operação financeira:

* `POST /transactions` (cobrança Pix)
* `POST /transfers` (transferência Pix)

Sem chave, a API responde `400` com `idempotency_key_required`.

## Como enviar a chave

Escolha **uma** das duas formas:

| Forma | Onde | Observação |
| - | - | - |
| `Idempotency-Key` | Cabeçalho | Boa para transferências |
| `externalId` | Corpo | Boa quando você já tem um número de pedido |

Nas duas formas o valor é salvo como o `externalId` da transação. Por isso você consulta a operação depois com `GET /transactions/{id}` usando a própria chave.

A chave tem até **100 caracteres** e aceita letras, números e `. _ : / -`. Fora disso, `400` com `invalid_idempotency_key`.

<Warning>
  Se enviar as duas, os valores precisam ser **iguais**. Valores diferentes retornam `409 idempotency_conflict`.
</Warning>

## O que a API faz

| Situação | Resposta |
| - | - |
| Chave nova | `201 Created`, operação criada |
| Mesma chave, **mesmo** corpo | `200 OK` com a operação original e `idempotent: true` |
| Mesma chave, corpo **diferente** | `409` com `idempotency_conflict` |
| Operação ainda em reconciliação | `202 Accepted`. Não é confirmação |

### O que conta como "mesmo corpo"

A comparação usa os campos que definem a operação. Mudar **qualquer um** deles com a mesma chave dá `409`.

| Operação | Campos comparados |
| - | - |
| Cobrança | `amount`, `payer` (nome, CPF e e-mail), `description`, `coverFee` e `split` |
| Transferência | `amount`, destino (`type` e `value`), tipo da chave Pix, `description` e `coverFee` |

`webhookUrl` **não** entra na comparação. Trocar a URL do webhook numa repetição não gera conflito.

## A regra de ouro: mesma chave para repetir, chave nova para refazer

Existem dois casos que parecem iguais e são opostos.

<Tabs>
  <Tab title="Resultado incerto: MESMA chave">
    A requisição pode ou não ter chegado: timeout, erro de rede, `5xx`, `202`.

    Reenvie **igual**, com a **mesma** chave. Se a primeira chegou, você recebe a original (`200`). Se não chegou, ela é criada (`201`).

    Nunca troque a chave aqui. Uma chave nova cria uma **segunda** operação.
  </Tab>

  <Tab title="Operação terminou: chave NOVA">
    A cobrança expirou (`cancelled`), a transferência falhou (`failed`) e você quer **tentar de novo**.

    A chave antiga continua apontando para a operação encerrada: reenviá-la devolve a mesma transação cancelada, não cria uma nova. Para gerar um novo Pix, use uma chave nova, por exemplo `PEDIDO-1042-t2`.

    Antes, confira que a anterior realmente terminou, para não cobrar duas vezes.
  </Tab>
</Tabs>

<Note>
  A chave **não expira**. Ela vale, na sua conta, enquanto a transação existir. Por isso o ideal é derivá-la do identificador do negócio mais o número da tentativa: `PEDIDO-1042-t1`, `PEDIDO-1042-t2`.
</Note>

## Exemplo: reenviar com segurança

<CodeGroup>
  ```bash cURL theme={"dark"}
  # A mesma chave em duas chamadas: a segunda devolve 200 e idempotent: true
  for i in 1 2; do
    curl -s -o /dev/null -w "%{http_code}\n" \
      -X POST https://api.nyxpag.com.br/v1/transactions \
      -H "Authorization: Bearer $NYXPAG_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: PEDIDO-1042-t1" \
      -d '{"amount":149.90,"payer":{"name":"Maria Silva","document":"52998224725"}}'
  done
  # 201
  # 200
  ```

  ```javascript JavaScript theme={"dark"}
  async function createCharge(orderId, attempt, payload) {
    const key = `${orderId}-t${attempt}`;

    // 1. persista a chave ANTES de chamar a API
    await db.charges.insertIfAbsent({ orderId, attempt, key });

    for (let i = 0; i < 3; i++) {
      try {
        const res = await fetch("https://api.nyxpag.com.br/v1/transactions", {
          method: "POST",
          headers: {
            Authorization: `Bearer ${process.env.NYXPAG_API_KEY}`,
            "Content-Type": "application/json",
            "Idempotency-Key": key, // 2. a MESMA chave em todas as tentativas
          },
          body: JSON.stringify(payload),
        });
        if (res.status < 500) return res.json(); // 200, 201, 202, 4xx
      } catch {
        // erro de rede: tente de novo com a mesma chave
      }
      await new Promise((r) => setTimeout(r, 2 ** i * 1000));
    }
    throw new Error("Sem resposta da NyxPag; reenvie depois com a mesma chave");
  }
  ```

  ```python Python theme={"dark"}
  import os
  import time
  import requests

  def create_charge(order_id: str, attempt: int, payload: dict) -> dict:
      key = f"{order_id}-t{attempt}"

      # 1. persista a chave ANTES de chamar a API
      db.charges.insert_if_absent(order_id=order_id, attempt=attempt, key=key)

      for i in range(3):
          try:
              res = requests.post(
                  "https://api.nyxpag.com.br/v1/transactions",
                  headers={
                      "Authorization": f"Bearer {os.environ['NYXPAG_API_KEY']}",
                      "Idempotency-Key": key,  # 2. a MESMA chave em todas as tentativas
                  },
                  json=payload,
                  timeout=15,
              )
              if res.status_code < 500:
                  return res.json()
          except requests.RequestException:
              pass  # erro de rede: tente de novo com a mesma chave
          time.sleep(2 ** i)

      raise RuntimeError("Sem resposta da NyxPag; reenvie depois com a mesma chave")
  ```

  ```php PHP theme={"dark"}
  <?php
  function createCharge(string $orderId, int $attempt, array $payload): array
  {
      $key = "{$orderId}-t{$attempt}";

      // 1. persista a chave ANTES de chamar a API
      db_charges_insert_if_absent($orderId, $attempt, $key);

      for ($i = 0; $i < 3; $i++) {
          $ch = curl_init('https://api.nyxpag.com.br/v1/transactions');
          curl_setopt_array($ch, [
              CURLOPT_POST => true,
              CURLOPT_RETURNTRANSFER => true,
              CURLOPT_TIMEOUT => 15,
              CURLOPT_HTTPHEADER => [
                  'Authorization: Bearer ' . getenv('NYXPAG_API_KEY'),
                  'Content-Type: application/json',
                  'Idempotency-Key: ' . $key, // 2. a MESMA chave em todas as tentativas
              ],
              CURLOPT_POSTFIELDS => json_encode($payload),
          ]);
          $raw = curl_exec($ch);
          $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);

          if ($raw !== false && $status < 500) {
              return json_decode($raw, true);
          }
          sleep(2 ** $i);
      }

      throw new RuntimeException('Sem resposta da NyxPag; reenvie depois com a mesma chave');
  }
  ```
</CodeGroup>

## Boas práticas

<Steps>
  <Step title="Derive a chave do negócio">
    `PEDIDO-1042-t1`, não um UUID aleatório por requisição. Um UUID novo a cada retry anula a proteção.
  </Step>

  <Step title="Persista antes de enviar">
    Grave a chave no seu banco antes de chamar a API. Se o processo cair no meio, o retry usa a chave gravada.
  </Step>

  <Step title="Trate 200 e 201 como sucesso">
    `201` criou agora, `200` já existia. O restante do tratamento é igual.
  </Step>

  <Step title="Em 409, não troque a chave">
    Trocar para "fazer passar" cria uma operação nova. Descubra o que mudou no corpo, ou consulte a original com `GET /transactions/{id}`.
  </Step>

  <Step title="Nunca reutilize entre operações diferentes">
    Duas cobranças distintas precisam de chaves distintas. Mesma chave com corpo diferente é `409`.
  </Step>
</Steps>

<CardGroup cols={2}>
  <Card title="Erros" icon="triangle-alert" href="/guides/erros">
    Todos os códigos e a estratégia de retry.
  </Card>

  <Card title="Acompanhar transações" icon="search" href="/guides/pix/acompanhar">
    Consulte a original pelo `externalId`.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.