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

# Vito API e compras

> Permita que apps aprovados cobrem seu Vito com confirmação por PIN e gerencie solicitações de pagamento na sua página Purchases.

<Info>
  Exige uma Assinatura **Silver** ou superior — tanto para se candidatar quanto para cada chamada autenticada. Se a Assinatura do dono da chave expirar, o projeto é congelado até ele reassinar.
</Info>

Uma API REST que permite ao seu aplicativo trabalhar com o saldo de [Vito](/pt/members/vito) de um usuário a partir do Discord: ler, cobrar, creditar ou mover entre usuários. Todos os endpoints retornam JSON e são versionados sob `/v1`.

<Warning>
  **Vito nunca se converte em dinheiro real, nem vem dele.** Ele só circula entre saldos da Vetox.
</Warning>

## Obter acesso

O acesso é concedido **por projeto**. Você precisa dos quatro requisitos:

<Steps>
  <Step title="Uma Assinatura ativa, Silver ou superior">
    Verificada a cada chamada, não apenas na aprovação.
  </Step>

  <Step title="Uma solicitação de desenvolvedor aprovada">
    Enviada pela página Vito API no seu painel. Revisada manualmente pela equipe Vetox.
  </Step>

  <Step title="Os Termos de Desenvolvedor da API aceitos">
    Confirmados ao enviar a solicitação.
  </Step>

  <Step title="Os scopes de que seu projeto precisa">
    Concedidos pela equipe Vetox com base no que você descreveu.
  </Step>
</Steps>

<Tip>
  Seja concreto sobre o que você está construindo e como vai guardar a chave. São as solicitações vagas que acabam recusadas.
</Tip>

### Scopes

| Scope               | Permite                                                      |
| ------------------- | ------------------------------------------------------------ |
| `balance:read`      | Ler o saldo de Vito de um usuário                            |
| `deduct:create`     | Cobrar do saldo de um usuário                                |
| `credit:create`     | Adicionar Vito a um usuário, custeado pelo seu próprio saldo |
| `transfer:create`   | Mover Vito entre dois usuários                               |
| `transactions:read` | Listar e ler as transações do seu projeto                    |

## Autenticação

Envie sua chave secreta como token Bearer:

```bash theme={null}
Authorization: Bearer vito_live_xxxxxxxxxxxxxxxx
```

Duas camadas opcionais reforçam ainda mais um projeto:

* **Lista de IPs permitidos** — restringe as chamadas a IPs de servidor específicos
* **Limites de taxa** — tetos por projeto que aumentam conforme o nível de Assinatura do dono

### Chaves, rotação e armazenamento

<Warning>
  **Sua chave e seu segredo de assinatura são exibidos exatamente uma vez.** Após a aprovação você tem uma **janela de 7 dias** para revelá-los na aba de chaves API. A Vetox guarda apenas um hash e não consegue exibi-los de novo — se perder a janela, será preciso rotacionar.
</Warning>

* Mantenha-a **apenas no servidor** — quem a tiver pode cobrar dos seus usuários
* Rotacione pela aba de chaves API. A chave anterior continua funcionando por um **período de carência de 24 horas**, para você implantar sem indisponibilidade
* O segredo de assinatura dos webhooks é rotacionado separadamente, com sua própria sobreposição de 24 horas
* Se vazar, rotacione imediatamente

## Cobrar de um usuário

<Warning>
  **Sua chave sozinha não move os Vito de um usuário.** Toda cobrança exige que o usuário a aprove com o PIN da carteira dele, em `vetox.io` — nunca dentro do seu app nem dentro do Discord.
</Warning>

<Steps>
  <Step title="Seu app chama POST /v1/deduct">
    Com o usuário, o valor, o `guildId` de origem e os detalhes do item.
  </Step>

  <Step title="O Vito retorna uma confirmUrl">
    Uma confirmação pendente, válida por **10 minutos**. O usuário também recebe uma DM.
  </Step>

  <Step title="O usuário aprova com o PIN dele">
    Em `vetox.io`.
  </Step>

  <Step title="O Vito liquida e notifica">
    O saldo é debitado, a transação registrada e um webhook assinado é enviado, caso você tenha um configurado.
  </Step>

  <Step title="Seu app verifica e conclui">
    Confira a assinatura e então libere o conteúdo ou entregue o item.
  </Step>
</Steps>

<Warning>
  **Conclua sua ação somente em `confirmation.completed`** — nunca na resposta do `/deduct`. Nesse ponto a cobrança ainda não é definitiva.
</Warning>

### Parâmetros da requisição — `/v1/deduct`

