spicyapiDocumentação
Desenvolver com a API
Conteúdo principal

Idempotência

Repita uma solicitação após um tempo limite sem gerar nem pagar duas vezes.

Uma chave de idempotência é um identificador que você associa a uma operação, como “gerar o vídeo do pedido 8814”. Se a mesma solicitação com esse identificador chegar duas vezes, executamos o trabalho apenas uma vez.

createTask recebe a chave no cabeçalho Idempotency-Key. Enviar duas vezes com a mesma chave, pela mesma conta e com a mesma chave de API, retorna a tarefa criada na primeira vez: não há uma segunda entrada na fila, reserva de saldo ou cobrança.

POST /api/v1/jobs/createTask
Idempotency-Key: order-8814-render-1

A chave fica vinculada ao que você enviou: o modelo, todo o input e o callBackUrl. A ordem dos campos e os espaços em branco não importam, mas qualquer alteração real importa. Enviar a mesma chave com uma solicitação diferente retorna 409, com Idempotency-Key has already been used for a different request. Repetir esse 409 nunca resolve o problema: se a solicitação mudou de fato, ela é uma nova operação e precisa de outra chave.

Por que usar

Um tempo limite de rede deixa você sem saber o que aconteceu: a solicitação pode nunca ter chegado, ou pode ter chegado e apenas a resposta se perdeu. Sem chave de idempotência, restam duas opções ruins: não tentar novamente, deixando o usuário esperar por nada, ou tentar novamente, correndo o risco de gerar e pagar duas vezes.

Com uma chave, a repetição é segura: a tarefa é criada ou você recebe a que foi criada instantes antes.

Quais endpoints leem o cabeçalho

EndpointO que acontece ao repetir com a mesma chave
POST /api/v1/jobs/createTaskRetorna a tarefa original em seu estado atual
POST /api/v1/jobs/retryRetorna a tarefa criada pela nova tentativa. O escopo da chave é a tarefa original; usar a mesma chave com tarefas originais diferentes não causa conflito
POST /v1/videosRetorna a tarefa original
POST /api/v1/jobs/stream e endpoints de texto em /v1 e /v1betaRetorna 400; veja abaixo

Os endpoints de leitura e os que emitem links de upload e download ignoram o cabeçalho. POST /api/v1/jobs/purge não precisa de chave: repeti-lo para o mesmo taskId já é inofensivo.

Em endpoints de texto e streaming, a chave evita uma cobrança duplicada, mas não recupera uma resposta perdida

A resposta de uma solicitação de texto é enviada à medida que é gerada e não é armazenada. Repetir uma chave cuja solicitação já foi concluída, com o mesmo corpo, retorna 400 (This Idempotency-Key was already used for a completed request, and its answer cannot be replayed. Use a new Idempotency-Key to send a new request) sem nova cobrança; o texto não pode ser recuperado em nenhum lugar. Se a solicitação anterior falhou, a repetição retorna o mesmo erro, para que você decida se deve tentar novamente com outra chave. Com um corpo diferente, a chave retorna 409.

Portanto, nesses endpoints, use uma nova chave para cada nova mensagem e reutilize uma chave somente para repetir exatamente a mesma solicitação após um erro de rede, aceitando que sua resposta foi perdida. Se quiser uma resposta novamente, envie a solicitação com outra chave e pague por ela. Veja Texto e streaming.

O prazo é de 24 horas

Uma chave permanece válida por 24 horas a partir do momento em que foi aceita pela primeira vez. As repetições não estendem esse prazo. Depois disso, a mesma chave é tratada como uma solicitação nova e cria uma tarefa nova e cobrada; encerre as tentativas automáticas bem antes desse prazo.

Vinte e quatro horas é um equilíbrio entre necessidades. As tentativas dos clientes terminam em segundos ou minutos, então basta que o prazo cubra esse intervalo; por outro lado, quanto maior o prazo, mais tempo a chave fica indisponível para reutilização. Com uma chave fixa no código, você só conseguiria enviar uma tarefa por dia.

Como escolher as chaves

  • Uma chave por solicitação lógica, não por chamada HTTP. As repetições precisam reutilizar a mesma chave; caso contrário, o mecanismo perde sua função.
  • Derive a chave de um identificador que você já tem: order-8814-render-1 é melhor que um UUID aleatório, pois você consegue reconstruí-la após reiniciar o processo.
  • Quando não houver um identificador de origem, gere um UUID antes da primeira tentativa e salve-o, por exemplo no banco de dados, junto ao pedido correspondente; as novas tentativas leem o valor salvo.
  • As chaves são registradas dentro da sua conta e não precisam ser globalmente únicas, mas todas as tentativas de uma solicitação lógica também precisam usar a mesma chave de API. O tamanho máximo é de 128 caracteres.

Outra chave de API da conta não pode recuperar a tarefa por repetição

O registro da chave de idempotência vale para a conta inteira, mas a visibilidade das tarefas é isolada por chave de API. Outra chave de API que usar uma chave de idempotência já registrada recebe 409 Conflict (Idempotency-Key is already in use by this account), e a resposta não revela o ID da tarefa original. Isso impede que uma chave investigue as tarefas de outra integração por repetição. Tentar novamente não ajuda; envie a repetição com a chave de API usada na primeira tentativa.

Nunca gere a chave dentro do laço de tentativas

// Wrong: a new key per attempt, which defeats the whole mechanism
for (let i = 0; i < 3; i++) {
  await create({ 'Idempotency-Key': crypto.randomUUID() });
}

