MeuPedido/developer

Boas práticas

Limites de requisição, tabela de erros, datas em UTC, backoff, guarda do segredo e versão do padrão.

Esta página reúne o que uma integração precisa fazer certo para operar sem intervenção: respeitar limites, tratar cada erro do jeito esperado, ler datas sem erro de fuso, repetir só o que pode ser repetido e proteger a credencial.

Limites de requisição

LimiteValorResposta ao exceder
Chamadas autenticadas, por credencial600 por minuto429 com Retry-After: 60
POST /oauth/token, por IP60 por minuto429 com Retry-After: 60
Falhas de autenticação em POST /oauth/token, por credencial10 por minuto429 com Retry-After: 60

Ao exceder, a API responde:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{ "error": "rate_limit_exceeded", "message": "Limite de requisições excedido." }

Como ficar longe do limite:

  • Faça polling em um intervalo fixo. Uma consulta a cada 5 segundos são 12 chamadas por minuto, mais a busca de cada pedido e a confirmação dos eventos. Uma loja movimentada fica bem abaixo de 600.
  • Reutilize o token. Ele vale por 3600 segundos. Peça um novo só quando faltar pouco para expirar ou quando receber 401. Pedir token a cada chamada esgota o limite por IP.
  • Não repita uma chamada que falhou com 4xx (exceto 429). O resultado será o mesmo.
  • Ao receber 429, espere o valor de Retry-After inteiro antes da próxima chamada, para qualquer rota da mesma credencial.

Tabela de erros

Todos os erros de negócio vêm em JSON com error (código estável, para o seu código) e message (texto em pt-BR, para o seu log). Duas exceções: o endpoint de token segue o RFC 6749 e usa error_description no lugar de message (o 401 vem só com error), e os 404 de rota e os 400 de validação de formato usam o formato ProblemDetails, descrito na seção seguinte.

StatuserrorQuando aconteceO que fazer
400invalid_requestCorpo do POST /oauth/token sem client_id, client_secret ou malformado.Corrija a requisição. Não repita.
400unsupported_grant_typegrant_type diferente de client_credentials.Envie grant_type=client_credentials.
400ValidationProblemDetailsCorpo com campo inválido em uma rota de negócio (ex.: acknowledgment sem id).Leia errors e corrija o campo.
401invalid_clientCredencial inexistente, segredo errado, credencial pausada ou revogada, no endpoint de token.Confira client_id e client_secret. Se a credencial foi pausada, peça ao lojista para retomar. Não repita em loop: a partir de 10 falhas por minuto o segredo errado passa a receber 429 por 60 s.
401invalid_tokenCredencial pausada ou revogada, em qualquer chamada, mesmo com token ainda dentro da validade.Pare o polling e avise o operador. Só volta a funcionar quando o lojista retomar a credencial.
401corpo vazio, header WWW-AuthenticateSem Authorization, token expirado ou assinatura inválida.Peça um token novo e repita a chamada uma vez.
403insufficient_scopeO token não tem o escopo exigido pela rota.Crie uma credencial com o escopo certo. Escopos não são editáveis.
404order_not_foundorderId desconhecido ou de outra loja.Não repita. Se o id veio de um evento, guarde o traceId e fale com o suporte.
404ProblemDetailsRota inexistente, merchantId diferente do da credencial ou orderId que não é um GUID.Confira a URL e o merchantId.
409idempotency_key_reuseA mesma Idempotency-Key foi usada com uma requisição diferente.Gere uma chave nova para a nova requisição.
415Unsupported Media TypeContent-Type não suportado no POST /oauth/token.Use application/x-www-form-urlencoded ou application/json.
422invalid_transitionA ação não vale para o estado atual do pedido (ex.: cancelar um pedido DONE).Leia message, busque o pedido para ver o estado e ajuste o fluxo. Não repita.
429rate_limit_exceededLimite de requisições excedido.Espere Retry-After segundos e repita.
500internal_errorFalha interna.Repita com backoff. Se persistir, envie o traceId ao suporte.

Respostas que não são erro

202 com status: "already_applied" significa que o pedido já estava no estado pedido. Trate como sucesso. Um array vazio em GET /v1/events:polling significa que não há eventos pendentes; a API nunca responde 204.

Formatos de erro

Erro de negócio. A maioria das respostas de erro:

{ "error": "invalid_transition", "message": "O pedido já foi concluído e não pode ser cancelado." }

Erro interno. Igual ao anterior, com traceId para o suporte:

{
  "error": "internal_error",
  "message": "Erro interno. Informe o traceId ao suporte.",
  "traceId": "00-9a3f1c7e2b5d4a6f8e1c0b9d7a5f3e21-4b8c2d1e6f0a9b37-00"
}

ProblemDetails. Os 404 automáticos (rota inexistente, merchantId de outra loja, orderId que não é GUID):

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404,
  "traceId": "00-9a3f1c7e2b5d4a6f8e1c0b9d7a5f3e21-4b8c2d1e6f0a9b37-00"
}

ValidationProblemDetails. Os 400 de validação de corpo, com um mapa errors por campo:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "[0].id": ["The id field is required."]
  },
  "traceId": "00-9a3f1c7e2b5d4a6f8e1c0b9d7a5f3e21-4b8c2d1e6f0a9b37-00"
}