| Campo         | Obrigatório | Observações                                                                              |
| ------------- | ----------- | ---------------------------------------------------------------------------------------- |
| `discordId`   | **Sim**     | O ID de Discord do usuário — um snowflake de 17 a 20 dígitos                             |
| `amount`      | **Sim**     | Inteiro positivo                                                                         |
| `guildId`     | **Sim**     | O servidor do Discord de onde parte a cobrança. Toda cobrança precisa vir de um servidor |
| `reason`      | Não         | Até 256 caracteres. Exibido ao usuário e devolvido no webhook                            |
| `merchantRef` | Não         | Sua própria referência, até 128 caracteres. Use para casar o webhook com seus registros  |
| `product`     | Não         | `{ type, name, description?, imageUrl? }` — aparece na página de confirmação e na DM     |
| `imageUrl`    | Não         | Precisa ser `https://`                                                                   |
| `metadata`    | Não         | Até **10** pares chave/valor de texto, repassados sem alteração                          |

## Creditar um usuário

`POST /v1/add` credita Vito a um usuário **a partir do seu próprio saldo** — para recompensas ou estornos. Os mesmos campos do `/deduct`, exceto `guildId` e `product`.

<Note>
  Diferente de uma cobrança, um crédito **não tem etapa de confirmação**: é liquidado na hora. Exige o scope `credit:create` e saldo suficiente, senão a chamada retorna **402 `VITO_INSUFFICIENT_OWNER_FUNDS`**.
</Note>

## Taxas

Toda cobrança é liquidada para você já descontada a taxa da plataforma — o mesmo esquema das transferências de Vito dentro do app, conforme o **seu** nível de Assinatura:

| Sua Assinatura       | Taxa |
| -------------------- | ---- |
| Normal, Silver, Gold | 7%   |
| Platinum             | 6%   |
| Diamond              | 5%   |

<Note>
  Valores de **5 Vito ou menos são isentos de taxa**, e créditos via `/v1/add` são sempre isentos.
</Note>

## Webhooks

Adicione uma ou mais URLs de callback `https` na aba de configurações. O Vito entrega um `POST` assinado sempre que uma confirmação chega a um estado final.

<Warning>
  Os webhooks só são disparados se o seu projeto tiver **ao mesmo tempo** uma URL de callback **e** um segredo de assinatura. Revele o segredo (`whsec_…`) uma única vez na aba de chaves API.
</Warning>

### Verificar a assinatura

Cada entrega traz um cabeçalho `X-Vito-Signature`:

```text theme={null}
X-Vito-Signature: ts=<unix>;h1=<hex>
```

Outros dois cabeçalhos acompanham cada entrega — use `X-Vito-Event-Id` como chave de deduplicação, já que uma retentativa reenvia o mesmo id:

| Cabeçalho           | Contém                                                |
| ------------------- | ----------------------------------------------------- |
| `X-Vito-Event-Id`   | Id estável deste evento — idêntico entre retentativas |
| `X-Vito-Event-Type` | Por exemplo `confirmation.completed`                  |

<Warning>
  **Durante uma rotação do segredo de assinatura, o cabeçalho carrega mais de uma assinatura**, a mais nova primeiro:

  ```text theme={null}
  X-Vito-Signature: ts=<unix>;h1=<nova>;h1=<antiga>
  ```

  Aceite a entrega se **qualquer** `h1` conferir. Um verificador que lê apenas o primeiro vai rejeitar todos os webhooks até implantar o segredo novo — o que anula justamente o propósito da sobreposição de 24 horas.
</Warning>

Recalcule o HMAC sobre `<ts>:<rawBody>` com seu segredo de assinatura e compare em tempo constante.

```js theme={null}
const crypto = require('crypto');

function verify(rawBody, header, secret) {
  const parts = header.split(';');
  const ts = parts.find(p => p.startsWith('ts='))?.slice(3);
  const sigs = parts.filter(p => p.startsWith('h1=')).map(p => p.slice(3));
  if (!ts || sigs.length === 0) return false;

  // Reject anything older than ~5 minutes — replay protection.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected = Buffer.from(
    crypto.createHmac('sha256', secret).update(`${ts}:${rawBody}`).digest('hex'),
  );

  // Any matching signature is valid — a rotation emits several.
  return sigs.some(sig => {
    const actual = Buffer.from(sig);
    // timingSafeEqual throws when the lengths differ, so check first.
    return (
      actual.length === expected.length && crypto.timingSafeEqual(actual, expected)
    );
  });
}
```

<Warning>
  Verifique contra o **corpo bruto**, antes de qualquer parsing de JSON ou middleware que o reescreva.