// Right: generate once, outside the loop
const key = crypto.randomUUID();
for (let i = 0; i < 3; i++) {
  await create({ 'Idempotency-Key': key });
}

Como é uma solicitação repetida

Em createTask, a resposta contém o mesmo taskId e estimatedCost, mas state é o estado atual da tarefa original. Ele pode ser queued, running ou um estado final. A repetição não cria outra tarefa, reserva de saldo ou cobrança.

Nenhum cabeçalho ou campo indica que você recebeu uma repetição. Para ler o registro completo da tarefa, use o taskId em recordInfo.

Envios simultâneos com a mesma chave

Quando duas solicitações da mesma chave de API, com a mesma chave de idempotência, chegam ao mesmo tempo, exatamente uma cria uma tarefa e a outra recebe o mesmo taskId. A decisão se apoia em uma restrição de unicidade no banco de dados, em vez de uma sequência de verificar e depois inserir: sob concorrência, ambas as verificações poderiam não encontrar nada, as duas tarefas seriam criadas e as duas reservas seriam mantidas.

Em casos raros, essa disputa aparece como:

{ "code": 409, "msg": "Idempotency key conflict, please retry shortly" }

Aguarde algumas centenas de milissegundos e tente novamente com a mesma chave.

Outros dois erros 409 em createTask não devem ser repetidos: Idempotency-Key has already been used for a different request, quando você alterou a solicitação, e Idempotency-Key is already in use by this account, quando outra chave de API da conta já usou essa chave. Os três compartilham code: 409, e seu código não deve tomar decisões com base em msg; portanto, permita no máximo uma ou duas novas tentativas para 409 e depois apresente o erro para análise humana.

Exemplo completo

A função recebe a chave como argumento. Calcule-a com base nos seus próprios registros, aqui o ID do pedido, para que um processo reiniciado envie a mesma chave novamente.

JavaScript
// These are business codes from the response body, not HTTP statuses.
const RETRYABLE = new Set([429, 500, 50301]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const backoff = (attempt) => sleep(2 ** attempt * 500 + Math.random() * 300);

async function createTaskWithRetry(payload, idempotencyKey, { attempts = 4 } = {}) {
  let conflicts = 0;

  for (let i = 0; i < attempts; i++) {
    // Declared outside try so the checks below can read them.
    let res;
    let body;
    try {
      res = await fetch('https://api.spicyapi.ai/api/v1/jobs/createTask', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.SPICY_API_KEY}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': idempotencyKey,
        },
        body: JSON.stringify(payload),
      });
      body = await res.json();
    } catch {
      // Transport failure or unreadable body: the request may well have landed.
      // Retrying with the same key is safe.
      await backoff(i);
      continue;
    }

    if (res.status === 202 && body.code === 200) return body.data;

    // Only one of the three 409s is a transient race, so cap 409 retries at one.
    const retry = body.code === 409 ? ++conflicts <= 1 : RETRYABLE.has(body.code);
    if (!retry) {
      throw new Error(`${body.code} ${body.msg} (request_id=${body.request_id})`);
    }
    await backoff(i);
  }
  // Still unknown whether the task exists. Try again later with the SAME key.
  throw new Error(`out of retries for Idempotency-Key ${idempotencyKey}`);
}

// payload is { model, input } built from the live catalog; order is your own record.
// The key is derived from the order, so it is identical after a restart.
const task = await createTaskWithRetry(payload, `order-${order.id}-render-1`);
console.log(task.taskId);
Python
import os, random, time, requests

# These are business codes from the response body, not HTTP statuses.
RETRYABLE = {429, 500, 50301}


def create_task_with_retry(payload: dict, idempotency_key: str, attempts: int = 4) -> dict:
    conflicts = 0

    for i in range(attempts):
        try:
            res = requests.post(
                "https://api.spicyapi.ai/api/v1/jobs/createTask",
                headers={
                    "Authorization": f"Bearer {os.environ['SPICY_API_KEY']}",
                    "Content-Type": "application/json",
                    "Idempotency-Key": idempotency_key,
                },
                json=payload,
                timeout=30,
            )
            body = res.json()
        except (requests.RequestException, ValueError):
            # Transport failure or unreadable body: the request may well have landed.
            # Retrying with the same key is safe.
            time.sleep(2**i * 0.5 + random.random() * 0.3)
            continue

        if res.status_code == 202 and body["code"] == 200:
            return body["data"]

        # Only one of the three 409s is a transient race, so cap 409 retries at one.
        if body["code"] == 409:
            conflicts += 1
            retry = conflicts <= 1
        else:
            retry = body["code"] in RETRYABLE
        if not retry:
            raise RuntimeError(f'{body["code"]} {body["msg"]} (request_id={body.get("request_id")})')
        time.sleep(2**i * 0.5 + random.random() * 0.3)

    # Still unknown whether the task exists. Try again later with the SAME key.
    raise RuntimeError(f"out of retries for Idempotency-Key {idempotency_key}")


# payload is {"model": ..., "input": ...} built from the live catalog; order_id is yours.
# The key is derived from the order, so it is identical after a restart.
task = create_task_with_retry(payload, f"order-{order_id}-render-1")
print(task["taskId"])

Falhas de transporte também exigem a mesma chave

Uma exceção em fetch, um tempo limite de conexão ou um tempo limite de leitura: em todos esses casos, você não sabe se a solicitação chegou. É justamente para isso que serve a chave de idempotência: repita com a mesma chave, e o pior resultado será receber a tarefa anterior. Se acabar o número de tentativas, mantenha a chave: uma chamada posterior ainda retornará a mesma tarefa dentro do prazo de 24 horas.

Nesta página