> ## Documentation Index
> Fetch the complete documentation index at: /docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Documentação das APIs da DeliveryVip, em português. Regras que mais quebram integrações: (1) Merchant API: faça polling em GET /merchant/v3/events:polling no máximo a cada 30 s por token; confirme todo evento recebido com POST /merchant/v3/events/acknowledgment e corpo [{"id": "<eventId>"}] (o campo lido é id, não eventId), inclusive eventos repetidos ou que o sistema não trata; deduplique por eventId; o evento é só um aviso, busque o pedido na orderURL. (2) Tokens client_credentials valem 24 h e não têm refresh_token: peça outro ao receber 401. (3) Webhooks: valide a assinatura HMAC sobre o corpo bruto e responda 2xx rápido; o polling é a reconciliação. (4) Open Delivery: host od.deliveryvip.com.br, um client_secret por loja, datas em UTC. (5) Fidelidade: o operador implementa 5 rotas assinadas com HMAC e chama o config push; o estorno precisa ser idempotente. (6) Partner API: scope dv.partner. Não invente endpoints: use a referência gerada das specs OpenAPI.

# Recebendo eventos

> Receba os eventos de pedido por webhook assinado, valide a assinatura e use o polling com acknowledgment como reconciliação.

A DeliveryVip entrega eventos de pedido pelos dois métodos previstos na spec Order `1.3.0`: **WEBHOOK** e **POLLING**. Recomendamos o **webhook como canal principal** e o **polling como reconciliação**: se o seu endpoint ficar fora do ar, os eventos das últimas 24 horas seguem consultáveis no polling.

