Idempotência
Redes falham. Se o seu POST para criar um agendamento estourar o tempo, você não sabe se ele foi gravado. Com
Idempotency-Key, pode repetir sem medo de agendar duas vezes.
Onde vale
Seção intitulada “Onde vale”Hoje, dois endpoints aceitam o header Idempotency-Key:
| Endpoint | Operação |
|---|---|
POST /api/salons/{salonId}/appointments |
CreateAppointment — criar agendamento |
POST /api/salons/{salonId}/customers |
CreateCustomer — cadastrar cliente |
Nos outros endpoints o header é ignorado. As ações sobre um agendamento que já existe não criam nada: remarcar de
novo para o mesmo horário dá o mesmo resultado, e cancelar ou mudar o status de novo responde 409 (transição
inválida) se a primeira tentativa já tinha dado certo — na dúvida, confira com
GET /api/salons/{salonId}/appointments/{appointmentId}. Quando outro endpoint passar a aceitar a chave, isso vai
aparecer na referência e no changelog.
Como usar
Seção intitulada “Como usar”Gere uma chave única por operação — um UUID v4 serve — e reenvie a mesma chave com o mesmo corpo em todas as tentativas daquela operação.
CHAVE=$(uuidgen) # gere uma vez por operação e guarde junto com o pedido
curl -s -i "https://api.pegavez.com.br/api/salons/$SALON_ID/customers" \ -H "Authorization: Bearer $PEGAVEZ_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $CHAVE" \ -d '{ "name": "Maria Souza", "phone": "+5511999998888" }'A chave tem de 1 a 255 caracteres visíveis (ASCII de ! a ~, sem espaço) e vem num único header. Fora disso, a
resposta é 400 com code: "idempotency_key_invalid".
O que acontece em cada caso
Seção intitulada “O que acontece em cada caso”| Situação | Resposta |
|---|---|
| Primeira vez com a chave | Processa normalmente e guarda a resposta. |
| Mesma chave, mesmo corpo, primeira já terminou | Devolve a resposta guardada (mesmo status, corpo e Location) com o header Idempotency-Replayed: true, sem executar de novo. |
| Mesma chave, primeira ainda em andamento | 409 com code: "idempotency_in_progress": espere alguns segundos e repita. Se a primeira não terminar em 2 minutos (ex.: o servidor caiu no meio) sem ter gravado nada, a repetição com o mesmo corpo assume a chave e roda. |
| Mesma chave, a primeira gravou mas não respondeu e passaram 2 minutos | 409 com code: "idempotency_unknown_result": a operação chegou a gravar, mas a resposta se perdeu. Nada é executado de novo. Consulte o recurso (ex.: liste os agendamentos do dia ou busque o cliente pelo telefone); se ele não estiver lá, repita com uma chave nova. |
| Mesma chave com corpo diferente ou em outro endpoint | 422 com code: "idempotency_mismatch": a chave já foi usada para outra operação. |
| Chave inválida (vazia, com espaço, longa demais, header repetido) | 400 com code: "idempotency_key_invalid". |
| Sem o header | Processa normalmente, sem proteção contra repetição. |
HTTP/1.1 201 CreatedContent-Type: application/json; charset=utf-8Location: /api/salons/3f6c2b1e-8d4a-4c4e-9b7a-2a5d0e9f1c11/customers/c2d8b4e6-7a1f-4e9c-b3d5-8f6a0e2c4b17Idempotency-Replayed: true{ "type": "https://tools.ietf.org/html/rfc9110#section-15.5.10", "title": "Uma requisição com esta Idempotency-Key ainda está em andamento. Tente de novo em instantes.", "status": 409, "code": "idempotency_in_progress", "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}O que fica guardado
Seção intitulada “O que fica guardado”- Ficam guardadas as respostas
2xxe4xx, menos409e429. Um400de validação, por exemplo, volta igual se você repetir a mesma chave com o mesmo corpo: corrija o corpo e use uma chave nova. - Não ficam guardados
409,429,5xxe erros de regra de negócio (como horário ocupado): a chave é apagada e a próxima tentativa com ela roda de novo, do zero. - Se a sua conexão cair depois que o Pega Vez gravou, a resposta fica guardada mesmo assim: repita com a mesma chave para recebê-la.
- Num erro inesperado no meio, a chave fica em andamento (
409idempotency_in_progress) por até 2 minutos, para não criar em dobro. Passado esse prazo, o Pega Vez sabe se a primeira tentativa chegou a gravar: se não gravou, a repetição roda; se gravou, mas a resposta se perdeu, a repetição recebe409idempotency_unknown_resulte não executa de novo — nesse caso não há resposta guardada para devolver, então confira o recurso com umGET.
- Validade de 24 horas. A chave e a resposta ficam guardadas por 24 h a partir da primeira requisição. Depois disso, a mesma chave é tratada como nova.
- Por token. As chaves de um token não colidem com as de outro, nem com as do painel.
- O corpo é comparado byte a byte (por um hash SHA-256). Para repetir, reenvie exatamente os mesmos bytes; não monte o JSON de novo com outra ordem de campos ou outro horário calculado.
- Uma chave por operação. Não reaproveite a chave de um agendamento para outro, mesmo que os dados sejam parecidos.
Exemplo de repetição segura
Seção intitulada “Exemplo de repetição segura”import { randomUUID } from 'node:crypto';
async function criarAgendamento(salonId, dados, tentativas = 4) { const chave = randomUUID(); const corpo = JSON.stringify(dados); // mesmo corpo em todas as tentativas
let ultima; for (let i = 0; i < tentativas; i++) { try { ultima = await fetch(`https://api.pegavez.com.br/api/salons/${salonId}/appointments`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.PEGAVEZ_TOKEN}`, 'Content-Type': 'application/json', 'Idempotency-Key': chave, }, body: corpo, signal: AbortSignal.timeout(15_000), }); } catch { await esperar(2 ** i * 1000); // rede ou timeout: repete com a mesma chave continue; }
if (ultima.status === 429) { await esperar(Number(ultima.headers.get('Retry-After') ?? 5) * 1000); continue; } if (ultima.status >= 500 || (await codigo(ultima)) === 'idempotency_in_progress') { await esperar(2 ** i * 1000); continue; } return ultima; // 2xx (talvez repetida: Idempotency-Replayed) ou erro definitivo (400, 403, 409 de horário, 422...) } if (ultima) return ultima; throw new Error('Não foi possível falar com a API do Pega Vez.');}
// Lê o "code" do Problem Details sem consumir o corpo da resposta original.async function codigo(resposta) { if (resposta.status !== 409) return undefined; const problema = await resposta.clone().json().catch(() => ({})); return problema.code;}
const esperar = (ms) => new Promise((r) => setTimeout(r, ms));