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

# Idempotência

> Como evitar duplicação de operações em retries e falhas de rede usando chaves naturais e padrões client-side.

Idempotência é a propriedade que garante que **executar a mesma operação
N vezes produz o mesmo efeito que executar uma única vez**. É essencial
para integrações robustas que precisam fazer retry em caso de timeout,
queda de conexão ou erro 5xx.

A API Pontua **não oferece um header `Idempotency-Key` server-side hoje**.
As recomendações abaixo são **boas práticas client-side** que evitam
duplicação no seu lado.

## Quando importa

Operações que mudam estado (POST/PATCH/DELETE) e podem causar **dano se
duplicadas**. Exemplos típicos:

* Criar colaborador duplicado (CPF idêntico)
* Registrar batida duplicada
* Disparar geração de relatório 2x e gastar processamento à toa

GET é naturalmente idempotente (não muda estado).

## Padrão: chaves naturais

Quando o recurso tem uma **chave única natural**, use ela para verificar
se já existe antes de criar.

### Exemplo: Colaborador (chave natural = CPF)

```javascript theme={null}
async function upsertColaborador(dados) {
  // 1. Tenta buscar por CPF
  const buscaResp = await fetch(
    `https://api.pontua.com.br/colaborador/${dados.cpf}/cpf`,
    { headers: { Authorization: `Bearer ${TOKEN}` } },
  )

  if (buscaResp.ok) {
    // Já existe — atualiza
    const existente = await buscaResp.json()
    return fetch(`https://api.pontua.com.br/colaborador/${existente.id}`, {
      method: 'PATCH',
      headers: {
        Authorization: `Bearer ${TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(dados),
    })
  }

  // 2. Não existe — cria
  return fetch('https://api.pontua.com.br/colaborador', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(dados),
  })
}
```

Ao retentar essa função, ela **não duplica** — porque a primeira coisa
que faz é checar se o CPF já existe.

## Padrão: tabela de intenção no cliente

Quando não há chave natural óbvia (ex.: criar registro de ponto pode
ser ambíguo), persiste a intenção **antes** de enviar:

```javascript theme={null}
async function criarRegistroPontoIdempotente(dados) {
  const idempotencyKey = crypto.randomUUID()

  // 1. Persiste intenção localmente
  await db.intencao.create({
    data: {
      idempotencyKey,
      operacao: 'criar_registro_ponto',
      payload: dados,
      status: 'pendente',
    },
  })

  // 2. Envia
  try {
    const resp = await fetch('https://api.pontua.com.br/registro-ponto', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(dados),
    })

    if (resp.ok) {
      const { id } = await resp.json()
      await db.intencao.update({
        where: { idempotencyKey },
        data: { status: 'sucesso', recursoId: id },
      })
      return id
    }
    throw new Error(`HTTP ${resp.status}`)
  } catch (err) {
    await db.intencao.update({
      where: { idempotencyKey },
      data: { status: 'erro', erro: String(err) },
    })
    throw err
  }
}

// Antes de retentar, verifique a intenção
async function criarRegistroPontoComRetry(dados) {
  const intencaoExistente = await db.intencao.findFirst({
    where: { operacao: 'criar_registro_ponto', payload: dados, status: 'sucesso' },
  })
  if (intencaoExistente) return intencaoExistente.recursoId

  return criarRegistroPontoIdempotente(dados)
}
```

## Backoff exponencial em retries

Quando precisar retentar (timeout, 5xx), espere progressivamente:

```javascript theme={null}
async function comBackoff(fn, maxAttempts = 5) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn()
    } catch (err) {
      const isRetriable = err.status >= 500 || err.code === 'ETIMEDOUT'
      if (!isRetriable || attempt === maxAttempts - 1) throw err

      // 1s, 2s, 4s, 8s, 16s + jitter aleatório
      const delayMs = Math.pow(2, attempt) * 1000 + Math.random() * 500
      await new Promise((r) => setTimeout(r, delayMs))
    }
  }
}
```

## Veja também

* [Erros](/conceitos/erros) — quais status codes são retriables
* [Rate Limits](/conceitos/rate-limits) — boas práticas de throttling
