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

# Vínculo e PIN

> Localize ou cadastre o cliente com consentimento no accounts:link, com desafio de PIN opcional, e sirva o saldo da conta para o checkout.

Duas rotas do contrato cuidam da identidade do cliente: o `accounts:link`, chamado uma vez na adesão, e a consulta de saldo, chamada sempre que o checkout precisa mostrar quanto o cliente tem. Depois do vínculo, a DeliveryVip só conhece o cliente pelo `accountId` que você devolveu.

![O cliente adere, a DeliveryVip chama accounts:link, opcionalmente pede o PIN, e depois consulta o saldo para mostrar no checkout](https://mintcdn.com/deliveryvip-a4256422/ELxpcniK2_BP8zxT/images/fidelidade-vinculo-pin.png?fit=max\&auto=format\&n=ELxpcniK2_BP8zxT\&q=85\&s=2da95bd6574fb89fd685507335463b97)

Vínculo da conta, com PIN opcional

## Vincular o cliente (`accounts:link`)

```
POST {api_base_url}/v1/loyalty/accounts:link
```

Chamada quando o cliente **aceita participar** do programa no checkout. O clique **é** o consentimento: `consent.grantedAt` é o momento do clique, com o offset da plataforma (`-03:00`). O perfil do cliente é atestado pela DeliveryVip: nunca chega um convidado nem um cadastro incompleto.

### Corpo da requisição

**programId** `string` *required*

O identificador do programa que você [enviou no config push](/docs/guides/merchant/loyalty/ativando-o-programa.md) (`program.id`).

**customer** `object` *required*

Perfil do cliente, completo.

**propriedades**

**document** `string` *required*

CPF, somente dígitos.

**phone** `string` *required*

Telefone em E.164 (ex.: `+5511999998888`).

**email** `string` *required*

E-mail do cliente.

**name** `string` *required*

Nome do cliente.

**birthDate** `string` *required*

Data de nascimento, `AAAA-MM-DD`.

**consent** `object` *required*

Registro do consentimento (LGPD). Guarde-o no seu lado.

**propriedades**

**purpose** `string` *required*

Sempre `loyalty_program`.

**status** `string` *required*

Sempre `granted`.

**grantedAt** `string` *required*

Momento do clique, RFC 3339 com offset.

**enroll** `boolean` *required*

Espelha a capability `enrollment` do programa. Se `true` e o CPF não for membro, **cadastre-o**. Se `false`, um não-membro deve receber `404`.

**challenge** `object`

Presente **apenas no reenvio** depois de um desafio de PIN: `{"type": "pin", "answer": "<PIN digitado>"}`.

```json Exemplo theme={null}
{
  "programId": "pb-123",
  "customer": {
    "document": "12345678909",
    "phone": "+5511999998888",
    "email": "maria@example.com",
    "name": "Maria",
    "birthDate": "1990-01-01"
  },
  "consent": {
    "purpose": "loyalty_program",
    "status": "granted",
    "grantedAt": "2026-09-04T12:00:00-03:00"
  },
  "enroll": true
}
```

### Respostas do link

> [tab] 200 conta
>
> Conta localizada ou criada.
>
> ```json theme={null}
> {
>   "id": "acc-1",
>   "programId": "pb-123",
>   "balanceType": "cashback"
> }
> ```
>
> **id** `string` *required*
>
> A conta do cliente no seu sistema. É por ela que a DeliveryVip consulta saldo e debita resgates.
>
> **programId** `string`
>
> O programa da conta.
>
> **balanceType** `string`
>
> `cashback` na v1.
>
> > [!WARNING]
> >
> > O `id` da conta é **opaco e estável**: revincular o mesmo cliente deve devolver **o mesmo id, byte a byte**. É por ele que a DeliveryVip consulta saldo e debita resgates, nunca pelo CPF, que só trafega no link.

> [tab] 200 desafio de PIN
>
> Quando o seu programa exige confirmação, responda com o desafio no lugar da conta:
>
> ```json theme={null}
> {
>   "challenge": { "type": "pin" }
> }
> ```
>
> A DeliveryVip pede o PIN ao cliente e **reenvia o mesmo corpo** acrescido de:
>
> ```json theme={null}
> "challenge": { "type": "pin", "answer": "1234" }
> ```
>
> * O PIN é validado **exclusivamente do seu lado**, inclusive o rate limiting. A DeliveryVip nunca armazena nem loga o PIN.
> * Nunca responda `id` e `challenge` juntos. Se fizer, a DeliveryVip trata o `challenge` como autoritativo e descarta o `id`.

> [tab] 403 PIN reprovado
>
> PIN errado. Responda com corpo **genérico**: a resposta não pode revelar se a conta existe.

> [tab] 404 adesão fechada
>
> O CPF não é membro e o programa não aceita adesão pelo checkout (`enroll: false`). O cliente vê que precisa se cadastrar na loja.

> [!NOTE]
>
> Qualquer outra resposta (outro código, timeout, JSON inválido) conta como [falha para o circuit breaker](/docs/guides/merchant/loyalty/contrato-do-operador.md#timeouts-e-circuit-breaker) e o cliente vê o programa como indisponível no momento.

## Saldo da conta

```
GET {api_base_url}/v1/loyalty/accounts/{accountId}
```

Consulta feita para exibir o saldo no checkout, só com o `accountId` opaco, nunca o CPF.

**accountId** `string` *required*

O `id` que você devolveu no `accounts:link`.

```json Resposta (200) theme={null}
{
  "id": "acc-1",
  "programId": "pb-123",
  "balanceType": "cashback",
  "balance": 25.00,
  "currency": "BRL"
}
```

**balance** `number` *required*

Saldo atual em BRL, como número JSON. Zeros à direita não são significativos: `25.0` e `25.00` são o mesmo número.

**currency** `string`

`BRL`. Se omitido, a DeliveryVip assume `BRL`.

**redemptionLimits** `object`

Opcional. Limites de resgate só deste cliente; veja [Limites por conta](#limites-por-conta).

* A DeliveryVip **cacheia a resposta por 60 segundos** por conta: o saldo exibido pode atrasar até 1 minuto em relação ao seu ledger.
* `404` significa que a conta **não existe mais** do seu lado: a DeliveryVip desfaz o vínculo e oferece a adesão de novo ao cliente. Nunca conta como falha para o [circuit breaker](/docs/guides/merchant/loyalty/contrato-do-operador.md#timeouts-e-circuit-breaker).

### Limites por conta

A resposta de saldo aceita, **opcionalmente**, o mesmo objeto `redemptionLimits` dos [limites do programa](/docs/guides/merchant/loyalty/ativando-o-programa.md#limites-de-resgate-redemptionlimits):

```json theme={null}
{
  "id": "acc-1",
  "programId": "pb-123",
  "balanceType": "cashback",
  "balance": 25.00,
  "currency": "BRL",
  "redemptionLimits": {
    "min": { "type": "fixed", "value": 10.00 },
    "max": { "type": "percent", "value": 50 }
  }
}
```

> [!CAUTION]
>
> **Substituição integral, não merge campo a campo.** Quando presente, o objeto da conta **substitui por inteiro** os limites do programa para aquele cliente.
>
> Declarar só `max` na conta significa que aquele cliente **não tem mínimo**, mesmo que o programa declare um:
>
> | Programa | Conta | Vale para o cliente |
> | - | - | - |
> | `min: fixed 5`, `max: percent 50` | *ausente* | `min: fixed 5`, `max: percent 50` |
> | `min: fixed 5`, `max: percent 50` | `max: percent 80` | **só** `max: percent 80`, sem mínimo |
> | `min: fixed 5`, `max: percent 50` | `min: fixed 10` | **só** `min: fixed 10`, sem máximo |
>
> O raciocínio: você é a autoridade sobre as suas próprias regras. Um merge produziria uma faixa que **nenhum dos dois lados declarou**. Este é o ponto que mais gera bug de integração: se o cliente deve manter o mínimo do programa, **repita-o** na conta.

Demais regras:

* **Omita o limite ausente; nunca envie `null`.** `null` afirmaria "este cliente não tem mínimo", que é diferente de "não declarei nada".
* Objeto **ausente ou malformado** (tipo fora de `fixed`/`percent`, valor ausente ou não positivo): valem os limites do programa.
* Viaja no **mesmo cache de 60 segundos** do saldo: mudar a regra de um cliente leva até 1 minuto para chegar ao checkout.

> [!NOTE]
>
> **Quando usar:** só se as suas regras de fato variam por cliente, como tier, segmento ou acordo individual. Uma regra única para todos pertence ao [programa](/docs/guides/merchant/loyalty/ativando-o-programa.md#limites-de-resgate-redemptionlimits), não repetida em toda conta.
>
> E um alerta de modelagem que vale generalizar: **o canal em que um dado chega não determina o escopo do dado.** Um limite que chega dentro de uma consulta por cliente não é, por isso, um limite por cliente. Se o valor é o mesmo para todos, ele é do programa, ainda que a sua API o devolva junto do cadastro do portador.

## Próximo passo

> [card] [Resgate e estorno](/docs/guides/merchant/loyalty/contrato-do-operador/resgate-e-estorno.md)
>
> Com o cliente vinculado e o saldo na tela, o próximo passo é o débito do resgate no fechamento do pedido.


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