> ## 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.

# Webhooks

> Receba eventos de pedido e de indoor em tempo real na sua URL, valide a assinatura e trate reenvios e suspensões.

Com o webhook, a DeliveryVip faz um `POST` com o evento em JSON na sua URL assim que ele acontece. Você recebe eventos de pedido e de indoor (chamar garçom, pedir a conta) sem fila para drenar, sem limite de 100 eventos e sem intervalo de consulta.

> [!NOTE]
>
> Webhook e [polling](/docs/guides/merchant/events.md) são independentes: receber um evento por webhook **não** o remove do polling, e vice-versa. Eventos entregues por webhook não precisam de acknowledgment.
>
> Use 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 continuam disponíveis no polling.

## Como configurar

**1. Abra a sua aplicação**

No painel do desenvolvedor, abra a aplicação que integra com as lojas.

**2. Informe a URL de notificação**

Na seção **Webhook**, preencha a **URL de notificação** com o endpoint do seu sistema que vai receber os eventos e salve.

![Seção Webhook do painel, com o campo URL de notificação preenchido](https://mintcdn.com/deliveryvip-a4256422/ELxpcniK2_BP8zxT/images/webhook-configuracao.png?fit=max\&auto=format\&n=ELxpcniK2_BP8zxT\&q=85\&s=ddfcf91e47dd66a7ad8109765772a0c1)

Seção Webhook: informe a URL de notificação e salve

**3. Responda 2xx**

O seu endpoint precisa aceitar `POST` e responder com um status **2xx** para confirmar o recebimento.

A aplicação tem **uma única URL** de webhook. Os eventos de todas as lojas que instalaram o seu app chegam nela; identifique a loja pelo header `X-App-MerchantId` ou pelo campo `merchantId` do corpo.

> [!NOTE]
>
> Apps com a extensão Open Delivery podem ter uma URL e um secret **por loja**. Veja [Recebendo eventos](/docs/guides/open-delivery/recebendo-eventos.md) no guia Open Delivery.

## A requisição

Cada evento chega numa requisição própria:

```http theme={null}
POST {sua URL de notificação}
Content-Type: application/json
User-Agent: DeliveryVip
X-App-Id: 019efa90-dcd1-7846-983d-6bab871b2f6b
X-App-MerchantId: 8158198a-4bb7-430e-9746-87cd02dfedda
X-App-Signature: 5d41402abc4b2a76b9719d911017c592...
```

**X-App-Signature** `string` *required*

HMAC-SHA256 do corpo bruto da requisição, em hexadecimal minúsculo. Veja [Validando a origem](#validando-a-origem).

**X-App-MerchantId** `string` *required*

A loja do evento: o `merchantId` que você configurou para ela, quando houver, ou o id da loja na DeliveryVip. É o mesmo valor do campo `merchantId` do corpo.

**X-App-Id** `string` *required*

Identificador **fixo** da DeliveryVip como plataforma emissora: `019efa90-dcd1-7846-983d-6bab871b2f6b`. É o mesmo em todas as entregas, para todos os apps, e **não** é o id do seu app. Serve como checagem simples de origem; quem garante a autenticidade é a assinatura.

## Validando a origem

O header `X-App-Signature` é o **HMAC-SHA256 do corpo bruto**, em hexadecimal minúsculo, usando o **Client Secret** da sua aplicação como chave. Recalcule a assinatura sobre o corpo recebido e compare com o header usando comparação de tempo constante.

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

const app = express()

// express.raw mantém o corpo exatamente como chegou
app.post('/webhooks/deliveryvip', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto.createHmac('sha256', process.env.CLIENT_SECRET).update(req.body).digest('hex')
  const received = req.get('X-App-Signature') ?? ''

  const valid =
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))

  if (!valid) return res.sendStatus(401)

  const event = JSON.parse(req.body)
  queue.push(event) // processe depois, fora da requisição
  res.sendStatus(200)
})
```

```python Python theme={null}
import hashlib
import hmac
import os

from flask import Flask, abort, request

app = Flask(__name__)

