# Changelog da API (/pt-BR/docs/changelog)

> Mudanças na API Open Delivery do MeuPedido, da mais recente para a mais antiga.

Toda mudança visível para integradores entra aqui, da mais recente para a mais antiga. Mudanças que quebram compatibilidade são anunciadas com antecedência e marcadas como tal.

## 19 de setembro de 2026 [#19-de-setembro-de-2026]

### Buscar pedido responde 404 para id desconhecido [#buscar-pedido-responde-404-para-id-desconhecido]

`GET /v1/orders/{orderId}` com um GUID que não existe, ou que pertence a outra loja, responde `404` com `{ "error": "order_not_found", "message": "..." }`, o mesmo corpo das ações. Antes, um id inexistente respondia `500`, e uma integração que seguia a orientação de repetir `500` com backoff ficava em loop num pedido que nunca ia existir.

### Endpoint de token: limite de corpo e bloqueio só do segredo errado [#endpoint-de-token-limite-de-corpo-e-bloqueio-só-do-segredo-errado]

`POST /oauth/token` passa a recusar corpos acima de 4 KB com `413` e `{ "error": "invalid_request", "error_description": "..." }`. O limite de 10 falhas por minuto por credencial agora vale apenas para o segredo errado: o segredo correto continua emitindo token mesmo enquanto outra origem tenta o segredo errado da mesma credencial, então conhecer um `client_id` não basta para derrubar a integração de ninguém. A tabela de erros do endpoint de token documenta o formato real do RFC 6749 (`error_description`).

### Lançamento do portal do desenvolvedor [#lançamento-do-portal-do-desenvolvedor]

Este portal, em `developer.meupedido.io`, passa a ser a documentação oficial da API Open Delivery do MeuPedido: guias, referência de API gerada da especificação OpenAPI, changelog e versões em Markdown de cada página para agentes de IA (`/llms.txt`, `/llms-full.txt` e `.md` por página).

### Rota de polling no formato do padrão [#rota-de-polling-no-formato-do-padrão]

`GET /v1/events:polling` é agora o caminho oficial de consulta de eventos, como o Open Delivery 1.4.0 define. O caminho anterior, `/v1/events/:polling`, continua funcionando como alias e não será removido sem aviso, mas não deve ser usado em integrações novas.

### Token aceita JSON e HTTP Basic [#token-aceita-json-e-http-basic]

`POST /oauth/token` passa a aceitar, além do formulário `application/x-www-form-urlencoded`, o corpo em `application/json` (com nomes em snake\_case ou camelCase) e a credencial em `Authorization: Basic`. A resposta traz cada campo em snake\_case e em camelCase (`access_token` e `accessToken`, `expires_in` e `expiresIn`). Veja [Autenticação](/pt-BR/docs/getting-started/authentication).

### Datas em UTC com sufixo `Z` [#datas-em-utc-com-sufixo-z]

Todos os campos de data e hora da API (`createdAt`, `preparationStartDateTime`, `estimatedDeliveryDateTime`, `scheduledDateTimeStart` e os demais) saem em ISO 8601 com o sufixo `Z`. Clientes que já liam as datas como UTC não precisam mudar nada; clientes que interpretavam as datas no fuso local passam a ler o valor correto.

### Campo `preparationStartDateTime` no pedido [#campo-preparationstartdatetime-no-pedido]

O pedido ganha `preparationStartDateTime`: igual a `createdAt` em pedidos `INSTANT` e igual a `schedule.scheduledDateTimeStart` em pedidos `SCHEDULED`. É o campo do padrão para saber quando a cozinha deve começar.

### CORS para o portal [#cors-para-o-portal]

A API libera CORS exclusivamente para `https://developer.meupedido.io`, o que permite o playground das páginas de documentação chamar a API a partir do navegador. Nenhuma outra origem é aceita; integrações continuam servidor a servidor.

## Próximos passos [#próximos-passos]

* [Compatibilidade](/pt-BR/docs/getting-started/compatibility): o que está implementado do Open Delivery 1.4.0 e o que ainda não está.
* [Referência de API](/pt-BR/docs/references): endpoints, campos e erros.


# Referência de API (/pt-BR/docs/references)

> URL base, autenticação, erros, limites e downloads da especificação OpenAPI e da coleção Postman.

Contrato da API Open Delivery do MeuPedido, versão 1.4.0 do padrão (Abrasel), módulos Order e Merchant. Esta página resume o que vale para todas as operações; cada operação tem a própria página com parâmetros, esquemas e exemplos, gerada a partir da especificação OpenAPI.

## URL base [#url-base]

```text
https://api.meupedido.io/open-delivery
```

Toda rota desta referência é relativa a essa URL. Há um único ambiente, o de produção. Para testar sem afetar uma loja real, use uma [loja de teste](/pt-BR/docs/getting-started/first-steps/test-store): os pedidos dela chegam com `test: true`.

Só HTTPS. Chamadas de navegador são aceitas apenas a partir de `https://developer.meupedido.io` (playground do portal); integrações rodam servidor a servidor.

## Autenticação [#autenticação]

OAuth 2.0 `client_credentials`. Troque `client_id` e `client_secret` por um token em `POST /oauth/token` e envie o token em toda chamada:

```http
Authorization: Bearer <access_token>
```

| Item                           | Valor                                                                                 |
| ------------------------------ | ------------------------------------------------------------------------------------- |
| Formato do token               | JWT HS256, com claims `merchant_id`, `client_id` e `scope`                            |
| Validade                       | 3600 segundos, sem refresh token                                                      |
| Formato do `client_id`         | `mp_` seguido de 24 caracteres hexadecimais, ex.: `mp_4f8a1c9e2b7d3a6f0e5c8b1d`       |
| Loja                           | Uma credencial pertence a uma única loja; o `merchantId` das rotas precisa ser o dela |
| Sem token                      | `401` com corpo vazio e header `WWW-Authenticate`                                     |
| Credencial pausada ou revogada | `401` com `{ "error": "invalid_token" }` em qualquer chamada, imediatamente           |

Detalhes, incluindo as três formas de enviar as credenciais, em [Autenticação](/pt-BR/docs/getting-started/authentication).

## Escopos [#escopos]

Definidos na criação da credencial. Não são editáveis: para mudar, o lojista cria outra credencial.

| Escopo          | Libera                                                                                 |
| --------------- | -------------------------------------------------------------------------------------- |
| `orders:read`   | `GET /v1/orders/{orderId}`, `GET /v1/events:polling`, `POST /v1/events/acknowledgment` |
| `orders:write`  | Todas as ações em `POST /v1/orders/{orderId}/...`                                      |
| `merchant:read` | `GET /v1/merchant/{merchantId}`                                                        |
| `catalog:read`  | `GET /v1/merchant/{merchantId}/menus`                                                  |

Chamada sem o escopo necessário: `403` com `{ "error": "insufficient_scope", "message": "..." }`.

## Formatos de erro [#formatos-de-erro]

O endpoint de token segue o formato do RFC 6749 (`error` e `error_description`); as demais rotas usam `error` e `message`.

| Status | Formato                        | `error`                                       | Quando                                                                                                      |
| ------ | ------------------------------ | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `400`  | `{ error, error_description }` | `invalid_request`                             | Corpo do token incompleto ou malformado                                                                     |
| `400`  | `{ error, error_description }` | `unsupported_grant_type`                      | `grant_type` diferente de `client_credentials`                                                              |
| `400`  | `{ error, message }`           | `invalid_idempotency_key`                     | `Idempotency-Key` acima de 128 caracteres; nada é executado                                                 |
| `400`  | `{ error, message }`           | `invalid_cancellation_reason`                 | `reason` do cancelamento acima de 500 caracteres; o pedido continua como estava                             |
| `400`  | ValidationProblemDetails       | (campo `errors`)                              | Corpo inválido em rota de negócio                                                                           |
| `401`  | `{ error }`                    | `invalid_client`                              | Credencial inexistente, segredo errado, pausada ou revogada, no endpoint de token                           |
| `401`  | `{ error, message }`           | `invalid_token`                               | Credencial pausada ou revogada, nas demais rotas                                                            |
| `401`  | corpo vazio                    | (header `WWW-Authenticate`)                   | Sem token, token expirado ou inválido                                                                       |
| `403`  | `{ error, message }`           | `insufficient_scope`                          | Token sem o escopo exigido                                                                                  |
| `404`  | `{ error, message }`           | `order_not_found`                             | `orderId` desconhecido ou de outra loja                                                                     |
| `404`  | ProblemDetails                 | (campos `type`, `title`, `status`, `traceId`) | Rota inexistente, `merchantId` de outra loja ou `orderId` que não é GUID                                    |
| `409`  | `{ error, message }`           | `idempotency_key_reuse`                       | Mesma `Idempotency-Key` com requisição diferente                                                            |
| `413`  | `{ error, error_description }` | `invalid_request`                             | Corpo do endpoint de token acima de 4 KB                                                                    |
| `415`  | `{ error, error_description }` | `invalid_request`                             | `Content-Type` não suportado no endpoint de token                                                           |
| `422`  | `{ error, message }`           | `invalid_transition`                          | Ação incompatível com o estado do pedido                                                                    |
| `429`  | `{ error, message }`           | `rate_limit_exceeded`                         | Limite de requisições excedido (por credencial ou por IP); header `Retry-After`                             |
| `429`  | `{ error, error_description }` | `rate_limit_exceeded`                         | Dez falhas de autenticação na mesma credencial em um minuto, no endpoint de token; header `Retry-After: 60` |
| `500`  | `{ error, message, traceId }`  | `internal_error`                              | Falha interna; informe o `traceId` ao suporte                                                               |

Exemplos de cada formato e o que fazer em cada caso em [Boas práticas](/pt-BR/docs/getting-started/best-practices#tabela-de-erros).

## Limites [#limites]

| Limite                                                        | Valor                                                   |
| ------------------------------------------------------------- | ------------------------------------------------------- |
| Chamadas autenticadas, por credencial                         | 600 por minuto                                          |
| `POST /oauth/token`, por IP                                   | 60 por minuto                                           |
| Corpo de `POST /oauth/token`                                  | 4 KB                                                    |
| Falhas de autenticação em `POST /oauth/token`, por credencial | 10 por minuto (o segredo correto continua sendo aceito) |
| `limit` em `GET /v1/events:polling`                           | Padrão 100, máximo 200                                  |
| Replay por `Idempotency-Key`                                  | 24 horas                                                |
| Timeout de entrega de webhook                                 | 10 segundos                                             |
| Retenção de eventos confirmados                               | 30 dias                                                 |

Ao exceder um limite de requisições: `429` com `Retry-After: 60`.

## Datas e valores [#datas-e-valores]

* Datas em ISO 8601, UTC, com sufixo `Z`: `2026-09-19T18:42:07Z`.
* Valores monetários como `{ "value": 49.9, "currency": "BRL" }`.
* Ids de pedido e de evento são GUIDs.
* Campos sem valor vêm como `null`, nunca são omitidos.

## Downloads [#downloads]

| Arquivo                                                              | Para quê                                                         |
| -------------------------------------------------------------------- | ---------------------------------------------------------------- |
| [Especificação OpenAPI (YAML)](/openapi/open-delivery-v1.yaml)       | Gerar clientes, validar requisições, alimentar agentes de IA     |
| [Especificação OpenAPI (JSON)](/openapi/open-delivery-v1.json)       | O mesmo contrato, para ferramentas que preferem JSON             |
| [Coleção Postman](/collections/meupedido-open-delivery.postman.json) | Executar todas as operações com variáveis para credenciais e ids |
| [llms.txt](/llms.txt)                                                | Índice de toda a documentação, para agentes de IA                |
| [llms-full.txt](/llms-full.txt)                                      | A documentação inteira em um único Markdown                      |

Cada página de documentação também existe em Markdown: acrescente `.md` à URL, como em `/pt-BR/docs/getting-started/authentication.md`. Veja [Para agentes de IA](/pt-BR/docs/getting-started/ai-agents).

## Operações [#operações]

### Autenticação [#autenticação-1]

| Operação                                                        | Rota                | Escopo |
| --------------------------------------------------------------- | ------------------- | ------ |
| [Obter token](/pt-BR/docs/references/authentication/oauthToken) | `POST /oauth/token` | nenhum |

### Loja e cardápio [#loja-e-cardápio]

| Operação                                                    | Rota                                  | Escopo          |
| ----------------------------------------------------------- | ------------------------------------- | --------------- |
| [Buscar loja](/pt-BR/docs/references/merchant/getMerchant)  | `GET /v1/merchant/{merchantId}`       | `merchant:read` |
| [Buscar cardápio](/pt-BR/docs/references/merchant/getMenus) | `GET /v1/merchant/{merchantId}/menus` | `catalog:read`  |

### Pedidos [#pedidos]

| Operação                                                                                        | Rota                                            | Escopo         |
| ----------------------------------------------------------------------------------------------- | ----------------------------------------------- | -------------- |
| [Buscar pedido](/pt-BR/docs/references/orders/getOrder)                                         | `GET /v1/orders/{orderId}`                      | `orders:read`  |
| [Confirmar](/pt-BR/docs/references/orders/confirmOrder)                                         | `POST /v1/orders/{orderId}/confirm`             | `orders:write` |
| [Iniciar preparo](/pt-BR/docs/references/orders/startPreparation) **Extensão MeuPedido** | `POST /v1/orders/{orderId}/startPreparation`    | `orders:write` |
| [Pronto para retirada](/pt-BR/docs/references/orders/readyForPickup)                            | `POST /v1/orders/{orderId}/readyForPickup`      | `orders:write` |
| [Despachar](/pt-BR/docs/references/orders/dispatchOrder)                                        | `POST /v1/orders/{orderId}/dispatch`            | `orders:write` |
| [Retirado](/pt-BR/docs/references/orders/pickedUp) **Extensão MeuPedido**                | `POST /v1/orders/{orderId}/pickedUp`            | `orders:write` |
| [Entregue](/pt-BR/docs/references/orders/deliverOrder)                                          | `POST /v1/orders/{orderId}/delivered`           | `orders:write` |
| [Cancelar](/pt-BR/docs/references/orders/requestCancellation)                                   | `POST /v1/orders/{orderId}/requestCancellation` | `orders:write` |

### Eventos [#eventos]

| Operação                                                             | Rota                             | Escopo        |
| -------------------------------------------------------------------- | -------------------------------- | ------------- |
| [Consultar eventos](/pt-BR/docs/references/events/pollEvents)        | `GET /v1/events:polling`         | `orders:read` |
| [Confirmar eventos](/pt-BR/docs/references/events/acknowledgeEvents) | `POST /v1/events/acknowledgment` | `orders:read` |

### Webhooks [#webhooks]

| Operação                                                                                     | Rota                                   | Escopo                               |
| -------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------ |
| [Evento de pedido](/pt-BR/docs/references/webhooks/orderEvent) **Extensão MeuPedido** | `POST` na URL configurada pelo lojista | (assinatura `X-MeuPedido-Signature`) |

## Próximos passos [#próximos-passos]

* [Primeira integração em 10 minutos](/pt-BR/docs/getting-started/first-steps/first-integration): token, polling, ack e confirmação na prática.
* [Autenticação](/pt-BR/docs/getting-started/authentication): as formas de enviar as credenciais e os erros do endpoint de token.
* [Changelog](/pt-BR/docs/changelog): mudanças na API e neste portal.


# Para agentes de IA (/pt-BR/docs/getting-started/ai-agents)

> llms.txt, llms-full.txt, Markdown por página, especificação OpenAPI e um prompt pronto para começar.

Este portal foi construído para ser lido por pessoas e por agentes de programação. Tudo o que está nas páginas existe também em texto puro, em URLs estáveis, sem HTML no meio. Se você usa Claude Code, Cursor, Codex, ChatGPT ou qualquer agente que consiga buscar uma URL, aponte para os arquivos abaixo e peça a integração.

## Arquivos para máquinas [#arquivos-para-máquinas]

| Arquivo                           | URL                                                                               | Uso                                                                                                               |
| --------------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Índice `llms.txt`                 | `https://developer.meupedido.io/llms.txt`                                         | Lista todas as páginas com título, URL e descrição. Ponto de partida: o agente decide o que ler.                  |
| Conteúdo completo `llms-full.txt` | `https://developer.meupedido.io/llms-full.txt`                                    | Toda a documentação em um único arquivo Markdown. Para contextos grandes ou indexação.                            |
| Markdown por página               | `https://developer.meupedido.io/pt-BR/docs/<página>.md`                           | Qualquer página de documentação, acrescentando `.md` à URL. Ex.: `/pt-BR/docs/getting-started/authentication.md`. |
| Especificação OpenAPI (YAML)      | `https://developer.meupedido.io/openapi/open-delivery-v1.yaml`                    | Contrato completo: rotas, parâmetros, esquemas, respostas e erros. Para gerar clientes e validar código.          |
| Especificação OpenAPI (JSON)      | `https://developer.meupedido.io/openapi/open-delivery-v1.json`                    | O mesmo contrato em JSON.                                                                                         |
| Coleção Postman                   | `https://developer.meupedido.io/collections/meupedido-open-delivery.postman.json` | Todas as operações prontas para executar, com variáveis para credenciais e ids.                                   |

No topo de cada página de documentação há três ações: **Copiar Markdown**, que copia a página em texto puro; **Abrir no ChatGPT** e **Abrir no Claude**, que abrem o assistente já com a página carregada como contexto.

## O que dizer ao agente [#o-que-dizer-ao-agente]

A integração mínima tem quatro partes: obter o token, consultar eventos, confirmar os eventos lidos e confirmar o pedido. O prompt abaixo cobre isso e já carrega as regras que mais causam erro em integrações novas (deduplicação, ack obrigatório, datas em UTC, `Retry-After`). Copie, ajuste a linguagem e o nome da loja de teste, e cole no seu agente.

```text
Você vai implementar uma integração com a API Open Delivery do MeuPedido.

Fontes de verdade, nesta ordem. Leia antes de escrever qualquer código:
1. https://developer.meupedido.io/llms.txt (índice; abra as páginas de Autenticação, Polling, Ações e idempotência, Catálogo de eventos e Boas práticas)
2. https://developer.meupedido.io/openapi/open-delivery-v1.yaml (contrato das rotas e esquemas)

Contexto:
- URL base: https://api.meupedido.io/open-delivery
- Credenciais: client_id e client_secret vêm das variáveis de ambiente MEUPEDIDO_CLIENT_ID e MEUPEDIDO_CLIENT_SECRET. Nunca as escreva no código.
- Linguagem e stack: <sua linguagem, framework e versão>

Entregue um serviço que:
1. Obtém um token em POST /oauth/token com grant_type=client_credentials (form urlencoded), guarda expires_in e renova cerca de 60 s antes de expirar. Em 401 com {"error":"invalid_token"} para e registra que a credencial foi pausada ou revogada, sem tentar de novo.
2. Consulta GET /v1/events:polling?limit=200 em um intervalo fixo de 5 s. A resposta é um array (vazio quando não há eventos). A entrega é at-least-once: o mesmo eventId pode chegar mais de uma vez; deduplique por eventId antes de processar.
3. Para cada evento, busca o pedido em orderURL com o mesmo token (o envelope nunca traz o pedido). Ignora eventType desconhecido, registrando em log, mas confirma o evento mesmo assim.
4. Confirma os eventos processados em POST /v1/events/acknowledgment com corpo [{"id":"<eventId>"}, ...]. Sem essa confirmação o evento volta em toda consulta. Confirme só depois de persistir o que precisava.
5. Ao receber um evento CREATED, confirma o pedido com POST /v1/orders/{orderId}/confirm enviando o header Idempotency-Key com um UUID v4 por tentativa lógica. Trata 202 com status "accepted" e "already_applied" como sucesso, 422 invalid_transition como estado incompatível (não repetir) e 409 idempotency_key_reuse como bug de chave.
6. Trata 429 esperando o valor do header Retry-After antes da próxima chamada e usa backoff exponencial com jitter para 500 e erro de rede. Nunca repete 400, 403, 404, 409 ou 422.
7. Lê todas as datas como UTC (sufixo Z) e só converte para America/Sao_Paulo na exibição.
8. Marca em log os pedidos com "test": true, que vêm da loja de teste.

Regras de qualidade:
- Escreva testes para a deduplicação, para o ack só após persistência e para a renovação do token.
- Não invente campos, rotas ou status que não estejam na spec. Se algo não estiver documentado, pergunte antes.
- Não use travessão em textos e comentários.
```

## Dicas [#dicas]

* **Dê a spec, não só as páginas.** A OpenAPI é o contrato completo. Agentes que geram cliente a partir dela erram menos nomes de campo do que os que leem prosa.
* **Peça testes contra a loja de teste.** Pedidos feitos no cardápio digital de uma [loja de teste](/pt-BR/docs/getting-started/first-steps/test-store) chegam com `test: true` e passam pelo fluxo completo. É a forma mais barata de validar o serviço antes de ligar em uma loja real.
* **Mantenha o segredo fora do prompt.** Passe `client_id` e `client_secret` por variável de ambiente. Um segredo colado em um chat fica no histórico.
* **Cole a página certa.** Para uma dúvida pontual, use **Abrir no Claude** ou **Abrir no ChatGPT** na página relevante. É mais preciso do que o `llms-full.txt`, que tem tudo.

## Próximos passos [#próximos-passos]

* [Primeira integração em 10 minutos](/pt-BR/docs/getting-started/first-steps/first-integration): o roteiro passo a passo que o prompt acima automatiza.
* [Referência de API](/pt-BR/docs/references): downloads da especificação e da coleção Postman.
* [Boas práticas](/pt-BR/docs/getting-started/best-practices): as regras que o prompt pede ao agente, explicadas.


# Autenticação (/pt-BR/docs/getting-started/authentication)

> OAuth client credentials, escopos, expiração de 3600 s, credencial pausada ou revogada e limites do endpoint de token.

A API usa OAuth 2.0 com o fluxo **client credentials**: o seu sistema troca `client_id` e `client_secret` por um token de acesso e envia esse token em todas as chamadas. Não há login de usuário, redirecionamento nem refresh token.

1. Seu sistema chama `POST /oauth/token` com a credencial.
2. A API responde com um JWT válido por 3600 segundos e os escopos concedidos.
3. Seu sistema envia o token em `Authorization: Bearer` em cada chamada.
4. Antes de o token expirar, seu sistema pede outro.

## Obtendo o token [#obtendo-o-token]

```http
POST https://api.meupedido.io/open-delivery/oauth/token
```

O caminho `POST /v1/oauth/token` é um alias e responde igual.

A credencial pode ser enviada de três formas. O formulário é a forma canônica do OAuth 2.0 e a que a maioria das bibliotecas usa por padrão.

**Formulário**

`Content-Type: application/x-www-form-urlencoded`, com os três campos no corpo.

```bash title="Terminal"
curl -X POST https://api.meupedido.io/open-delivery/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=mp_7f3a9c1e5b2d4a6f8e0c1b3d" \
  -d "client_secret=$CLIENT_SECRET"
```

**JSON**

`Content-Type: application/json`. Os nomes podem vir em snake\_case ou camelCase (`clientId`, `clientSecret`, `grantType`).

```bash title="Terminal"
curl -X POST https://api.meupedido.io/open-delivery/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "mp_7f3a9c1e5b2d4a6f8e0c1b3d",
    "client_secret": "'"$CLIENT_SECRET"'"
  }'
```

**HTTP Basic**

`Authorization: Basic` com `client_id:client_secret` em Base64 e apenas o `grant_type` no corpo do formulário.

```bash title="Terminal"
curl -X POST https://api.meupedido.io/open-delivery/oauth/token \
  -u "mp_7f3a9c1e5b2d4a6f8e0c1b3d:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials"
```

Qualquer outro `Content-Type` responde `415`.

### Resposta [#resposta]

```json title="200 OK"
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudF9pZCI6IjZkMmY0YzhhLTFiM2UtNGY1YS05YzdkLTJlOGIwYTFmM2M1ZCIsImNsaWVudF9pZCI6Im1wXzdmM2E5YzFlNWIyZDRhNmY4ZTBjMWIzZCIsInNjb3BlIjoib3JkZXJzOnJlYWQgb3JkZXJzOndyaXRlIG1lcmNoYW50OnJlYWQgY2F0YWxvZzpyZWFkIn0.Q1w7Xy0Yt3cJ5w8NnZK2p9Vb4Ls6Hd1Ff0Rr8Mm3Aa0",
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudF9pZCI6IjZkMmY0YzhhLTFiM2UtNGY1YS05YzdkLTJlOGIwYTFmM2M1ZCIsImNsaWVudF9pZCI6Im1wXzdmM2E5YzFlNWIyZDRhNmY4ZTBjMWIzZCIsInNjb3BlIjoib3JkZXJzOnJlYWQgb3JkZXJzOndyaXRlIG1lcmNoYW50OnJlYWQgY2F0YWxvZzpyZWFkIn0.Q1w7Xy0Yt3cJ5w8NnZK2p9Vb4Ls6Hd1Ff0Rr8Mm3Aa0",
  "token_type": "bearer",
  "tokenType": "bearer",
  "expires_in": 3600,
  "expiresIn": 3600,
  "scope": "orders:read orders:write merchant:read catalog:read"
}
```

Cada campo vem em snake\_case (como o RFC 6749 define) e em camelCase (como a especificação Open Delivery escreve). Os valores são idênticos; leia o que a sua biblioteca esperar.

| Campo          | Significado                                    |
| -------------- | ---------------------------------------------- |
| `access_token` | O JWT a enviar em `Authorization: Bearer`      |
| `token_type`   | Sempre `bearer`                                |
| `expires_in`   | Validade em segundos, sempre `3600`            |
| `scope`        | Os escopos da credencial, separados por espaço |

O token é um JWT assinado com HS256 pelo MeuPedido. Ele carrega as claims `merchant_id` (a loja), `client_id` e `scope`. Trate-o como opaco: não dependa do formato interno, e nunca tente validá-lo localmente, porque a chave de assinatura não é pública.

### Em código [#em-código]

**cURL**

```bash
curl -X POST https://api.meupedido.io/open-delivery/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET"
```

**Node.js**

```ts title="auth.ts"
const BASE_URL = 'https://api.meupedido.io/open-delivery';

export async function getToken(clientId: string, clientSecret: string) {
  const body = new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: clientId,
    client_secret: clientSecret,
  });

  const res = await fetch(`${BASE_URL}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  });

  if (!res.ok) {
    const error = await res.json().catch(() => ({}));
    throw new Error(`token ${res.status}: ${error.error ?? 'unknown'}`);
  }

  const data = await res.json();
  return {
    accessToken: data.access_token as string,
    // Renova 5 minutos antes de expirar, para nunca usar um token no limite.
    expiresAt: Date.now() + (data.expires_in - 300) * 1000,
  };
}
```

**Python**

```python title="auth.py"
import time
import requests

BASE_URL = "https://api.meupedido.io/open-delivery"

def get_token(client_id: str, client_secret: str) -> tuple[str, float]:
    res = requests.post(
        f"{BASE_URL}/oauth/token",
        data={
            "grant_type": "client_credentials",
            "client_id": client_id,
            "client_secret": client_secret,
        },
        timeout=10,
    )
    res.raise_for_status()
    data = res.json()
    # Renova 5 minutos antes de expirar, para nunca usar um token no limite.
    expires_at = time.time() + data["expires_in"] - 300
    return data["access_token"], expires_at
```

**C#**

```csharp title="MeuPedidoAuth.cs"
using System.Net.Http.Json;

public sealed class MeuPedidoAuth(HttpClient http, string clientId, string clientSecret)
{
    private const string BaseUrl = "https://api.meupedido.io/open-delivery";

    private string? _token;
    private DateTimeOffset _expiresAt;

    public async Task<string> GetTokenAsync(CancellationToken ct = default)
    {
        if (_token is not null && DateTimeOffset.UtcNow < _expiresAt)
            return _token;

        using var content = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            ["grant_type"] = "client_credentials",
            ["client_id"] = clientId,
            ["client_secret"] = clientSecret,
        });

        using var res = await http.PostAsync($"{BaseUrl}/oauth/token", content, ct);
        res.EnsureSuccessStatusCode();

        var body = await res.Content.ReadFromJsonAsync<TokenResponse>(ct)
            ?? throw new InvalidOperationException("Resposta vazia do endpoint de token.");

        _token = body.AccessToken;
        // Renova 5 minutos antes de expirar, para nunca usar um token no limite.
        _expiresAt = DateTimeOffset.UtcNow.AddSeconds(body.ExpiresIn - 300);
        return _token;
    }

    private sealed record TokenResponse(string AccessToken, int ExpiresIn, string Scope);
}
```

## Usando o token [#usando-o-token]

Envie o token no header `Authorization` de todas as chamadas à API.

```bash title="Terminal"
curl "https://api.meupedido.io/open-delivery/v1/events:polling" \
  -H "Authorization: Bearer $TOKEN"