Para decidir o que fazer, use o status HTTP primeiro e o campo error depois. Não dependa do texto de message: ele pode mudar.

Datas: sempre UTC com Z

Toda data da API é ISO 8601 em UTC, com o sufixo Z: 2026-09-19T18:42:07Z. Isso vale para createdAt de eventos e pedidos, preparationStartDateTime, scheduledDateTimeStart, deliveryDateTime, lastUpdate e todas as outras.

Três regras:

  1. Leia como UTC. Bibliotecas que ignoram o Z interpretam a data no fuso local e produzem um erro de horas. Converta para o fuso da loja só na hora de exibir.
  2. Compare em UTC. Ordenação e cálculo de SLA (tempo até confirmar, tempo até despachar) devem ser feitos sobre o instante, não sobre a hora local.
  3. Não envie datas. Nenhuma rota atual recebe data no corpo; o servidor registra o instante das ações.
// `Date` respeita o sufixo Z. Guarde o instante, formate na exibição.
const createdAt = new Date(event.createdAt);

const local = new Intl.DateTimeFormat('pt-BR', {
  timeZone: 'America/Sao_Paulo',
  dateStyle: 'short',
  timeStyle: 'medium',
}).format(createdAt);

Repetição com backoff

Repita só falhas transitórias: 429, 500, timeout e erro de rede. Use backoff exponencial com jitter, respeite Retry-After quando ele existir e limite o número de tentativas. Nunca repita 400, 401 (exceto uma vez, após renovar o token), 403, 404, 409 ou 422.

async function withRetry<T>(fn: () => Promise<Response>, attempts = 5): Promise<Response> {
  for (let attempt = 0; ; attempt++) {
    let response: Response | undefined;
    try {
      response = await fn();
    } catch (error) {
      // Erro de rede ou timeout: cai no backoff abaixo.
      if (attempt + 1 >= attempts) throw error;
    }

    if (response && response.status !== 429 && response.status < 500) return response;
    if (response && attempt + 1 >= attempts) return response;

    const retryAfter = Number(response?.headers.get('Retry-After'));
    const base = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt;
    const jitter = Math.random() * 250;
    await new Promise((resolve) => setTimeout(resolve, Math.min(base + jitter, 60_000)));
  }
}

Duas garantias da API tornam a repetição segura:

  • Polling é at-least-once. Se a sua chamada de acknowledgment falhar, o evento volta na próxima consulta. Repetir nunca perde evento; por isso a deduplicação por eventId é obrigatória do seu lado.
  • Ações aceitam Idempotency-Key. Ao repetir um confirm com a mesma chave, a API devolve a resposta gravada em vez de aplicar a ação de novo. Veja Ações e idempotência.

Token: renove antes de expirar

O token vale 3600 segundos e não existe refresh token. Guarde expires_in junto com o instante em que o token chegou e peça outro quando faltar cerca de 60 segundos. Se uma chamada devolver 401 sem corpo, renove e repita uma única vez. Se devolver 401 com invalid_token, a credencial foi pausada ou revogada: renovar não resolve; pare e avise o operador.

Uma credencial vale para uma loja. Não compartilhe token entre lojas nem entre processos que não confiam um no outro.

Segurança do segredo

  • O client_secret é mostrado uma única vez, ao criar a credencial. Guarde em um cofre de segredos ou variável de ambiente do servidor. Nunca em código-fonte, repositório, planilha ou mensagem.
  • A API só aceita chamadas de navegador vindas deste portal (CORS liberado apenas para https://developer.meupedido.io). Integrações são servidor a servidor. Um segredo embutido em app móvel ou página web está exposto e não funcionaria de qualquer forma.
  • Não registre client_secret, Authorization nem o segredo do webhook em logs. Registre client_id, traceId, eventId e orderId: são os campos que o suporte pede.
  • Se o segredo vazou, o lojista revoga a credencial no painel (permanente) e cria outra. O painel não oferece troca do segredo de uma credencial existente: o caminho é revogar e criar de novo.
  • O segredo do webhook rotaciona ao salvar a URL de novo no painel. Prepare o seu endpoint para aceitar o segredo novo antes de rotacionar.
  • Verifique a assinatura de todo webhook com comparação em tempo constante e rejeite X-MeuPedido-Timestamp com mais de 5 minutos de diferença do seu relógio. Veja Webhooks.

Versão do padrão e evolução

A API implementa o Open Delivery 1.4.0 (Abrasel), módulos Order e Merchant, com o MeuPedido no papel de aplicação de pedidos. Rotas, campos e valores seguem o padrão; o que é acréscimo do MeuPedido está marcado como Extensão MeuPedido na documentação e listado em Compatibilidade.

Para não quebrar quando a API evoluir:

  • Ignore campos desconhecidos. Novos campos são adicionados sem mudar a versão da rota.
  • Ignore eventType desconhecido, registrando em log. Novos tipos de evento podem surgir como extensão. Confirme o evento mesmo assim, ou ele volta em toda consulta.
  • Não dependa da ordem de campos nem do texto de message.
  • Acompanhe o Changelog. Toda mudança de comportamento é publicada lá.

Próximos passos

Nesta página