Saltar para o conteúdo
Última atualização

A idempotência permite a resubmissão segura de solicitações, eliminando o risco de duplicar operações involuntariamente.

O Que é Idempotência?

A idempotência garante que fazer a mesma chamada à API múltiplas vezes tem o mesmo efeito que fazê-la uma vez. Isto é crucial quando:

  • Ocorrem timeouts de rede
  • Não tem certeza se uma solicitação foi processada
  • Precisa tentar novamente solicitações falhadas de forma segura

Como Funciona

Para fazer uma solicitação idempotente, inclua um cabeçalho Idempotency-Key com um identificador único:

curl -L -X POST 'https://api.test.easypay.pt/2.0/single' \
  -H 'AccountId: 2b0f63e2-9fb5-4e52-aca0-b4bf0339bbe6' \
  -H 'ApiKey: eae4aa59-8e5b-4ec2-887d-b02768481a92' \
  -H 'Idempotency-Key: 435e08a0-e5a9-4216-acb5-44d6b96de612' \
  -H 'Content-Type: application/json' \
  --data-raw '{
    "type": "sale",
    "value": 10.00,
    "currency": "EUR",
    "method": "cc"
  }'

O Que Acontece

  1. Primeira Solicitação: A Easypay processa a solicitação e armazena a resposta com a chave de idempotência
  2. Solicitações Duplicadas: Solicitações subsequentes com a mesma chave retornam a resposta armazenada sem reprocessar
  3. Payload Diferente: Se o corpo da solicitação diferir, é retornado um erro para prevenir uso acidental incorreto

Detetar Solicitações Repetidas

Quando uma solicitação é uma repetição, a resposta inclui um cabeçalho Idempotency-Replay:

Idempotency-Replay: true

Isto ajuda-o a distinguir entre solicitações originais e repetidas na lógica da sua aplicação.

Gerar Chaves de Idempotência

Boas Práticas:

  • Use UUIDs (v4) ou ULIDs para chaves de idempotência
  • Gere uma nova chave para cada operação única
  • Comprimento máximo da chave: 50 caracteres
  • As chaves devem ter entropia suficiente para prevenir colisões

Exemplos:

// JavaScript (usando UUID v4)
const { v4: uuidv4 } = require('uuid');
const idempotencyKey = uuidv4();
# Python (usando UUID v4)
import uuid
idempotency_key = str(uuid.uuid4())
// PHP (usando ramsey/uuid)
use Ramsey\Uuid\Uuid;
$idempotencyKey = Uuid::uuid4()->toString();

Comportamento da Chave

Chaves Específicas da Conta

Duas solicitações com a mesma Idempotency-Key mas diferentes cabeçalhos AccountId são tratadas como solicitações diferentes.

Solicitação 1: AccountId: account-1, Idempotency-Key: key-123
Solicitação 2: AccountId: account-2, Idempotency-Key: key-123
Resultado: Ambas as solicitações são processadas independentemente

Comparação do Corpo da Solicitação

A camada de idempotência compara o corpo da solicitação das solicitações recebidas com o original:

  • Mesmo corpo: Retorna a resposta em cache
  • Corpo diferente: Retorna um erro para prevenir uso acidental incorreto
{
  "status": "error",
  "message": "Idempotency Error: Request body differs from original request",
  "code": "IDEMPOTENCY_MISMATCH"
}

Erros Transitórios

As chaves de idempotência não são armazenadas para erros transitórios, permitindo-lhe tentar novamente com segurança:

  • 429 Too Many Requests
  • 502 Bad Gateway
  • 503 Service Unavailable

Para estes erros, pode tentar novamente com a mesma chave de idempotência.

Expiração da Chave

As chaves de idempotência são automaticamente removidas do sistema 24 horas após a criação. Após a expiração, a mesma chave pode ser usada para uma nova solicitação.

Quando Usar Idempotência

Operações Suportadas

As chaves de idempotência são suportadas em:

  • Solicitações POST
  • Solicitações PATCH

Não Necessário Para

  • Solicitações GET (inerentemente idempotentes)
  • Solicitações DELETE (inerentemente idempotentes)

Casos de Uso

Essencial:

  • Criação de pagamentos
  • Processamento de reembolsos
  • Qualquer operação que modifique dados financeiros

Recomendado:

  • Qualquer solicitação POST ou PATCH
  • Operações que criam ou atualizam recursos
  • Solicitações que podem ser tentadas novamente

Estratégia de Retentativa com Idempotência

Ao usar chaves de idempotência, siga esta estratégia de retentativa:

async function safeRequest(url, payload, maxRetries = 3) {
  const idempotencyKey = generateUUID();

  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'AccountId': process.env.EASYPAY_ACCOUNT_ID,
          'ApiKey': process.env.EASYPAY_API_KEY,
          'Idempotency-Key': idempotencyKey,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(payload)
      });

      const shouldRetry = response.headers.get('X-Easypay-Should-Retry');

      if (response.ok) {
        return await response.json();
      }

      if (shouldRetry === 'true' && i < maxRetries - 1) {
        await sleep(Math.pow(2, i) * 1000); // Backoff exponencial
        continue;
      }

      throw new Error(`Request failed: ${response.status}`);

    } catch (error) {
      if (i < maxRetries - 1) {
        await sleep(Math.pow(2, i) * 1000);
        continue;
      }
      throw error;
    }
  }
}

Boas Práticas

  1. Use Sempre para Pagamentos: Inclua chaves de idempotência em todas as solicitações relacionadas com pagamentos
  2. Gere Chaves Novas: Crie uma nova chave de idempotência para cada operação única
  3. Armazene Chaves: Guarde chaves de idempotência na sua base de dados para rastrear quais operações foram concluídas
  4. Verifique o Cabeçalho de Repetição: Use o cabeçalho Idempotency-Replay para detetar solicitações repetidas
  5. Respeite a Expiração: Não reutilize chaves após 24 horas
  6. Trate Incompatibilidades: Se receber um erro de incompatibilidade de idempotência, gere uma nova chave

Exemplo: Criar um Pagamento com Segurança

const axios = require('axios');
const { v4: uuidv4 } = require('uuid');

async function createPayment(paymentData) {
  const idempotencyKey = uuidv4();

  try {
    const response = await axios.post(
      'https://api.prod.easypay.pt/2.0/single',
      paymentData,
      {
        headers: {
          'AccountId': process.env.EASYPAY_ACCOUNT_ID,
          'ApiKey': process.env.EASYPAY_API_KEY,
          'Idempotency-Key': idempotencyKey,
          'Content-Type': 'application/json'
        }
      }
    );

    // Verificar se esta foi uma solicitação repetida
    const wasReplayed = response.headers['idempotency-replay'] === 'true';

    if (wasReplayed) {
      console.log('Este pagamento já foi processado');
    }

    return response.data;

  } catch (error) {
    // Verificar se devemos tentar novamente
    const shouldRetry = error.response?.headers['x-easypay-should-retry'];

    if (shouldRetry === 'true') {
      // Tentar novamente com a mesma chave de idempotência
      // ... implementar lógica de retentativa
    }

    throw error;
  }
}

Próximos Passos