```

Sem o header, a resposta é `401` com corpo vazio e o header `WWW-Authenticate`.

## Expiração e renovação [#expiração-e-renovação]

O token vale por **3600 segundos** a partir da emissão. Não existe refresh token: renovar é chamar `POST /oauth/token` de novo.

A estratégia recomendada:

* Guarde o token junto com o instante de expiração calculado a partir de `expires_in`.
* Renove **antes** de expirar, com uma margem de alguns minutos, em vez de esperar o `401`.
* Se ainda assim uma chamada responder `401`, peça um token novo uma vez e repita a chamada. Se o endpoint de token também responder `401 invalid_client`, a credencial foi pausada ou revogada: pare e avise o operador.
* Um token por credencial, compartilhado entre as threads ou workers do seu processo. Pedir um token a cada requisição esgota o limite do endpoint.

## Escopos [#escopos]

Os escopos são definidos pelo lojista ao criar a credencial e não podem ser alterados depois. O token carrega exatamente os escopos da credencial.

| Escopo          | Libera                                                                                 |
| --------------- | -------------------------------------------------------------------------------------- |
| `orders:read`   | `GET /v1/events:polling`, `POST /v1/events/acknowledgment`, `GET /v1/orders/{orderId}` |
| `orders:write`  | `POST /v1/orders/{orderId}/confirm` e as demais ações                                  |
| `merchant:read` | `GET /v1/merchant/{merchantId}`                                                        |
| `catalog:read`  | `GET /v1/merchant/{merchantId}/menus`                                                  |

Uma chamada a um endpoint cujo escopo a credencial não tem responde:

```json title="403 Forbidden"
{
  "error": "insufficient_scope",
  "message": "A credencial não possui o escopo orders:write."
}
```

## Credencial pausada ou revogada [#credencial-pausada-ou-revogada]

O lojista pode pausar, retomar ou revogar a credencial a qualquer momento no painel. O efeito é imediato e vale para tokens já emitidos, mesmo dentro do prazo de validade:

```json title="401 Unauthorized"
{
  "error": "invalid_token",
  "message": "A credencial foi pausada ou revogada pelo lojista."
}
```

Com a credencial pausada, os eventos continuam sendo acumulados no feed e ficam disponíveis assim que ela for retomada. Revogação é permanente: para voltar, o lojista cria outra credencial.

## Erros do endpoint de token [#erros-do-endpoint-de-token]

O endpoint de token responde no formato do RFC 6749: `error` com um código estável e `error_description` com o texto em pt-BR. É o formato que toda biblioteca OAuth 2.0 já sabe ler, e por isso é diferente do `error` e `message` das demais rotas.

| Status | `error`                  | Quando                                                                         |
| ------ | ------------------------ | ------------------------------------------------------------------------------ |
| `400`  | `unsupported_grant_type` | `grant_type` diferente de `client_credentials`                                 |
| `400`  | `invalid_request`        | Falta `client_id`, `client_secret` ou `grant_type`, ou o corpo está malformado |
| `401`  | `invalid_client`         | Credencial inexistente, segredo errado, pausada ou revogada                    |
| `413`  | `invalid_request`        | Corpo acima de 4 KB. Um pedido de token tem só três campos                     |
| `415`  | `invalid_request`        | `Content-Type` diferente de formulário ou JSON                                 |
| `429`  | `rate_limit_exceeded`    | Limite do endpoint atingido; espere o `Retry-After`                            |

```json title="400 Bad Request"
{
  "error": "invalid_request",
  "error_description": "Informe client_id e client_secret."
}
```

O `401` vem só com o código, de propósito. Credencial inexistente e segredo errado recebem exatamente a mesma resposta, para o endpoint não revelar quais `client_id` existem:

```json title="401 Unauthorized"
{
  "error": "invalid_client"
}
```

## Limites do endpoint de token [#limites-do-endpoint-de-token]

O endpoint de token tem dois limites independentes, ambos respondendo `429` com `Retry-After: 60`:

* **60 requisições por minuto por endereço IP.** Um único processo bem comportado nunca chega perto disso. O `429` vem com `error` e `message`.
* **10 falhas por minuto por credencial.** Se o seu sistema entrar em loop com um segredo errado, ele vai bater neste limite antes de qualquer outro, e o `429` vem com `error` e `error_description`. O limite vale só para o segredo errado: o segredo correto continua emitindo token normalmente, então ninguém que conheça apenas o seu `client_id` consegue derrubar a sua integração.

Os demais endpoints têm o limite geral de 600 requisições por minuto por credencial, descrito em [Boas práticas](/pt-BR/docs/getting-started/best-practices).

## Segurança do segredo [#segurança-do-segredo]

* O `client_secret` é mostrado uma única vez, ao criar a credencial. Guarde-o em um cofre de segredos ou em variável de ambiente.
* Nunca o coloque em código, repositório, log, mensagem de erro, URL ou ticket de suporte. O suporte nunca pede o segredo.
* Use uma credencial por loja e por sistema. Se um segredo vazar, o lojista revoga só aquela credencial.
* Toda comunicação é HTTPS. Não há endpoint HTTP.
* CORS está liberado apenas para o playground deste portal. A integração é servidor a servidor: chamar a API a partir de um navegador expõe o segredo e é bloqueado.

## Próximos passos [#próximos-passos]

* [Primeira integração em 10 minutos](/pt-BR/docs/getting-started/first-steps/first-integration): use o token para receber o primeiro pedido.
* [Polling](/pt-BR/docs/getting-started/orders/polling): o loop de consulta de eventos.
* [Boas práticas](/pt-BR/docs/getting-started/best-practices): limites, erros e backoff.


# Boas práticas (/pt-BR/docs/getting-started/best-practices)

> 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 [#limites-de-requisição]

| Limite                                                        | Valor          | Resposta ao exceder         |
| ------------------------------------------------------------- | -------------- | --------------------------- |
| Chamadas autenticadas, por credencial                         | 600 por minuto | `429` com `Retry-After: 60` |
| `POST /oauth/token`, por IP                                   | 60 por minuto  | `429` com `Retry-After: 60` |
| Falhas de autenticação em `POST /oauth/token`, por credencial | 10 por minuto  | `429` com `Retry-After: 60` |

Ao exceder, a API responde:

```http
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 [#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.

| Status | `error`                                | Quando acontece                                                                                | O que fazer                                                                                                                                                                                             |
| ------ | -------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `invalid_request`                      | Corpo do `POST /oauth/token` sem `client_id`, `client_secret` ou malformado.                   | Corrija a requisição. Não repita.                                                                                                                                                                       |
| `400`  | `unsupported_grant_type`               | `grant_type` diferente de `client_credentials`.                                                | Envie `grant_type=client_credentials`.                                                                                                                                                                  |
| `400`  | ValidationProblemDetails               | Corpo com campo inválido em uma rota de negócio (ex.: `acknowledgment` sem `id`).              | Leia `errors` e corrija o campo.                                                                                                                                                                        |
| `401`  | `invalid_client`                       | Credencial 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. |
| `401`  | `invalid_token`                        | Credencial 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.                                                                                                          |
| `401`  | corpo vazio, header `WWW-Authenticate` | Sem `Authorization`, token expirado ou assinatura inválida.                                    | Peça um token novo e repita a chamada uma vez.                                                                                                                                                          |
| `403`  | `insufficient_scope`                   | O token não tem o escopo exigido pela rota.                                                    | Crie uma credencial com o escopo certo. Escopos não são editáveis.                                                                                                                                      |
| `404`  | `order_not_found`                      | `orderId` desconhecido ou de outra loja.                                                       | Não repita. Se o id veio de um evento, guarde o `traceId` e fale com o suporte.                                                                                                                         |
| `404`  | ProblemDetails                         | Rota inexistente, `merchantId` diferente do da credencial ou `orderId` que não é um GUID.      | Confira a URL e o `merchantId`.                                                                                                                                                                         |
| `409`  | `idempotency_key_reuse`                | A mesma `Idempotency-Key` foi usada com uma requisição diferente.                              | Gere uma chave nova para a nova requisição.                                                                                                                                                             |
| `415`  | Unsupported Media Type                 | `Content-Type` não suportado no `POST /oauth/token`.                                           | Use `application/x-www-form-urlencoded` ou `application/json`.                                                                                                                                          |
| `422`  | `invalid_transition`                   | A 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.                                                                                                                         |
| `429`  | `rate_limit_exceeded`                  | Limite de requisições excedido.                                                                | Espere `Retry-After` segundos e repita.                                                                                                                                                                 |
| `500`  | `internal_error`                       | Falha 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 [#formatos-de-erro]

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

```json
{ "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:

```json
{
  "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):

```json
{
  "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:

```json
{
  "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` [#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.

**Node.js**

```ts
// `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);
```

**Python**

```python
from datetime import datetime
from zoneinfo import ZoneInfo

# fromisoformat entende o sufixo Z a partir do Python 3.11.
created_at = datetime.fromisoformat(event["createdAt"])
local = created_at.astimezone(ZoneInfo("America/Sao_Paulo"))
```

**C#**

```csharp
// DateTimeOffset preserva o offset zero; DateTime.Parse converteria para o fuso da máquina.
var createdAt = DateTimeOffset.Parse(evt.CreatedAt, CultureInfo.InvariantCulture);

var saoPaulo = TimeZoneInfo.FindSystemTimeZoneById("America/Sao_Paulo");
var local = TimeZoneInfo.ConvertTime(createdAt, saoPaulo);
```

## Repetição com backoff [#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`.

```ts
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](/pt-BR/docs/getting-started/orders/actions).

## Token: renove antes de expirar [#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 [#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](/pt-BR/docs/getting-started/orders/webhooks).

## Versão do padrão e evolução [#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](/pt-BR/docs/getting-started/compatibility).

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](/pt-BR/docs/changelog).** Toda mudança de comportamento é publicada lá.

## Próximos passos [#próximos-passos]

* [Compatibilidade](/pt-BR/docs/getting-started/compatibility): o que está implementado do padrão 1.4.0 e o que é extensão.
* [Checklist de entrada em produção](/pt-BR/docs/getting-started/first-steps/go-live): confira tudo isto antes de ligar a integração em uma loja real.
* [Suporte](/pt-BR/docs/getting-started/support): o que enviar quando algo não bate.


# Compatibilidade (/pt-BR/docs/getting-started/compatibility)

> O que está implementado do Open Delivery 1.4.0, o que é extensão MeuPedido e o que ainda não existe.

A API segue o Open Delivery 1.4.0 (Abrasel), módulos Order e Merchant, com o MeuPedido no papel de aplicação de pedidos: o pedido nasce no MeuPedido e o seu sistema o recebe e o conduz. Esta página é o inventário exato do que existe, para você saber o que pode reaproveitar de uma integração Open Delivery já feita e o que precisa tratar como específico do MeuPedido.

Legenda da coluna **Situação**:

* **Implementado** segue o padrão 1.4.0. Uma integração Open Delivery genérica funciona sem mudança.
* **Extensão** não existe no padrão, ou existe com comportamento diferente. Está documentado aqui e marcado nas páginas.
* **Não implementado**: previsto no padrão, ainda não disponível. Chamadas a essas rotas devolvem `404`.

## Autenticação [#autenticação]

| Recurso do padrão 1.4.0                                 | Situação         | Observação                                                                                                                   |
| ------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `POST /oauth/token` com `grant_type=client_credentials` | Implementado     | Corpo `application/x-www-form-urlencoded` (canônico). Alias `POST /v1/oauth/token`.                                          |
| Credenciais em JSON no corpo                            | Extensão         | `application/json` com `grant_type`, `client_id` e `client_secret`, aceitando também camelCase.                              |
| Credenciais em HTTP Basic                               | Extensão         | `Authorization: Basic base64(client_id:client_secret)` com `grant_type` no corpo.                                            |
| Resposta do token                                       | Implementado     | Traz `access_token`, `token_type`, `expires_in` e `scope`, mais as cópias camelCase `accessToken`, `tokenType`, `expiresIn`. |
| Escopo `od.all`                                         | Não implementado | Use os escopos granulares `orders:read`, `orders:write`, `merchant:read` e `catalog:read`.                                   |
| Refresh token                                           | Não implementado | O token vale 3600 s. Peça outro com as mesmas credenciais.                                                                   |

## Pedidos (módulo Order) [#pedidos-módulo-order]

| Recurso do padrão 1.4.0                         | Situação         | Observação                                                                                                                                                       |
| ----------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v1/orders/{orderId}`                      | Implementado     | Estrutura do pedido campo a campo em [Estrutura do pedido](/pt-BR/docs/getting-started/orders/order-object).                                                     |
| `POST /v1/orders/{orderId}/confirm`             | Implementado     | Leva o pedido a `ACCEPTED`.                                                                                                                                      |
| `POST /v1/orders/{orderId}/readyForPickup`      | Implementado     | Leva o pedido a `READY`.                                                                                                                                         |
| `POST /v1/orders/{orderId}/dispatch`            | Implementado     | Leva o pedido a `DELIVERY`. Não vale para pedido `INDOOR`.                                                                                                       |
| `POST /v1/orders/{orderId}/delivered`           | Implementado     | Leva o pedido a `DONE`.                                                                                                                                          |
| `POST /v1/orders/{orderId}/requestCancellation` | Extensão         | No padrão é um pedido de cancelamento que o outro lado aceita ou nega. No MeuPedido cancela imediatamente (`CANCELLED`). `reason` é opcional; `code` é ignorado. |
| `POST /v1/orders/{orderId}/acceptCancellation`  | Não implementado | Não há fluxo de aceite porque o cancelamento é imediato.                                                                                                         |
| `POST /v1/orders/{orderId}/denyCancellation`    | Não implementado | Idem.                                                                                                                                                            |
| `POST /v1/orders/{orderId}/startPreparation`    | Extensão         | Leva o pedido a `PREPARING`.                                                                                                                                     |
| `POST /v1/orders/{orderId}/pickedUp`            | Extensão         | Leva o pedido a `DONE` em pedidos de retirada ou mesa.                                                                                                           |
| Header `Idempotency-Key` nas ações              | Extensão         | Replay da resposta gravada por 24 h; reuso com requisição diferente devolve `409`.                                                                               |
| Resposta `202` com `status` e `situation`       | Extensão         | O padrão só define o `202`. O corpo com `accepted` ou `already_applied` e o estado resultante é acréscimo.                                                       |
| Retrocesso de estado                            | Extensão         | A partir de `ACCEPTED`, qualquer ação para um estado anterior é aceita (ex.: `confirm` em `PREPARING` volta a `ACCEPTED`) e não gera evento.                     |
| Valores monetários `{ value, currency }`        | Implementado     | `currency` é sempre `BRL`.                                                                                                                                       |
| Datas ISO 8601                                  | Implementado     | Sempre em UTC com sufixo `Z`.                                                                                                                                    |

## Eventos [#eventos]

| Recurso do padrão 1.4.0              | Situação         | Observação                                                                                                                                                                                                                                               |
| ------------------------------------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v1/events:polling`             | Implementado     | Query `limit` (padrão 100, máximo 200). Responde array vazio quando não há eventos, nunca `204`.                                                                                                                                                         |
| `POST /v1/events/acknowledgment`     | Implementado     | Corpo `[{ "id": "<eventId>" }]`. Responde `202` com `{ "acknowledged": n }`.                                                                                                                                                                             |
| Alias `GET /v1/events/:polling`      | Extensão         | Caminho antigo, com barra antes de `:polling`. Continua funcionando por compatibilidade com integrações já feitas, mas o caminho oficial é `/v1/events:polling`. Não use em integrações novas.                                                           |
| Filtro `eventType` no polling        | Não implementado | Todos os tipos vêm no mesmo feed. Filtre do seu lado.                                                                                                                                                                                                    |
| Header `x-polling-merchants`         | Não implementado | Cada credencial pertence a uma loja; o feed já é por loja.                                                                                                                                                                                               |
| `POST /v1/newEvent` (push do padrão) | Não implementado | Para receber eventos por push, use os [webhooks MeuPedido](/pt-BR/docs/getting-started/orders/webhooks).                                                                                                                                                 |
| Campo `orderURL` no evento           | Implementado     | URL completa do pedido, para `GET` com o mesmo token.                                                                                                                                                                                                    |
| Campo `sourceAppId` no evento        | Implementado     | Presente quando houver.                                                                                                                                                                                                                                  |
| Evento `CREATED`                     | Implementado     | Pedido criado, ou PIX confirmado no caso de pagamento online.                                                                                                                                                                                            |
| Evento `CONFIRMED`                   | Implementado     | Pedido `ACCEPTED`. `metadata` vem como `{}`.                                                                                                                                                                                                             |
| Evento `READY_FOR_PICKUP`            | Implementado     | Pedido `READY`; também quando um `TAKEOUT` ou `INDOOR` vai a `DELIVERY`.                                                                                                                                                                                 |
| Evento `DISPATCHED`                  | Implementado     | Pedido de entrega em `DELIVERY`.                                                                                                                                                                                                                         |
| Evento `CONCLUDED`                   | Implementado     | Pedido `DONE`.                                                                                                                                                                                                                                           |
| Evento `CANCELLED`                   | Implementado     | `metadata` traz `reason` e `code` (`CONSUMER_CANCELLATION_REQUESTED` ou `OTHER_CANCELLATION_REASON`).                                                                                                                                                    |
| Evento `PICKUP_AREA_ASSIGNED`        | Não implementado | Nunca emitido.                                                                                                                                                                                                                                           |
| Evento `DELIVERED`                   | Não implementado | Nunca emitido. A conclusão chega como `CONCLUDED`.                                                                                                                                                                                                       |
| Evento `CANCELLATION_REQUESTED`      | Não implementado | Nunca emitido: o cancelamento é imediato.                                                                                                                                                                                                                |
| Evento `CANCELLATION_REQUEST_DENIED` | Não implementado | Nunca emitido.                                                                                                                                                                                                                                           |
| Evento `PREPARING`                   | Extensão         | Pedido em preparo.                                                                                                                                                                                                                                       |
| Evento `MODIFIED`                    | Extensão         | Conteúdo do pedido editado. Busque o pedido de novo em `orderURL`.                                                                                                                                                                                       |
| Eventos `COURIER_*`                  | Extensão         | `COURIER_ASSIGNED`, `COURIER_ROUTE_STARTED`, `COURIER_PICKED_UP`, `COURIER_ARRIVED`, `COURIER_DELIVERED`, `COURIER_DELIVERY_FAILED`, `COURIER_UNASSIGNED`, `COURIER_ROUTE_COMPLETED`. Trazem o bloco `delivery` com rota, entregador e status da parada. |
| Retenção de eventos                  | Extensão         | Eventos confirmados são apagados após 30 dias; não confirmados permanecem no feed.                                                                                                                                                                       |

## Webhooks [#webhooks]

| Recurso do padrão 1.4.0        | Situação | Observação                                                                                                                                                                                               |
| ------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Entrega por webhook            | Extensão | O padrão prevê `POST /v1/newEvent` no lado do integrador. O MeuPedido entrega o mesmo envelope do polling na URL HTTPS configurada pelo lojista, com assinatura HMAC-SHA256 nos headers `X-MeuPedido-*`. |
| Modo de entrega por credencial | Extensão | `POLLING`, `WEBHOOK` ou `BOTH`, definido ao criar a credencial.                                                                                                                                          |
| Retentativas                   | Extensão | 5, 10 e 20 s; depois `WEBHOOK_FAILED` e o evento fica no polling. Após 20 falhas consecutivas a URL é desativada.                                                                                        |
| Confirmação                    | Extensão | Receber por webhook não confirma o evento. `POST /v1/events/acknowledgment` continua obrigatório.                                                                                                        |

## Loja e cardápio [#loja-e-cardápio]

| Recurso do padrão 1.4.0                                                     | Situação         | Observação                                                                       |
| --------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------- |
| `GET /v1/merchant/{merchantId}`                                             | Implementado     | Escopo `merchant:read`. O `merchantId` precisa ser o da credencial.              |
| `GET /v1/merchant/{merchantId}/menus`                                       | Implementado     | Escopo `catalog:read`. Array com um único cardápio.                              |
| Grupo "Tamanho" para variações                                              | Extensão         | Produto com vários preços vira grupo obrigatório com id `{productId}-variacoes`. |
| Item indisponível                                                           | Extensão         | Continua no cardápio com `status: "UNAVAILABLE"` em vez de sumir.                |
| Módulo Merchant do padrão (envio e atualização de cardápio pelo integrador) | Não implementado | O cardápio é editado pelo lojista no painel do MeuPedido. A API oferece leitura. |

## Outros [#outros]

| Recurso                          | Situação         | Observação                                                                                                                  |
| -------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Campo `test` no pedido           | Implementado     | `true` em pedidos de loja de teste.                                                                                         |
| Campo `preparationStartDateTime` | Implementado     | Igual a `createdAt` em pedidos `INSTANT` e a `scheduledDateTimeStart` em `SCHEDULED`.                                       |
| Campo `extraInfo`                | Implementado     | Texto livre, por exemplo `"obs \| origem=X \| canal=Y \| numeroCanal=Z"`.                                                   |
| Ambiente de sandbox              | Não implementado | Usa-se uma [loja de teste](/pt-BR/docs/getting-started/first-steps/test-store) em produção, criada pelo suporte sob pedido. |
| CORS                             | Extensão         | Liberado apenas para `https://developer.meupedido.io`, para o playground do portal. Integrações são servidor a servidor.    |

## Como tratar as diferenças [#como-tratar-as-diferenças]

* **Integração Open Delivery já pronta:** aponte para `https://api.meupedido.io/open-delivery`, troque a rota de polling para `/v1/events:polling` se ainda usa a forma com barra, e ignore `eventType` que não conhece (as extensões `PREPARING`, `MODIFIED` e `COURIER_*`), confirmando o evento mesmo assim.
* **Cancelamento:** não espere `CANCELLATION_REQUESTED` nem `acceptCancellation`. Quando `requestCancellation` responde `202`, o pedido já está `CANCELLED` e o evento `CANCELLED` chega no feed.
* **Estados extras:** `PREPARING` é opcional. Você pode ir de `ACCEPTED` direto para `READY`, `DELIVERY` ou `DONE`.

Quando um item desta lista mudar de situação, a mudança é publicada no [Changelog](/pt-BR/docs/changelog).

## Próximos passos [#próximos-passos]

* [Catálogo de eventos](/pt-BR/docs/getting-started/orders/events): quando cada tipo é emitido e o que traz.
* [Ações e idempotência](/pt-BR/docs/getting-started/orders/actions): as transições e o header `Idempotency-Key`.
* [Referência de API](/pt-BR/docs/references): todas as operações disponíveis.


# Conceitos (/pt-BR/docs/getting-started/concepts)

> Loja, credencial, escopo, evento, entrega, pedido e loja de teste: o vocabulário da API.

Os termos abaixo aparecem em toda a documentação e nos payloads da API. Cada um tem um significado preciso.

## Loja (merchant) [#loja-merchant]

A unidade de negócio que recebe pedidos no MeuPedido. Na API ela é o `merchant`, identificado por um GUID (`merchantId`). Toda credencial pertence a uma loja, e todo pedido vem com `merchant.id` e `merchant.name`.

Uma rede com várias unidades tem várias lojas. Cada uma precisa da própria credencial; não existe credencial que enxergue mais de uma loja.

```text
merchantId  6d2f4c8a-1b3e-4f5a-9c7d-2e8b0a1f3c5d
```

## Credencial [#credencial]

O par `client_id` e `client_secret` que o lojista gera no painel, em Integrações > Open Delivery, para autorizar o seu sistema em uma loja. O `client_id` começa com `mp_` seguido de 24 caracteres hexadecimais; o `client_secret` é mostrado uma única vez, no momento da criação.

```text
client_id      mp_7f3a9c1e5b2d4a6f8e0c1b3d
client_secret  mostrado uma vez, no painel
```

A credencial carrega tudo que define o acesso: os escopos, o modo de entrega de eventos, a URL do webhook e se pedidos de marketplace entram no feed. O lojista pode **pausar** (o acesso para de funcionar, mas os eventos continuam acumulando e voltam quando ele retomar), **retomar** e **revogar** (permanente). Com a credencial pausada ou revogada, qualquer chamada responde `401 invalid_token` imediatamente.

Cada credencial tem o próprio feed de eventos. Duas credenciais na mesma loja recebem os mesmos eventos, com confirmações independentes.

## Escopo [#escopo]

O que a credencial pode fazer. São quatro, definidos na criação e não editáveis depois: para mudar, o lojista cria outra credencial.

| Escopo          | Permite                                                                    |
| --------------- | -------------------------------------------------------------------------- |
| `orders:read`   | Consultar eventos, confirmar eventos e ler pedidos                         |
| `orders:write`  | Executar ações no pedido (`confirm`, `dispatch`, `requestCancellation`...) |
| `merchant:read` | Ler os dados da loja                                                       |
| `catalog:read`  | Ler o cardápio                                                             |

Uma chamada sem o escopo necessário responde `403 insufficient_scope`.

## Token [#token]

O JWT (HS256) que o seu sistema obtém em `POST /oauth/token` com a credencial e envia em `Authorization: Bearer` em todas as outras chamadas. Vale por 3600 segundos, não tem refresh token e carrega as claims `merchant_id`, `client_id` e `scope`. Renovar é pedir outro token.

## Evento [#evento]

O registro de que algo aconteceu com um pedido: foi criado, confirmado, ficou pronto, saiu para entrega, foi concluído ou cancelado. Cada evento tem um `eventId` (GUID) único, um `eventType`, o `orderId` e a `orderURL` para buscar o pedido. O evento nunca carrega o pedido inteiro.

```json
{
  "eventId": "c0a8f1e2-3d4b-4c5a-9e6f-7a8b9c0d1e2f",
  "eventType": "CREATED",
  "orderId": "9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e",
  "orderURL": "https://api.meupedido.io/open-delivery/v1/orders/9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e",
  "createdAt": "2026-09-19T14:32:10Z"
}
```

Eventos ficam no feed da credencial até serem **confirmados** (`POST /v1/events/acknowledgment`). O `eventId` é a chave de deduplicação do seu lado: o mesmo evento pode chegar mais de uma vez. O catálogo completo está em [Eventos](/pt-BR/docs/getting-started/orders/events).

## Entrega de eventos [#entrega-de-eventos]

O canal pelo qual os eventos chegam ao seu sistema, escolhido pelo lojista na credencial:

* **`POLLING`**: seu sistema consulta `GET /v1/events:polling` no ritmo que preferir.
* **`WEBHOOK`**: o MeuPedido faz `POST` na sua URL HTTPS, com assinatura HMAC-SHA256.
* **`BOTH`**: os dois. O webhook dá latência baixa; o polling garante que nada se perde.

Não confunda com o **tipo de entrega do pedido** (`type`: `DELIVERY`, `TAKEOUT` ou `INDOOR`), que descreve como o cliente recebe a comida, nem com o bloco `delivery` do pedido, que traz o endereço.

## Pedido [#pedido]

O objeto central da API. Tem `id` (GUID), `displayId` (o número curto que aparece para o lojista e o cliente), `type`, `orderTiming` (`INSTANT` ou `SCHEDULED`), itens com opções, taxas, descontos, totais, pagamentos, cliente e, conforme o tipo, `delivery` ou `takeout`. Campos sem valor vêm como `null`. A estrutura campo a campo está em [Estrutura do pedido](/pt-BR/docs/getting-started/orders/order-object).

O status do pedido não é um campo do objeto: ele é comunicado pelos eventos e pelas respostas das ações (`situation`). A máquina de estados está em [Ciclo de vida](/pt-BR/docs/getting-started/orders/lifecycle).

## Ação [#ação]

Uma chamada `POST /v1/orders/{orderId}/<ação>` que avança o pedido: `confirm`, `startPreparation`, `readyForPickup`, `dispatch`, `pickedUp`, `delivered` e `requestCancellation`. A resposta é `202` com `status` (`accepted` ou `already_applied`) e a `situation` resultante. O header opcional `Idempotency-Key` protege contra reenvios. Detalhes em [Ações e idempotência](/pt-BR/docs/getting-started/orders/actions).

## Extensão MeuPedido [#extensão-meupedido]

Qualquer evento, ação ou campo que o MeuPedido oferece além do que o Open Delivery 1.4.0 define. Estão marcados com **Extensão MeuPedido** na documentação. Exemplos: a ação `startPreparation`, o evento `PREPARING`, os eventos `COURIER_*` e os webhooks com assinatura `X-MeuPedido-*`. Uma integração estritamente Open Delivery pode ignorá-los.

## Loja de teste [#loja-de-teste]

Uma loja real, em produção, criada pelo suporte para você desenvolver e homologar. Não existe sandbox. A diferença é que todo pedido dela sai com `"test": true`, e você mesmo gera os pedidos pelo cardápio digital. Veja [Loja de teste](/pt-BR/docs/getting-started/first-steps/test-store).

## Próximos passos [#próximos-passos]

* [Obter credenciais](/pt-BR/docs/getting-started/first-steps/get-credentials): como o lojista cria a credencial no painel.
* [Autenticação](/pt-BR/docs/getting-started/authentication): do `client_secret` ao token.
* [Catálogo de eventos](/pt-BR/docs/getting-started/orders/events): todos os `eventType` e quando cada um dispara.


# Como funciona (/pt-BR/docs/getting-started/how-it-works)

> Os três lados da integração: lojista, MeuPedido e o seu sistema, com entrega por polling ou webhook.

Uma integração Open Delivery tem três participantes. Entender o papel de cada um evita a maioria das dúvidas que chegam ao suporte.

## Os três lados [#os-três-lados]

**O lojista** usa o MeuPedido para receber pedidos do cardápio digital, do balcão, de marketplaces e do WhatsApp. É ele quem decide se o seu sistema pode ver os pedidos da loja: no painel, em Integrações > Open Delivery, ele cria uma credencial, escolhe o que ela pode fazer e pode pausar ou revogar o acesso a qualquer momento.

**O MeuPedido** é a aplicação de pedidos, no vocabulário do Open Delivery. Ele guarda o pedido, controla o status, gera um evento a cada mudança relevante e expõe tudo isso pela API em `https://api.meupedido.io/open-delivery`.

**O seu sistema** consome a API. Ele se autentica com a credencial da loja, recebe os eventos, busca os pedidos que precisar e devolve ações (`confirm`, `dispatch`, `delivered`...) conforme a operação avança do seu lado.

## O caminho de um pedido [#o-caminho-de-um-pedido]

Considere um pedido de entrega feito pelo cardápio digital da loja.

### O pedido nasce no MeuPedido [#o-pedido-nasce-no-meupedido]

O cliente fecha o carrinho. O MeuPedido cria o pedido com status `PENDING` (ou `PENDING_PAYMENT`, se o pagamento for Pix online e ainda não tiver sido confirmado) e emite o evento `CREATED`. Pedidos aguardando pagamento só geram `CREATED` quando o Pix é confirmado.

### Seu sistema recebe o evento [#seu-sistema-recebe-o-evento]

Por polling, o evento aparece na próxima chamada a `GET /v1/events:polling`. Por webhook, o MeuPedido faz um `POST` na sua URL em segundos. Nos dois casos o corpo é o mesmo envelope, com `eventId`, `eventType`, `orderId` e `orderURL`. O envelope nunca traz o pedido.

### Seu sistema busca o pedido [#seu-sistema-busca-o-pedido]

`GET /v1/orders/{orderId}` devolve o pedido completo: itens, opções, valores, pagamento, cliente e endereço de entrega. É esse payload que você grava no seu banco.

### Seu sistema confirma o evento [#seu-sistema-confirma-o-evento]

`POST /v1/events/acknowledgment` com o `eventId` retira o evento do feed. Sem isso, ele volta a cada consulta, para sempre. Esse passo é obrigatório também para eventos recebidos por webhook.

### Seu sistema avança o pedido [#seu-sistema-avança-o-pedido]

Quando o operador aceita o pedido no seu PDV, chame `POST /v1/orders/{orderId}/confirm`. O MeuPedido muda o status para `ACCEPTED`, mostra isso ao lojista e ao cliente, e emite `CONFIRMED`. O mesmo vale para `startPreparation`, `readyForPickup`, `dispatch` e `delivered`.

### O ciclo fecha [#o-ciclo-fecha]

Em `DONE` o pedido está concluído e o evento `CONCLUDED` é emitido. Se em algum momento o pedido for cancelado, pela loja, pelo cliente ou pela sua API, o evento é `CANCELLED` e o estado é final.

O detalhe de cada transição, incluindo o que é permitido voltar, está em [Ciclo de vida](/pt-BR/docs/getting-started/orders/lifecycle).

## Polling ou webhook [#polling-ou-webhook]

O modo de entrega é definido pelo lojista na credencial: `POLLING`, `WEBHOOK` ou `BOTH`. O envelope do evento é idêntico nos dois canais, então o código que interpreta o evento é um só.

|                   | Polling                                                           | Webhook                                                             |
| ----------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------- |
| Quem inicia       | Seu sistema consulta `GET /v1/events:polling`                     | O MeuPedido faz `POST` na sua URL HTTPS                             |
| Latência          | O intervalo que você escolher                                     | Segundos                                                            |
| Exige URL pública | Não                                                               | Sim, com TLS válido                                                 |
| Garantia          | At-least-once: o que não foi confirmado volta na próxima consulta | Quatro tentativas em cerca de 35 s; depois o evento fica no polling |
| Autenticidade     | Bearer token                                                      | Assinatura HMAC-SHA256 nos headers `X-MeuPedido-*`                  |
| Confirmação       | `POST /v1/events/acknowledgment`                                  | `POST /v1/events/acknowledgment`, igual                             |

Recomendação prática: comece por polling, que funciona de qualquer rede e não tem infraestrutura para manter. Ative webhook (modo `BOTH`) quando precisar de latência menor, mantendo o polling como rede de segurança para o que o webhook não conseguir entregar.

> **Webhook entregue não é evento confirmado**
> Um `2xx` na sua URL diz ao MeuPedido que a entrega funcionou, mas o evento continua no feed até você chamar `POST /v1/events/acknowledgment`. Integrações que esquecem esse passo acumulam eventos que voltam a cada polling.

## O que fica de cada lado [#o-que-fica-de-cada-lado]

* **No MeuPedido:** o pedido, o status atual, o histórico de eventos por credencial e as tentativas de webhook. Eventos confirmados são apagados após 30 dias; eventos não confirmados nunca são apagados.
* **No seu sistema:** a cópia do pedido, o `eventId` de cada evento já processado (para deduplicar) e o token vigente, renovado antes de expirar.

## Próximos passos [#próximos-passos]

* [Conceitos](/pt-BR/docs/getting-started/concepts): loja, credencial, escopo, evento e os outros termos desta documentação.
* [Obter credenciais](/pt-BR/docs/getting-started/first-steps/get-credentials): o que o lojista faz no painel.
* [Polling](/pt-BR/docs/getting-started/orders/polling): o canal recomendado para a primeira versão.


# Visão geral (/pt-BR/docs/getting-started)

> O que é a API Open Delivery do MeuPedido, para quem ela serve e por onde começar.

A API Open Delivery do MeuPedido entrega ao seu sistema, em tempo real, os pedidos que chegam às lojas que usam o MeuPedido, e recebe de volta cada avanço de status até a conclusão. Ela segue o padrão [Open Delivery 1.4.0](https://www.opendelivery.com.br) da Abrasel, módulos **Order** e **Merchant**, com o MeuPedido no papel de aplicação de pedidos.

Se você mantém um ERP, PDV, KDS, sistema de gestão de entregas ou qualquer software que precisa saber o que a loja vendeu, é aqui que você conecta.

```text title="URL base"
https://api.meupedido.io/open-delivery
```

## O que a API faz [#o-que-a-api-faz]

| Capacidade                                                            | Como                                                      | Escopo          |
| --------------------------------------------------------------------- | --------------------------------------------------------- | --------------- |
| Receber pedidos novos e mudanças de status                            | Polling em `GET /v1/events:polling` ou webhook na sua URL | `orders:read`   |
| Ler um pedido completo                                                | `GET /v1/orders/{orderId}`                                | `orders:read`   |
| Avançar o pedido (confirmar, preparar, despachar, concluir, cancelar) | `POST /v1/orders/{orderId}/confirm` e as demais ações     | `orders:write`  |
| Ler os dados da loja                                                  | `GET /v1/merchant/{merchantId}`                           | `merchant:read` |
| Ler o cardápio                                                        | `GET /v1/merchant/{merchantId}/menus`                     | `catalog:read`  |

Tudo é autenticado com OAuth 2.0 client credentials. Uma credencial dá acesso a exatamente uma loja; para integrar cinco lojas, o lojista de cada uma gera uma credencial e você guarda cinco pares de `client_id` e `client_secret`.

## Como a integração funciona [#como-a-integração-funciona]

1. **O lojista concede acesso.** No painel do MeuPedido, em Integrações > Open Delivery, ele cria uma credencial para o seu sistema e escolhe os escopos e o modo de entrega.
2. **Seu sistema pede um token.** `POST /oauth/token` com `client_id` e `client_secret` devolve um JWT válido por 3600 segundos.
3. **Seu sistema recebe eventos.** Cada pedido criado, confirmado, despachado ou cancelado vira um evento. Você consulta por polling, recebe por webhook, ou os dois.
4. **Seu sistema responde com ações.** Ao confirmar o pedido no seu lado, chame `confirm`; ao despachar, `dispatch`; e assim até `delivered` ou `pickedUp`.

A leitura completa desse fluxo está em [Como funciona](/pt-BR/docs/getting-started/how-it-works).

## Sem sandbox, com loja de teste [#sem-sandbox-com-loja-de-teste]

Não existe um ambiente separado de homologação. O suporte cria uma **loja de teste** em produção para você: ela se comporta como qualquer loja, mas todo pedido sai com `"test": true`, e você mesmo gera pedidos pelo cardápio digital dela. Veja [Loja de teste](/pt-BR/docs/getting-started/first-steps/test-store).

## Duas formas de receber eventos [#duas-formas-de-receber-eventos]

- [Polling](/pt-BR/docs/getting-started/orders/polling): Seu sistema consulta a API. Entrega at-least-once: o que não for confirmado volta na próxima consulta. Funciona atrás de qualquer firewall.

- [Webhooks](/pt-BR/docs/getting-started/orders/webhooks): A API chama a sua URL HTTPS com o mesmo envelope, assinado com HMAC-SHA256. Retentativas automáticas e o polling continua como rede de segurança.

Nos dois casos, o evento só sai do feed quando você chama `POST /v1/events/acknowledgment`. Receber por webhook não confirma o evento.

## Além do padrão [#além-do-padrão]

O MeuPedido emite alguns eventos e aceita algumas ações que o Open Delivery 1.4.0 não prevê, como `PREPARING`, `MODIFIED` e a família `COURIER_*` de rastreio do entregador. Eles aparecem nesta documentação com o selo **Extensão MeuPedido**. Se o seu sistema já fala Open Delivery, pode ignorá-los sem prejuízo. A lista completa do que está e do que ainda não está implementado fica em [Compatibilidade](/pt-BR/docs/getting-started/compatibility).

## Por onde começar [#por-onde-começar]

- [Obter credenciais](/pt-BR/docs/getting-started/first-steps/get-credentials): O lojista gera o client_id e o segredo no painel.

- [Primeira integração em 10 minutos](/pt-BR/docs/getting-started/first-steps/first-integration): Token, polling, busca do pedido, ack e confirm, com playground em cada passo.

- [Referência de API](/pt-BR/docs/references): Todos os endpoints, campos e erros, com a especificação OpenAPI para download.

## Próximos passos [#próximos-passos]

* [Como funciona](/pt-BR/docs/getting-started/how-it-works): os três lados da integração e o caminho de um pedido.
* [Conceitos](/pt-BR/docs/getting-started/concepts): o vocabulário que o resto da documentação usa.
* [Obter credenciais](/pt-BR/docs/getting-started/first-steps/get-credentials): o primeiro passo prático.


# Loja e cardápio (/pt-BR/docs/getting-started/merchant)

> GET merchant e GET menus: dados da loja, categorias, itens, grupos de opções e o grupo Tamanho.

A API expõe duas operações de leitura sobre a loja ligada à credencial: os dados cadastrais (`GET /v1/merchant/{merchantId}`) e o cardápio publicado (`GET /v1/merchant/{merchantId}/menus`). Ambas são somente leitura. O lojista continua editando loja e cardápio pelo painel do MeuPedido; o seu sistema lê o resultado.

| Operação                              | Escopo          | Retorna                        |
| ------------------------------------- | --------------- | ------------------------------ |
| `GET /v1/merchant/{merchantId}`       | `merchant:read` | Um objeto com os dados da loja |
| `GET /v1/merchant/{merchantId}/menus` | `catalog:read`  | Um array com um cardápio       |

## O `merchantId` é o da credencial [#o-merchantid-é-o-da-credencial]

Uma credencial pertence a uma única loja. O `merchantId` da URL precisa ser o id dessa loja; qualquer outro id devolve `404`, mesmo que a loja exista. O id está no claim `merchant_id` do token de acesso e no bloco `merchant.id` de todo pedido devolvido pela API.

> **Várias lojas**
> Para integrar N lojas, o lojista de cada uma gera a própria credencial. Cada token só enxerga a loja que o emitiu. Veja [Conceitos](/pt-BR/docs/getting-started/concepts).

## Buscar a loja [#buscar-a-loja]

**cURL**

```bash
curl https://api.meupedido.io/open-delivery/v1/merchant/8f1c2b6e-4a3d-4c7e-9b21-6d0f3a5e8c47 \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

**Node.js**

```ts
const merchantId = '8f1c2b6e-4a3d-4c7e-9b21-6d0f3a5e8c47';

const response = await fetch(
  `https://api.meupedido.io/open-delivery/v1/merchant/${merchantId}`,
  { headers: { Authorization: `Bearer ${accessToken}` } },
);

if (!response.ok) throw new Error(`merchant: HTTP ${response.status}`);
const merchant = await response.json();
```

**Python**

```python
import requests

merchant_id = "8f1c2b6e-4a3d-4c7e-9b21-6d0f3a5e8c47"

response = requests.get(
    f"https://api.meupedido.io/open-delivery/v1/merchant/{merchant_id}",
    headers={"Authorization": f"Bearer {access_token}"},
    timeout=10,
)
response.raise_for_status()
merchant = response.json()
```

**C#**

```csharp
var merchantId = "8f1c2b6e-4a3d-4c7e-9b21-6d0f3a5e8c47";

using var http = new HttpClient { BaseAddress = new Uri("https://api.meupedido.io/open-delivery/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

using var response = await http.GetAsync($"v1/merchant/{merchantId}");
response.EnsureSuccessStatusCode();
var merchant = await response.Content.ReadFromJsonAsync<JsonElement>();
```

Resposta `200`:

```json
{
  "id": "8f1c2b6e-4a3d-4c7e-9b21-6d0f3a5e8c47",
  "name": "Cantina da Vila",
  "description": "Massas artesanais e pizzas de fermentação natural.",
  "document": "12345678000190",
  "status": "AVAILABLE",
  "contactEmails": ["contato@cantinadavila.com.br"],
  "contactPhones": ["+5511912345678"],
  "address": {
    "country": "BR",
    "state": "SP",
    "city": "São Paulo",
    "district": "Vila Madalena",
    "street": "Rua Harmonia",
    "number": "512",
    "postalCode": "05435-000",
    "complement": null,
    "latitude": -23.5537,
    "longitude": -46.6883
  },
  "services": [
    { "serviceType": "DELIVERY", "status": "AVAILABLE", "menuId": "c9d4e2a1-7b3f-4f8e-a6d5-2e1b9c0f4a73" },
    { "serviceType": "TAKEOUT", "status": "AVAILABLE", "menuId": "c9d4e2a1-7b3f-4f8e-a6d5-2e1b9c0f4a73" }
  ],
  "createdAt": "2025-03-12T14:02:11Z",
  "lastUpdate": "2026-09-18T21:45:09Z"
}
```

### Campos da loja [#campos-da-loja]

| Campo           | Tipo                         | Descrição                                                                                                                                     |
| --------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`            | string                       | Id da loja. É o mesmo `merchantId` da URL e do claim `merchant_id` do token.                                                                  |
| `name`          | string                       | Nome de exibição da loja.                                                                                                                     |
| `description`   | string ou null               | Descrição livre, como aparece no cardápio digital.                                                                                            |
| `document`      | string ou null               | CNPJ ou CPF da loja, só dígitos.                                                                                                              |
| `status`        | `AVAILABLE` ou `UNAVAILABLE` | Se a loja está aberta para receber pedidos neste momento.                                                                                     |
| `contactEmails` | string\[]                    | E-mails de contato.                                                                                                                           |
| `contactPhones` | string\[]                    | Telefones de contato.                                                                                                                         |
| `address`       | objeto                       | Endereço com `country`, `state`, `city`, `district`, `street`, `number`, `postalCode`, `complement`, `latitude` e `longitude`.                |
| `services`      | objeto\[]                    | Modalidades de atendimento. Cada item traz `serviceType` (`DELIVERY`, `TAKEOUT` ou `INDOOR`), `status` e o `menuId` usado naquela modalidade. |
| `createdAt`     | data                         | Criação da loja, em UTC com sufixo `Z`.                                                                                                       |
| `lastUpdate`    | data                         | Última alteração dos dados da loja, em UTC com sufixo `Z`.                                                                                    |

Campos sem valor vêm como `null`, nunca são omitidos.

## Buscar o cardápio [#buscar-o-cardápio]

O cardápio é devolvido em um array, como pede o padrão Open Delivery. No MeuPedido cada loja tem um único cardápio, então o array sempre traz um elemento e o `id` dele é o `menuId` referenciado em `services`.

**cURL**

```bash
curl https://api.meupedido.io/open-delivery/v1/merchant/8f1c2b6e-4a3d-4c7e-9b21-6d0f3a5e8c47/menus \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

**Node.js**

```ts
const response = await fetch(
  `https://api.meupedido.io/open-delivery/v1/merchant/${merchantId}/menus`,
  { headers: { Authorization: `Bearer ${accessToken}` } },
);

if (!response.ok) throw new Error(`menus: HTTP ${response.status}`);
const [menu] = await response.json();
```

**Python**

```python
response = requests.get(
    f"https://api.meupedido.io/open-delivery/v1/merchant/{merchant_id}/menus",
    headers={"Authorization": f"Bearer {access_token}"},
    timeout=10,
)
response.raise_for_status()
(menu,) = response.json()
```

**C#**

```csharp
using var response = await http.GetAsync($"v1/merchant/{merchantId}/menus");
response.EnsureSuccessStatusCode();
var menus = await response.Content.ReadFromJsonAsync<JsonElement>();
var menu = menus[0];
```

Resposta `200`, resumida a uma categoria, dois itens e dois grupos de opções:

```json
[
  {
    "id": "c9d4e2a1-7b3f-4f8e-a6d5-2e1b9c0f4a73",
    "name": "Cardápio principal",
    "categories": [
      {
        "id": "5b7e1f3a-9c2d-4e6b-8a1f-0d3c7e9b2a54",
        "name": "Pizzas",
        "description": "Massa de fermentação natural, 35 cm ou 25 cm.",
        "index": 0,
        "status": "AVAILABLE",
        "itemOfferIds": [
          "a3f9c1d2-6e4b-4a8c-9f1d-7b2e5c0a8d36",
          "e7d2b4c9-1a5f-4c3e-b8a2-9f6d0e1c3b75"
        ]
      }
    ],
    "items": [
      {
        "id": "a3f9c1d2-6e4b-4a8c-9f1d-7b2e5c0a8d36",
        "name": "Pizza Margherita",
        "description": "Molho de tomate, mussarela de búfala e manjericão.",
        "externalCode": "PZ-001",
        "status": "AVAILABLE",
        "image": "https://cdn.meupedido.io/lojas/cantina-da-vila/pizza-margherita.jpg",
        "price": { "value": 0, "originalValue": 0, "currency": "BRL" },
        "optionGroupIds": [
          "a3f9c1d2-6e4b-4a8c-9f1d-7b2e5c0a8d36-variacoes",
          "2c8e4b1d-7f3a-4d9e-a5c6-1b0f8d3e7a92"
        ]
      },
      {
        "id": "e7d2b4c9-1a5f-4c3e-b8a2-9f6d0e1c3b75",
        "name": "Pizza Calabresa",
        "description": "Calabresa artesanal, cebola roxa e azeitonas.",
        "externalCode": "PZ-002",
        "status": "UNAVAILABLE",
        "image": null,
        "price": { "value": 62.9, "originalValue": 69.9, "currency": "BRL" },
        "optionGroupIds": ["2c8e4b1d-7f3a-4d9e-a5c6-1b0f8d3e7a92"]
      }
    ],
    "optionGroups": [
      {
        "id": "a3f9c1d2-6e4b-4a8c-9f1d-7b2e5c0a8d36-variacoes",
        "name": "Tamanho",
        "description": null,
        "index": 0,
        "status": "AVAILABLE",
        "minPermitted": 1,
        "maxPermitted": 1,
        "options": [
          {
            "id": "4d1a7c3e-8b5f-4e2a-9c6d-3f0b1e8a5d27",
            "name": "Grande (35 cm)",
            "externalCode": "PZ-001-G",
            "index": 0,
            "status": "AVAILABLE",
            "price": { "value": 69.9, "originalValue": 69.9, "currency": "BRL" }
          },
          {
            "id": "9e3b5d7f-2c1a-4f6e-8d4b-6a0c2f9e1b58",
            "name": "Média (25 cm)",
            "externalCode": "PZ-001-M",
            "index": 1,
            "status": "AVAILABLE",
            "price": { "value": 49.9, "originalValue": 49.9, "currency": "BRL" }
          }
        ]
      },
      {
        "id": "2c8e4b1d-7f3a-4d9e-a5c6-1b0f8d3e7a92",
        "name": "Adicionais",
        "description": "Escolha até 3.",
        "index": 1,
        "status": "AVAILABLE",
        "minPermitted": 0,
        "maxPermitted": 3,
        "options": [
          {
            "id": "7a2f9d4b-3e6c-4b1a-8f5d-0c9e2a7b4d63",
            "name": "Borda recheada",
            "externalCode": "AD-010",
            "index": 0,
            "status": "AVAILABLE",
            "price": { "value": 8, "originalValue": 8, "currency": "BRL" }
          }
        ]
      }
    ]
  }
]
```

### Estrutura do cardápio [#estrutura-do-cardápio]

O cardápio é normalizado: categorias apontam para itens por id, itens apontam para grupos de opções por id. Nada é aninhado. Monte índices por id em memória antes de percorrer.

**Cardápio**

| Campo          | Tipo      | Descrição                                                |
| -------------- | --------- | -------------------------------------------------------- |
| `id`           | string    | Id do cardápio. Igual ao `menuId` em `services` da loja. |
| `name`         | string    | Nome do cardápio.                                        |
| `categories`   | objeto\[] | Categorias, na ordem de exibição.                        |
| `items`        | objeto\[] | Todos os itens vendáveis do cardápio.                    |
| `optionGroups` | objeto\[] | Todos os grupos de opções referenciados pelos itens.     |

**Categoria**

| Campo          | Tipo                         | Descrição                                                      |
| -------------- | ---------------------------- | -------------------------------------------------------------- |
| `id`           | string                       | Id da categoria.                                               |
| `name`         | string                       | Nome da categoria.                                             |
| `description`  | string ou null               | Descrição opcional.                                            |
| `index`        | número                       | Posição na ordem de exibição, a partir de 0.                   |
| `status`       | `AVAILABLE` ou `UNAVAILABLE` | Disponibilidade da categoria inteira.                          |
| `itemOfferIds` | string\[]                    | Ids dos itens que pertencem à categoria, na ordem de exibição. |

**Item**

| Campo            | Tipo                         | Descrição                                                                              |
| ---------------- | ---------------------------- | -------------------------------------------------------------------------------------- |
| `id`             | string                       | Id do item. É o mesmo `id` que chega em `items[].id` de um pedido.                     |
| `name`           | string                       | Nome do item.                                                                          |
| `description`    | string ou null               | Descrição.                                                                             |
| `externalCode`   | string ou null               | Código do produto no sistema do lojista (PLU, SKU). Use para casar com o seu cadastro. |
| `status`         | `AVAILABLE` ou `UNAVAILABLE` | Disponibilidade do item.                                                               |
| `image`          | string ou null               | URL da imagem.                                                                         |
| `price`          | objeto                       | `value` (preço atual), `originalValue` (preço sem promoção) e `currency` (`BRL`).      |
| `optionGroupIds` | string\[]                    | Ids dos grupos de opções que o item oferece, na ordem de exibição.                     |

**Grupo de opções**

| Campo          | Tipo                         | Descrição                                                                                          |
| -------------- | ---------------------------- | -------------------------------------------------------------------------------------------------- |
| `id`           | string                       | Id do grupo.                                                                                       |
| `name`         | string                       | Nome do grupo, como "Tamanho" ou "Adicionais".                                                     |
| `description`  | string ou null               | Descrição opcional.                                                                                |
| `index`        | número                       | Posição na ordem de exibição.                                                                      |
| `status`       | `AVAILABLE` ou `UNAVAILABLE` | Disponibilidade do grupo.                                                                          |
| `minPermitted` | número                       | Quantidade mínima de opções que o cliente precisa escolher. `1` ou mais torna o grupo obrigatório. |
| `maxPermitted` | número                       | Quantidade máxima de opções.                                                                       |
| `options`      | objeto\[]                    | Opções do grupo, cada uma com `id`, `name`, `externalCode`, `index`, `status` e `price`.           |

## Variações viram o grupo "Tamanho" [#variações-viram-o-grupo-tamanho]

No MeuPedido um produto pode ter vários preços (por exemplo, pizza grande e média). O padrão Open Delivery não tem esse conceito no item, então a API converte cada produto com variações em:

* um item, que representa o produto, e
* um grupo de opções obrigatório (`minPermitted: 1`, `maxPermitted: 1`) chamado **Tamanho**, com id `{productId}-variacoes`, em que cada opção é uma variação com o próprio preço.

O preço que conta é o da variação escolhida. Em um pedido, a variação **não** aparece em `options[]`: o item chega com `externalCode` igual ao id do produto, `name` com o nome do produto já incluindo a variação (por exemplo, `"Pizza Margherita Grande"`) e `unitPrice` com o preço da variação escolhida. O `options[]` do item traz apenas os complementos, e `totalPrice` já considera `unitPrice` mais `optionsPrice`. Não há um id de variação no pedido; para saber qual foi escolhida, compare o `unitPrice` com os preços das opções do grupo Tamanho no cardápio, ou use o `name`.

> **Não trate Tamanho como adicional**
> Um grupo cujo id termina em `-variacoes` representa o próprio produto, não um acréscimo. Se o seu PDV tem o conceito de variação ou grade, mapeie para ele no cardápio, e no pedido resolva a variação pelo preço unitário ou pelo nome do item.

## Itens indisponíveis não somem [#itens-indisponíveis-não-somem]

Quando o lojista pausa um item, ele continua no cardápio com `status: "UNAVAILABLE"`. O mesmo vale para categorias, grupos e opções. Isso mantém os ids estáveis: um pedido antigo sempre aponta para um item que existe no cardápio. Filtre por `status` na hora de exibir; não use a ausência do item como sinal de nada.

## Quando sincronizar [#quando-sincronizar]

Não há evento de cardápio na API. Para manter uma cópia local, busque o cardápio em um intervalo que faça sentido para a sua operação e compare `lastUpdate` da loja para saber se algo mudou nos dados cadastrais. Cada chamada conta no [limite de 600 requisições por minuto](/pt-BR/docs/getting-started/best-practices#limites-de-requisição) da credencial.

## Próximos passos [#próximos-passos]

* [Estrutura do pedido](/pt-BR/docs/getting-started/orders/order-object): como os itens e opções do cardápio aparecem em um pedido.
* [Autenticação](/pt-BR/docs/getting-started/authentication): escopos `merchant:read` e `catalog:read`.
* [Referência de API](/pt-BR/docs/references): parâmetros e esquemas completos de `getMerchant` e `getMenus`.


# Suporte (/pt-BR/docs/getting-started/support)

> Como falar com o suporte pelo WhatsApp e o que enviar: client_id, traceId, horário e eventId.

O suporte ao integrador é feito pelo WhatsApp, pelo time do MeuPedido que conhece a API e o painel do lojista.

> **WhatsApp do suporte**
> [(11) 95502-1289](https://wa.me/5511955021289)

## Quando falar com o suporte [#quando-falar-com-o-suporte]

* Para pedir uma [loja de teste](/pt-BR/docs/getting-started/first-steps/test-store).
* Quando uma chamada responde `500 internal_error`.
* Quando um evento que deveria existir não aparece no polling nem chega no webhook.
* Quando o comportamento observado diverge do que esta documentação descreve.
* Para tirar dúvidas sobre um caso que a documentação não cobre.

Antes de escrever, vale conferir com o lojista se a credencial está ativa: uma credencial pausada ou revogada responde `401 invalid_token` em tudo, e a solução está no painel, não no suporte.

## O que enviar [#o-que-enviar]

Quanto mais precisa a mensagem, mais rápida a resposta. Inclua o que se aplicar:

| Item                     | Onde encontrar                                                                   | Por que ajuda                                                     |
| ------------------------ | -------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `client_id`              | No painel do lojista ou na configuração do seu sistema                           | Identifica a loja e a credencial. Nunca envie o `client_secret`   |
| `traceId`                | No corpo das respostas `500` e das respostas `404` automáticas                   | Localiza a requisição exata nos logs do MeuPedido                 |
| Horário                  | Em UTC, com data. O horário local também serve, desde que diga o fuso            | Delimita a busca nos logs                                         |
| `eventId`                | No envelope do evento                                                            | Permite ver o histórico e as tentativas de entrega daquele evento |
| `orderId` ou `displayId` | No pedido                                                                        | Localiza o pedido do lado da loja                                 |
| Requisição e resposta    | O que você enviou (sem o segredo e sem o token) e o que recebeu, com status HTTP | Evita uma rodada de perguntas                                     |

Um exemplo de mensagem completa:

```text
Integração Acme PDV, client_id mp_7f3a9c1e5b2d4a6f8e0c1b3d.
POST /v1/orders/9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e/confirm às 2026-09-19T14:35:02Z
respondeu 500 com traceId 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00.
O evento CREATED do pedido é o eventId c0a8f1e2-3d4b-4c5a-9e6f-7a8b9c0d1e2f.
```

> **Nunca envie segredos**
> O suporte não precisa do `client_secret`, do segredo do webhook nem de um token de acesso para investigar. Se algum deles foi exposto, peça ao lojista para revogar a credencial ou rotacionar o segredo do webhook no painel.

## O que o lojista resolve sozinho [#o-que-o-lojista-resolve-sozinho]

Várias situações são resolvidas no painel do lojista, em Integrações > Open Delivery, sem abrir chamado:

* credencial pausada ou revogada: retomar ou criar outra;
* webhook desativado após 20 falhas consecutivas: **Limpar fila**;
* evento que não chegou pelo webhook: **Reenviar evento**, ou buscar no polling, onde ele continua disponível;
* segredo do webhook comprometido: salvar a URL de novo, o que rotaciona o segredo.

## Próximos passos [#próximos-passos]

* [Boas práticas](/pt-BR/docs/getting-started/best-practices): a tabela completa de erros e o que fazer com cada um.
* [Obter credenciais](/pt-BR/docs/getting-started/first-steps/get-credentials): as ações disponíveis para o lojista no painel.
* [Changelog](/pt-BR/docs/changelog): mudanças recentes na API.


# Obter token de acesso (/pt-BR/docs/references/authentication/oauthToken)

> Troca client_id e client_secret por um token de acesso.

`POST https://api.meupedido.io/open-delivery/oauth/token`

Troca `client_id` e `client_secret` por um token de acesso. É o fluxo `client_credentials`
do OAuth 2.0 (RFC 6749): não há login de usuário, redirecionamento nem refresh token.

As credenciais podem ser enviadas de três formas, e o corpo em formulário é a canônica:

- `application/x-www-form-urlencoded` com `grant_type`, `client_id` e `client_secret`.
- `application/json` com os mesmos campos, em snake_case ou camelCase (`grantType`,
  `clientId`, `clientSecret`).
- `Authorization: Basic base64(client_id:client_secret)` com apenas `grant_type` no corpo.
  Quando corpo e cabeçalho trazem credenciais, o corpo prevalece.

A resposta traz cada campo em snake_case (RFC 6749) e em camelCase (texto do padrão Open
Delivery), com valores idênticos. O token é um JWT HS256 com as claims `merchant_id`,
`client_id` e `scope`, válido por 3600 segundos. Trate-o como opaco.

Renove antes de expirar, com margem de alguns minutos, e compartilhe um token por
credencial entre os workers do seu processo: pedir um token por requisição esgota o
limite do endpoint.

Erros seguem o RFC 6749 (`error` e `error_description`). O `401 invalid_client` vem só com
o código, de propósito: credencial inexistente e segredo errado recebem a mesma resposta
para o endpoint não revelar quais `client_id` existem. Se o endpoint responder
`401 invalid_client` para uma credencial que funcionava, ela foi pausada ou revogada pelo
lojista: pare e avise o operador.

Limites: 60 requisições por minuto por endereço IP e 10 falhas de autenticação por minuto
por credencial. O segredo correto continua sendo aceito mesmo com a credencial no limite de
falhas, para que ninguém derrube a sua integração conhecendo só o seu `client_id`. O corpo
é limitado a 4 KB. O caminho `POST /v1/oauth/token` é um alias que responde igual, mantido
por compatibilidade; use o caminho canônico.

## Autenticação

Operação anônima: não envie `Authorization`.

## Corpo da requisição

Obrigatório, em `application/x-www-form-urlencoded` ou `application/json`.

Credenciais da integração. Também aceitas em `Authorization: Basic`, com apenas `grant_type` no corpo.

## Respostas

- `200`: Token emitido. Guarde `access_token` e o instante de expiração calculado a partir de `expires_in`.
- `400`: Pedido inválido. `invalid_request` quando falta `grant_type`, `client_id` ou `client_secret`, quando o corpo não pôde ser lido no formato declarado ou quando o cabeçalho `Authorization: Basic` está malformado; `unsupported_grant_type` quando `grant_type` não é `client_credentials`. Corrija a requisição; repetir não resolve.
- `401`: Credencial inexistente, segredo errado, ou credencial pausada ou revogada pelo lojista. Uma resposta só, sem `error_description`, de propósito. Não repita em loop: confira o segredo e, se ele estava funcionando, avise o operador da loja.
- `413`: Corpo acima de 4 KB. Um pedido de token tem só três campos e cabe em 200 bytes.
- `415`: `Content-Type` diferente de `application/x-www-form-urlencoded` e `application/json`.
- `429`: Limite atingido. Dois limites independentes produzem esta resposta: 60 requisições por minuto por endereço IP (corpo com `error` e `message`, escrito pelo limitador global) e 10 falhas de autenticação por minuto por credencial (corpo no formato do RFC 6749). Nos dois casos, espere o `Retry-After` antes de tentar de novo. No segundo, confira o segredo: o correto continua sendo aceito.
- `500`: Falha interna. A mensagem não descreve a causa de propósito; informe o `traceId` ao suporte. Repita com backoff exponencial.

## Exemplo de requisição

```shell
curl -X POST "https://api.meupedido.io/open-delivery/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=mp_7f3a9c1e5b2d4a6f8e0c1b3d" \
  -d "client_secret=$CLIENT_SECRET"
```

Contrato completo (OpenAPI 3.1): https://developer.meupedido.io/openapi/open-delivery-v1.yaml


# Confirmar eventos (/pt-BR/docs/references/events/acknowledgeEvents)

> Confirma o recebimento dos eventos e os tira do feed da sua credencial.

`POST https://api.meupedido.io/open-delivery/v1/events/acknowledgment`

Confirma o recebimento dos eventos e os tira do feed da sua credencial. Chame depois de
processar e gravar; nunca antes. É a confirmação, e só ela, que move o feed: consultar não
confirma, e receber por webhook também não.

O corpo é um array de objetos com o `id` de cada evento (o `eventId` do envelope). Envie
a lista inteira de um ciclo num POST só, e não um POST por evento. A resposta é `202` com
`acknowledged`, a quantidade que saiu do feed. Ids que não pertencem à sua credencial, ids
repetidos, ids já confirmados e valores que não são GUID são ignorados em silêncio: a
resposta continua `202`, apenas com a contagem menor. Isso é de propósito, para a operação
não virar um oráculo de quais eventos existem para outro consumidor. Um array vazio, ou
nenhum corpo, responde `202` com `acknowledged: 0`.

O padrão 1.4.0 pede também `orderId` e `eventType` em cada item; esta API aceita só o
`id` e ignora os demais campos, então um cliente aderente que envie os três funciona.

## Autenticação

`Authorization: Bearer <access_token>`. Escopo exigido: `orders:read`.

## Corpo da requisição

Obrigatório, em `application/json`.

Os eventos processados, um objeto por `eventId`.

## Respostas

- `202`: Confirmação registrada. `acknowledged` é quantos eventos saíram do feed.
- `400`: O corpo não é JSON válido para esta operação. Formato ValidationProblemDetails do ASP.NET, com `Content-Type: application/problem+json`; `errors` aponta o campo. Corrija a requisição.
- `401`: Sem token, token expirado ou inválido: corpo vazio, com `WWW-Authenticate`. Peça um token novo uma vez e repita a chamada. Credencial pausada ou revogada pelo lojista: corpo com `invalid_token`, imediato, mesmo para tokens dentro da validade; o endpoint de token também vai recusá-la. Pare e avise o operador.
- `403`: O token é válido, mas a credencial não tem o escopo `orders:read`. Não peça outro token; o lojista precisa criar uma credencial com o escopo.
- `415`: `Content-Type` diferente de `application/json` com corpo presente. Formato ProblemDetails do ASP.NET, com `Content-Type: application/problem+json`.
- `429`: Mais de 600 requisições por minuto nesta credencial. Espere o `Retry-After` antes de tentar de novo; tentar antes não faz o limite abrir.
- `500`: Falha interna. A mensagem não descreve a causa de propósito; informe o `traceId` ao suporte. Repita com backoff exponencial.

## Exemplo de requisição

```shell
curl -X POST "https://api.meupedido.io/open-delivery/v1/events/acknowledgment" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[{ "id": "c0a8f1e2-3d4b-4c5a-9e6f-7a8b9c0d1e2f" }, { "id": "e2c0f3a4-5d6b-4e7c-9f8a-0b1c2d3e4f5a" }]'
```

Contrato completo (OpenAPI 3.1): https://developer.meupedido.io/openapi/open-delivery-v1.yaml


# Consultar eventos (/pt-BR/docs/references/events/pollEvents)

> Os eventos pendentes da sua credencial, em ordem de createdAt, do mais antigo para o mais novo.

`GET https://api.meupedido.io/open-delivery/v1/events:polling`

Os eventos pendentes da sua credencial, em ordem de `createdAt`, do mais antigo para o
mais novo. Cada item é um envelope com `eventId`, `eventType`, `orderId`, `orderURL` e
`createdAt`; o envelope **nunca traz o pedido**. Para `CREATED` e `MODIFIED`, faça `GET`
em `orderURL` e leia o pedido de agora.

A resposta é `200` com um array, vazio quando não há nada. A API nunca responde `204`.

Consultar não confirma. Um evento devolvido aqui continua pendente até você chamar
`POST /v1/events/acknowledgment` com o `eventId`; enquanto isso, ele volta em toda
consulta. É a garantia at-least-once: se o seu processo cair entre a consulta e a
gravação, nada se perde. Por isso o mesmo `eventId` pode chegar mais de uma vez: trate-o
como chave única e grave-o na mesma transação em que aplica o efeito.

O estado "o que ainda falta" vive na API, por credencial. Não há cursor, `since` nem
filtro por tipo; duas credenciais na mesma loja têm feeds independentes; uma credencial
criada hoje não recebe os eventos de ontem; uma pausada acumula e entrega quando retomada.
Eventos confirmados são apagados após 30 dias; não confirmados, nunca.

Consulte a cada 5 a 10 segundos em operação normal. Se a resposta vier cheia (`limit`
itens), consulte de novo logo depois de confirmar, sem esperar o intervalo. Em `429`,
respeite o `Retry-After`. Os campos opcionais do envelope (`sourceAppId`, `metadata`,
`delivery`) só aparecem quando têm valor. O caminho antigo `/v1/events/:polling`, com
barra, continua respondendo por compatibilidade e não deve ser usado em integração nova.

## Autenticação

`Authorization: Bearer <access_token>`. Escopo exigido: `orders:read`.

## Parâmetros de consulta

- `limit` (integer, int32, opcional): Extensão MeuPedido: não existe no padrão 1.4.0. Quantidade máxima de eventos na resposta. Padrão 100, máximo 200: valores maiores são reduzidos para 200 e valores menores que 1, elevados para 1. Se a resposta vier cheia, consulte de novo logo depois de confirmar. Exemplo: `100`.

## Respostas

- `200`: Eventos pendentes. Array vazio quando não há nada.
- `400`: `limit` não é um inteiro, como em `?limit=abc` ou num valor fora de int32. Formato ValidationProblemDetails do ASP.NET, com `Content-Type: application/problem+json`, e `errors` aponta o parâmetro. Estar fora da faixa não é erro: 0 ou menos vira 1, e acima de 200 vira 200.
- `401`: Sem token, token expirado ou inválido: corpo vazio, com `WWW-Authenticate`. Peça um token novo uma vez e repita a chamada. Credencial pausada ou revogada pelo lojista: corpo com `invalid_token`, imediato, mesmo para tokens dentro da validade; o endpoint de token também vai recusá-la. Pare e avise o operador.
- `403`: O token é válido, mas a credencial não tem o escopo `orders:read`. Não peça outro token; o lojista precisa criar uma credencial com o escopo.
- `429`: Mais de 600 requisições por minuto nesta credencial. Espere o `Retry-After` antes de tentar de novo; tentar antes não faz o limite abrir.
- `500`: Falha interna. A mensagem não descreve a causa de propósito; informe o `traceId` ao suporte. Repita com backoff exponencial.

## Exemplo de requisição

```shell
curl "https://api.meupedido.io/open-delivery/v1/events:polling?limit=100" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

Contrato completo (OpenAPI 3.1): https://developer.meupedido.io/openapi/open-delivery-v1.yaml


# Buscar cardápio (/pt-BR/docs/references/merchant/getMenus)

> O cardápio publicado da loja, completo, em uma resposta só: categorias, itens e grupos de opções.

`GET https://api.meupedido.io/open-delivery/v1/merchant/{merchantId}/menus`

O cardápio publicado da loja, completo, em uma resposta só: categorias, itens e grupos de
opções. O padrão prevê que uma loja tenha mais de um cardápio, por isso a resposta é um
array; no MeuPedido cada loja tem um único cardápio, então o array traz sempre um elemento,
cujo `id` é o `menuId` referenciado em `services` da loja.

O cardápio é normalizado: categorias apontam para itens por id (`itemOfferIds`), itens
apontam para grupos de opções por id (`optionGroupIds`). Monte índices por id em memória
antes de percorrer.

Duas traduções merecem atenção. Um produto com variações de tamanho vira um item mais um
grupo obrigatório de escolha única chamado **Tamanho**, com id `{productId}-variacoes`,
em que cada opção é uma variação com o próprio preço, da mais barata para a mais cara; o
`price` do item é o da primeira opção desse grupo, ou seja, o da variação mais barata. E
item pausado é enviado como `UNAVAILABLE`, nunca omitido: os ids ficam estáveis e um
pedido antigo sempre aponta para um item que existe.

Não existe evento de cardápio. Para manter uma cópia local, busque em um intervalo que
faça sentido para a sua operação; cada chamada conta no limite de 600 por minuto. O
`merchantId` segue a mesma regra de `getMerchant`: precisa ser o da credencial.

## Autenticação

`Authorization: Bearer <access_token>`. Escopo exigido: `catalog:read`.

## Parâmetros de caminho

- `merchantId` (string, uuid, obrigatório): Identificador da loja (GUID). Precisa ser o da credencial, o mesmo da claim `merchant_id` do token e de `merchant.id` de todo pedido. Qualquer outro valor responde `404`. Exemplo: `6d2f4c8a-1b3e-4f5a-9c7d-2e8b0a1f3c5d`.

## Respostas

- `200`: Lista com o cardápio da loja. Sempre um elemento.
- `401`: Sem token, token expirado ou inválido: corpo vazio, com `WWW-Authenticate`. Peça um token novo uma vez e repita a chamada. Credencial pausada ou revogada pelo lojista: corpo com `invalid_token`, imediato, mesmo para tokens dentro da validade; o endpoint de token também vai recusá-la. Pare e avise o operador.
- `403`: O token é válido, mas a credencial não tem o escopo `catalog:read`. Não peça outro token; o lojista precisa criar uma credencial com o escopo.
- `404`: O `merchantId` não é o da credencial, ou não é GUID. Formato ProblemDetails do ASP.NET, com `Content-Type: application/problem+json`. Use o id da claim `merchant_id` do token.
- `429`: Mais de 600 requisições por minuto nesta credencial. Espere o `Retry-After` antes de tentar de novo; tentar antes não faz o limite abrir.
- `500`: Falha interna. A mensagem não descreve a causa de propósito; informe o `traceId` ao suporte. Repita com backoff exponencial.

## Exemplo de requisição

```shell
curl "https://api.meupedido.io/open-delivery/v1/merchant/6d2f4c8a-1b3e-4f5a-9c7d-2e8b0a1f3c5d/menus" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

Contrato completo (OpenAPI 3.1): https://developer.meupedido.io/openapi/open-delivery-v1.yaml


# Buscar loja (/pt-BR/docs/references/merchant/getMerchant)

> Dados cadastrais da loja ligada à credencial: nome, documento, status, contatos, endereço e modalidades de atendimento (services).

`GET https://api.meupedido.io/open-delivery/v1/merchant/{merchantId}`

Dados cadastrais da loja ligada à credencial: nome, documento, status, contatos, endereço
e modalidades de atendimento (`services`). Somente leitura.

O `merchantId` da rota precisa ser o da credencial. Ele está na claim `merchant_id` do
token e em `merchant.id` de todo pedido. Qualquer outro id, mesmo de uma loja que exista,
responde `404` no formato ProblemDetails, sem distinção: a API não revela quais lojas
existem. Um id que não é GUID responde o mesmo `404`.

`status` é `UNAVAILABLE` quando a loja está desativada ou fechada temporariamente. É status
de loja, não ausência de loja: mostre "fechado agora" em vez de sumir com o restaurante.
Campo sem valor vem como `null`. Para saber se algo mudou, compare `lastUpdate`.

## Autenticação

`Authorization: Bearer <access_token>`. Escopo exigido: `merchant:read`.

## Parâmetros de caminho

- `merchantId` (string, uuid, obrigatório): Identificador da loja (GUID). Precisa ser o da credencial, o mesmo da claim `merchant_id` do token e de `merchant.id` de todo pedido. Qualquer outro valor responde `404`. Exemplo: `6d2f4c8a-1b3e-4f5a-9c7d-2e8b0a1f3c5d`.

## Respostas

- `200`: A loja da credencial.
- `401`: Sem token, token expirado ou inválido: corpo vazio, com `WWW-Authenticate`. Peça um token novo uma vez e repita a chamada. Credencial pausada ou revogada pelo lojista: corpo com `invalid_token`, imediato, mesmo para tokens dentro da validade; o endpoint de token também vai recusá-la. Pare e avise o operador.
- `403`: O token é válido, mas a credencial não tem o escopo `merchant:read`. Não peça outro token; o lojista precisa criar uma credencial com o escopo.
- `404`: O `merchantId` não é o da credencial, ou não é GUID. Formato ProblemDetails do ASP.NET, com `Content-Type: application/problem+json`. Use o id da claim `merchant_id` do token.
- `429`: Mais de 600 requisições por minuto nesta credencial. Espere o `Retry-After` antes de tentar de novo; tentar antes não faz o limite abrir.
- `500`: Falha interna. A mensagem não descreve a causa de propósito; informe o `traceId` ao suporte. Repita com backoff exponencial.

## Exemplo de requisição

```shell
curl "https://api.meupedido.io/open-delivery/v1/merchant/6d2f4c8a-1b3e-4f5a-9c7d-2e8b0a1f3c5d" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

Contrato completo (OpenAPI 3.1): https://developer.meupedido.io/openapi/open-delivery-v1.yaml


# Confirmar pedido (/pt-BR/docs/references/orders/confirmOrder)

> A loja aceitou o pedido.

`POST https://api.meupedido.io/open-delivery/v1/orders/{orderId}/confirm`

A loja aceitou o pedido. A situação passa a `ACCEPTED`, o lojista e o cliente veem a
mudança e um evento `CONFIRMED` entra no feed. É a primeira ação de todo pedido: um
pedido `PENDING` só sai dali por `confirm` ou por cancelamento.

A resposta é `202` com `status: "accepted"` quando a transição foi aplicada, ou
`status: "already_applied"` quando o pedido já estava em `ACCEPTED`. O segundo caso não é
erro, de propósito: é o comando reenviado depois de um timeout, e tratá-lo como falha
faria o seu sistema mostrar erro numa operação que já está feita. Em pedido que já
avançou (`PREPARING`, `READY`, `DELIVERY`), `confirm` é um retrocesso: é aceito, devolve
o pedido a `ACCEPTED` e não gera evento. O mesmo vale a partir de `DONE`: a API pode
reabrir um pedido concluído para corrigir um engano, como o painel faz.

Envie `Idempotency-Key` desde o início: a resposta da primeira execução é gravada por 24
horas e devolvida em toda repetição com a mesma chave. A chave tem no máximo 128
caracteres; acima disso a resposta é `400 invalid_idempotency_key` e nada é executado.
Mesma chave com outra rota ou outro corpo responde `409`. `422 invalid_transition` (em
pedido `CANCELLED`, que é final) é bug de fluxo, não erro transitório: repetir não resolve.

## Autenticação

`Authorization: Bearer <access_token>`. Escopo exigido: `orders:write`.

## Parâmetros de caminho

- `orderId` (string, uuid, obrigatório): Identificador do pedido (GUID). É o `orderId` dos eventos e o `id` do pedido. Um valor que não é GUID responde `404` no formato ProblemDetails. Exemplo: `9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e`.

## Parâmetros de cabeçalho

- `Idempotency-Key` (string, opcional): Extensão MeuPedido: não existe no padrão 1.4.0. Chave de idempotência, qualquer string de **até 128 caracteres** escolhida por você, única por credencial (um UUID por tentativa lógica, ou um valor derivado como `confirm-{orderId}`). Mesma chave com a mesma requisição (mesma rota, mesma ação, mesmo corpo) devolve a resposta gravada na primeira execução, com o mesmo status e o mesmo corpo, sem executar de novo, por 24 horas. Mesma chave com requisição diferente responde `409 idempotency_key_reuse`. Respostas `202`, `404` e `422` são gravadas; `5xx` não, para que a repetição execute de verdade. Em timeout, repita com a mesma chave. Acima de 128 caracteres a resposta é `400 invalid_idempotency_key` e **nada é executado**: 128 é o tamanho que a API grava, e aceitar uma chave maior seria aplicar o comando sem a proteção que você pediu. Este parâmetro não traz `example` de propósito: a chave muda a cada intenção nova, e um valor fixo, repetido do playground ou do cURL copiado numa segunda ação, responderia `409`.

## Respostas

- `202`: Transição aplicada, ou já aplicada antes. `situation` é a situação atual do pedido.
- `400`: A `Idempotency-Key` passou de 128 caracteres, o tamanho que a API grava. Nada foi executado e o pedido continua como estava. Encurte a chave (um UUID basta) e repita; a mesma chave longa responde sempre isto.
- `401`: Sem token, token expirado ou inválido: corpo vazio, com `WWW-Authenticate`. Peça um token novo uma vez e repita a chamada. Credencial pausada ou revogada pelo lojista: corpo com `invalid_token`, imediato, mesmo para tokens dentro da validade; o endpoint de token também vai recusá-la. Pare e avise o operador.
- `403`: O token é válido, mas a credencial não tem o escopo `orders:write`. Não peça outro token; o lojista precisa criar uma credencial com o escopo.
- `404`: `order_not_found` (`Content-Type: application/json`): o pedido não existe ou não pertence à loja da credencial, sem distinção. Um `orderId` que não é GUID responde no formato ProblemDetails do ASP.NET, com `Content-Type: application/problem+json`. Nos dois casos, não repita: confira o id.
- `409`: A mesma `Idempotency-Key` já foi usada nesta credencial com outra rota, outra ação ou outro corpo. Nada foi executado. Gere uma chave nova para a nova intenção.
- `422`: A transição não é permitida a partir da situação atual. `message` explica em pt-BR. Não repita.
- `429`: Mais de 600 requisições por minuto nesta credencial. Espere o `Retry-After` antes de tentar de novo; tentar antes não faz o limite abrir.
- `500`: Falha interna. A mensagem não descreve a causa de propósito; informe o `traceId` ao suporte. Repita com backoff exponencial.

## Exemplo de requisição

```shell
curl -X POST "https://api.meupedido.io/open-delivery/v1/orders/9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e/confirm" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: confirm-9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e"
```

Contrato completo (OpenAPI 3.1): https://developer.meupedido.io/openapi/open-delivery-v1.yaml


# Pedido entregue (/pt-BR/docs/references/orders/deliverOrder)

> O pedido foi entregue ao cliente.

`POST https://api.meupedido.io/open-delivery/v1/orders/{orderId}/delivered`

O pedido foi entregue ao cliente. Encerra o pedido: a situação passa a `DONE` e um evento
`CONCLUDED` entra no feed. O evento é `CONCLUDED`, e não `DELIVERED`, porque o MeuPedido
tem um único estado final, válido para entrega e retirada; afirmar uma entrega num pedido
retirado seria mentir num campo que o seu sistema usa para decidir.

Aceito a partir de `ACCEPTED`, `PREPARING`, `READY` e `DELIVERY`; em pedido `PENDING` ou
`CANCELLED` responde `422`. Mesma semântica de `202 accepted` e `already_applied`,
`Idempotency-Key`, `400`, `409` e `422` de `confirmOrder`.

## Autenticação

`Authorization: Bearer <access_token>`. Escopo exigido: `orders:write`.

## Parâmetros de caminho

- `orderId` (string, uuid, obrigatório): Identificador do pedido (GUID). É o `orderId` dos eventos e o `id` do pedido. Um valor que não é GUID responde `404` no formato ProblemDetails. Exemplo: `9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e`.

## Parâmetros de cabeçalho

- `Idempotency-Key` (string, opcional): Extensão MeuPedido: não existe no padrão 1.4.0. Chave de idempotência, qualquer string de **até 128 caracteres** escolhida por você, única por credencial (um UUID por tentativa lógica, ou um valor derivado como `confirm-{orderId}`). Mesma chave com a mesma requisição (mesma rota, mesma ação, mesmo corpo) devolve a resposta gravada na primeira execução, com o mesmo status e o mesmo corpo, sem executar de novo, por 24 horas. Mesma chave com requisição diferente responde `409 idempotency_key_reuse`. Respostas `202`, `404` e `422` são gravadas; `5xx` não, para que a repetição execute de verdade. Em timeout, repita com a mesma chave. Acima de 128 caracteres a resposta é `400 invalid_idempotency_key` e **nada é executado**: 128 é o tamanho que a API grava, e aceitar uma chave maior seria aplicar o comando sem a proteção que você pediu. Este parâmetro não traz `example` de propósito: a chave muda a cada intenção nova, e um valor fixo, repetido do playground ou do cURL copiado numa segunda ação, responderia `409`.

## Respostas

- `202`: Transição aplicada, ou já aplicada antes.
- `400`: A `Idempotency-Key` passou de 128 caracteres, o tamanho que a API grava. Nada foi executado e o pedido continua como estava. Encurte a chave (um UUID basta) e repita; a mesma chave longa responde sempre isto.
- `401`: Sem token, token expirado ou inválido: corpo vazio, com `WWW-Authenticate`. Peça um token novo uma vez e repita a chamada. Credencial pausada ou revogada pelo lojista: corpo com `invalid_token`, imediato, mesmo para tokens dentro da validade; o endpoint de token também vai recusá-la. Pare e avise o operador.
- `403`: O token é válido, mas a credencial não tem o escopo `orders:write`. Não peça outro token; o lojista precisa criar uma credencial com o escopo.
- `404`: `order_not_found` (`Content-Type: application/json`): o pedido não existe ou não pertence à loja da credencial, sem distinção. Um `orderId` que não é GUID responde no formato ProblemDetails do ASP.NET, com `Content-Type: application/problem+json`. Nos dois casos, não repita: confira o id.
- `409`: A mesma `Idempotency-Key` já foi usada nesta credencial com outra rota, outra ação ou outro corpo. Nada foi executado. Gere uma chave nova para a nova intenção.
- `422`: A transição não é permitida a partir da situação atual. Não repita.
- `429`: Mais de 600 requisições por minuto nesta credencial. Espere o `Retry-After` antes de tentar de novo; tentar antes não faz o limite abrir.
- `500`: Falha interna. A mensagem não descreve a causa de propósito; informe o `traceId` ao suporte. Repita com backoff exponencial.

## Exemplo de requisição

```shell
curl -X POST "https://api.meupedido.io/open-delivery/v1/orders/9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e/delivered" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: delivered-9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e"
```

Contrato completo (OpenAPI 3.1): https://developer.meupedido.io/openapi/open-delivery-v1.yaml


# Despachar pedido (/pt-BR/docs/references/orders/dispatchOrder)

> O pedido saiu para entrega.

`POST https://api.meupedido.io/open-delivery/v1/orders/{orderId}/dispatch`

O pedido saiu para entrega. A situação passa a `DELIVERY` e um evento `DISPATCHED` entra
no feed. Só faz sentido em pedido do tipo `DELIVERY`: em pedido `INDOOR` responde `422`,
porque a situação não existe para mesa (a mensagem cita o nome interno do tipo, `TABLE`);
em `TAKEOUT`, prefira `readyForPickup` seguido de `pickedUp`.

Aceito a partir de `ACCEPTED`, `PREPARING` e `READY`. Em pedido `PENDING` responde `422`:
confirme primeiro. Mesma semântica de `202 accepted` e `already_applied`,
`Idempotency-Key`, `400`, `409` e `422` de `confirmOrder`.

## Autenticação

`Authorization: Bearer <access_token>`. Escopo exigido: `orders:write`.

## Parâmetros de caminho

- `orderId` (string, uuid, obrigatório): Identificador do pedido (GUID). É o `orderId` dos eventos e o `id` do pedido. Um valor que não é GUID responde `404` no formato ProblemDetails. Exemplo: `9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e`.

## Parâmetros de cabeçalho

- `Idempotency-Key` (string, opcional): Extensão MeuPedido: não existe no padrão 1.4.0. Chave de idempotência, qualquer string de **até 128 caracteres** escolhida por você, única por credencial (um UUID por tentativa lógica, ou um valor derivado como `confirm-{orderId}`). Mesma chave com a mesma requisição (mesma rota, mesma ação, mesmo corpo) devolve a resposta gravada na primeira execução, com o mesmo status e o mesmo corpo, sem executar de novo, por 24 horas. Mesma chave com requisição diferente responde `409 idempotency_key_reuse`. Respostas `202`, `404` e `422` são gravadas; `5xx` não, para que a repetição execute de verdade. Em timeout, repita com a mesma chave. Acima de 128 caracteres a resposta é `400 invalid_idempotency_key` e **nada é executado**: 128 é o tamanho que a API grava, e aceitar uma chave maior seria aplicar o comando sem a proteção que você pediu. Este parâmetro não traz `example` de propósito: a chave muda a cada intenção nova, e um valor fixo, repetido do playground ou do cURL copiado numa segunda ação, responderia `409`.

## Respostas

- `202`: Transição aplicada, ou já aplicada antes.
- `400`: A `Idempotency-Key` passou de 128 caracteres, o tamanho que a API grava. Nada foi executado e o pedido continua como estava. Encurte a chave (um UUID basta) e repita; a mesma chave longa responde sempre isto.
- `401`: Sem token, token expirado ou inválido: corpo vazio, com `WWW-Authenticate`. Peça um token novo uma vez e repita a chamada. Credencial pausada ou revogada pelo lojista: corpo com `invalid_token`, imediato, mesmo para tokens dentro da validade; o endpoint de token também vai recusá-la. Pare e avise o operador.
- `403`: O token é válido, mas a credencial não tem o escopo `orders:write`. Não peça outro token; o lojista precisa criar uma credencial com o escopo.
- `404`: `order_not_found` (`Content-Type: application/json`): o pedido não existe ou não pertence à loja da credencial, sem distinção. Um `orderId` que não é GUID responde no formato ProblemDetails do ASP.NET, com `Content-Type: application/problem+json`. Nos dois casos, não repita: confira o id.
- `409`: A mesma `Idempotency-Key` já foi usada nesta credencial com outra rota, outra ação ou outro corpo. Nada foi executado. Gere uma chave nova para a nova intenção.
- `422`: A transição não é permitida a partir da situação atual ou para este tipo de pedido. Não repita.
- `429`: Mais de 600 requisições por minuto nesta credencial. Espere o `Retry-After` antes de tentar de novo; tentar antes não faz o limite abrir.
- `500`: Falha interna. A mensagem não descreve a causa de propósito; informe o `traceId` ao suporte. Repita com backoff exponencial.

## Exemplo de requisição

```shell
curl -X POST "https://api.meupedido.io/open-delivery/v1/orders/9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e/dispatch" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: dispatch-9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e"
```

Contrato completo (OpenAPI 3.1): https://developer.meupedido.io/openapi/open-delivery-v1.yaml


# Buscar pedido (/pt-BR/docs/references/orders/getOrder)

> O pedido completo, no formato do padrão Open Delivery 1.4.0.

`GET https://api.meupedido.io/open-delivery/v1/orders/{orderId}`

O pedido completo, no formato do padrão Open Delivery 1.4.0. É o mesmo corpo para qualquer
origem: app, site, cardápio digital, mesa ou marketplace produzem a mesma estrutura, com a
origem exposta em `extraInfo` (`origem=`, `canal=`, `numeroCanal=`).

É o que você lê depois de receber um evento `CREATED` ou `MODIFIED`: o envelope traz só
`orderId` e `orderURL`, e esta operação devolve o pedido de agora, e não uma fotografia do
instante da transição. Use `id` como chave; `displayId` é o número curto que a loja e o
cliente veem e se repete ao longo do tempo.

Regras do formato: campo sem valor vem como `null` (a chave está sempre lá); datas em UTC
com `Z`; dinheiro como `{ value, currency }`, copiado do que a loja cobrou, nunca
recalculado. `delivery` só existe em pedido `DELIVERY`; `takeout`, em `TAKEOUT` e `INDOOR`;
`schedule`, em pedido `SCHEDULED`. `test: true` marca pedido de loja de teste: não o leve
para o seu faturamento.

Pedido inexistente e pedido de outra loja respondem o mesmo `404 order_not_found`, sem
distinção, para a rota não revelar quais ids existem. Um `orderId` que não é GUID responde
`404` no formato ProblemDetails do ASP.NET.

## Autenticação

`Authorization: Bearer <access_token>`. Escopo exigido: `orders:read`.

## Parâmetros de caminho

- `orderId` (string, uuid, obrigatório): Identificador do pedido (GUID). É o `orderId` dos eventos e o `id` do pedido. Um valor que não é GUID responde `404` no formato ProblemDetails. Exemplo: `9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e`.

## Respostas

- `200`: O pedido, no formato do padrão.
- `401`: Sem token, token expirado ou inválido: corpo vazio, com `WWW-Authenticate`. Peça um token novo uma vez e repita a chamada. Credencial pausada ou revogada pelo lojista: corpo com `invalid_token`, imediato, mesmo para tokens dentro da validade; o endpoint de token também vai recusá-la. Pare e avise o operador.
- `403`: O token é válido, mas a credencial não tem o escopo `orders:read`. Não peça outro token; o lojista precisa criar uma credencial com o escopo.
- `404`: `order_not_found` (`Content-Type: application/json`): o pedido não existe ou não pertence à loja da credencial, sem distinção. Um `orderId` que não é GUID responde no formato ProblemDetails do ASP.NET, com `Content-Type: application/problem+json`. Nos dois casos, não repita: confira o id.
- `429`: Mais de 600 requisições por minuto nesta credencial. Espere o `Retry-After` antes de tentar de novo; tentar antes não faz o limite abrir.
- `500`: Falha interna. A mensagem não descreve a causa de propósito; informe o `traceId` ao suporte. Repita com backoff exponencial.

## Exemplo de requisição

```shell
curl "https://api.meupedido.io/open-delivery/v1/orders/9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

Contrato completo (OpenAPI 3.1): https://developer.meupedido.io/openapi/open-delivery-v1.yaml


# Pedido retirado (/pt-BR/docs/references/orders/pickedUp)

> O cliente retirou o pedido no balcão, ou foi servido na mesa.

`POST https://api.meupedido.io/open-delivery/v1/orders/{orderId}/pickedUp`

Extensão MeuPedido: não existe no padrão 1.4.0.

O cliente retirou o pedido no balcão, ou foi servido na mesa. Encerra o pedido: a situação
passa a `DONE` e um evento `CONCLUDED` entra no feed. É o fechamento natural de pedido
`TAKEOUT` e `INDOOR`, cujo caminho termina em `readyForPickup` seguido de `pickedUp`.

`pickedUp` e `delivered` levam à mesma situação, porque para a loja os dois significam
pedido encerrado. Use o que descreve o que aconteceu. Aceito a partir de `ACCEPTED`,
`PREPARING`, `READY` e `DELIVERY`; em pedido `PENDING` ou `CANCELLED` responde `422`.
`DONE` é final para avanço: só um retrocesso corretivo (`confirm`, `startPreparation`,
`readyForPickup`, `dispatch`) ou uma decisão externa (estorno, canal) o tira dali;
`requestCancellation` responde `422`.
Mesma semântica de `202 accepted` e `already_applied`, `Idempotency-Key`, `400`, `409` e
`422` de `confirmOrder`.

## Autenticação

`Authorization: Bearer <access_token>`. Escopo exigido: `orders:write`.

## Parâmetros de caminho

- `orderId` (string, uuid, obrigatório): Identificador do pedido (GUID). É o `orderId` dos eventos e o `id` do pedido. Um valor que não é GUID responde `404` no formato ProblemDetails. Exemplo: `9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e`.

## Parâmetros de cabeçalho

- `Idempotency-Key` (string, opcional): Extensão MeuPedido: não existe no padrão 1.4.0. Chave de idempotência, qualquer string de **até 128 caracteres** escolhida por você, única por credencial (um UUID por tentativa lógica, ou um valor derivado como `confirm-{orderId}`). Mesma chave com a mesma requisição (mesma rota, mesma ação, mesmo corpo) devolve a resposta gravada na primeira execução, com o mesmo status e o mesmo corpo, sem executar de novo, por 24 horas. Mesma chave com requisição diferente responde `409 idempotency_key_reuse`. Respostas `202`, `404` e `422` são gravadas; `5xx` não, para que a repetição execute de verdade. Em timeout, repita com a mesma chave. Acima de 128 caracteres a resposta é `400 invalid_idempotency_key` e **nada é executado**: 128 é o tamanho que a API grava, e aceitar uma chave maior seria aplicar o comando sem a proteção que você pediu. Este parâmetro não traz `example` de propósito: a chave muda a cada intenção nova, e um valor fixo, repetido do playground ou do cURL copiado numa segunda ação, responderia `409`.

## Respostas

- `202`: Transição aplicada, ou já aplicada antes.
- `400`: A `Idempotency-Key` passou de 128 caracteres, o tamanho que a API grava. Nada foi executado e o pedido continua como estava. Encurte a chave (um UUID basta) e repita; a mesma chave longa responde sempre isto.
- `401`: Sem token, token expirado ou inválido: corpo vazio, com `WWW-Authenticate`. Peça um token novo uma vez e repita a chamada. Credencial pausada ou revogada pelo lojista: corpo com `invalid_token`, imediato, mesmo para tokens dentro da validade; o endpoint de token também vai recusá-la. Pare e avise o operador.
- `403`: O token é válido, mas a credencial não tem o escopo `orders:write`. Não peça outro token; o lojista precisa criar uma credencial com o escopo.
- `404`: `order_not_found` (`Content-Type: application/json`): o pedido não existe ou não pertence à loja da credencial, sem distinção. Um `orderId` que não é GUID responde no formato ProblemDetails do ASP.NET, com `Content-Type: application/problem+json`. Nos dois casos, não repita: confira o id.
- `409`: A mesma `Idempotency-Key` já foi usada nesta credencial com outra rota, outra ação ou outro corpo. Nada foi executado. Gere uma chave nova para a nova intenção.
- `422`: A transição não é permitida a partir da situação atual. Não repita.
- `429`: Mais de 600 requisições por minuto nesta credencial. Espere o `Retry-After` antes de tentar de novo; tentar antes não faz o limite abrir.
- `500`: Falha interna. A mensagem não descreve a causa de propósito; informe o `traceId` ao suporte. Repita com backoff exponencial.

## Exemplo de requisição

```shell
curl -X POST "https://api.meupedido.io/open-delivery/v1/orders/9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e/pickedUp" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: pickedUp-9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e"
```

Contrato completo (OpenAPI 3.1): https://developer.meupedido.io/openapi/open-delivery-v1.yaml


# Pronto para retirada (/pt-BR/docs/references/orders/readyForPickup)

> O pedido está pronto: para sair para entrega, para o cliente retirar ou para ser servido na mesa.

`POST https://api.meupedido.io/open-delivery/v1/orders/{orderId}/readyForPickup`

O pedido está pronto: para sair para entrega, para o cliente retirar ou para ser servido
na mesa. A situação passa a `READY` e um evento `READY_FOR_PICKUP` entra no feed.

Aceito a partir de `ACCEPTED` e `PREPARING`; como retrocesso, a partir de `DELIVERY` e
de `DONE` (sem evento). Em pedido `PENDING` responde `422`: confirme primeiro. Em pedido
`CANCELLED` responde `422`. Mesma semântica de `202 accepted` e `already_applied`,
`Idempotency-Key`, `400`, `409` e `422` de `confirmOrder`.

## Autenticação

`Authorization: Bearer <access_token>`. Escopo exigido: `orders:write`.

## Parâmetros de caminho

- `orderId` (string, uuid, obrigatório): Identificador do pedido (GUID). É o `orderId` dos eventos e o `id` do pedido. Um valor que não é GUID responde `404` no formato ProblemDetails. Exemplo: `9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e`.

## Parâmetros de cabeçalho

- `Idempotency-Key` (string, opcional): Extensão MeuPedido: não existe no padrão 1.4.0. Chave de idempotência, qualquer string de **até 128 caracteres** escolhida por você, única por credencial (um UUID por tentativa lógica, ou um valor derivado como `confirm-{orderId}`). Mesma chave com a mesma requisição (mesma rota, mesma ação, mesmo corpo) devolve a resposta gravada na primeira execução, com o mesmo status e o mesmo corpo, sem executar de novo, por 24 horas. Mesma chave com requisição diferente responde `409 idempotency_key_reuse`. Respostas `202`, `404` e `422` são gravadas; `5xx` não, para que a repetição execute de verdade. Em timeout, repita com a mesma chave. Acima de 128 caracteres a resposta é `400 invalid_idempotency_key` e **nada é executado**: 128 é o tamanho que a API grava, e aceitar uma chave maior seria aplicar o comando sem a proteção que você pediu. Este parâmetro não traz `example` de propósito: a chave muda a cada intenção nova, e um valor fixo, repetido do playground ou do cURL copiado numa segunda ação, responderia `409`.

## Respostas

- `202`: Transição aplicada, ou já aplicada antes.
- `400`: A `Idempotency-Key` passou de 128 caracteres, o tamanho que a API grava. Nada foi executado e o pedido continua como estava. Encurte a chave (um UUID basta) e repita; a mesma chave longa responde sempre isto.
- `401`: Sem token, token expirado ou inválido: corpo vazio, com `WWW-Authenticate`. Peça um token novo uma vez e repita a chamada. Credencial pausada ou revogada pelo lojista: corpo com `invalid_token`, imediato, mesmo para tokens dentro da validade; o endpoint de token também vai recusá-la. Pare e avise o operador.
- `403`: O token é válido, mas a credencial não tem o escopo `orders:write`. Não peça outro token; o lojista precisa criar uma credencial com o escopo.
- `404`: `order_not_found` (`Content-Type: application/json`): o pedido não existe ou não pertence à loja da credencial, sem distinção. Um `orderId` que não é GUID responde no formato ProblemDetails do ASP.NET, com `Content-Type: application/problem+json`. Nos dois casos, não repita: confira o id.
- `409`: A mesma `Idempotency-Key` já foi usada nesta credencial com outra rota, outra ação ou outro corpo. Nada foi executado. Gere uma chave nova para a nova intenção.
- `422`: A transição não é permitida a partir da situação atual. Não repita.
- `429`: Mais de 600 requisições por minuto nesta credencial. Espere o `Retry-After` antes de tentar de novo; tentar antes não faz o limite abrir.
- `500`: Falha interna. A mensagem não descreve a causa de propósito; informe o `traceId` ao suporte. Repita com backoff exponencial.

## Exemplo de requisição

```shell
curl -X POST "https://api.meupedido.io/open-delivery/v1/orders/9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e/readyForPickup" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: readyForPickup-9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e"
```

Contrato completo (OpenAPI 3.1): https://developer.meupedido.io/openapi/open-delivery-v1.yaml


# Cancelar pedido (/pt-BR/docs/references/orders/requestCancellation)

> Cancela o pedido imediatamente.

`POST https://api.meupedido.io/open-delivery/v1/orders/{orderId}/requestCancellation`

Cancela o pedido **imediatamente**. Apesar do nome herdado do padrão, não existe etapa de
aprovação: a situação passa a `CANCELLED` na hora, a resposta já vem com
`situation: "CANCELLED"` e um evento `CANCELLED` entra no feed, com `metadata.reason`
igual ao motivo informado e `metadata.code` igual a `OTHER_CANCELLATION_REASON`
(cancelamento pela loja ou pela API). No padrão 1.4.0 esta operação é um pedido de
cancelamento que o outro lado aceita ou nega; `acceptCancellation` e `denyCancellation`
não existem aqui porque não há nada a aprovar.

O corpo é opcional. `reason` é registrado no histórico do pedido e mostrado ao lojista;
sem ele, o motivo é "Cancelamento solicitado pela API pública.". São no máximo 500
caracteres: acima disso a resposta é `400 invalid_cancellation_reason` e o pedido **não**
é cancelado. `code` é aceito por compatibilidade com o padrão e **ignorado**: o `code` do
evento `CANCELLED` deriva de quem cancelou, não deste campo. Enviar sem corpo e sem
`Content-Type` também funciona.

Aceito em qualquer situação, exceto `DONE` (`422`: pedido concluído não é cancelado por
aqui). `CANCELLED` é final: nenhuma ação tira o pedido dessa situação. Cancelar de novo
responde `202 already_applied`. Mesma semântica de `Idempotency-Key`, `400`, `409` e `422`
de `confirmOrder`.

## Autenticação

`Authorization: Bearer <access_token>`. Escopo exigido: `orders:write`.

## Parâmetros de caminho

- `orderId` (string, uuid, obrigatório): Identificador do pedido (GUID). É o `orderId` dos eventos e o `id` do pedido. Um valor que não é GUID responde `404` no formato ProblemDetails. Exemplo: `9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e`.

## Parâmetros de cabeçalho

- `Idempotency-Key` (string, opcional): Extensão MeuPedido: não existe no padrão 1.4.0. Chave de idempotência, qualquer string de **até 128 caracteres** escolhida por você, única por credencial (um UUID por tentativa lógica, ou um valor derivado como `confirm-{orderId}`). Mesma chave com a mesma requisição (mesma rota, mesma ação, mesmo corpo) devolve a resposta gravada na primeira execução, com o mesmo status e o mesmo corpo, sem executar de novo, por 24 horas. Mesma chave com requisição diferente responde `409 idempotency_key_reuse`. Respostas `202`, `404` e `422` são gravadas; `5xx` não, para que a repetição execute de verdade. Em timeout, repita com a mesma chave. Acima de 128 caracteres a resposta é `400 invalid_idempotency_key` e **nada é executado**: 128 é o tamanho que a API grava, e aceitar uma chave maior seria aplicar o comando sem a proteção que você pediu. Este parâmetro não traz `example` de propósito: a chave muda a cada intenção nova, e um valor fixo, repetido do playground ou do cURL copiado numa segunda ação, responderia `409`.

## Corpo da requisição

Opcional, em `application/json`.

Motivo do cancelamento. Opcional; sem corpo, o motivo padrão é registrado.

## Respostas

- `202`: Pedido cancelado, ou já estava cancelado.
- `400`: `invalid_cancellation_reason` (`Content-Type: application/json`): `reason` passou de 500 caracteres, o tamanho que o histórico do pedido grava. `invalid_idempotency_key`: a `Idempotency-Key` passou de 128. Corpo que não é JSON válido para esta operação responde no formato ValidationProblemDetails do ASP.NET, com `Content-Type: application/problem+json` e `errors` apontando o campo. Nos três casos o pedido continua como estava; corrija a requisição, repetir não muda a resposta.
- `401`: Sem token, token expirado ou inválido: corpo vazio, com `WWW-Authenticate`. Peça um token novo uma vez e repita a chamada. Credencial pausada ou revogada pelo lojista: corpo com `invalid_token`, imediato, mesmo para tokens dentro da validade; o endpoint de token também vai recusá-la. Pare e avise o operador.
- `403`: O token é válido, mas a credencial não tem o escopo `orders:write`. Não peça outro token; o lojista precisa criar uma credencial com o escopo.
- `404`: `order_not_found` (`Content-Type: application/json`): o pedido não existe ou não pertence à loja da credencial, sem distinção. Um `orderId` que não é GUID responde no formato ProblemDetails do ASP.NET, com `Content-Type: application/problem+json`. Nos dois casos, não repita: confira o id.
- `409`: A mesma `Idempotency-Key` já foi usada nesta credencial com outra rota, outra ação ou outro corpo. Nada foi executado. Gere uma chave nova para a nova intenção.
- `415`: `Content-Type` diferente de `application/json` com corpo presente. Formato ProblemDetails do ASP.NET, com `Content-Type: application/problem+json`.
- `422`: O pedido está em `DONE` e não pode ser cancelado por aqui. Não repita.
- `429`: Mais de 600 requisições por minuto nesta credencial. Espere o `Retry-After` antes de tentar de novo; tentar antes não faz o limite abrir.
- `500`: Falha interna. A mensagem não descreve a causa de propósito; informe o `traceId` ao suporte. Repita com backoff exponencial.

## Exemplo de requisição

```shell
curl -X POST "https://api.meupedido.io/open-delivery/v1/orders/9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e/requestCancellation" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: cancel-9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Produto em falta.", "code": "OTHER_CANCELLATION_REASON" }'
```

Contrato completo (OpenAPI 3.1): https://developer.meupedido.io/openapi/open-delivery-v1.yaml


# Iniciar preparo (/pt-BR/docs/references/orders/startPreparation)

> A cozinha começou.

`POST https://api.meupedido.io/open-delivery/v1/orders/{orderId}/startPreparation`

Extensão MeuPedido: não existe no padrão 1.4.0.

A cozinha começou. A situação passa a `PREPARING` e um evento `PREPARING` entra no feed.
É opcional: um pedido `ACCEPTED` pode ir direto para `READY`, `DELIVERY` ou `DONE`. Existe
porque o PDV que integra conosco precisa dizer à tela da loja e ao cliente que o pedido
está em produção, e o padrão não descreve esse fato.

Só é aceito a partir de `ACCEPTED` (ou como retrocesso a partir de `READY` e `DELIVERY`,
sem evento). Em pedido `PENDING` responde `422`: confirme primeiro. Mesma semântica de
`202 accepted` e `already_applied`, `Idempotency-Key`, `400`, `409` e `422` de
`confirmOrder`.

## Autenticação

`Authorization: Bearer <access_token>`. Escopo exigido: `orders:write`.

## Parâmetros de caminho

- `orderId` (string, uuid, obrigatório): Identificador do pedido (GUID). É o `orderId` dos eventos e o `id` do pedido. Um valor que não é GUID responde `404` no formato ProblemDetails. Exemplo: `9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e`.

## Parâmetros de cabeçalho

- `Idempotency-Key` (string, opcional): Extensão MeuPedido: não existe no padrão 1.4.0. Chave de idempotência, qualquer string de **até 128 caracteres** escolhida por você, única por credencial (um UUID por tentativa lógica, ou um valor derivado como `confirm-{orderId}`). Mesma chave com a mesma requisição (mesma rota, mesma ação, mesmo corpo) devolve a resposta gravada na primeira execução, com o mesmo status e o mesmo corpo, sem executar de novo, por 24 horas. Mesma chave com requisição diferente responde `409 idempotency_key_reuse`. Respostas `202`, `404` e `422` são gravadas; `5xx` não, para que a repetição execute de verdade. Em timeout, repita com a mesma chave. Acima de 128 caracteres a resposta é `400 invalid_idempotency_key` e **nada é executado**: 128 é o tamanho que a API grava, e aceitar uma chave maior seria aplicar o comando sem a proteção que você pediu. Este parâmetro não traz `example` de propósito: a chave muda a cada intenção nova, e um valor fixo, repetido do playground ou do cURL copiado numa segunda ação, responderia `409`.

## Respostas

- `202`: Transição aplicada, ou já aplicada antes.
- `400`: A `Idempotency-Key` passou de 128 caracteres, o tamanho que a API grava. Nada foi executado e o pedido continua como estava. Encurte a chave (um UUID basta) e repita; a mesma chave longa responde sempre isto.
- `401`: Sem token, token expirado ou inválido: corpo vazio, com `WWW-Authenticate`. Peça um token novo uma vez e repita a chamada. Credencial pausada ou revogada pelo lojista: corpo com `invalid_token`, imediato, mesmo para tokens dentro da validade; o endpoint de token também vai recusá-la. Pare e avise o operador.
- `403`: O token é válido, mas a credencial não tem o escopo `orders:write`. Não peça outro token; o lojista precisa criar uma credencial com o escopo.
- `404`: `order_not_found` (`Content-Type: application/json`): o pedido não existe ou não pertence à loja da credencial, sem distinção. Um `orderId` que não é GUID responde no formato ProblemDetails do ASP.NET, com `Content-Type: application/problem+json`. Nos dois casos, não repita: confira o id.
- `409`: A mesma `Idempotency-Key` já foi usada nesta credencial com outra rota, outra ação ou outro corpo. Nada foi executado. Gere uma chave nova para a nova intenção.
- `422`: A transição não é permitida a partir da situação atual. Não repita.
- `429`: Mais de 600 requisições por minuto nesta credencial. Espere o `Retry-After` antes de tentar de novo; tentar antes não faz o limite abrir.
- `500`: Falha interna. A mensagem não descreve a causa de propósito; informe o `traceId` ao suporte. Repita com backoff exponencial.

## Exemplo de requisição

```shell
curl -X POST "https://api.meupedido.io/open-delivery/v1/orders/9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e/startPreparation" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: startPreparation-9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e"
```

Contrato completo (OpenAPI 3.1): https://developer.meupedido.io/openapi/open-delivery-v1.yaml


# Evento de pedido (/pt-BR/docs/references/webhooks/orderEvent)

> Com o modo de entrega WEBHOOK ou BOTH, configurado pelo lojista ao criar a credencial, o MeuPedido faz um POST na sua URL a cada evento, em vez de esperar a sua consulta.

`POST` na URL de webhook configurada pelo lojista (evento `orderEvent`).

Extensão MeuPedido: não existe no padrão 1.4.0 (não é o `/v1/newEvent`).

Com o modo de entrega `WEBHOOK` ou `BOTH`, configurado pelo lojista ao criar a credencial,
o MeuPedido faz um `POST` na sua URL a cada evento, em vez de esperar a sua consulta. O
corpo é **o mesmo envelope** que `GET /v1/events:polling` devolve, byte a byte: um único
desserializador atende os dois modos. Um envio por evento; o corpo nunca traz o pedido,
busque em `orderURL`.

**Contrato da URL.** Obrigatoriamente HTTPS. O segredo do webhook (43 caracteres, base64url)
é gerado pelo MeuPedido e mostrado uma única vez ao salvar a URL; salvar de novo rotaciona
o segredo e o anterior deixa de valer.

**Assinatura.** `X-MeuPedido-Signature` é `HMAC-SHA256(secret, "{timestamp}.{body}")` em
hexadecimal minúsculo, em que `timestamp` é o valor de `X-MeuPedido-Timestamp` (segundos
Unix) e `body` é o corpo bruto, exatamente como chegou. Verifique antes de qualquer
processamento: rejeite se o timestamp estiver a mais de 5 minutos do seu relógio e compare
a assinatura em tempo constante. Não desserialize e serialize de novo antes de calcular.

**Resposta.** Qualquer `2xx` em até 10 segundos. O corpo da resposta é ignorado. Faça o
mínimo dentro desse prazo (verificar, enfileirar, responder) e processe depois. Um `2xx`
tardio conta como timeout.

**Entregue não é confirmado.** O `2xx` diz que a requisição chegou; ele não tira o evento
do feed. É obrigatório chamar `POST /v1/events/acknowledgment` com o `eventId` depois de
processar, mesmo recebendo por webhook. Sem isso o evento continua no polling.

**Retentativas.** Resposta fora de `2xx`, conexão recusada ou timeout contam como falha, e
o MeuPedido tenta de novo após 5, 10 e 20 segundos (4 tentativas em cerca de 35 s).
Esgotadas, o evento é marcado como `WEBHOOK_FAILED` e **continua disponível no polling**.
Após 20 falhas consecutivas a URL é desativada e o lojista a reativa no painel ("Limpar
fila"); os eventos continuam sendo gerados e ficam no polling. Retentativa e modo `BOTH`
fazem o mesmo `eventId` chegar mais de uma vez: deduplique por ele.

## Autenticação

Operação anônima: não envie `Authorization`.

## Parâmetros de cabeçalho

- `X-MeuPedido-Signature` (string, obrigatório): HMAC-SHA256 sobre `"{timestamp}.{body}"` com o segredo do webhook, em hexadecimal minúsculo (64 caracteres). Compare em tempo constante. Exemplo: `5f1d6a9c2b3e4f7a8d9c0b1e2f3a4d5c6b7a8f9e0d1c2b3a4f5e6d7c8b9a0f1e`.
- `X-MeuPedido-Timestamp` (string, obrigatório): Instante do envio em segundos Unix. Entra no cálculo da assinatura; rejeite se estiver a mais de 5 minutos do seu relógio. Exemplo: `1789828331`.
- `X-MeuPedido-Event-Id` (string, uuid, obrigatório): O mesmo `eventId` do corpo, para roteamento e deduplicação sem desserializar. Exemplo: `c0a8f1e2-3d4b-4c5a-9e6f-7a8b9c0d1e2f`.
- `X-MeuPedido-Event-Type` (string, obrigatório): O mesmo `eventType` do corpo. Exemplo: `CREATED`.

## Corpo da requisição

Obrigatório, em `application/json`.

O envelope do evento, idêntico ao item do polling.

## Respostas

- `2XX`: Entregue. Qualquer status 2xx em até 10 segundos conta como entrega; o corpo é ignorado. Não confirma o evento.
- `4XX`: Falha de entrega. Conta como tentativa falha e agenda a retentativa (5, 10 e 20 s). Vinte falhas consecutivas desativam a URL.
- `5XX`: Falha de entrega. Mesmo tratamento de um 4xx.

## Exemplo de requisição

```shell
# O que o MeuPedido envia para a sua URL (o valor da assinatura depende do seu segredo).
curl -X POST "https://sua-url.example/webhooks/meupedido" \
  -H "Content-Type: application/json" \
  -H "X-MeuPedido-Signature: 5f1d6a9c2b3e4f7a8d9c0b1e2f3a4d5c6b7a8f9e0d1c2b3a4f5e6d7c8b9a0f1e" \
  -H "X-MeuPedido-Timestamp: 1789828331" \
  -H "X-MeuPedido-Event-Id: c0a8f1e2-3d4b-4c5a-9e6f-7a8b9c0d1e2f" \
  -H "X-MeuPedido-Event-Type: CREATED" \
  -d '{"eventId":"c0a8f1e2-3d4b-4c5a-9e6f-7a8b9c0d1e2f","eventType":"CREATED","orderId":"9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e","orderURL":"https://api.meupedido.io/open-delivery/v1/orders/9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e","createdAt":"2026-09-19T14:32:10Z"}'
```

Contrato completo (OpenAPI 3.1): https://developer.meupedido.io/openapi/open-delivery-v1.yaml


# Primeira integração em 10 minutos (/pt-BR/docs/getting-started/first-steps/first-integration)

> Do token ao pedido confirmado, passo a passo, com playground em cada operação e o cURL equivalente.

Em seis passos você vai autenticar, gerar um pedido na loja de teste, recebê-lo por polling, buscar o pedido completo, confirmar o evento e confirmar o pedido. Cada passo tem o playground da operação, que chama a API de verdade a partir desta página, e o cURL equivalente para rodar no terminal.

Antes de começar você precisa de uma [loja de teste](/pt-BR/docs/getting-started/first-steps/test-store) e de uma [credencial](/pt-BR/docs/getting-started/first-steps/get-credentials) com os escopos `orders:read` e `orders:write`.

Nos exemplos, exporte as variáveis uma vez:

```bash title="Terminal"
export BASE_URL="https://api.meupedido.io/open-delivery"
export CLIENT_ID="mp_7f3a9c1e5b2d4a6f8e0c1b3d"
export CLIENT_SECRET="cole-aqui-o-segredo-mostrado-no-painel"
```

### Autorizar [#autorizar]

Troque `client_id` e `client_secret` por um token de acesso. O token vale por 3600 segundos e vai no header `Authorization: Bearer` de todas as chamadas seguintes.

> Playground interativo da operação `oauthToken` disponível na versão web desta página. O cURL abaixo é equivalente.

```bash title="Terminal"
curl -X POST "$BASE_URL/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET"
```

```json title="200 OK"
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudF9pZCI6IjZkMmY0YzhhLTFiM2UtNGY1YS05YzdkLTJlOGIwYTFmM2M1ZCIsImNsaWVudF9pZCI6Im1wXzdmM2E5YzFlNWIyZDRhNmY4ZTBjMWIzZCIsInNjb3BlIjoib3JkZXJzOnJlYWQgb3JkZXJzOndyaXRlIn0.Q1w7Xy0Yt3cJ5w8NnZK2p9Vb4Ls6Hd1Ff0Rr8Mm3Aa0",
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudF9pZCI6IjZkMmY0YzhhLTFiM2UtNGY1YS05YzdkLTJlOGIwYTFmM2M1ZCIsImNsaWVudF9pZCI6Im1wXzdmM2E5YzFlNWIyZDRhNmY4ZTBjMWIzZCIsInNjb3BlIjoib3JkZXJzOnJlYWQgb3JkZXJzOndyaXRlIn0.Q1w7Xy0Yt3cJ5w8NnZK2p9Vb4Ls6Hd1Ff0Rr8Mm3Aa0",
  "token_type": "bearer",
  "tokenType": "bearer",
  "expires_in": 3600,
  "expiresIn": 3600,
  "scope": "orders:read orders:write"
}
```

Guarde o valor de `access_token`:

```bash title="Terminal"
export TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...."
```

### Fazer um pedido no cardápio da loja de teste [#fazer-um-pedido-no-cardápio-da-loja-de-teste]

Abra o cardápio digital da loja de teste, monte um carrinho e finalize um pedido de entrega. Nada de API aqui: é o caminho que o cliente da loja percorre. Em segundos o MeuPedido cria o pedido e emite o evento `CREATED` no feed da sua credencial.

Anote o número do pedido que o cardápio mostra ao final (o `displayId`). Ele ajuda a reconhecer o pedido no próximo passo.

### Consultar eventos (polling) [#consultar-eventos-polling]

Peça os eventos pendentes. A resposta é um array, vazio quando não há nada, com até `limit` itens (padrão 100, máximo 200). O envelope traz o `orderId` e a `orderURL`, nunca o pedido.

> Playground interativo da operação `pollEvents` disponível na versão web desta página. O cURL abaixo é equivalente.

```bash title="Terminal"
curl "$BASE_URL/v1/events:polling?limit=100" \
  -H "Authorization: Bearer $TOKEN"
```

```json title="200 OK"
[
  {
    "eventId": "c0a8f1e2-3d4b-4c5a-9e6f-7a8b9c0d1e2f",
    "eventType": "CREATED",
    "orderId": "9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e",
    "orderURL": "https://api.meupedido.io/open-delivery/v1/orders/9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e",
    "createdAt": "2026-09-19T14:32:10Z"
  }
]
```

Se o array vier vazio, espere alguns segundos e consulte de novo. Enquanto você não confirmar o evento, ele continua aparecendo a cada consulta.

### Buscar o pedido [#buscar-o-pedido]

Use o `orderId` (ou a `orderURL`) do evento para buscar o pedido completo. É esse payload que o seu sistema grava.

> Playground interativo da operação `getOrder` disponível na versão web desta página. O cURL abaixo é equivalente.

```bash title="Terminal"
curl "$BASE_URL/v1/orders/9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e" \
  -H "Authorization: Bearer $TOKEN"
```

```json title="200 OK"
{
  "id": "9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e",
  "displayId": "1042",
  "type": "DELIVERY",
  "orderTiming": "INSTANT",
  "createdAt": "2026-09-19T14:32:10Z",
  "preparationStartDateTime": "2026-09-19T14:32:10Z",
  "merchant": {
    "id": "6d2f4c8a-1b3e-4f5a-9c7d-2e8b0a1f3c5d",
    "name": "Loja de teste Acme"
  },
  "customer": {
    "id": "a4b5c6d7-e8f9-4a0b-8c1d-2e3f4a5b6c7d",
    "name": "Ana Souza",
    "phone": { "number": "11999990000", "extension": null },
    "documentNumber": null,
    "email": null,
    "ordersCountOnMerchant": null
  },
  "items": [
    {
      "id": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
      "index": null,
      "name": "Pizza Margherita Grande",
      "externalCode": "3c2b1a09-8f7e-4d6c-b5a4-9382716f5e4d",
      "unit": null,
      "ean": null,
      "quantity": 1,
      "specialInstructions": "Sem manjericão",
      "unitPrice": { "value": 49.9, "currency": "BRL" },
      "optionsPrice": { "value": 8, "currency": "BRL" },
      "totalPrice": { "value": 57.9, "currency": "BRL" },
      "options": [
        {
          "id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
          "index": null,
          "name": "Borda recheada",
          "externalCode": "b8a7c6d5-e4f3-4210-9876-543210fedcba",
          "quantity": 1,
          "unit": null,
          "unitPrice": { "value": 8, "currency": "BRL" },
          "price": { "value": 8, "currency": "BRL" },
          "groupName": null
        }
      ]
    }
  ],
  "otherFees": [
    {
      "name": "Taxa de entrega",
      "type": "DELIVERY",
      "receivedBy": "MERCHANT",
      "price": { "value": 7, "currency": "BRL" }
    }
  ],
  "discounts": null,
  "total": {
    "items": { "value": 57.9, "currency": "BRL" },
    "otherFees": { "value": 7, "currency": "BRL" },
    "discount": { "value": 0, "currency": "BRL" },
    "orderAmount": { "value": 64.9, "currency": "BRL" }
  },
  "payments": {
    "prepaid": 0,
    "pending": 64.9,
    "methods": [
      {
        "value": 64.9,
        "currency": "BRL",
        "method": "CREDIT",
        "methodInfo": "Cartão de crédito",
        "type": "OFFLINE",
        "changeFor": null,
        "brand": null,
        "transaction": null
      }
    ]
  },
  "delivery": {
    "deliveryDateTime": null,
    "estimatedDeliveryDateTime": null,
    "deliveredBy": "MERCHANT",
    "deliveryAddress": {
      "country": "BR",
      "state": "SP",
      "city": "São Paulo",
      "district": "Pinheiros",
      "street": "Rua dos Pinheiros",
      "number": "1000",
      "postalCode": "05422001",
      "complement": "Apto 42",
      "reference": null,
      "formattedAddress": null,
      "coordinates": { "latitude": -23.5656, "longitude": -46.6898 }
    }
  },
  "takeout": null,
  "schedule": null,
  "extraInfo": "Interfone 42 | origem=SITE",
  "test": true
}
```

O exemplo é a saída real do serializador da API: `prepaid`, `pending` e `methods[].value` são números, o tamanho vem embutido em `items[].name`, e campos que a API ainda não preenche (`index`, `unit`, `groupName`, `estimatedDeliveryDateTime`, `formattedAddress`, `ordersCountOnMerchant`) chegam como `null`. Todos os campos e seus significados estão em [Estrutura do pedido](/pt-BR/docs/getting-started/orders/order-object).

### Confirmar o recebimento do evento (ack) [#confirmar-o-recebimento-do-evento-ack]

Depois de gravar o pedido, confirme o evento. Isso o retira do feed. A confirmação é obrigatória e é o que garante a entrega at-least-once: se o seu sistema cair entre o polling e a gravação, o evento volta na próxima consulta.

> Playground interativo da operação `acknowledgeEvents` disponível na versão web desta página. O cURL abaixo é equivalente.

```bash title="Terminal"
curl -X POST "$BASE_URL/v1/events/acknowledgment" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '[{ "id": "c0a8f1e2-3d4b-4c5a-9e6f-7a8b9c0d1e2f" }]'
```

```json title="202 Accepted"
{ "acknowledged": 1 }
```

Consulte o polling de novo: o array volta vazio.

### Confirmar o pedido [#confirmar-o-pedido]

Diga ao MeuPedido que a loja aceitou o pedido. O status passa a `ACCEPTED`, o lojista e o cliente veem a mudança, e um evento `CONFIRMED` entra no feed. O header `Idempotency-Key` é opcional, mas use-o desde o início: se a mesma chamada for repetida com a mesma chave, a API devolve a resposta gravada em vez de processar de novo.

> Playground interativo da operação `confirmOrder` disponível na versão web desta página. O cURL abaixo é equivalente.

```bash title="Terminal"
curl -X POST "$BASE_URL/v1/orders/9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e/confirm" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: 3f9c2a7e-confirm-1042"
```

```json title="202 Accepted"
{ "status": "accepted", "situation": "ACCEPTED" }
```

Repita a chamada: a resposta é a mesma. Repita sem o header: `{ "status": "already_applied", "situation": "ACCEPTED" }`, porque o pedido já está nesse estado.

## O que você acabou de fazer [#o-que-você-acabou-de-fazer]

Percorreu o ciclo mínimo de uma integração: **token, polling, buscar, ack, ação**. Uma integração de produção é esse mesmo ciclo em loop, com três cuidados a mais:

1. **Deduplicar por `eventId`.** O mesmo evento pode chegar mais de uma vez. Guarde os ids processados.
2. **Renovar o token antes de expirar.** Peça outro por volta dos 55 minutos, ou ao receber `401`.
3. **Avançar o pedido até o fim.** `startPreparation`, `readyForPickup`, `dispatch`, `delivered` ou `pickedUp`, conforme a operação acontece no seu lado.

## Próximos passos [#próximos-passos]

* [Polling](/pt-BR/docs/getting-started/orders/polling): o loop de produção, com intervalo, limite e dedupe.
* [Ações e idempotência](/pt-BR/docs/getting-started/orders/actions): todas as ações, o `Idempotency-Key` e os erros `409` e `422`.
* [Entrada em produção](/pt-BR/docs/getting-started/first-steps/go-live): o checklist antes de ligar em uma loja real.


# Obter credenciais (/pt-BR/docs/getting-started/first-steps/get-credentials)

> Como o lojista gera o client_id e o segredo em Integrações > Open Delivery, com escopos e modo de entrega.

Quem cria a credencial é o lojista, no painel do MeuPedido. Você, como integrador, recebe dele o `client_id` e o `client_secret` e configura no seu sistema. Esta página descreve o que ele vê, para que você possa orientá-lo, e o que fazer com o resultado.

> **Uma credencial por loja**
> A credencial pertence a uma loja e o token que ela gera só enxerga essa loja. Para integrar várias lojas, repita o processo em cada uma.

## No painel do lojista [#no-painel-do-lojista]

### Abrir Integrações > Open Delivery [#abrir-integrações--open-delivery]

No [painel do MeuPedido](https://dashboard.meupedido.io), com a loja selecionada, o lojista acessa **Integrações** e depois **Open Delivery**. A tela lista as credenciais já concedidas, com status e ações.

### Conceder acesso [#conceder-acesso]

O botão **Conceder acesso** abre o formulário da nova credencial:

| Campo                          | O que preencher                                                                                   |
| ------------------------------ | ------------------------------------------------------------------------------------------------- |
| Nome                           | Um rótulo para reconhecer o seu sistema na lista, por exemplo o nome do produto                   |
| Escopos                        | `orders:read`, `orders:write`, `merchant:read`, `catalog:read`. Marque só o que o seu sistema usa |
| Modo de entrega                | `POLLING`, `WEBHOOK` ou `BOTH`. Na dúvida, `POLLING`                                              |
| URL do webhook                 | Obrigatória em `WEBHOOK` e `BOTH`. Precisa ser HTTPS                                              |
| Incluir pedidos de marketplace | Se marcado, pedidos que chegam à loja por marketplaces também entram no feed desta credencial     |

Os escopos não podem ser editados depois. Se o seu sistema passar a precisar de um escopo novo, o lojista cria outra credencial.

### Copiar o segredo [#copiar-o-segredo]

Ao salvar, o painel mostra:

* o `client_id`, no formato `mp_` seguido de 24 caracteres hexadecimais;
* o `client_secret`, **exibido uma única vez**;
* o segredo do webhook, também exibido uma única vez, se o modo de entrega incluir webhook;
* a URL base da API, `https://api.meupedido.io/open-delivery`.

Se o segredo for perdido, não há como recuperá-lo: o lojista revoga a credencial e cria outra.

### Entregar ao integrador [#entregar-ao-integrador]

O lojista repassa `client_id` e `client_secret` para você por um canal seguro. Guarde o segredo em um cofre de segredos ou variável de ambiente, nunca em código, repositório ou log.

## Escolhendo os escopos [#escolhendo-os-escopos]

Peça o mínimo. Uma integração típica que recebe pedidos e devolve status precisa de `orders:read` e `orders:write`. Adicione `merchant:read` se você exibe dados da loja, e `catalog:read` se sincroniza o cardápio.

| Se o seu sistema...                                             | Escopos                              |
| --------------------------------------------------------------- | ------------------------------------ |
| Só lê pedidos (relatórios, BI, impressão)                       | `orders:read`                        |
| Recebe pedidos e avança o status (PDV, KDS, gestão de entregas) | `orders:read`, `orders:write`        |
| Também mostra o cardápio ou dados da loja                       | mais `catalog:read`, `merchant:read` |

## O que o lojista pode fazer depois [#o-que-o-lojista-pode-fazer-depois]

Na lista de credenciais, cada uma tem as ações:

* **Pausar** e **retomar**: com a credencial pausada, toda chamada responde `401 invalid_token`. Os eventos continuam acumulando e ficam disponíveis quando ela é retomada.
* **Revogar**: permanente. A credencial deixa de existir para a API.
* **Renomear**: só muda o rótulo.
* **Ver eventos e tentativas**: o histórico do feed e, no caso de webhook, cada tentativa de entrega com o resultado.
* **Reenviar evento**: força uma nova entrega por webhook.
* **Limpar fila**: reativa a URL do webhook depois de 20 falhas consecutivas.
* **Rotacionar segredo do webhook**: salvar a URL de novo gera um segredo novo, mostrado uma vez.

Se a sua integração parar de responder de repente com `401`, a primeira pergunta ao lojista é se a credencial foi pausada ou revogada.

## Verificando a credencial [#verificando-a-credencial]

Com o par em mãos, peça um token. Um `200` confirma que a credencial está ativa e mostra os escopos concedidos.

```bash title="Terminal"
curl -X POST https://api.meupedido.io/open-delivery/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=mp_7f3a9c1e5b2d4a6f8e0c1b3d" \
  -d "client_secret=$CLIENT_SECRET"
```

```json title="200 OK"
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudF9pZCI6IjZkMmY0YzhhLTFiM2UtNGY1YS05YzdkLTJlOGIwYTFmM2M1ZCIsImNsaWVudF9pZCI6Im1wXzdmM2E5YzFlNWIyZDRhNmY4ZTBjMWIzZCIsInNjb3BlIjoib3JkZXJzOnJlYWQgb3JkZXJzOndyaXRlIn0.Q1w7Xy0Yt3cJ5w8NnZK2p9Vb4Ls6Hd1Ff0Rr8Mm3Aa0",
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudF9pZCI6IjZkMmY0YzhhLTFiM2UtNGY1YS05YzdkLTJlOGIwYTFmM2M1ZCIsImNsaWVudF9pZCI6Im1wXzdmM2E5YzFlNWIyZDRhNmY4ZTBjMWIzZCIsInNjb3BlIjoib3JkZXJzOnJlYWQgb3JkZXJzOndyaXRlIn0.Q1w7Xy0Yt3cJ5w8NnZK2p9Vb4Ls6Hd1Ff0Rr8Mm3Aa0",
  "token_type": "bearer",
  "tokenType": "bearer",
  "expires_in": 3600,
  "expiresIn": 3600,
  "scope": "orders:read orders:write"
}
```

Um `401 invalid_client` significa credencial inexistente, segredo errado, pausada ou revogada.

## Próximos passos [#próximos-passos]

* [Loja de teste](/pt-BR/docs/getting-started/first-steps/test-store): peça uma loja para desenvolver sem afetar pedidos reais.
* [Autenticação](/pt-BR/docs/getting-started/authentication): as três formas de enviar a credencial e como renovar o token.
* [Primeira integração em 10 minutos](/pt-BR/docs/getting-started/first-steps/first-integration): do token ao pedido confirmado.


# Entrada em produção (/pt-BR/docs/getting-started/first-steps/go-live)

> Checklist do que conferir antes de ligar a integração em uma loja real.

A loja de teste e a loja real usam a mesma API. Entrar em produção é trocar a credencial e ter certeza de que o seu sistema aguenta o que acontece fora do caminho feliz. Percorra a lista antes de pedir a credencial da primeira loja real.

## Credenciais e segredos [#credenciais-e-segredos]

* [ ] Uma credencial por loja, com o **mínimo de escopos** que a integração usa.
* [ ] `client_secret` guardado em cofre de segredos ou variável de ambiente. Nunca em código, repositório, log ou mensagem de erro.
* [ ] O token é reaproveitado até perto de expirar (3600 s), não pedido a cada chamada. O endpoint de token tem limite por IP e por credencial.
* [ ] Um `401 invalid_token` fora do horário de expiração é tratado como credencial pausada ou revogada: a integração avisa o operador em vez de tentar em loop.

## Eventos [#eventos]

* [ ] Eventos são &#x2A;*deduplicados por `eventId`** antes de qualquer efeito colateral. O mesmo evento pode chegar mais de uma vez, por polling ou por webhook.
* [ ] O `ack` é enviado **depois** de gravar o pedido, nunca antes. Se o processo cair no meio, o evento volta.
* [ ] Todo evento recebido é confirmado, inclusive os que chegaram por webhook e os `eventType` que a integração não usa. Evento sem ack fica no feed para sempre.
* [ ] O `eventType` desconhecido é ignorado e confirmado, não derruba o consumidor. O MeuPedido emite extensões (`PREPARING`, `MODIFIED`, `COURIER_*`) além do padrão.
* [ ] O pedido é sempre buscado em `orderURL`; nada é inferido a partir do envelope além de `eventType` e `orderId`.
* [ ] Em polling, o intervalo é fixo e razoável (poucos segundos) e o `limit` é o maior que o seu consumidor processa com folga, até 200.

## Webhooks (se usados) [#webhooks-se-usados]

* [ ] A URL é HTTPS com certificado válido e responde `2xx` em menos de 10 s. Processamento pesado fica para depois da resposta.
* [ ] A assinatura em `X-MeuPedido-Signature` é verificada em tempo constante, e requisições com `X-MeuPedido-Timestamp` a mais de 5 minutos do relógio são rejeitadas.
* [ ] O segredo do webhook está guardado como o `client_secret`, e existe um procedimento para rotacioná-lo.
* [ ] O polling continua ativo (modo `BOTH`) para pegar o que o webhook não conseguir entregar após as retentativas.

## Ações no pedido [#ações-no-pedido]

* [ ] Cada ação envia um `Idempotency-Key` único por intenção (por exemplo, `confirm` do pedido X), para que reenvios não sejam processados duas vezes.
* [ ] `422 invalid_transition` é tratado como conflito de estado, não como erro transitório: a integração relê o estado do pedido em vez de tentar de novo.
* [ ] `already_applied` é tratado como sucesso.
* [ ] Pedidos `INDOOR` (mesa) nunca recebem `dispatch`.

## Resiliência [#resiliência]

* [ ] `429 rate_limit_exceeded` respeita o header `Retry-After` (60 s). O limite é de 600 requisições por minuto por credencial.
* [ ] `5xx` e falhas de rede usam backoff exponencial com jitter, com um teto.
* [ ] O `traceId` das respostas `500` é registrado no log, para enviar ao suporte.
* [ ] Datas são lidas como **UTC** (todas terminam em `Z`) e convertidas para o fuso da loja só na exibição.
* [ ] Valores monetários (`value`) são tratados como decimais, nunca como ponto flutuante binário em cálculos fiscais.

## Operação [#operação]

* [ ] O campo `test` do pedido tem um comportamento definido em produção.
* [ ] Existe um alerta para feed parado: se a loja está aberta e nenhum evento chega por um período incomum, alguém é avisado.
* [ ] O time de operação sabe como pedir ao lojista para pausar, retomar ou revogar a credencial, e como reenviar um evento pelo painel.
* [ ] O contato do suporte está à mão: [(11) 95502-1289](https://wa.me/5511955021289), com `client_id`, `traceId`, horário e `eventId` do que deu errado.

> **Pronto para ligar**
> Com a lista fechada, peça ao lojista da primeira loja real que conceda o acesso em Integrações > Open Delivery e troque a credencial. Acompanhe os primeiros pedidos de perto e mantenha a loja de teste para validar as próximas versões.

## Próximos passos [#próximos-passos]

* [Boas práticas](/pt-BR/docs/getting-started/best-practices): limites, erros, backoff e segurança em detalhe.
* [Webhooks](/pt-BR/docs/getting-started/orders/webhooks): verificação da assinatura com código.
* [Suporte](/pt-BR/docs/getting-started/support): o que enviar quando precisar de ajuda.


# Loja de teste (/pt-BR/docs/getting-started/first-steps/test-store)

> Como pedir uma loja de teste ao suporte e gerar pedidos reais marcados com test: true.

A API não tem sandbox. O ambiente de desenvolvimento é uma **loja de teste em produção**, criada pelo suporte sob pedido. Ela usa a mesma URL base, os mesmos endpoints e o mesmo comportamento de qualquer loja, com uma diferença: todo pedido dela sai com `"test": true`.

Isso significa que o que você homologa é exatamente o que vai rodar. Não há divergência entre ambientes para descobrir depois.

## Pedindo a loja [#pedindo-a-loja]

Mande uma mensagem para o suporte pelo WhatsApp: [(11) 95502-1289](https://wa.me/5511955021289). Inclua:

* o nome do seu sistema ou empresa;
* o e-mail de quem vai acessar o painel da loja de teste;
* os escopos que a credencial precisa (`orders:read`, `orders:write`, `merchant:read`, `catalog:read`);
* o modo de entrega (`POLLING`, `WEBHOOK` ou `BOTH`) e, se houver webhook, a URL HTTPS.

O suporte cria a loja, e a credencial é gerada no painel dela como em qualquer loja: [Integrações > Open Delivery > Conceder acesso](/pt-BR/docs/getting-started/first-steps/get-credentials). Peça também o endereço do cardápio digital da loja, que é por onde você vai gerar pedidos.

> **Uma loja de teste por integrador**
> A loja de teste é sua para desenvolver, homologar e reproduzir problemas. Depois de entrar em produção com lojas reais, mantenha a de teste: ela continua útil para validar mudanças na sua integração.

## Gerando um pedido [#gerando-um-pedido]

Pedidos de teste nascem do mesmo jeito que pedidos reais: pelo **cardápio digital** da loja. Abra o endereço do cardápio no navegador ou no celular, monte um carrinho, escolha entrega ou retirada, informe um telefone e finalize.

Em segundos o evento `CREATED` aparece no polling ou chega no seu webhook, e `GET /v1/orders/{orderId}` devolve o pedido com `"test": true`:

```json title="Trecho de GET /v1/orders/{orderId}"
{
  "id": "9b1e2d3c-4f5a-4b6c-8d7e-0f1a2b3c4d5e",
  "displayId": "1042",
  "type": "DELIVERY",
  "orderTiming": "INSTANT",
  "createdAt": "2026-09-19T14:32:10Z",
  "merchant": {
    "id": "6d2f4c8a-1b3e-4f5a-9c7d-2e8b0a1f3c5d",
    "name": "Loja de teste Acme"
  },
  "test": true
}
```

Para testar cada tipo de pedido:

| Cenário                 | Como gerar                                                                            |
| ----------------------- | ------------------------------------------------------------------------------------- |
| Entrega (`DELIVERY`)    | Finalize com endereço de entrega                                                      |
| Retirada (`TAKEOUT`)    | Escolha retirar na loja                                                               |
| Agendado (`SCHEDULED`)  | Escolha um horário futuro, se a loja de teste permitir agendamento                    |
| Cancelado (`CANCELLED`) | Cancele pelo painel da loja, ou chame `POST /v1/orders/{orderId}/requestCancellation` |
| Editado (`MODIFIED`)    | Altere o pedido pelo painel da loja                                                   |

Os cenários de pagamento que dependem de um provedor real, como Pix online, podem não estar disponíveis na loja de teste. Para o fluxo da integração isso não muda nada: o evento `CREATED` de um pedido pago no Pix só sai quando o pagamento é confirmado, e a partir dali o comportamento é idêntico.

## Tratando o campo `test` [#tratando-o-campo-test]

Uma loja real nunca gera `"test": true`. Mesmo assim, trate o campo no seu sistema:

* em desenvolvimento, use-o para separar pedidos de teste nos seus relatórios;
* em produção, decida explicitamente o que fazer se ele vier `true` (por exemplo, não imprimir na cozinha nem faturar).

Ignorar o campo funciona hoje; tratá-lo evita que um pedido de teste acabe em um relatório fiscal.

## Próximos passos [#próximos-passos]

* [Primeira integração em 10 minutos](/pt-BR/docs/getting-started/first-steps/first-integration): use a loja de teste para percorrer o fluxo completo.
* [Obter credenciais](/pt-BR/docs/getting-started/first-steps/get-credentials): o que o painel mostra ao conceder o acesso.
* [Suporte](/pt-BR/docs/getting-started/support): o que enviar quando algo não funcionar.


# Ações e idempotência (/pt-BR/docs/getting-started/orders/actions)

> confirm, startPreparation, readyForPickup, dispatch, pickedUp, delivered e requestCancellation, com Idempotency-Key e as respostas 202, 404, 409 e 422.

Ações são as chamadas que movem o pedido pelo [ciclo de vida](/pt-BR/docs/getting-started/orders/lifecycle). Todas são `POST /v1/orders/{orderId}/{ação}`, exigem o escopo `orders:write` e, com exceção de `requestCancellation`, não têm corpo.

Uma ação pela API percorre exatamente as mesmas regras que um clique no painel: mesma validação, mesmo histórico, mesmos efeitos (impressão, notificação ao cliente, entregador). O ator fica registrado como a sua credencial.

## As ações [#as-ações]

| Ação                                                                       | Situação resultante | Evento emitido     | Uso                                           |
| -------------------------------------------------------------------------- | ------------------- | ------------------ | --------------------------------------------- |
| `POST /v1/orders/{orderId}/confirm`                                        | `ACCEPTED`          | `CONFIRMED`        | A loja aceitou o pedido.                      |
| `POST /v1/orders/{orderId}/startPreparation` **Extensão MeuPedido** | `PREPARING`         | `PREPARING`        | A cozinha começou. Opcional.                  |
| `POST /v1/orders/{orderId}/readyForPickup`                                 | `READY`             | `READY_FOR_PICKUP` | Pronto para sair ou para o cliente retirar.   |
| `POST /v1/orders/{orderId}/dispatch`                                       | `DELIVERY`          | `DISPATCHED`       | Saiu para entrega. Só pedido `DELIVERY`.      |
| `POST /v1/orders/{orderId}/pickedUp` **Extensão MeuPedido**         | `DONE`              | `CONCLUDED`        | O cliente retirou. Encerra o pedido.          |
| `POST /v1/orders/{orderId}/delivered`                                      | `DONE`              | `CONCLUDED`        | Entregue. Encerra o pedido.                   |
| `POST /v1/orders/{orderId}/requestCancellation`                            | `CANCELLED`         | `CANCELLED`        | Cancela imediatamente. Aceita corpo opcional. |

`pickedUp` e `delivered` levam à mesma situação: para a loja, os dois significam pedido encerrado. Use o que descreve o que aconteceu.

## Exemplo: confirmar um pedido [#exemplo-confirmar-um-pedido]

**cURL**

```bash
curl -X POST "https://api.meupedido.io/open-delivery/v1/orders/3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d/confirm" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: confirm-3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d"
```

**Node.js**

```js
const orderId = '3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d';

const res = await fetch(`https://api.meupedido.io/open-delivery/v1/orders/${orderId}/confirm`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'Idempotency-Key': `confirm-${orderId}`,
  },
});
const result = await res.json(); // { status: 'accepted', situation: 'ACCEPTED' }
```

**Python**

```python
order_id = "3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d"

res = requests.post(
    f"https://api.meupedido.io/open-delivery/v1/orders/{order_id}/confirm",
    headers={
        "Authorization": f"Bearer {access_token}",
        "Idempotency-Key": f"confirm-{order_id}",
    },
    timeout=15,
)
result = res.json()  # {"status": "accepted", "situation": "ACCEPTED"}
```

**C#**

```csharp
var orderId = "3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d";

using var request = new HttpRequestMessage(HttpMethod.Post, $"v1/orders/{orderId}/confirm");
request.Headers.Add("Idempotency-Key", $"confirm-{orderId}");

var res = await http.SendAsync(request);
var result = await res.Content.ReadFromJsonAsync<JsonElement>();
// { "status": "accepted", "situation": "ACCEPTED" }
```

```json title="202 Accepted"
{ "status": "accepted", "situation": "ACCEPTED" }
```

## Respostas [#respostas]

Toda ação responde com um dos quatro resultados abaixo.

| Status | Corpo                                                 | Significado                                                                             |
| ------ | ----------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `202`  | `{"status":"accepted","situation":"ACCEPTED"}`        | A transição foi aplicada. `situation` é a situação nova.                                |
| `202`  | `{"status":"already_applied","situation":"ACCEPTED"}` | O pedido já estava nessa situação. Nada mudou e nenhum evento foi emitido.              |
| `404`  | `{"error":"order_not_found","message":"..."}`         | O pedido não existe ou não pertence à loja da credencial.                               |
| `422`  | `{"error":"invalid_transition","message":"..."}`      | A transição não é permitida a partir da situação atual. `message` explica em português. |

`already_applied` é `202`, e não erro, de propósito: o caso real é um comando reenviado depois de um timeout. Tratar isso como falha faria o seu sistema mostrar erro em uma operação que já está feita.

Um `orderId` que não é um GUID também responde `404`, no formato ProblemDetails (`type`, `title`, `status`, `traceId`).

```json title="422 Unprocessable Entity"
{
  "error": "invalid_transition",
  "message": "Pedido encerrado não pode ser cancelado."
}
```

Exemplos de `422`: `dispatch` em pedido `INDOOR`, `requestCancellation` em pedido `DONE`, qualquer ação em pedido `CANCELLED`.

## Cancelar [#cancelar]

`requestCancellation` é a única ação com corpo, e o corpo é opcional:

```bash
curl -X POST "https://api.meupedido.io/open-delivery/v1/orders/3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d/requestCancellation" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Produto em falta.", "code": "OTHER_CANCELLATION_REASON" }'
```

```json title="202 Accepted"
{ "status": "accepted", "situation": "CANCELLED" }
```

| Campo    | Tipo             | Descrição                                                                                                                            |
| -------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `reason` | string, opcional | Motivo registrado no histórico do pedido e enviado ao lojista. Padrão: `"Cancelamento solicitado pela API pública."`.                |
| `code`   | string, opcional | Aceito por compatibilidade com o padrão e **ignorado**. O `code` do evento `CANCELLED` é derivado de quem cancelou, não deste campo. |

O cancelamento é **imediato**: não existe uma etapa de aprovação, e o evento `CANCELLED` sai com `metadata.reason` igual ao motivo informado. Cancelar um pedido `DONE` responde `422`.

> **Sem corpo também funciona**
> Enviar `requestCancellation` sem corpo e sem `Content-Type` cancela com o motivo padrão.

## Idempotência [#idempotência]

Comandos de mudança de estado não são seguros de repetir por natureza, e a rede repete: um timeout do seu lado não diz se a ação foi aplicada. O cabeçalho opcional `Idempotency-Key` resolve isso.

```http
POST /v1/orders/3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d/dispatch
Authorization: Bearer {access_token}
Idempotency-Key: 2c0f6b3a-8e1d-4a9b-b7c5-0d4e8f1a2b3c
```

Como funciona:

* A chave é qualquer string escolhida por você, única por credencial. Um UUID por tentativa lógica, ou um valor derivado como `dispatch-{orderId}`, funcionam.
* **Mesma chave, mesma requisição** (mesma rota, mesma ação, mesmo corpo): a API devolve a resposta gravada na primeira vez, com o mesmo status e o mesmo corpo, sem executar de novo. Vale por **24 horas**.
* **Mesma chave, requisição diferente**: `409 {"error":"idempotency_key_reuse","message":"..."}`. Nada é executado.
* Respostas `202`, `404` e `422` são gravadas. Erros `5xx` não são, para que a repetição execute de verdade.

```json title="409 Conflict"
{
  "error": "idempotency_key_reuse",
  "message": "Esta chave de idempotência já foi usada com outro conteúdo."
}
```

Mesmo sem `Idempotency-Key`, repetir uma ação já aplicada devolve `already_applied` e não muda nada. A chave acrescenta uma garantia a mais: a resposta exata da primeira execução, útil quando a situação do pedido já avançou por outro caminho entre a primeira tentativa e a repetição.

## Boas práticas [#boas-práticas]

* Envie `Idempotency-Key` em toda ação. Custa um cabeçalho e elimina uma classe inteira de bugs de rede.
* Em timeout, repita com a **mesma** chave. Nunca gere chave nova para a mesma intenção.
* Antes de avançar, confira a situação atual em `GET /v1/orders/{orderId}` se o seu sistema pode ter ficado para trás (por exemplo, o operador avançou o pedido no painel).
* Trate `422` como bug de fluxo ou disputa com o painel, e não como erro transitório: repetir não resolve.

## Próximos passos [#próximos-passos]

* [Ciclo de vida do pedido](/pt-BR/docs/getting-started/orders/lifecycle): a máquina de estados que define quais ações são válidas.
* [Catálogo de eventos](/pt-BR/docs/getting-started/orders/events): o que cada ação emite no feed.
* [Boas práticas](/pt-BR/docs/getting-started/best-practices): limites, erros gerais e backoff.


# Catálogo de eventos (/pt-BR/docs/getting-started/orders/events)

> Os seis eventos do padrão e as extensões MeuPedido, quando cada um dispara, e o que vem em metadata e delivery.

Um evento é o aviso de que algo aconteceu com um pedido. Ele chega pelo [polling](/pt-BR/docs/getting-started/orders/polling) ou por [webhook](/pt-BR/docs/getting-started/orders/webhooks), sempre no mesmo envelope, e nunca traz o pedido: o conteúdo está em `orderURL`.

## O envelope [#o-envelope]

```json
{
  "eventId": "e1b9c0d2-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
  "eventType": "CANCELLED",
  "orderId": "3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d",
  "orderURL": "https://api.meupedido.io/open-delivery/v1/orders/3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d",
  "createdAt": "2026-09-19T14:03:40Z",
  "metadata": {
    "reason": "Cliente desistiu do pedido.",
    "code": "CONSUMER_CANCELLATION_REQUESTED"
  }
}
```

| Campo         | Tipo   | Presença       | Descrição                                                                                                |
| ------------- | ------ | -------------- | -------------------------------------------------------------------------------------------------------- |
| `eventId`     | GUID   | sempre         | Identificador único do evento. É o que você confirma e a sua chave de deduplicação.                      |
| `eventType`   | string | sempre         | Um dos tipos da tabela abaixo.                                                                           |
| `orderId`     | GUID   | sempre         | O pedido a que o evento se refere.                                                                       |
| `orderURL`    | string | sempre         | URL absoluta de `GET /v1/orders/{orderId}`.                                                              |
| `createdAt`   | data   | sempre         | Instante em que o evento foi gerado, ISO 8601 em UTC com `Z`.                                            |
| `sourceAppId` | string | quando existe  | Identificador da aplicação de origem, quando o pedido veio por um canal integrado.                       |
| `metadata`    | objeto | por tipo       | `CANCELLED` traz `reason` e `code`. `CONFIRMED` traz um objeto vazio `{}`. Os demais não trazem o campo. |
| `delivery`    | objeto | só `COURIER_*` | Dados da rota e do entregador. Veja [O bloco delivery](#o-bloco-delivery).                               |

Campos opcionais são **omitidos** quando não têm valor, e não enviados como `null`. Desserialize com tolerância a campos ausentes e a campos novos.

## Tipos de evento [#tipos-de-evento]

| Evento                    | Quando dispara                                                                                      | Origem                        | Extras                             |
| ------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------- | ---------------------------------- |
| `CREATED`                 | O pedido entrou em `PENDING`: foi criado, ou o PIX foi confirmado.                                  | **Open Delivery**  |                                    |
| `CONFIRMED`               | A loja aceitou (`ACCEPTED`).                                                                        | **Open Delivery**  | `metadata: {}`                     |
| `PREPARING`               | A cozinha começou (`PREPARING`).                                                                    | **Extensão MeuPedido** |                                    |
| `READY_FOR_PICKUP`        | O pedido está pronto (`READY`). Em `TAKEOUT` e `INDOOR`, também quando entra em `DELIVERY`.         | **Open Delivery**  |                                    |
| `DISPATCHED`              | Saiu para entrega (`DELIVERY`) em pedido do tipo `DELIVERY`.                                        | **Open Delivery**  |                                    |
| `CONCLUDED`               | Entregue ou retirado (`DONE`).                                                                      | **Open Delivery**  |                                    |
| `CANCELLED`               | O pedido foi cancelado, por qualquer caminho: API, painel ou cliente.                               | **Open Delivery**  | `metadata.reason`, `metadata.code` |
| `MODIFIED`                | O conteúdo do pedido foi editado (itens, valores, endereço). Busque o pedido de novo em `orderURL`. | **Extensão MeuPedido** |                                    |
| `COURIER_ASSIGNED`        | Um entregador foi atribuído ao pedido (rota criada).                                                | **Extensão MeuPedido** | `delivery`                         |
| `COURIER_ROUTE_STARTED`   | O entregador iniciou a rota.                                                                        | **Extensão MeuPedido** | `delivery`                         |
| `COURIER_PICKED_UP`       | O entregador retirou o pedido na loja.                                                              | **Extensão MeuPedido** | `delivery`                         |
| `COURIER_ARRIVED`         | O entregador chegou ao endereço do cliente.                                                         | **Extensão MeuPedido** | `delivery`                         |
| `COURIER_DELIVERED`       | O entregador registrou a entrega.                                                                   | **Extensão MeuPedido** | `delivery`                         |
| `COURIER_DELIVERY_FAILED` | A tentativa de entrega falhou.                                                                      | **Extensão MeuPedido** | `delivery.failureReason`           |
| `COURIER_UNASSIGNED`      | O entregador foi desatribuído; o pedido volta a aguardar rota.                                      | **Extensão MeuPedido** | `delivery`                         |
| `COURIER_ROUTE_COMPLETED` | A rota do entregador terminou.                                                                      | **Extensão MeuPedido** | `delivery`                         |

### O que não é emitido [#o-que-não-é-emitido]

Alguns tipos previstos no padrão Open Delivery 1.4.0 **nunca** aparecem no feed do MeuPedido: `PICKUP_AREA_ASSIGNED`, `DELIVERED`, `CANCELLATION_REQUESTED` e `CANCELLATION_REQUEST_DENIED`. A entrega é sinalizada por `CONCLUDED`; o cancelamento é imediato, então não existe pedido de cancelamento pendente ou negado. Detalhes em [Compatibilidade](/pt-BR/docs/getting-started/compatibility).

Também não existe evento para `PENDING` e `PENDING_PAYMENT`: o pedido nasce para a sua integração no `CREATED`. Retrocessos de situação (por exemplo, `confirm` em um pedido `PREPARING`) não geram evento.

## Eventos por tipo de pedido [#eventos-por-tipo-de-pedido]

| Situação    | Pedido `DELIVERY`  | Pedido `TAKEOUT` ou `INDOOR` |
| ----------- | ------------------ | ---------------------------- |
| `PENDING`   | `CREATED`          | `CREATED`                    |
| `ACCEPTED`  | `CONFIRMED`        | `CONFIRMED`                  |
| `PREPARING` | `PREPARING`        | `PREPARING`                  |
| `READY`     | `READY_FOR_PICKUP` | `READY_FOR_PICKUP`           |
| `DELIVERY`  | `DISPATCHED`       | `READY_FOR_PICKUP`           |
| `DONE`      | `CONCLUDED`        | `CONCLUDED`                  |
| `CANCELLED` | `CANCELLED`        | `CANCELLED`                  |

## metadata [#metadata]

### CANCELLED [#cancelled]

```json
"metadata": {
  "reason": "Produto em falta.",
  "code": "OTHER_CANCELLATION_REASON"
}
```

| Campo    | Valores                                                        | Descrição                                                                                                                                                                              |
| -------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reason` | texto                                                          | O motivo registrado por quem cancelou. Pela API, é o `reason` enviado em `requestCancellation` (ou o padrão).                                                                          |
| `code`   | `CONSUMER_CANCELLATION_REQUESTED`, `OTHER_CANCELLATION_REASON` | `CONSUMER_CANCELLATION_REQUESTED` quando o cliente cancelou; `OTHER_CANCELLATION_REASON` para loja, painel e API. O `code` enviado em `requestCancellation` não influencia este valor. |

### CONFIRMED [#confirmed]

`metadata` vem como `{}`. O campo existe porque o padrão o exige presente para este tipo; não há nada dentro.

## O bloco delivery [#o-bloco-delivery]

Presente apenas nos eventos `COURIER_*`. Descreve a rota e a parada do pedido dentro dela.

```json
{
  "eventId": "7a2d9f4b-1e6c-4d3a-b8f0-5c2e9d1a4b7f",
  "eventType": "COURIER_ARRIVED",
  "orderId": "3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d",
  "orderURL": "https://api.meupedido.io/open-delivery/v1/orders/3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d",
  "createdAt": "2026-09-19T14:41:07Z",
  "delivery": {
    "routeId": "b4c1d2e3-f4a5-4b6c-8d7e-9f0a1b2c3d4e",
    "routeCode": "R-0217",
    "courierId": "c9d8e7f6-a5b4-4c3d-9e2f-1a0b9c8d7e6f",
    "courierName": "Carlos Andrade",
    "stopStatus": "ARRIVED",
    "sequence": 2,
    "failureReason": null,
    "etaAt": "2026-09-19T14:45:00Z",
    "claimSource": "MERCHANT_DISPATCH"
  }
}
```

| Campo           | Tipo    | Descrição                                                                                                         |
| --------------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
| `routeId`       | GUID    | Identificador da rota. Uma rota pode ter vários pedidos.                                                          |
| `routeCode`     | string  | Código curto da rota, o mesmo que o lojista vê no painel.                                                         |
| `courierId`     | GUID    | Identificador do entregador.                                                                                      |
| `courierName`   | string  | Nome do entregador.                                                                                               |
| `stopStatus`    | string  | Situação da parada deste pedido na rota: `PENDING`, `EN_ROUTE`, `ARRIVED`, `DELIVERED`, `FAILED` ou `UNASSIGNED`. |
| `sequence`      | inteiro | Posição da parada na rota, a partir de 1.                                                                         |
| `failureReason` | string  | Motivo da falha, em `COURIER_DELIVERY_FAILED`.                                                                    |
| `etaAt`         | data    | Previsão de chegada, UTC com `Z`, quando calculada.                                                               |
| `claimSource`   | string  | Como o entregador assumiu a rota (por exemplo, `MERCHANT_DISPATCH` quando a loja despachou).                      |

Os campos de `delivery` podem vir como `null` quando não se aplicam ao evento. Trate `sequence`, `etaAt` e `failureReason` como opcionais.

## Como reagir a cada evento [#como-reagir-a-cada-evento]

| Evento                                                                  | O que fazer                                                                                                     |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `CREATED`                                                               | Buscar o pedido em `orderURL`, gravar, exibir. Depois, [`confirm`](/pt-BR/docs/getting-started/orders/actions). |
| `MODIFIED`                                                              | Buscar o pedido de novo e substituir a cópia local.                                                             |
| `CONFIRMED`, `PREPARING`, `READY_FOR_PICKUP`, `DISPATCHED`, `CONCLUDED` | Atualizar a situação local. Se a ação partiu do seu sistema, o evento confirma o que você já sabe.              |
| `CANCELLED`                                                             | Marcar como cancelado com `metadata.reason`; parar preparo e entrega.                                           |
| `COURIER_*`                                                             | Opcional. Exibir o entregador e a previsão para a loja ou para o cliente.                                       |

Em todos os casos, confirme o `eventId` em `acknowledgment` depois de processar.

## Próximos passos [#próximos-passos]

* [Polling](/pt-BR/docs/getting-started/orders/polling): consumir e confirmar eventos.
* [Estrutura do pedido](/pt-BR/docs/getting-started/orders/order-object): o que vem em `orderURL`.
* [Ciclo de vida do pedido](/pt-BR/docs/getting-started/orders/lifecycle): as transições por trás de cada evento.


# Ciclo de vida do pedido (/pt-BR/docs/getting-started/orders/lifecycle)

> A máquina de estados do pedido, as transições permitidas, o retrocesso a partir de ACCEPTED e o cancelamento.

Todo pedido no MeuPedido está em exatamente um estado, chamado de **situação**. As ações da API movem o pedido entre situações e cada mudança gera um [evento](/pt-BR/docs/getting-started/orders/events) no feed da sua credencial. Esta página descreve a máquina de estados que as ações obedecem.

## As situações [#as-situações]

| Situação          | Significado                                                                          | Como o pedido chega aqui                                                                    |
| ----------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| `PENDING_PAYMENT` | Aguardando pagamento on-line (PIX). O pedido ainda não existe para a sua integração. | Criado com pagamento on-line pendente.                                                      |
| `PENDING`         | Novo pedido, esperando a loja aceitar.                                               | Criação do pedido, ou confirmação do PIX. Gera o evento `CREATED`.                          |
| `ACCEPTED`        | A loja aceitou o pedido.                                                             | `POST /confirm`. Gera `CONFIRMED`.                                                          |
| `PREPARING`       | A cozinha começou a preparar. Extensão MeuPedido.                                    | `POST /startPreparation`. Gera `PREPARING`.                                                 |
| `READY`           | Pronto para retirada ou para sair.                                                   | `POST /readyForPickup`. Gera `READY_FOR_PICKUP`.                                            |
| `DELIVERY`        | Saiu para entrega. Só faz sentido em pedido `DELIVERY`.                              | `POST /dispatch`. Gera `DISPATCHED`.                                                        |
| `DONE`            | Entregue ou retirado. Encerrado.                                                     | `POST /delivered` ou `POST /pickedUp`. Gera `CONCLUDED`.                                    |
| `CANCELLED`       | Cancelado. Final.                                                                    | `POST /requestCancellation`, ou cancelamento pelo painel ou pelo cliente. Gera `CANCELLED`. |

`PENDING_PAYMENT` e `PENDING` não geram evento próprio: o pedido nasce para a sua integração no `CREATED`, que é emitido quando ele entra em `PENDING`. Um pedido de PIX que nunca é pago nunca aparece no seu feed.

## Transições para frente [#transições-para-frente]

Cada seta abaixo é uma transição que a API aceita. Não é preciso passar por todas as situações: um pedido em `ACCEPTED` pode ir direto para `DONE`.

```text
PENDING_PAYMENT -> PENDING
PENDING         -> ACCEPTED
ACCEPTED        -> PREPARING | READY | DELIVERY | DONE
PREPARING       -> READY | DELIVERY | DONE
READY           -> DELIVERY | DONE
DELIVERY        -> DONE
qualquer uma    -> CANCELLED   (exceto DONE)
```

Regras que valem para todas as transições:

* **Mesma situação** responde `202` com `status: "already_applied"`, e não erro. Um comando reenviado depois de um timeout não quebra o seu fluxo.
* **`DELIVERY` não vale para pedido `INDOOR`** (mesa). Para `TAKEOUT` e `INDOOR`, use `readyForPickup` e depois `pickedUp`.
* **`CANCELLED` é final.** Nenhuma ação tira um pedido cancelado dessa situação.
* **`DONE` não pode ser cancelado.** `requestCancellation` em um pedido encerrado responde `422 invalid_transition`.

## Retrocesso [#retrocesso]

A partir de `ACCEPTED`, o pedido pode voltar para qualquer situação anterior. O caso típico é um operador que avançou o pedido por engano: um `POST /confirm` em um pedido `PREPARING` devolve o pedido para `ACCEPTED`.

O retrocesso **não gera evento**. O feed da sua credencial só recebe transições para frente e o cancelamento. Se você mantém uma cópia local do pedido e precisa refletir um retrocesso, busque o pedido em `GET /v1/orders/{orderId}` antes de aplicar a próxima ação.

> **Retrocesso não é desfazer**
> Voltar para `ACCEPTED` não reverte nada que aconteceu fora do estado: entregador atribuído, cupom impresso ou notificação enviada ao cliente continuam como estão. Use com cuidado e prefira avançar o pedido pelo caminho normal.

## Cancelamento [#cancelamento]

`POST /v1/orders/{orderId}/requestCancellation` cancela o pedido **imediatamente**. Apesar do nome herdado do padrão, não existe uma etapa de aprovação: a resposta já vem com `situation: "CANCELLED"` e o evento `CANCELLED` é emitido na sequência, com `metadata.reason` e `metadata.code`.

O cancelamento é aceito em qualquer situação, exceto `DONE`. Detalhes do corpo opcional e do código de motivo estão em [Ações e idempotência](/pt-BR/docs/getting-started/orders/actions).

## Um pedido de entrega, do início ao fim [#um-pedido-de-entrega-do-início-ao-fim]

```text
CREATED          o pedido entrou em PENDING
CONFIRMED        você chamou /confirm             ACCEPTED
PREPARING        você chamou /startPreparation    PREPARING
READY_FOR_PICKUP você chamou /readyForPickup      READY
DISPATCHED       você chamou /dispatch            DELIVERY
CONCLUDED        você chamou /delivered           DONE
```

Para um pedido de retirada (`TAKEOUT`) ou de mesa (`INDOOR`), o caminho termina em `readyForPickup` seguido de `pickedUp`, e o `DISPATCHED` não existe.

## Próximos passos [#próximos-passos]

* [Ações e idempotência](/pt-BR/docs/getting-started/orders/actions): as chamadas que movem o pedido e como repeti-las com segurança.
* [Catálogo de eventos](/pt-BR/docs/getting-started/orders/events): o que cada transição emite no seu feed.
* [Polling](/pt-BR/docs/getting-started/orders/polling): como consumir os eventos.


# Estrutura do pedido (/pt-BR/docs/getting-started/orders/order-object)

> Todos os campos do pedido, de id e displayId a items, payments, delivery, takeout, schedule e test, com exemplos completos de DELIVERY e TAKEOUT.

`GET /v1/orders/{orderId}` devolve o pedido no formato do padrão Open Delivery 1.4.0. É o mesmo corpo para qualquer origem: app, site, cardápio digital, mesa ou marketplace produzem a mesma estrutura, com a origem exposta em `extraInfo`.

```http
GET /v1/orders/3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d
Authorization: Bearer {access_token}
```

Escopo: `orders:read`. Pedido de outra loja ou id inexistente respondem `404`, sem distinção. Um `orderId` que não é GUID também responde `404`.

Regras gerais do formato:

* **Campos sem valor vêm como `null`**, e não são omitidos. A chave está sempre lá.
* **Datas** são ISO 8601 em UTC com sufixo `Z`.
* **Dinheiro** é sempre um objeto `Money`: `{"value": 49.90, "currency": "BRL"}`. Os valores são copiados do pedido, nunca recalculados: o que você lê é o que a loja cobrou.
* **Ids** são GUIDs. `displayId` é o número curto que a loja e o cliente veem.

## Campos [#campos]

### Raiz [#raiz]

| Campo                      | Tipo                            | Nulo? | Descrição                                                                                                                                    |
| -------------------------- | ------------------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                       | GUID                            | não   | Identificador do pedido. É o `orderId` dos eventos e das ações.                                                                              |
| `displayId`                | string                          | não   | Número curto do pedido, o que aparece no cupom e na tela da loja. Não é único para sempre.                                                   |
| `type`                     | `DELIVERY`, `TAKEOUT`, `INDOOR` | não   | Entrega, retirada ou consumo no local (mesa).                                                                                                |
| `orderTiming`              | `INSTANT`, `SCHEDULED`          | não   | Imediato ou agendado. `SCHEDULED` vem acompanhado de `schedule`.                                                                             |
| `createdAt`                | data                            | não   | Instante de criação.                                                                                                                         |
| `preparationStartDateTime` | data                            | não   | Quando começar a preparar. Igual a `createdAt` em pedido `INSTANT`; igual a `schedule.scheduledDateTimeStart` em `SCHEDULED`.                |
| `merchant`                 | objeto                          | não   | `{id, name}` da loja. `id` é o `merchant_id` da sua credencial.                                                                              |
| `customer`                 | objeto                          | não   | Dados do cliente. Veja [customer](#customer).                                                                                                |
| `items`                    | array                           | não   | Itens do pedido. Veja [items](#items).                                                                                                       |
| `otherFees`                | array                           | sim   | Taxas além dos itens (entrega, acréscimo). `null` quando não há.                                                                             |
| `discounts`                | array                           | sim   | Descontos aplicados. `null` quando não há.                                                                                                   |
| `total`                    | objeto                          | não   | Totais do pedido. Veja [total](#total).                                                                                                      |
| `payments`                 | objeto                          | não   | Pagamentos. Veja [payments](#payments).                                                                                                      |
| `delivery`                 | objeto                          | sim   | Só em pedido `DELIVERY`. Veja [delivery](#delivery).                                                                                         |
| `takeout`                  | objeto                          | sim   | Só em pedido `TAKEOUT` ou `INDOOR`. Veja [takeout](#takeout).                                                                                |
| `schedule`                 | objeto                          | sim   | Só em pedido `SCHEDULED`. Veja [schedule](#schedule).                                                                                        |
| `extraInfo`                | string                          | sim   | Observação do pedido e origem, no formato `"obs \| origem=X \| canal=Y \| numeroCanal=Z"`. Partes sem valor são omitidas.                    |
| `test`                     | boolean                         | não   | `true` em pedido de [loja de teste](/pt-BR/docs/getting-started/first-steps/test-store). Nunca envie um pedido de teste para a sua produção. |

### customer [#customer]

| Campo                   | Tipo    | Nulo? | Descrição                                                                                                        |
| ----------------------- | ------- | ----- | ---------------------------------------------------------------------------------------------------------------- |
| `id`                    | GUID    | sim   | Identificador do cliente no MeuPedido.                                                                           |
| `name`                  | string  | sim   | Nome informado no pedido.                                                                                        |
| `phone`                 | objeto  | sim   | `{number, extension}`. `number` é o telefone como o cliente informou; `extension` é o ramal, normalmente `null`. |
| `documentNumber`        | string  | sim   | CPF ou CNPJ, quando informado para a nota.                                                                       |
| `email`                 | string  | sim   | E-mail, quando informado.                                                                                        |
| `ordersCountOnMerchant` | inteiro | sim   | Quantidade de pedidos do cliente nesta loja, quando disponível.                                                  |

### items [#items]

Cada item é um produto do pedido com as opções escolhidas.

| Campo                 | Tipo    | Nulo? | Descrição                                                                 |
| --------------------- | ------- | ----- | ------------------------------------------------------------------------- |
| `id`                  | GUID    | não   | Identificador da linha do pedido.                                         |
| `index`               | inteiro | sim   | Posição do item no pedido.                                                |
| `name`                | string  | não   | Nome do produto como foi vendido (inclui o tamanho, quando há variações). |
| `externalCode`        | string  | sim   | Id do produto no cardápio (`GET /v1/merchant/{merchantId}/menus`).        |
| `unit`                | string  | sim   | Unidade de medida, quando aplicável.                                      |
| `ean`                 | string  | sim   | Código de barras, quando cadastrado.                                      |
| `quantity`            | número  | não   | Quantidade.                                                               |
| `specialInstructions` | string  | sim   | Observação do cliente para este item.                                     |
| `unitPrice`           | Money   | não   | Preço unitário do produto, sem opções.                                    |
| `optionsPrice`        | Money   | não   | Soma das opções.                                                          |
| `totalPrice`          | Money   | não   | Total da linha, o valor cobrado.                                          |
| `options`             | array   | não   | Complementos escolhidos. Vazio quando não há.                             |

#### items\[].options [#itemsoptions]

| Campo          | Tipo    | Nulo? | Descrição                                         |
| -------------- | ------- | ----- | ------------------------------------------------- |
| `id`           | GUID    | não   | Identificador da linha da opção.                  |
| `index`        | inteiro | sim   | Posição da opção dentro do item.                  |
| `name`         | string  | não   | Nome do complemento.                              |
| `externalCode` | string  | sim   | Id da opção no cardápio.                          |
| `quantity`     | número  | não   | Quantidade.                                       |
| `unit`         | string  | sim   | Unidade, quando aplicável.                        |
| `unitPrice`    | Money   | não   | Preço unitário da opção.                          |
| `price`        | Money   | não   | Total da opção.                                   |
| `groupName`    | string  | sim   | Nome do grupo de complementos, quando disponível. |

### otherFees [#otherfees]

| Campo        | Tipo                      | Nulo? | Descrição                                      |
| ------------ | ------------------------- | ----- | ---------------------------------------------- |
| `name`       | string                    | não   | Nome da taxa, por exemplo `"Taxa de entrega"`. |
| `type`       | `DELIVERY`, `SERVICE_FEE` | não   | Taxa de entrega ou acréscimo.                  |
| `receivedBy` | `MERCHANT`                | não   | Quem recebe. Sempre a loja.                    |
| `price`      | Money                     | não   | Valor.                                         |

### discounts [#discounts]

| Campo               | Tipo   | Nulo? | Descrição                                                                           |
| ------------------- | ------ | ----- | ----------------------------------------------------------------------------------- |
| `amount`            | Money  | não   | Valor do desconto.                                                                  |
| `target`            | `CART` | não   | O desconto incide sobre o carrinho inteiro.                                         |
| `targetId`          | string | sim   | Não usado em desconto de carrinho.                                                  |
| `sponsorshipValues` | array  | não   | Quem banca: `[{name: "MERCHANT", amount}]`. Desconto do MeuPedido é sempre da loja. |

### total [#total]

| Campo         | Tipo  | Descrição                                    |
| ------------- | ----- | -------------------------------------------- |
| `items`       | Money | Soma dos itens (`totalPrice` de cada um).    |
| `otherFees`   | Money | Soma de `otherFees`.                         |
| `discount`    | Money | Soma de `discounts`.                         |
| `orderAmount` | Money | Valor final: `items + otherFees - discount`. |

### payments [#payments]

| Campo     | Tipo   | Descrição                                                         |
| --------- | ------ | ----------------------------------------------------------------- |
| `prepaid` | número | Soma dos pagamentos `ONLINE` (já pagos).                          |
| `pending` | número | Soma dos pagamentos `OFFLINE` (a cobrar na entrega ou no balcão). |
| `methods` | array  | Um item por pagamento.                                            |

#### payments.methods\[] [#paymentsmethods]

| Campo         | Tipo                                                      | Nulo? | Descrição                                                                                                                               |
| ------------- | --------------------------------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `value`       | número                                                    | não   | Valor deste pagamento.                                                                                                                  |
| `currency`    | `BRL`                                                     | não   | Moeda.                                                                                                                                  |
| `method`      | `CREDIT`, `DEBIT`, `CASH`, `PIX`, `MEAL_VOUCHER`, `OTHER` | não   | Meio de pagamento.                                                                                                                      |
| `methodInfo`  | string                                                    | sim   | Nome do meio como a loja cadastrou (por exemplo, `"Cartão de crédito na entrega"`).                                                     |
| `type`        | `ONLINE`, `OFFLINE`                                       | não   | `ONLINE` quando já foi pago; `OFFLINE` quando a loja cobra. Deriva do pagamento efetivo, não do meio: um PIX pago na porta é `OFFLINE`. |
| `changeFor`   | número                                                    | sim   | Em dinheiro: o valor com que o cliente vai pagar, para calcular o troco.                                                                |
| `brand`       | string                                                    | sim   | Bandeira do cartão, quando informada.                                                                                                   |
| `transaction` | objeto                                                    | sim   | `{transactionId, authorizationCode, acquirerDocument}` em pagamento on-line.                                                            |

### delivery [#delivery]

Presente apenas em pedido `DELIVERY`.

| Campo                       | Tipo                      | Nulo? | Descrição                                                |
| --------------------------- | ------------------------- | ----- | -------------------------------------------------------- |
| `deliveryDateTime`          | data                      | sim   | Quando foi entregue, quando registrado.                  |
| `estimatedDeliveryDateTime` | data                      | sim   | Previsão de entrega, quando calculada.                   |
| `deliveredBy`               | `MERCHANT`, `MARKETPLACE` | não   | Quem entrega: a própria loja ou o marketplace de origem. |
| `deliveryAddress`           | objeto                    | sim   | Endereço. Veja abaixo.                                   |

#### delivery.deliveryAddress [#deliverydeliveryaddress]

| Campo              | Tipo   | Nulo? | Descrição                                  |
| ------------------ | ------ | ----- | ------------------------------------------ |
| `country`          | string | não   | `"BR"`.                                    |
| `state`            | string | sim   | UF.                                        |
| `city`             | string | sim   | Cidade.                                    |
| `district`         | string | sim   | Bairro.                                    |
| `street`           | string | não   | Logradouro.                                |
| `number`           | string | sim   | Número.                                    |
| `postalCode`       | string | sim   | CEP.                                       |
| `complement`       | string | sim   | Complemento.                               |
| `reference`        | string | sim   | Ponto de referência.                       |
| `formattedAddress` | string | sim   | Endereço em uma linha, quando disponível.  |
| `coordinates`      | objeto | sim   | `{latitude, longitude}` em graus decimais. |

### takeout [#takeout]

Presente em pedido `TAKEOUT` e `INDOOR`.

| Campo             | Tipo      | Nulo? | Descrição                                                                                      |
| ----------------- | --------- | ----- | ---------------------------------------------------------------------------------------------- |
| `mode`            | `DEFAULT` | não   | Modo de retirada.                                                                              |
| `takeoutDateTime` | data      | sim   | Horário agendado da retirada. `null` em pedido imediato: o MeuPedido não inventa uma previsão. |

### schedule [#schedule]

Presente em pedido `SCHEDULED`.

| Campo                    | Tipo | Nulo? | Descrição                                                        |
| ------------------------ | ---- | ----- | ---------------------------------------------------------------- |
| `scheduledDateTimeStart` | data | não   | Início da janela agendada. Também em `preparationStartDateTime`. |
| `scheduledDateTimeEnd`   | data | sim   | Fim da janela, quando definido.                                  |

## Exemplo: pedido DELIVERY [#exemplo-pedido-delivery]

Pedido imediato de entrega, pago por PIX on-line, com desconto e taxa de entrega.

```json title="GET /v1/orders/3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d"
{
  "id": "3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d",
  "displayId": "1042",
  "type": "DELIVERY",
  "orderTiming": "INSTANT",
  "createdAt": "2026-09-19T14:02:11Z",
  "preparationStartDateTime": "2026-09-19T14:02:11Z",
  "merchant": {
    "id": "7f3b2c1e-9d4a-4b8e-a1c6-2e5f8d9a0b1c",
    "name": "Pizzaria Bella Massa"
  },
  "customer": {
    "id": "a8c4e2f0-3b1d-4e6a-9f5c-7d2b8a1c0e3f",
    "name": "Mariana Souza",
    "phone": { "number": "11987654321", "extension": null },
    "documentNumber": null,
    "email": "mariana.souza@example.com",
    "ordersCountOnMerchant": null
  },
  "items": [
    {
      "id": "5d1e2f3a-4b5c-4d6e-8f7a-9b0c1d2e3f4a",
      "index": null,
      "name": "Pizza Margherita Grande",
      "externalCode": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
      "unit": null,
      "ean": null,
      "quantity": 1,
      "specialInstructions": "Bem assada.",
      "unitPrice": { "value": 49.90, "currency": "BRL" },
      "optionsPrice": { "value": 6.00, "currency": "BRL" },
      "totalPrice": { "value": 55.90, "currency": "BRL" },
      "options": [
        {
          "id": "6e2f3a4b-5c6d-4e7f-9a8b-0c1d2e3f4a5b",
          "index": null,
          "name": "Borda recheada com catupiry",
          "externalCode": "d3e4f5a6-b7c8-4d9e-8f0a-2b3c4d5e6f7a",
          "quantity": 1,
          "unit": null,
          "unitPrice": { "value": 6.00, "currency": "BRL" },
          "price": { "value": 6.00, "currency": "BRL" },
          "groupName": null
        }
      ]
    },
    {
      "id": "7f3a4b5c-6d7e-4f8a-9b0c-1d2e3f4a5b6c",
      "index": null,
      "name": "Refrigerante lata 350 ml",
      "externalCode": "e4f5a6b7-c8d9-4e0f-9a1b-3c4d5e6f7a8b",
      "unit": null,
      "ean": null,
      "quantity": 2,
      "specialInstructions": null,
      "unitPrice": { "value": 6.50, "currency": "BRL" },
      "optionsPrice": { "value": 0, "currency": "BRL" },
      "totalPrice": { "value": 13.00, "currency": "BRL" },
      "options": []
    }
  ],
  "otherFees": [
    {
      "name": "Taxa de entrega",
      "type": "DELIVERY",
      "receivedBy": "MERCHANT",
      "price": { "value": 8.00, "currency": "BRL" }
    }
  ],
  "discounts": [
    {
      "amount": { "value": 5.00, "currency": "BRL" },
      "target": "CART",
      "targetId": null,
      "sponsorshipValues": [
        { "name": "MERCHANT", "amount": { "value": 5.00, "currency": "BRL" } }
      ]
    }
  ],
  "total": {
    "items": { "value": 68.90, "currency": "BRL" },
    "otherFees": { "value": 8.00, "currency": "BRL" },
    "discount": { "value": 5.00, "currency": "BRL" },
    "orderAmount": { "value": 71.90, "currency": "BRL" }
  },
  "payments": {
    "prepaid": 71.90,
    "pending": 0,
    "methods": [
      {
        "value": 71.90,
        "currency": "BRL",
        "method": "PIX",
        "methodInfo": "PIX",
        "type": "ONLINE",
        "changeFor": null,
        "brand": null,
        "transaction": {
          "transactionId": "E18236120202609191402s0f3c9a1b2c",
          "authorizationCode": null,
          "acquirerDocument": null
        }
      }
    ]
  },
  "delivery": {
    "deliveryDateTime": null,
    "estimatedDeliveryDateTime": null,
    "deliveredBy": "MERCHANT",
    "deliveryAddress": {
      "country": "BR",
      "state": "SP",
      "city": "São Paulo",
      "district": "Pinheiros",
      "street": "Rua dos Pinheiros",
      "number": "1200",
      "postalCode": "05422-001",
      "complement": "Apto 42",
      "reference": "Portão azul, ao lado da farmácia",
      "formattedAddress": null,
      "coordinates": { "latitude": -23.566432, "longitude": -46.690178 }
    }
  },
  "takeout": null,
  "schedule": null,
  "extraInfo": "Tocar o interfone 42 | origem=SITE",
  "test": false
}
```

Conferência dos totais: `items` 55,90 + 13,00 = 68,90; `orderAmount` 68,90 + 8,00 (taxa) - 5,00 (desconto) = 71,90; `prepaid` 71,90 porque o PIX foi pago on-line.

## Exemplo: pedido TAKEOUT agendado [#exemplo-pedido-takeout-agendado]

Retirada agendada para as 19h30 (UTC 22:30), paga em dinheiro no balcão, com troco para 50.

```json title="GET /v1/orders/9b2e4f6a-1c3d-4e5f-8a7b-6c5d4e3f2a1b"
{
  "id": "9b2e4f6a-1c3d-4e5f-8a7b-6c5d4e3f2a1b",
  "displayId": "1043",
  "type": "TAKEOUT",
  "orderTiming": "SCHEDULED",
  "createdAt": "2026-09-19T14:10:52Z",
  "preparationStartDateTime": "2026-09-19T22:30:00Z",
  "merchant": {
    "id": "7f3b2c1e-9d4a-4b8e-a1c6-2e5f8d9a0b1c",
    "name": "Pizzaria Bella Massa"
  },
  "customer": {
    "id": "b1d5f3a7-4c2e-4f8b-a0d6-8e3c9b2d1f4a",
    "name": "Rafael Lima",
    "phone": { "number": "11991234567", "extension": null },
    "documentNumber": "12345678909",
    "email": null,
    "ordersCountOnMerchant": null
  },
  "items": [
    {
      "id": "8a4b5c6d-7e8f-4a9b-8c0d-2e3f4a5b6c7d",
      "index": null,
      "name": "Combo Burger Clássico",
      "externalCode": "f5a6b7c8-d9e0-4f1a-8b2c-4d5e6f7a8b9c",
      "unit": null,
      "ean": null,
      "quantity": 1,
      "specialInstructions": "Sem cebola.",
      "unitPrice": { "value": 32.00, "currency": "BRL" },
      "optionsPrice": { "value": 4.00, "currency": "BRL" },
      "totalPrice": { "value": 36.00, "currency": "BRL" },
      "options": [
        {
          "id": "9b5c6d7e-8f9a-4b0c-9d1e-3f4a5b6c7d8e",
          "index": null,
          "name": "Bacon extra",
          "externalCode": "a6b7c8d9-e0f1-4a2b-9c3d-5e6f7a8b9c0d",
          "quantity": 1,
          "unit": null,
          "unitPrice": { "value": 4.00, "currency": "BRL" },
          "price": { "value": 4.00, "currency": "BRL" },
          "groupName": null
        }
      ]
    },
    {
      "id": "0c6d7e8f-9a0b-4c1d-8e2f-4a5b6c7d8e9f",
      "index": null,
      "name": "Batata frita média",
      "externalCode": "b7c8d9e0-f1a2-4b3c-8d4e-6f7a8b9c0d1e",
      "unit": null,
      "ean": null,
      "quantity": 1,
      "specialInstructions": null,
      "unitPrice": { "value": 9.00, "currency": "BRL" },
      "optionsPrice": { "value": 0, "currency": "BRL" },
      "totalPrice": { "value": 9.00, "currency": "BRL" },
      "options": []
    }
  ],
  "otherFees": null,
  "discounts": null,
  "total": {
    "items": { "value": 45.00, "currency": "BRL" },
    "otherFees": { "value": 0, "currency": "BRL" },
    "discount": { "value": 0, "currency": "BRL" },
    "orderAmount": { "value": 45.00, "currency": "BRL" }
  },
  "payments": {
    "prepaid": 0,
    "pending": 45.00,
    "methods": [
      {
        "value": 45.00,
        "currency": "BRL",
        "method": "CASH",
        "methodInfo": "Dinheiro",
        "type": "OFFLINE",
        "changeFor": 50.00,
        "brand": null,
        "transaction": null
      }
    ]
  },
  "delivery": null,
  "takeout": {
    "mode": "DEFAULT",
    "takeoutDateTime": "2026-09-19T22:30:00Z"
  },
  "schedule": {
    "scheduledDateTimeStart": "2026-09-19T22:30:00Z",
    "scheduledDateTimeEnd": null
  },
  "extraInfo": null,
  "test": true
}
```

Repare em três coisas: `preparationStartDateTime`, `takeout.takeoutDateTime` e `schedule.scheduledDateTimeStart` carregam o mesmo instante, porque o agendamento é decidido uma vez e alimenta os três; `pending` é 45,00 porque o pagamento é `OFFLINE`; e `test` é `true`, indicando um pedido de loja de teste.

## Lendo o pedido com segurança [#lendo-o-pedido-com-segurança]

* Use `id` como chave. `displayId` se repete ao longo do tempo.
* Trate `test: true` de forma separada. O pedido é real na loja de teste, mas não deve entrar no seu faturamento.
* Não some `items[].unitPrice * quantity` para chegar ao total: use `totalPrice` e `total.orderAmount`. Eles são os valores cobrados.
* Um `MODIFIED` no feed significa que o pedido mudou: substitua a sua cópia inteira, não só a situação.
* Guarde `extraInfo` como texto. O formato `chave=valor` é estável, mas os valores de `origem` e `canal` dependem do canal de origem.

## Próximos passos [#próximos-passos]

* [Loja e cardápio](/pt-BR/docs/getting-started/merchant): de onde vêm `externalCode` e os grupos de opções.
* [Catálogo de eventos](/pt-BR/docs/getting-started/orders/events): quando buscar o pedido de novo.
* [Ações e idempotência](/pt-BR/docs/getting-started/orders/actions): mover o pedido depois de lê-lo.


# Polling (/pt-BR/docs/getting-started/orders/polling)

> Consulta de eventos com entrega at-least-once, dedupe por eventId, limite de 200 por consulta e confirmação obrigatória.

Polling é o modo padrão de receber eventos: o seu sistema consulta `GET /v1/events:polling` em intervalo fixo, processa cada evento e confirma o recebimento em `POST /v1/events/acknowledgment`. Nada é perdido entre uma consulta e outra, porque o que não foi confirmado volta na próxima.

## O ciclo [#o-ciclo]

### Consultar [#consultar]

`GET /v1/events:polling` devolve os eventos pendentes da sua credencial, em ordem de `createdAt`. Cada item é um envelope com `eventId`, `eventType`, `orderId`, `orderURL` e `createdAt`. O envelope **nunca traz o pedido**.

### Buscar o pedido [#buscar-o-pedido]

Para os eventos que precisam do conteúdo (`CREATED`, `MODIFIED`), faça `GET` em `orderURL`. Você lê o pedido de agora, e não uma fotografia do instante da transição.

### Processar e gravar [#processar-e-gravar]

Aplique o evento no seu sistema e grave o `eventId` como processado. Só depois disso passe ao próximo passo.

### Confirmar [#confirmar]

`POST /v1/events/acknowledgment` com a lista de `eventId` processados. Eventos confirmados saem do feed. Eventos não confirmados voltam na próxima consulta.

## Consultar eventos [#consultar-eventos]

```http
GET /v1/events:polling?limit=100
Authorization: Bearer {access_token}
```

Escopo: `orders:read`.

| Parâmetro | Tipo    | Padrão | Descrição                                                                                          |
| --------- | ------- | ------ | -------------------------------------------------------------------------------------------------- |
| `limit`   | inteiro | `100`  | Quantidade máxima de eventos por resposta. Máximo `200`; valores maiores são reduzidos para `200`. |

Não existe cursor, `since` ou filtro por tipo. O feed é o conjunto de eventos ainda não confirmados pela sua credencial, sempre a partir do mais antigo.

**cURL**

```bash
curl "https://api.meupedido.io/open-delivery/v1/events:polling?limit=100" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

**Node.js**

```js
const response = await fetch(
  'https://api.meupedido.io/open-delivery/v1/events:polling?limit=100',
  { headers: { Authorization: `Bearer ${accessToken}` } },
);
const events = await response.json(); // sempre um array, vazio quando não há nada
```

**Python**

```python
import requests

response = requests.get(
    "https://api.meupedido.io/open-delivery/v1/events:polling",
    params={"limit": 100},
    headers={"Authorization": f"Bearer {access_token}"},
    timeout=15,
)
events = response.json()  # sempre uma lista, vazia quando não há nada
```

**C#**

```csharp
using var http = new HttpClient { BaseAddress = new Uri("https://api.meupedido.io/open-delivery/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

var events = await http.GetFromJsonAsync<List<JsonElement>>("v1/events:polling?limit=100");
// sempre uma lista, vazia quando não há nada
```

### Resposta [#resposta]

`200` com um array. Quando não há eventos pendentes o array vem vazio; a API &#x2A;*nunca responde `204`**.

```json title="200 OK"
[
  {
    "eventId": "e1b9c0d2-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "eventType": "CREATED",
    "orderId": "3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d",
    "orderURL": "https://api.meupedido.io/open-delivery/v1/orders/3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d",
    "createdAt": "2026-09-19T14:02:11Z"
  },
  {
    "eventId": "0c7d2e91-6f3a-4b58-9e1d-4a2b3c4d5e6f",
    "eventType": "CANCELLED",
    "orderId": "9b2e4f6a-1c3d-4e5f-8a7b-6c5d4e3f2a1b",
    "orderURL": "https://api.meupedido.io/open-delivery/v1/orders/9b2e4f6a-1c3d-4e5f-8a7b-6c5d4e3f2a1b",
    "createdAt": "2026-09-19T14:03:40Z",
    "metadata": {
      "reason": "Cliente desistiu do pedido.",
      "code": "CONSUMER_CANCELLATION_REQUESTED"
    }
  }
]
```

Os campos opcionais (`sourceAppId`, `metadata`, `delivery`) só aparecem quando têm valor. A descrição de cada um está no [catálogo de eventos](/pt-BR/docs/getting-started/orders/events).

## Confirmar eventos [#confirmar-eventos]

```http
POST /v1/events/acknowledgment
Authorization: Bearer {access_token}
Content-Type: application/json
```

Escopo: `orders:read`. O corpo é um array de objetos com o `id` de cada evento:

**cURL**

```bash
curl -X POST "https://api.meupedido.io/open-delivery/v1/events/acknowledgment" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[
    { "id": "e1b9c0d2-4a5b-4c6d-8e7f-9a0b1c2d3e4f" },
    { "id": "0c7d2e91-6f3a-4b58-9e1d-4a2b3c4d5e6f" }
  ]'
```

**Node.js**

```js
await fetch('https://api.meupedido.io/open-delivery/v1/events/acknowledgment', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(events.map((event) => ({ id: event.eventId }))),
});
```

**Python**

```python
requests.post(
    "https://api.meupedido.io/open-delivery/v1/events/acknowledgment",
    headers={"Authorization": f"Bearer {access_token}"},
    json=[{"id": event["eventId"]} for event in events],
    timeout=15,
)
```

**C#**

```csharp
var body = events.Select(e => new { id = e.GetProperty("eventId").GetString() });
await http.PostAsJsonAsync("v1/events/acknowledgment", body);
```

```json title="202 Accepted"
{ "acknowledged": 2 }
```

`acknowledged` é a quantidade de eventos que saiu do feed. Ids que não pertencem à sua credencial, ids repetidos e ids já confirmados são ignorados em silêncio: a resposta continua `202`, apenas com a contagem menor.

## Garantias [#garantias]

**At-least-once.** Todo evento é entregue pelo menos uma vez. Consultar não confirma: um evento devolvido pela consulta continua pendente até você chamar `acknowledgment`. Se o seu processo cair entre a consulta e a confirmação, o evento volta na próxima consulta.

**Sem perda entre consultas.** Não existe cursor a manter. O estado "o que ainda falta" vive na API, por credencial, e a única coisa que o move é a confirmação.

**Ordem.** Os eventos vêm ordenados por `createdAt`. A ordem entre pedidos diferentes não importa para o seu fluxo; a ordem dentro do mesmo pedido segue o [ciclo de vida](/pt-BR/docs/getting-started/orders/lifecycle).

**Um feed por credencial.** Duas credenciais na mesma loja têm feeds independentes. Confirmar em uma não tira o evento da outra. Uma credencial criada hoje não recebe os eventos de ontem. Uma credencial pausada continua acumulando eventos, que ficam disponíveis quando ela for retomada.

**Retenção.** Eventos confirmados são apagados após 30 dias. Eventos não confirmados nunca são apagados: eles continuam voltando até serem confirmados.

## Deduplicação [#deduplicação]

Porque a entrega é at-least-once, o mesmo `eventId` pode chegar mais de uma vez, seja por uma confirmação que não chegou, seja porque você recebe por polling e por webhook ao mesmo tempo (modo `BOTH`). Trate o `eventId` como chave única:

```js
async function handle(event) {
  // Já processado: só confirmar de novo, sem reaplicar.
  if (await store.hasEvent(event.eventId)) return;

  const order = await fetchOrder(event.orderURL);
  await store.applyInTransaction(order, event); // grava o pedido e o eventId juntos
}
```

Grave o `eventId` na mesma transação em que aplica o efeito. Gravar antes cria a chance de marcar como processado algo que falhou; gravar depois cria a chance de aplicar duas vezes.

## Intervalo e limite [#intervalo-e-limite]

* Consulte a cada **5 a 10 segundos** em operação normal. Intervalo menor que isso não faz o pedido chegar antes e consome o seu [limite de 600 requisições por minuto](/pt-BR/docs/getting-started/best-practices).
* Use `limit=200` se a sua integração processa em lote. Se a resposta vier cheia (200 itens), consulte de novo imediatamente após confirmar, sem esperar o intervalo.
* Confirme em lote, mas não segure a confirmação por muito tempo: enquanto não confirmar, os mesmos eventos ocupam espaço na próxima resposta.
* Em `429`, respeite `Retry-After` (60 segundos) antes de tentar de novo.

> **Webhook não substitui a confirmação**
> Se a sua credencial está em modo `WEBHOOK` ou `BOTH`, receber o evento na sua URL **não** o confirma. Chame `acknowledgment` também para eventos recebidos por webhook; caso contrário, eles continuam no polling e nunca são limpos. Veja [Webhooks](/pt-BR/docs/getting-started/orders/webhooks).

## Um loop completo [#um-loop-completo]

**Node.js**

```js
const BASE = 'https://api.meupedido.io/open-delivery';

async function poll(accessToken) {
  const res = await fetch(`${BASE}/v1/events:polling?limit=200`, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  if (res.status === 429) {
    const wait = Number(res.headers.get('retry-after') ?? 60) * 1000;
    await new Promise((r) => setTimeout(r, wait));
    return;
  }
  const events = await res.json();
  const done = [];

  for (const event of events) {
    try {
      await handle(event);
      done.push({ id: event.eventId });
    } catch (err) {
      // Não confirma: o evento volta na próxima consulta.
      console.error('Falha ao processar', event.eventId, err);
    }
  }

  if (done.length > 0) {
    await fetch(`${BASE}/v1/events/acknowledgment`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
      body: JSON.stringify(done),
    });
  }

  // Resposta cheia: há mais, consulta de novo sem esperar.
  if (events.length === 200) return poll(accessToken);
}

setInterval(() => poll(getToken()).catch(console.error), 5000);
```

**Python**

```python
import time
import requests

BASE = "https://api.meupedido.io/open-delivery"

def poll(access_token: str) -> None:
    headers = {"Authorization": f"Bearer {access_token}"}
    res = requests.get(f"{BASE}/v1/events:polling", params={"limit": 200}, headers=headers, timeout=15)

    if res.status_code == 429:
        time.sleep(int(res.headers.get("Retry-After", "60")))
        return

    events = res.json()
    done = []

    for event in events:
        try:
            handle(event)
            done.append({"id": event["eventId"]})
        except Exception as exc:  # não confirma: o evento volta na próxima consulta
            print("Falha ao processar", event["eventId"], exc)

    if done:
        requests.post(f"{BASE}/v1/events/acknowledgment", headers=headers, json=done, timeout=15)

    if len(events) == 200:  # resposta cheia: há mais, consulta de novo sem esperar
        poll(access_token)

while True:
    poll(get_token())
    time.sleep(5)
```

**C#**

```csharp
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var http = new HttpClient { BaseAddress = new Uri("https://api.meupedido.io/open-delivery/") };

async Task PollAsync(string accessToken, CancellationToken ct)
{
    http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

    var res = await http.GetAsync("v1/events:polling?limit=200", ct);
    if ((int)res.StatusCode == 429)
    {
        var wait = res.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(60);
        await Task.Delay(wait, ct);
        return;
    }

    var events = await res.Content.ReadFromJsonAsync<List<JsonElement>>(cancellationToken: ct) ?? [];
    var done = new List<object>();

    foreach (var evt in events)
    {
        var eventId = evt.GetProperty("eventId").GetString()!;
        try
        {
            await HandleAsync(evt, ct);
            done.Add(new { id = eventId });
        }
        catch (Exception ex)
        {
            // Não confirma: o evento volta na próxima consulta.
            Console.Error.WriteLine($"Falha ao processar {eventId}: {ex.Message}");
        }
    }

    if (done.Count > 0)
        await http.PostAsJsonAsync("v1/events/acknowledgment", done, ct);

    // Resposta cheia: há mais, consulta de novo sem esperar.
    if (events.Count == 200)
        await PollAsync(accessToken, ct);
}

while (true)
{
    await PollAsync(GetToken(), CancellationToken.None);
    await Task.Delay(TimeSpan.FromSeconds(5));
}
```

## Erros [#erros]

| Status | Corpo                             | Quando                                                      |
| ------ | --------------------------------- | ----------------------------------------------------------- |
| `401`  | vazio, com `WWW-Authenticate`     | Sem token ou token expirado.                                |
| `401`  | `{"error":"invalid_token"}`       | Credencial pausada ou revogada pelo lojista.                |
| `403`  | `{"error":"insufficient_scope"}`  | A credencial não tem `orders:read`.                         |
| `429`  | `{"error":"rate_limit_exceeded"}` | Mais de 600 requisições por minuto. Respeite `Retry-After`. |

## Próximos passos [#próximos-passos]

* [Catálogo de eventos](/pt-BR/docs/getting-started/orders/events): o que cada `eventType` significa e o que vem em `metadata` e `delivery`.
* [Webhooks](/pt-BR/docs/getting-started/orders/webhooks): receber os mesmos envelopes por push.
* [Estrutura do pedido](/pt-BR/docs/getting-started/orders/order-object): o que você encontra em `orderURL`.


# Webhooks (/pt-BR/docs/getting-started/orders/webhooks)

> Entrega por webhook: envelope idêntico ao polling, cabeçalhos X-MeuPedido-*, verificação da assinatura HMAC, retentativas e desativação.

**Extensão MeuPedido**

Com o modo de entrega `WEBHOOK` ou `BOTH`, o MeuPedido faz um `POST` na sua URL a cada evento, em vez de esperar a sua consulta. O corpo é **o mesmo envelope** que o polling devolve, byte a byte: um único desserializador atende os dois modos.

Webhook é uma extensão MeuPedido. Não é o `/v1/newEvent` descrito no padrão Open Delivery; a diferença está em [Compatibilidade](/pt-BR/docs/getting-started/compatibility).

## Configuração [#configuração]

A URL é configurada pelo lojista (ou por quem tem acesso ao painel dele) em **Integrações > Open Delivery**, ao criar ou editar a credencial:

* **Modo de entrega** `WEBHOOK` (só push) ou `BOTH` (push e polling).
* **URL** obrigatoriamente HTTPS.
* **Segredo do webhook**, gerado pelo MeuPedido e mostrado **uma única vez** ao salvar a URL. Guarde-o no seu cofre de segredos. Salvar a URL de novo rotaciona o segredo, e o anterior deixa de valer.

Não há como recuperar um segredo perdido: rotacione e atualize o seu servidor.

## A requisição [#a-requisição]

```http
POST {sua URL}
Content-Type: application/json
X-MeuPedido-Signature: 5f1d6a9c2b3e4f7a8d9c0b1e2f3a4d5c6b7a8f9e0d1c2b3a4f5e6d7c8b9a0f1e
X-MeuPedido-Timestamp: 1758290531
X-MeuPedido-Event-Id: e1b9c0d2-4a5b-4c6d-8e7f-9a0b1c2d3e4f
X-MeuPedido-Event-Type: CREATED
```

```json title="Corpo"
{
  "eventId": "e1b9c0d2-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
  "eventType": "CREATED",
  "orderId": "3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d",
  "orderURL": "https://api.meupedido.io/open-delivery/v1/orders/3f8a1c2d-5b6e-4f7a-9c8d-1e2f3a4b5c6d",
  "createdAt": "2026-09-19T14:02:11Z"
}
```

| Cabeçalho                | Conteúdo                                                                                           |
| ------------------------ | -------------------------------------------------------------------------------------------------- |
| `X-MeuPedido-Signature`  | HMAC-SHA256 em hexadecimal minúsculo, calculado com o segredo sobre a string `{timestamp}.{body}`. |
| `X-MeuPedido-Timestamp`  | Instante do envio, em segundos Unix. Entra no cálculo da assinatura.                               |
| `X-MeuPedido-Event-Id`   | O mesmo `eventId` do corpo, para roteamento e deduplicação sem desserializar.                      |
| `X-MeuPedido-Event-Type` | O mesmo `eventType` do corpo.                                                                      |

Um envio por evento. O corpo nunca traz o pedido: busque em `orderURL`, como no polling.

## Responder [#responder]

Responda &#x2A;*qualquer `2xx`** em até **10 segundos**. Isso é tudo que o MeuPedido espera; o corpo da resposta é ignorado.

Faça o mínimo dentro desses 10 segundos: verifique a assinatura, enfileire o evento e responda. Buscar o pedido, gravar no banco e imprimir ficam para depois da resposta. Um `2xx` tardio conta como timeout.

> **Entregue não é confirmado**
> O `2xx` diz ao MeuPedido que a requisição chegou. Ele **não** tira o evento do feed. É obrigatório chamar [`POST /v1/events/acknowledgment`](/pt-BR/docs/getting-started/orders/polling#confirmar-eventos) com o `eventId` depois de processar, mesmo recebendo por webhook. Sem isso o evento continua no polling e nunca é limpo.

## Verificar a assinatura [#verificar-a-assinatura]

Toda requisição deve ser verificada antes de qualquer processamento. A verificação tem duas partes:

1. **Tempo.** Rejeite se `X-MeuPedido-Timestamp` estiver a mais de 5 minutos do relógio do seu servidor, para frente ou para trás. Isso impede o reenvio de uma requisição capturada.
2. **Assinatura.** Calcule `HMAC-SHA256(secret, "{timestamp}.{body}")`, converta para hexadecimal minúsculo e compare com `X-MeuPedido-Signature` em **tempo constante**.

`body` é o corpo bruto da requisição, exatamente como chegou. Não desserialize e serialize de novo antes de calcular: qualquer mudança de espaço ou ordem de chaves invalida a assinatura.

As funções abaixo são puras: recebem o segredo, o timestamp, o corpo bruto e a assinatura, e devolvem `true` ou `false`. O parâmetro `now` é opcional e existe para testes; em produção, deixe o padrão.

**Node.js**

```js title="verify-signature.js"
import { createHmac, timingSafeEqual } from 'node:crypto';

const TOLERANCE_SECONDS = 5 * 60;

export function verifySignature(secret, timestamp, body, signature, now = Math.floor(Date.now() / 1000)) {
  const sent = Number(timestamp);
  if (!Number.isInteger(sent) || Math.abs(now - sent) > TOLERANCE_SECONDS) return false;

  const expected = createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
  const received = String(signature ?? '').toLowerCase();

  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(received, 'utf8');
  return a.length === b.length && timingSafeEqual(a, b);
}
```

**Python**

```python title="verify_signature.py"
import hashlib
import hmac
import time
from typing import Optional

TOLERANCE_SECONDS = 5 * 60

def verify_signature(secret: str, timestamp: str, body: str, signature: str, now: Optional[int] = None) -> bool:
    current = int(time.time()) if now is None else now
    try:
        sent = int(timestamp)
    except (TypeError, ValueError):
        return False
    if abs(current - sent) > TOLERANCE_SECONDS:
        return False

    expected = hmac.new(
        secret.encode("utf-8"),
        f"{timestamp}.{body}".encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, (signature or "").lower())
```

**C#**

```csharp title="WebhookSignature.cs"
using System.Security.Cryptography;
using System.Text;

public static class WebhookSignature
{
    private const long ToleranceSeconds = 5 * 60;

    public static bool VerifySignature(string secret, string timestamp, string body, string signature, long? now = null)
    {
        var current = now ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        if (!long.TryParse(timestamp, out var sent) || Math.Abs(current - sent) > ToleranceSeconds)
            return false;

        var hash = HMACSHA256.HashData(
            Encoding.UTF8.GetBytes(secret),
            Encoding.UTF8.GetBytes($"{timestamp}.{body}"));

        var expected = Encoding.UTF8.GetBytes(Convert.ToHexString(hash).ToLowerInvariant());
        var received = Encoding.UTF8.GetBytes((signature ?? string.Empty).ToLowerInvariant());

        // FixedTimeEquals devolve false para tamanhos diferentes sem vazar em quanto diferem.
        return CryptographicOperations.FixedTimeEquals(expected, received);
    }
}
```

**PHP**

```php title="verify-signature.php"
<?php

const TOLERANCE_SECONDS = 5 * 60;

function verifySignature(string $secret, string $timestamp, string $body, string $signature, ?int $now = null): bool
{
    $current = $now ?? time();
    if (!ctype_digit($timestamp) || abs($current - (int) $timestamp) > TOLERANCE_SECONDS) {
        return false;
    }

    $expected = hash_hmac('sha256', "{$timestamp}.{$body}", $secret);
    return hash_equals($expected, strtolower($signature));
}
```

### Um endpoint completo [#um-endpoint-completo]

**Node.js**

```js
import express from 'express';
import { verifySignature } from './verify-signature.js';

const app = express();

// Corpo bruto: a assinatura é calculada sobre os bytes originais.
app.post('/webhooks/meupedido', express.raw({ type: 'application/json' }), async (req, res) => {
  const body = req.body.toString('utf8');
  const ok = verifySignature(
    process.env.MEUPEDIDO_WEBHOOK_SECRET,
    req.get('X-MeuPedido-Timestamp'),
    body,
    req.get('X-MeuPedido-Signature'),
  );
  if (!ok) return res.status(401).end();

  await queue.push(JSON.parse(body)); // processa fora da requisição
  res.status(204).end();
});
```

**Python**

```python
import os
from flask import Flask, request, abort
from verify_signature import verify_signature

app = Flask(__name__)

@app.post("/webhooks/meupedido")
def meupedido_webhook():
    body = request.get_data(as_text=True)  # corpo bruto, antes de qualquer parse
    ok = verify_signature(
        os.environ["MEUPEDIDO_WEBHOOK_SECRET"],
        request.headers.get("X-MeuPedido-Timestamp", ""),
        body,
        request.headers.get("X-MeuPedido-Signature", ""),
    )
    if not ok:
        abort(401)

    queue.push(request.get_json())  # processa fora da requisição
    return "", 204
```

## Retentativas e falha [#retentativas-e-falha]

Uma tentativa falha quando a sua URL responde algo fora de `2xx`, recusa a conexão ou não responde em 10 segundos. Nesse caso o MeuPedido tenta de novo com intervalos de **5, 10 e 20 segundos**: são 4 tentativas em cerca de 35 segundos.

```text
tentativa 1   t = 0 s
tentativa 2   t = 5 s
tentativa 3   t = 15 s
tentativa 4   t = 35 s
```

Esgotadas as tentativas, o evento é marcado como `WEBHOOK_FAILED` e **continua disponível no polling**, de onde você o recupera na próxima consulta. Nada é perdido por causa de uma queda da sua URL.

O lojista vê cada tentativa, o status e o motivo em **Integrações > Open Delivery**, e pode reenviar um evento manualmente.

## Desativação da URL [#desativação-da-url]

Após **20 falhas consecutivas**, a URL é desativada e o MeuPedido para de enviar. Os eventos continuam sendo gerados normalmente e ficam disponíveis no polling. Quando o seu servidor voltar, o lojista reativa a entrega em **Limpar fila**, no painel.

Enquanto a URL estiver desativada, o polling é o único caminho. Uma integração resiliente em modo `BOTH` não percebe a desativação: o loop de polling continua trazendo tudo.

## Recomendações [#recomendações]

* **Idempotência pelo `eventId`.** Retentativa e modo `BOTH` fazem o mesmo evento chegar mais de uma vez. Use o `eventId` como chave única, exatamente como no [polling](/pt-BR/docs/getting-started/orders/polling#deduplicação).
* **Responda antes de processar.** Enfileire e devolva `2xx`; o processamento pesado fica para um worker.
* **Trate o webhook como um aviso, não como a fonte da verdade.** Busque o pedido em `orderURL`. Se um webhook falhar por completo, o polling entrega o mesmo evento.
* **Restrinja o segredo.** Ele vive só no servidor que recebe o webhook. Nunca em cliente, repositório ou log.
* **Rotacione ao suspeitar de vazamento.** Salvar a URL de novo no painel gera um segredo novo; atualize o servidor no mesmo momento, porque o anterior deixa de valer imediatamente.

## Próximos passos [#próximos-passos]

* [Polling](/pt-BR/docs/getting-started/orders/polling): a confirmação obrigatória e o loop que recupera eventos com falha de webhook.
* [Catálogo de eventos](/pt-BR/docs/getting-started/orders/events): todos os `eventType` que a sua URL pode receber.
* [Obter credenciais](/pt-BR/docs/getting-started/first-steps/get-credentials): onde a URL e o modo de entrega são configurados.