![A DeliveryVip envia o evento assinado ao webhook, o PDV responde 2xx em até 5 s e reconcilia pelo polling com acknowledgment](https://mintcdn.com/deliveryvip-a4256422/ELxpcniK2_BP8zxT/images/od-recebendo-eventos.png?fit=max\&auto=format\&n=ELxpcniK2_BP8zxT\&q=85\&s=6460916f57fa7cbc9aaec94f6d84aabe)

Webhook como canal principal e polling como reconciliação

## Estrutura do evento

```json theme={null}
{
  "eventId": "9a24ea9e-5743-4957-9b83-7a4b4f2a4597",
  "eventType": "CREATED",
  "orderId": "64939842-b9d7-44d1-a0fd-f68d18d410a6",
  "orderURL": "https://od.deliveryvip.com.br/v1/orders/64939842-b9d7-44d1-a0fd-f68d18d410a6",
  "orderType": "DELIVERY",
  "merchantId": "loja-centro-001",
  "createdAt": "2026-08-07T15:00:00Z"
}
```

**eventId** `string`

Identificador único do evento. Use para deduplicação e no acknowledgment.

**eventType** `string`

Tipo do evento; veja a lista abaixo.

**orderId** `string`

Identificador do pedido.

**orderURL** `string`

URL do pedido na API OD. Use-a para [obter os detalhes](/docs/guides/open-delivery/ciclo-de-vida-do-pedido.md).

**orderType** `string`

`DELIVERY`, `TAKEOUT` ou `INDOOR`.

**merchantId** `string`

O `merchantId` configurado no [onboarding](/docs/guides/open-delivery/onboarding-de-lojas.md), ou o id interno da loja, se não configurado.

**createdAt** `string`

Data e hora do evento em RFC 3339, em **UTC** (sufixo `Z`), tanto no polling quanto no webhook. Veja [Datas e horários](/docs/inicio/conceitos/datas-e-horarios.md).

**metadata** `object`

Dados extras de alguns eventos, como o motivo de um cancelamento. Só vem quando há conteúdo.

### Tipos de evento

| Evento | Quando |
| - | - |
| `CREATED` | Novo pedido. |
| `CONFIRMED` | Pedido confirmado. |
| `PREPARING` | Pedido em preparo. |
| `READY_FOR_PICKUP` | Pedido pronto para retirada. |
| `DISPATCHED` | Pedido despachado para entrega. |
| `PICKED_UP` | Pedido retirado. |
| `DELIVERED` | Pedido entregue. |
| `CONCLUDED` | Pedido concluído. Fim do ciclo. |
| `CANCELLED` | Pedido cancelado, pela loja, pelo seu sistema ou pela plataforma. |

`PICKUP_AREA_ASSIGNED`, `CANCELLATION_REQUESTED`, `CANCELLATION_REQUEST_DENIED` e `ORDER_CANCELLATION_REQUEST` existem na spec e são aceitos no filtro `eventType`, mas **a DeliveryVip ainda não os emite**. Trate tipos desconhecidos confirmando o recebimento e seguindo em frente.

## Webhook (canal principal)

A URL de webhook é configurada **por loja**, no [onboarding](/docs/guides/open-delivery/onboarding-de-lojas.md) (`ordersWebhookURL`) ou pelo formulário do painel de developers. Lojas sem URL própria caem na URL de webhook geral do app, se você tiver configurado uma.

Cada evento é enviado **individualmente** via `POST` na URL da loja, com o mesmo JSON dos eventos do polling e os headers:

| Header | Descrição |
| - | - |
| `X-App-Id` | Identificador **fixo** da DeliveryVip como plataforma emissora: `019efa90-dcd1-7846-983d-6bab871b2f6b`. É o mesmo valor em todas as entregas, para todos os apps, e **não** é o id do seu app. Use-o como checagem simples de origem; a validação que importa é a assinatura. |
| `X-App-MerchantId` | O `merchantId` da loja do evento. |
| `X-App-Signature` | **HMAC-SHA256** (hex minúsculo) do corpo bruto da requisição, assinado com o **`client_secret` da loja** (`ods_...`). |
| `User-Agent` | `DeliveryVip` |
| `Content-Type` | `application/json` |

### Validando a assinatura

A chave é o `client_secret` **da loja** que você recebeu na instalação, o mesmo que você usa para pedir o token dela. Use o `X-App-MerchantId` para descobrir de qual loja é o evento e escolher o secret certo. Só lojas sem credencial própria (integrações antigas, anteriores à extensão Open Delivery) são assinadas com o `client_secret` do app.

Recompute o HMAC sobre o **corpo bruto** recebido, antes de qualquer parse, e compare com o header em tempo constante:

```js Node.js (Express) theme={null}
import crypto from 'node:crypto'
import express from 'express'

const app = express()

app.post('/webhooks/deliveryvip', express.raw({ type: 'application/json' }), (req, res) => {
  const secret = storeSecretFor(req.get('X-App-MerchantId')) // o ods_... da loja, do seu banco
  const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex')
  const received = req.get('X-App-Signature') ?? ''

  const valid =
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
  if (!valid) return res.status(401).end()

  const event = JSON.parse(req.body) // parse só depois de validar
  enqueue(event) // processe de forma assíncrona
  res.status(200).end()
})
```

```python Python (Flask) theme={null}
import hashlib
import hmac

from flask import Flask, abort, request

app = Flask(__name__)


@app.post("/webhooks/deliveryvip")
def webhook():
    secret = store_secret_for(request.headers.get("X-App-MerchantId"))  # o ods_... da loja
    raw = request.get_data()  # bytes exatamente como chegaram
    expected = hmac.new(secret.encode(), raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-App-Signature", "")):
        abort(401)

    enqueue(request.get_json())  # processe de forma assíncrona
    return "", 200
```

```ruby Ruby (Rails) theme={null}
secret = store_secret_for(request.headers["X-App-MerchantId"])
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, request.raw_post)
valid = ActiveSupport::SecurityUtils.secure_compare(expected, request.headers["X-App-Signature"].to_s)
```

> [!CAUTION]
>
> Valide sobre os **bytes como chegaram**. Fazer parse e re-serializar o JSON muda espaçamento, escapes e números, e o digest não bate. A assinatura cobre só o corpo e não tem timestamp: um reenvio repete o mesmo `eventId`, por isso a deduplicação é obrigatória.

### O que seu endpoint deve fazer

* Responder **2xx em até 5 segundos** (3 s para conectar, 5 s para ler). Se o processamento for demorado, processe de forma **assíncrona**.
* Validar a assinatura antes de processar.
* Deduplicar por `eventId`.

### Reentrega e suspensão

| Resposta do seu endpoint | O que a DeliveryVip faz |
| - | - |
| Qualquer `2xx` | Entrega concluída. |
| `408` ou `429` | Tenta de novo, até **3 tentativas** no total. |
| Timeout ou erro de conexão | Tenta de novo, até **3 tentativas** no total. |
| Qualquer outro código, **inclusive `5xx`** | Falha definitiva na primeira tentativa. Reenvio manual pelo painel de developers. |

* **Toda tentativa que falha conta** para a suspensão, inclusive as retentativas. Uma entrega bem-sucedida zera a contagem.
* Após **10 falhas consecutivas** as entregas daquela **loja** são **suspensas** (você é notificado por e-mail, com o nome da loja). As demais lojas continuam recebendo normalmente.

Para religar as entregas de uma loja suspensa, depois de corrigir o endpoint:

* reenvie o [onboarding](/docs/guides/open-delivery/onboarding-de-lojas.md) da loja; **ou**
* use o botão **"Reativar envio de webhooks"** no painel de developers; **ou**
* salve novamente os dados da loja no formulário do painel.

> [!TIP]
>
> Enquanto as entregas estiverem suspensas, os eventos continuam disponíveis no polling por 24 horas. Use-o para recuperar o que ficou para trás.

## Polling (reconciliação)

### Consultar eventos

```bash cURL theme={null}
curl --request GET 'https://od.deliveryvip.com.br/v1/events:polling' \
  --header 'Authorization: Bearer {token}'
```

```js Node.js theme={null}
const response = await fetch('https://od.deliveryvip.com.br/v1/events:polling', {
  headers: { Authorization: `Bearer ${token}` },
})
const events = response.status === 204 ? [] : await response.json()
```

```python Python theme={null}
response = requests.get(
    "https://od.deliveryvip.com.br/v1/events:polling",
    headers={"Authorization": f"Bearer {token}"},
)
events = [] if response.status_code == 204 else response.json()
```

Retorna os eventos das **últimas 24 horas** ainda não confirmados pelo seu app, no máximo **100 por chamada**, sempre do mais antigo para o mais recente. Quando não há eventos, a resposta é **204 No Content**.

> [!WARNING]
>
> **Fila com mais de 100 eventos.** Se a resposta vier com 100 eventos, há mais eventos pendentes na fila: envie o acknowledgment e consulte novamente **imediatamente**, até receber menos de 100 eventos (ou 204).

### Filtros

Os filtros podem ser combinados, e um valor inválido em qualquer filtro retorna **403 Forbidden**. Os parâmetros de array seguem a serialização padrão da spec OpenAPI (`style: form`, `explode: true`): repita o parâmetro na query, uma vez por valor, sem colchetes.

**eventType** `string[]`

Tipos de evento: `?eventType=CREATED&eventType=CANCELLED`.

**orderType** `string[]`

Tipos de pedido, `DELIVERY`, `TAKEOUT` ou `INDOOR`: `?orderType=DELIVERY&orderType=TAKEOUT`.

> [!NOTE]
>
> O token por loja já restringe o polling àquela loja, então não é preciso filtrar por loja. O header `x-polling-merchants` da spec só tem efeito em integrações antigas, com um token para várias lojas.

### Acknowledgment

Confirme os eventos processados para que saiam das próximas consultas. Envie um array com o `eventId` de cada evento no campo `id`.

```bash cURL theme={null}
curl --request POST 'https://od.deliveryvip.com.br/v1/events/acknowledgment' \
  --header 'Authorization: Bearer {token}' \
  --header 'Content-Type: application/json' \
  --data '[{ "id": "9a24ea9e-5743-4957-9b83-7a4b4f2a4597" }]'
```

```js Node.js theme={null}
await fetch('https://od.deliveryvip.com.br/v1/events/acknowledgment', {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
  body: JSON.stringify(events.map(({ eventId }) => ({ id: eventId }))),
})
```

```python Python theme={null}
requests.post(
    "https://od.deliveryvip.com.br/v1/events/acknowledgment",
    headers={"Authorization": f"Bearer {token}"},
    json=[{"id": event["eventId"]} for event in events],
)
```

```json Resposta (202) theme={null}
{ "total": 1 }
```

A operação é **idempotente**: confirmar o mesmo evento duas vezes não causa erro. Eventos não confirmados continuam sendo retornados (os mais antigos primeiro) até serem confirmados ou saírem da janela de 24 horas.

> [!CAUTION]
>
> Eventos entregues por webhook **também** aparecem no polling até serem confirmados. Se você usa os dois canais, envie o acknowledgment dos eventos já processados via webhook, ou trate o polling como reconciliação deduplicando por `eventId`.

## Erros comuns

| Sintoma | Causa | Como resolver |
| - | - | - |
| Assinatura nunca confere | Validando com o secret do app, ou sobre o JSON re-serializado | Use o `client_secret` da loja (`ods_...`) e o corpo bruto |
| Os mesmos eventos voltam em toda chamada do polling | Acknowledgment não enviado, ou enviado com `eventId` em vez de `id` | Envie `[{ "id": "<eventId>" }]` para cada evento |
| Entregas pararam de chegar | Loja suspensa após 10 falhas consecutivas | Corrija o endpoint e reative (onboarding, botão ou formulário) |
| `5xx` do seu lado e o evento não voltou | `5xx` é falha definitiva, sem retentativa | Recupere pelo polling ou pelo reenvio manual no painel |
| `403` no polling | Valor inválido em `eventType` ou `orderType` | Use só os valores da lista |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.