@app.post("/webhooks/deliveryvip")
def deliveryvip_webhook():
    raw_body = request.get_data()  # bytes exatamente como chegaram
    expected = hmac.new(os.environ["CLIENT_SECRET"].encode(), raw_body, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, request.headers.get("X-App-Signature", "")):
        abort(401)

    queue.put(request.get_json())  # processe depois, fora da requisição
    return "", 200
```

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

> [!WARNING]
>
> Calcule a assinatura sobre o **corpo exato recebido**, antes de qualquer parse. Fazer parse e serializar de novo muda os bytes, e a assinatura deixa de bater.

> [!CAUTION]
>
> Rejeite toda requisição cuja assinatura não confere. A assinatura é a única prova de que o evento veio da DeliveryVip; sem ela, qualquer um consegue forjar eventos de pedido no seu endpoint.

> [!NOTE]
>
> Ao renovar o Client Secret, a troca vale na hora: toda entrega enviada depois, incluindo tentativas automáticas e reenvios manuais, já é assinada com o novo secret.

## O corpo do evento

O corpo é **um único evento**, no mesmo formato que o polling devolve.

**eventId** `uuid` *required*

Identificador único do evento. Use-o para descartar entregas repetidas.

**eventType** `string` *required*

Tipo do evento, em maiúsculas. Veja [os tipos](#tipos-de-evento).

**merchantId** `string` *required*

A loja do evento.

**createdAt** `datetime` *required*

Quando o evento aconteceu, em RFC 3339 com o fuso de Brasília (`-03:00`). Para lojas integradas pelo [Open Delivery](/docs/guides/open-delivery.md), vem em UTC (sufixo `Z`).

**metadata** `object`

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

**orderId** `uuid`

Eventos de pedido. O pedido ao qual o evento pertence.

**orderURL** `string`

Eventos de pedido. URL para [buscar os detalhes do pedido](/docs/guides/merchant/order/fluxo-de-integracao/obter-detalhes-do-pedido.md).

**orderType** `string`

Eventos de pedido. `DELIVERY`, `TAKEOUT` ou `INDOOR`.

**placeName, placeId, tab, tabId, tabCode, tabName** `string`

Eventos de indoor. A mesa ou a comanda que gerou o evento. Cada campo só vem quando está preenchido.

```json Pedido theme={null}
{
  "eventId": "209c060f-41f2-4420-81ab-15f2b5559c58",
  "createdAt": "2026-06-30T12:00:00-03:00",
  "merchantId": "8158198a-4bb7-430e-9746-87cd02dfedda",
  "eventType": "CREATED",
  "orderId": "52122852-6626-11ed-9022-0242ac120002",
  "orderURL": "https://api.deliveryvip.com.br/merchant/v3/orders/52122852-6626-11ed-9022-0242ac120002",
  "orderType": "DELIVERY"
}
```

```json Indoor theme={null}
{
  "eventId": "6f1d2c3b-8a4e-4b5f-9c6d-7e8f9a0b1c2d",
  "createdAt": "2026-06-30T20:15:00-03:00",
  "merchantId": "8158198a-4bb7-430e-9746-87cd02dfedda",
  "eventType": "CALL_WAITER",
  "placeName": "Mesa 10",
  "placeId": "a3c1e5f7-2b4d-4f6a-8c0e-1d3f5a7b9c2e"
}
```

### Tipos de evento

| Grupo | `eventType` | Detalhes |
| - | - | - |
| Pedido | `CREATED`, `CONFIRMED`, `PREPARING`, `READY_FOR_PICKUP`, `PICKED_UP`, `DISPATCHED`, `DELIVERED`, `CONCLUDED`, `CANCELLED` | [Eventos de pedidos](/docs/guides/merchant/order/fluxo-de-integracao/eventos-de-pedidos.md) |
| Indoor | `CALL_WAITER`, `CLOSE_ACCOUNT` | [Eventos de indoor](/docs/guides/merchant/indoor/fluxo-de-integracao/eventos-de-indoor.md) |

`PICKUP_AREA_ASSIGNED`, `CANCELLATION_REQUESTED`, `CANCELLATION_REQUEST_DENIED` e `ORDER_CANCELLATION_REQUEST` estão previstos na especificação, mas [ainda não são emitidos](/docs/guides/merchant/order/fluxo-de-integracao/eventos-de-pedidos.md#previstos-na-especificação). Se chegar um tipo que você não conhece, responda `2xx` e ignore.

## O que o seu endpoint deve fazer

> [!TIP]
>
> * **Validar a assinatura** antes de qualquer processamento.
> * **Responder 2xx rápido.** Grave o evento e processe de forma assíncrona: a DeliveryVip desiste após **3 segundos** para conectar e **5 segundos** para receber a resposta, e conta isso como falha.
> * **Descartar repetidos pelo `eventId`.** A assinatura não tem timestamp e o mesmo evento pode chegar de novo numa nova tentativa ou num reenvio manual.

## Tentativas e reenvio

| Resposta do seu endpoint | O que a DeliveryVip faz |
| - | - |
| `2xx` | Entrega concluída. |
| `408`, `429`, timeout ou erro de conexão | Tenta de novo automaticamente, até **3 tentativas** no total. Se todas falharem, a entrega fica como falha. |
| Qualquer outro status, **inclusive `5xx`** | Marca a entrega como falha na hora, sem nova tentativa automática. |

Entregas com falha podem ser **reenviadas manualmente** no painel. Enquanto ainda há tentativas automáticas em curso, o reenvio manual fica indisponível para evitar entregas duplicadas.

> [!TIP]
>
> Se o seu servidor estiver sobrecarregado e quiser que a DeliveryVip tente de novo, responda `429` ou `408`, não `500` ou `503`.

## Suspensão automática

> [!WARNING]
>
> Após **10 falhas consecutivas**, a DeliveryVip **suspende** o envio de webhooks da sua aplicação e avisa por e-mail, com o motivo da última falha. Cada tentativa que falha conta; uma entrega bem-sucedida zera o contador.

Enquanto o envio está suspenso, os eventos continuam disponíveis no [polling](/docs/guides/merchant/events.md). Para retomar:

**1. Corrija o endpoint**

Confirme que a URL responde `2xx` para um `POST` com corpo JSON.

**2. Reative o envio**

No painel do desenvolvedor, abra a aplicação e entre em **Ver webhooks enviados**. No aviso **Envio de webhooks suspenso**, clique em **Reativar envios**.

**3. Reenvie o que falhou**

Na mesma tela, filtre por **Com falha** e reenvie as entregas que você precisa.

## Acompanhamento

A tela **Webhooks enviados** (painel do desenvolvedor, sua aplicação, **Ver webhooks enviados**) mostra o histórico de entregas: tipo do evento, loja, status HTTP e número de tentativas. Filtre por **Com falha** ou **Sucesso** e reenvie as entregas que falharam.

![Tela Webhooks enviados com histórico de entregas e botões de reenvio](https://mintcdn.com/deliveryvip-a4256422/ELxpcniK2_BP8zxT/images/webhooks-enviados.png?fit=max\&auto=format\&n=ELxpcniK2_BP8zxT\&q=85\&s=1ea47ce8608d4736c48107e490f60f2e)

Tela Webhooks enviados: histórico, status HTTP, tentativas e reenvio

## Erros comuns

> [accordion] A assinatura nunca confere
>
> Quase sempre o corpo foi transformado antes do cálculo: o framework fez parse do JSON e você serializou de novo. Leia o corpo bruto (`express.raw`, `request.get_data()`, `request.raw_post`) e calcule o HMAC sobre esses bytes. Confira também se está usando o **Client Secret** da aplicação, e não o Client ID.

> [accordion] Recebo o mesmo evento duas vezes
>
> É esperado: novas tentativas e reenvios manuais repetem o mesmo `eventId`, e o mesmo evento também aparece no polling. Guarde os `eventId` processados e descarte os repetidos.

> [accordion] O envio foi suspenso
>
> O seu endpoint acumulou 10 falhas seguidas. Corrija o endpoint, clique em **Reativar envios** na tela **Webhooks enviados** e reenvie as entregas com falha. Enquanto isso, recupere os eventos pelo polling.

> [accordion] Meu endpoint retornou 500 e o evento não voltou
>
> Respostas `5xx` não são tentadas de novo automaticamente. Reenvie pelo painel ou recupere o evento pelo polling. Para pedir uma nova tentativa automática, responda `429` ou `408`.


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