</Warning>

### Eventos

Cinco tipos de evento, todos com o mesmo formato de payload. `data.status` carrega o desfecho.

| Evento                   | Significado                                                                            |
| ------------------------ | -------------------------------------------------------------------------------------- |
| `confirmation.completed` | Aprovado e debitado. Traz `transactionId`. **Conclua sua ação aqui**                   |
| `confirmation.failed`    | Não foi possível concluir — saldo insuficiente ou erro interno. Traz `failureReason`   |
| `confirmation.expired`   | Não confirmado em 10 minutos. Nenhum Vito movido                                       |
| `confirmation.cancelled` | O usuário cancelou. Nenhum Vito movido                                                 |
| `credit.completed`       | Um `/add` custeado pelo dono foi liquidado. Dispara na hora — sem etapa de confirmação |

<Note>
  Os webhooks são retentados **5 vezes com backoff**. Responda 2xx rapidamente e faça sua entrega de forma assíncrona.
</Note>

## Códigos de erro

Toda resposta vem encapsulada. Um sucesso traz `data` e uma falha traz `error`, nunca os dois:

```json theme={null}
{
  "success": false,
  "error": {
    "type": "Forbidden",
    "message": "API key lacks the required scope for this operation.",
    "code": "VITO_INSUFFICIENT_SCOPE"
  },
  "requestId": "…",
  "timestamp": 1735161600000
}
```

<Warning>
  **Todo código tem o prefixo `VITO_`.** Compare a string completa — um `RATE_LIMITED` ou `FORBIDDEN` isolado nunca aparece na resposta.
</Warning>

| Código                                | Status | Significado                                                                  |
| ------------------------------------- | ------ | ---------------------------------------------------------------------------- |
| `VITO_VALIDATION_ERROR`               | 400    | Um campo está faltando ou malformado                                         |
| `VITO_INVALID_AMOUNT`                 | 400    | `amount` não é um inteiro positivo                                           |
| `VITO_AMOUNT_LIMIT_EXCEEDED`          | 400    | Acima do seu teto por transação                                              |
| `VITO_DAILY_VOLUME_EXCEEDED`          | 400    | Esta chamada estouraria seu teto de volume diário                            |
| `VITO_SELF_TRANSFER`                  | 400    | Remetente e destinatário são o mesmo usuário                                 |
| `VITO_INVALID_API_KEY`                | 401    | Chave ausente, malformada, revogada ou desconhecida                          |
| `VITO_INSUFFICIENT_FUNDS`             | 402    | O usuário não tem saldo para a cobrança                                      |
| `VITO_INSUFFICIENT_OWNER_FUNDS`       | 402    | **Seu** saldo é baixo demais para custear um `/add`                          |
| `VITO_INSUFFICIENT_SCOPE`             | 403    | A chave não tem o scope exigido pelo endpoint                                |
| `VITO_IP_NOT_ALLOWED`                 | 403    | O IP que chamou não está na lista de permitidos                              |
| `VITO_OWNER_MEMBERSHIP_INACTIVE`      | 403    | A Assinatura do dono expirou — congelado até ele reassinar                   |
| `VITO_GUILD_NOT_ALLOWED`              | 403    | `guildId` não está na lista de servidores permitidos do projeto              |
| `VITO_TOS_NOT_ACCEPTED`               | 403    | Termos de desenvolvedor não aceitos, ou há uma versão mais nova pendente     |
| `VITO_PROJECT_FROZEN`                 | 403    | Congelado — normalmente por Assinatura do dono expirada                      |
| `VITO_PROJECT_SUSPENDED`              | 403    | Suspenso pela equipe Vetox                                                   |
| `VITO_PROJECT_BANNED`                 | 403    | Banido pela equipe Vetox                                                     |
| `VITO_USER_BLACKLISTED`               | 403    | O usuário está bloqueado para operações com Vito                             |
| `VITO_ACCOUNT_LOCKED`                 | 403    | A carteira do usuário está travada após tentativas de PIN malsucedidas       |
| `VITO_USER_NOT_FOUND`                 | 404    | Não há conta Vito para esse ID do Discord                                    |
| `VITO_CONFIRMATION_NOT_FOUND`         | 404    | Token de confirmação desconhecido                                            |
| `VITO_IDEMPOTENCY_CONFLICT`           | 409    | Mesmo `Idempotency-Key` com corpo diferente                                  |
| `VITO_IDEMPOTENCY_IN_PROGRESS`        | 409    | Uma requisição idêntica ainda está em andamento — tente de novo em instantes |
| `VITO_CONFIRMATION_ALREADY_PROCESSED` | 409    | Essa confirmação já chegou a um estado final                                 |
| `VITO_KEY_ROTATION_IN_PROGRESS`       | 409    | Já há uma rotação em andamento                                               |
| `VITO_CONFIRMATION_EXPIRED`           | 410    | A janela de 10 minutos passou                                                |
| `VITO_RATE_LIMITED`                   | 429    | Reduza o ritmo e tente novamente                                             |
| `VITO_INTERNAL_ERROR`                 | 500    | Falha inesperada do nosso lado                                               |

