# Cobrança e limites



## Modelo de créditos

* O saldo é exibido em **créditos** no dashboard, e todos os valores desta documentação estão em créditos.
* Operações pagas debitam na confirmação — nunca no dry-run.
* As regras de preço de cada ferramenta ficam na página da ferramenta: [QR Dinâmico](/ferramentas/qr-dinamico#custos), [PDF Convert](/ferramentas/pdf-convert#custos), [Base de conhecimento](/ferramentas/base-de-conhecimento#custos), [Áudio](/ferramentas/audio#custos).
* Cada token pode ter um [limite diário de gasto](/autenticacao#controles-da-chave): passar dele responde `billing.spend_limit_reached`.

<Callout title="Preço exato">
  Custos de criação podem somar adicionais (slug customizado, design, retenção de PDF…). Faça um dry-run com o payload real para ver o total exato.
</Callout>

## Dry-run e preview de custo

| Header                 | Efeito                                             |
| ---------------------- | -------------------------------------------------- |
| `X-Dry-Run: true`      | Valida e calcula o custo sem persistir nem debitar |
| `X-Preview-Cost: true` | Alias de `X-Dry-Run`                               |
| `X-Include-Cost: true` | Inclui `meta.cost` na resposta                     |

Dry-run é aceito nas operações pagas marcadas no card de cada endpoint e nas **leituras medidas** (exceto downloads). A resposta é `200` e nada é persistido nem debitado.

As rotas de [Áudio](/ferramentas/audio#dry-run) pedem o dry-run com `?dry_run=true` na query, não com o header. O áudio é cobrado por minuto de áudio, por 1.000 caracteres ou por segundo gerado, e o valor da transcrição, da fala e dos demais jobs é devolvido se o motor falhar.

### Orçamento de uma operação paga

Em `POST` de criação e conversão, `data` traz o orçamento item a item:

```json
{
  "success": true,
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "total_cost_credits": 15000,
    "total_cost_units": 15000,
    "items": [
      { "description": { "key": "billing.pdf.convert_base" }, "cost_credits": 10000, "type": "base" },
      { "description": { "key": "billing.pdf.retention.7" }, "cost_credits": 5000, "type": "storage" }
    ],
    "is_monthly_recurring": false,
    "currency": "CREDITS"
  }
}
```

<Callout type="warn" title="Valores em unidades">
  Apesar do nome, `total_cost_credits` e `cost_credits` do orçamento (e `cost_credits` das conversões de PDF) vêm em **unidades internas**: divida por 10.000 para obter créditos. No exemplo, 15.000 unidades = 1,5 crédito.
</Callout>

No QR Dinâmico o orçamento também traz `slot_usage` (uso de slots e se a criação abrirá um novo slot pago) e `recurring_cost`.

### Preview de uma leitura medida

Em leituras medidas, o dry-run não executa a consulta — mostra apenas a posição na franquia e se a leitura seria cobrada:

```json
{
  "success": true,
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "operation": "qr_dynamic.analytics",
    "metering": {
      "within_quota": true,
      "quota_limit": 2000,
      "quota_used": 125,
      "quota_remaining": 1875,
      "cost_units": 300,
      "would_charge": false
    }
  },
  "meta": {
    "cost": {
      "units": 300,
      "credits": 0.03,
      "charged": false,
      "within_quota": true,
      "quota_limit": 2000,
      "quota_used": 125,
      "quota_remaining": 1875
    }
  }
}
```

Com `X-Include-Cost: true` numa leitura real, o mesmo objeto `meta.cost` acompanha a resposta normal (`charged: true` quando a leitura foi cobrada).

## Idempotência

Envie `X-Idempotency-Key` (UUID recomendado) em qualquer rota de ferramentas:

* a chave vale por **<Limit name="idempotencyTtl" />**;
* mesma chave + mesmo payload → a resposta original é reaproveitada, sem nova cobrança;
* mesma chave + payload diferente → `idempotency.conflict` (409).

Use sempre em `POST` de criação e conversão para que retries de rede não cobrem duas vezes.

Exceção: as rotas da [Base de conhecimento](/ferramentas/base-de-conhecimento#limites) ignoram o header, porque já são idempotentes (conteúdo repetido não é reindexado nem cobrado).

## Rate limit

Cada token de API pode fazer **<Limit name="rateLimitPerMinute" /> requisições por minuto** nas rotas `/v1/tools/*`. Toda resposta informa a situação:

```http
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1712345678
```

Ao exceder, a API retorna `rate_limit.exceeded` (429). Aguarde até `X-RateLimit-Reset` antes de reenviar.

## Repetir com espera (retry)

Repita só o que pode dar certo na tentativa seguinte:

* **`429`**: espere até o horário de `X-RateLimit-Reset` (segundos Unix).
* **`5xx` e falhas de rede**: espere cada vez mais, com um pouco de aleatoriedade (*backoff* exponencial com *jitter*): cerca de 1 s, 2 s, 4 s… até 30 s, no máximo 5 tentativas.
* **Demais `4xx`** (validação, escopo, saldo): não repita. A resposta não muda até você corrigir a requisição.

Em `POST` que cria ou cobra algo, envie **a mesma** `X-Idempotency-Key` em todas as tentativas: se a primeira chegou e só a resposta se perdeu, a repetição devolve o resultado original sem cobrar de novo.

<Tabs groupId="code-language" persist items={['JavaScript', 'Python']}>
  <Tab value="JavaScript">
    ```js
    // Repete 429, 5xx e falhas de rede; devolve na hora os demais erros.
    async function withRetry(send, attempts = 5) {
      for (let attempt = 1; ; attempt++) {
        let response;
        try {
          response = await send();
        } catch (error) {
          if (attempt === attempts) throw error; // falha de rede
        }
        if (response && response.status !== 429 && response.status < 500) return response;
        if (attempt === attempts) return response;

        const reset = Number(response?.headers.get('X-RateLimit-Reset'));
        const wait =
          response?.status === 429 && reset
            ? Math.max(0, reset * 1000 - Date.now())
            : Math.min(2 ** (attempt - 1) * 1000, 30_000) * (0.5 + Math.random());
        await new Promise((resolve) => setTimeout(resolve, wait));
      }
    }

    const key = crypto.randomUUID(); // a mesma chave em todas as tentativas
    const response = await withRetry(() =>
      fetch(url, {
        method: 'POST',
        headers: {
          'X-API-Token': process.env.RESPAWN_API_TOKEN,
          'X-Idempotency-Key': key,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(payload),
      }),
    );
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import os, random, time, uuid
    import requests

    # Repete 429, 5xx e falhas de rede; devolve na hora os demais erros.
    def with_retry(send, attempts=5):
        for attempt in range(1, attempts + 1):
            try:
                response = send()
            except (requests.ConnectionError, requests.Timeout):
                if attempt == attempts:
                    raise
                response = None
            if response is not None and response.status_code != 429 and response.status_code < 500:
                return response
            if attempt == attempts:
                return response
            reset = response.headers.get("X-RateLimit-Reset") if response is not None else None
            if response is not None and response.status_code == 429 and reset:
                wait = max(0, int(reset) - time.time())
            else:
                wait = min(2 ** (attempt - 1), 30) * (0.5 + random.random())
            time.sleep(wait)

    key = str(uuid.uuid4())  # a mesma chave em todas as tentativas
    response = with_retry(lambda: requests.post(
        url,
        json=payload,
        headers={"X-API-Token": os.environ["RESPAWN_API_TOKEN"], "X-Idempotency-Key": key},
        timeout=30,
    ))
    ```
  </Tab>
</Tabs>

## Franquia diária de leituras

Leituras feitas com **token de API** (listagens, status, analytics, downloads…) entram em uma franquia diária compartilhada entre todas as ferramentas:

| Item                    | Valor                                  |
| ----------------------- | -------------------------------------- |
| Leituras grátis por dia | **<Limit name="dailyQuota" />**        |
| Contas premium          | **<Limit name="dailyQuotaPremium" />** |
| Reset                   | <Limit name="quotaReset" />            |

Toda leitura medida devolve a posição na franquia:

```http
X-API-Quota-Limit: 2000
X-API-Quota-Used: 847
X-API-Quota-Remaining: 1153
```

Requisições feitas pelo [dashboard](https://dashboard.respawntech.dev) **não** entram na franquia nem são cobradas.

### Leituras medidas

Depois de esgotar a franquia, cada leitura via API debita:

<MeteredReadsTable />

Operações de escrita sem custo próprio (`PATCH`, ativar, desativar, `DELETE`) e o dry-run de criação são sempre gratuitos.

<Callout type="idea" title="Evite polling">
  A franquia cobre a maioria das integrações. Para acompanhar mudanças de estado, prefira [webhooks](/webhooks) em vez de consultar o status repetidamente.
</Callout>

## Timeouts

* Maioria das rotas: **<Limit name="requestTimeout" />**.
* Downloads de arquivo: **<Limit name="downloadTimeout" />**.