## Limites de taxa

| Assinatura do dono  | Por minuto | Por hora |
| ------------------- | ---------- | -------- |
| Nenhuma             | 60         | 1.000    |
| Silver ou Gold      | 180        | 5.000    |
| Platinum ou Diamond | 600        | 15.000   |

Há também um limite por IP igual à metade da sua cota por minuto, com piso de 30.

<Warning>
  Sob carga, **os endpoints de escrita falham de forma fechada** — a cobrança é recusada em vez de arriscar um gasto duplicado. Os de leitura falham de forma aberta. Trate uma escrita recusada como "não aconteceu" e tente de novo.
</Warning>

<Note>
  Envie um cabeçalho `Idempotency-Key` para deduplicar retentativas com segurança.
</Note>

## Endpoints

| Endpoint                        | Scope               |
| ------------------------------- | ------------------- |
| `GET /v1/auth/verify`           | qualquer            |
| `POST /v1/auth/rotate-key`      | qualquer            |
| `GET /v1/balance/:discordId`    | `balance:read`      |
| `POST /v1/deduct`               | `deduct:create`     |
| `POST /v1/add`                  | `credit:create`     |
| `POST /v1/transfer`             | `transfer:create`   |
| `GET /v1/transactions` e `/:id` | `transactions:read` |
| `GET /v1/webhooks/events`       | `transactions:read` |

## Limites

* Confirmações expiram após **10 minutos** — trate as não confirmadas como abandonadas
* `amount` precisa ser um inteiro positivo
* `metadata` é limitado a 10 chaves
* Os tetos por transação e diários são definidos pela equipe Vetox e aparecem somente para leitura na aba de configurações

## Checklist de segurança

<AccordionGroup>
  <Accordion title="Mantenha os segredos no servidor" icon="lock">
    A chave API e o segredo de assinatura nunca pertencem ao código do cliente. Rotacione na hora se algum vazar.
  </Accordion>

  <Accordion title="Verifique todo webhook" icon="signature">
    Cheque a assinatura contra o corpo bruto e recuse entregas com mais de \~5 minutos.
  </Accordion>

  <Accordion title="Liquide apenas em completed" icon="circle-check">
    Nunca entregue com base na resposta do `/deduct` — a cobrança só é definitiva em `confirmation.completed`.
  </Accordion>

  <Accordion title="Privilégio mínimo" icon="key">
    Peça apenas os scopes que você realmente usa e ative a lista de IPs permitidos.
  </Accordion>
</AccordionGroup>

## Solução de problemas

<AccordionGroup>
  <Accordion title="Toda chamada retorna não autorizado">
    A Assinatura do dono expirou. Ela é reverificada a cada chamada.
  </Accordion>

  <Accordion title="Perdi minha chave">
    Não dá para recuperar — só um hash é armazenado. Rotacione para obter uma nova.
  </Accordion>

  <Accordion title="As assinaturas de webhook falham após rotacionar">
    Aceite os dois segredos durante a sobreposição de 24 horas.
  </Accordion>

  <Accordion title="Uma cobrança nunca se conclui">
    O usuário não aprovou. Confirmações expiram após 10 minutos.
  </Accordion>

  <Accordion title="Não chega nenhum webhook">
    Um projeto precisa de URL de callback e segredo de assinatura. Com apenas um dos dois, nada é entregue.
  </Accordion>

  <Accordion title="Chegou menos Vito do que eu cobrei">
    É a taxa de liquidação. Use valores de 5 Vito ou menos para evitá-la, ou embuta no seu preço.
  </Accordion>

  <Accordion title="402 VITO_INSUFFICIENT_OWNER_FUNDS">
    `/v1/add` é custeado pelo seu próprio saldo, não criado do nada. Recarregue.
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Vito" icon="coins" href="/pt/members/vito">
    Saldos, o PIN e as taxas.
  </Card>

  <Card title="Solicitações de pagamento" icon="receipt" href="/pt/account/payment-requests">
    O que o usuário vê quando você cobra dele.
  </Card>
</CardGroup>
