# Project — API de Revenda: guia de implementação

> Este arquivo é para o seu assistente de IA (Claude Code, Cursor, ChatGPT, Copilot, Windsurf…).
> Coloque o arquivo no projeto (por exemplo em `docs/project-api.md`) ou anexe na conversa, e diga o que você quer construir.

## Para o assistente

Você vai integrar um sistema à **API de revenda da Project**. Ela consulta catálogo e saldo, gera keys (licenças) que custam créditos de verdade, reseta o HWID de keys e manda avisos por webhook. A documentação completa está no fim deste arquivo, na seção "Documentação completa". Leia antes de escrever código, e use os campos, erros e limites exatamente como estão lá.

## Regras que não podem ser quebradas

1. **Token só no servidor.** Leia de `PROJECT_TOKEN`. Nunca escreva o token no código, em log, em mensagem de erro, no navegador ou num app que roda no computador do cliente. Nunca peça o token na conversa.
2. **Endereço da API só por variável.** Leia de `PROJECT_API_URL`. Não invente nem escreva o endereço no código.
3. **Gerar keys gasta créditos e não existe ambiente de teste.** Testes automatizados nunca chamam a API de verdade: use um cliente HTTP falso. Para testar a conexão, use só rotas de consulta (`GET /me`, `GET /products`).
4. **Idempotency-Key em toda geração.** Crie um UUID por venda, salve junto da venda **antes** de chamar `POST /keys` e reuse o mesmo valor em toda repetição. Nunca crie um código novo para repetir.
5. **Repetição só quando é seguro.** Repita (com a mesma Idempotency-Key) em erro de rede, timeout, `5xx` e `409` de pedido ainda em andamento. Em `429`, espere os segundos do cabeçalho `Retry-After`. Não repita `400`, `401`, `402`, `403`, `404` nem `422`: mostre a `message`.
6. **Preço vem da API.** Não calcule nem envie preço: o corpo de `POST /keys` tem só `product_id`, `quantity` e `days`.
7. **Webhooks:** valide a assinatura HMAC-SHA256 sobre o **corpo cru**, compare em tempo constante, recuse avisos com mais de 5 minutos, ignore `id` repetido e responda `2xx` em menos de 5 segundos (processe depois, se for demorado).
8. **Timeout** de uns 30 segundos em toda chamada.

## Plano sugerido

1. Um cliente HTTP único: monta a URL a partir de `PROJECT_API_URL`, põe `Authorization: Bearer`, `Accept: application/json` e o timeout; lê `{ success, message, data, errors }` e, em erro, lança uma exceção que guarda o status HTTP, a `message` e o `errors`.
2. Uma função por rota que o sistema usa (veja "Referência" na documentação).
3. O fluxo do sistema (veja o guia "Fluxo de venda"), salvando `generation_id`, `user_id` e `key_code` de cada key.
4. Se o sistema receber avisos: o endpoint de webhook com a validação da assinatura.
5. Testes com o cliente HTTP falso: sucesso, `402`, `422`, `429` e a repetição com a mesma Idempotency-Key.

## Antes de dizer que terminou

- [ ] Nenhum token, segredo ou endereço da API no código, nos logs ou no repositório.
- [ ] `.env.example` com `PROJECT_API_URL`, `PROJECT_TOKEN` (e `PROJECT_WEBHOOK_SECRET`, se houver webhook) vazios.
- [ ] Toda geração de key usa uma Idempotency-Key salva antes da chamada.
- [ ] Os erros mostram a `message` da API; `402` avisa que falta saldo.
- [ ] Os testes não chamam a API de verdade.
- [ ] O webhook recusa assinatura errada e aviso antigo.
- [ ] Você disse ao dono do sistema como testar com `GET /me` antes de gerar a primeira key.

---

# Documentação completa

# Introdução (/docs)

A API de revenda deixa o seu sistema fazer sozinho o que você faz no painel: consultar o catálogo e o saldo, gerar keys e resetar HWID. Uma loja, um bot do Discord ou um sistema de pedidos pode entregar a key ao cliente na hora da venda, sem ninguém abrir o painel.

<Cards>
  <Card title="Listar produtos" href="/docs/reference/products/list" description="Os mods liberados e o preço de cada key." />
  <Card title="Gerar keys" href="/docs/reference/keys/generate" description="Cria keys e desconta do saldo." />
  <Card title="Resetar HWID" href="/docs/reference/keys/reset-hwid" description="Libera a key para outro computador." />
  <Card title="Ver saldo" href="/docs/reference/credits/balance" description="Saldo e extrato de créditos." />
</Cards>

## Endereço da API [#endereço-da-api]

Todas as chamadas começam pelo mesmo endereço. Ele aparece no painel, em **API › Tokens**, no exemplo de uso.

Os exemplos desta documentação leem o endereço e o token de duas variáveis de ambiente:

```bash
export PROJECT_API_URL="cole aqui o endereço do painel"
export PROJECT_TOKEN="prj_live_..."
```

<Callout type="warn" title="Não existe ambiente de teste">
  Toda chamada usa o seu saldo de verdade. Gere key só quando for entregar para um cliente.
</Callout>

## Formato das respostas [#formato-das-respostas]

Toda resposta é JSON e tem sempre os mesmos campos:

| Campo | O que é |
| --- | --- |
| `success` | `true` quando deu certo, `false` quando deu erro. |
| `message` | Texto para mostrar ou registrar, quase sempre em português. |
| `data` | Os dados da resposta (só quando deu certo). |
| `errors` | Os detalhes do erro (só quando deu errado). |

```json title="Deu certo"
{
  "success": true,
  "message": "Saldo carregado.",
  "data": { "balance": 480 }
}
```

```json title="Deu errado"
{
  "success": false,
  "message": "Créditos insuficientes.",
  "errors": { "balance": 12 }
}
```

<Callout>
  As respostas podem trazer campos além dos descritos aqui. Ignore os que o seu sistema não conhece.
</Callout>

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

- **Datas** como `created_at` vêm no formato `AAAA-MM-DD HH:MM:SS`.
- **Prazos** como `access_end` vêm em segundos desde 1970 (Unix).
- **Créditos** são sempre números inteiros.

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

<Cards>
  <Card title="Início rápido" href="/docs/quickstart" description="Do token à primeira key em quatro passos." />
  <Card title="Fluxo de venda" href="/docs/guides/sales-flow" description="Como integrar a venda de ponta a ponta." />
</Cards>


---

# Início rápido (/docs/quickstart)

<Steps>
<Step>

### Crie um token [#crie-um-token]

No painel, em **API › Tokens**, crie um token com as permissões que o seu sistema precisa. Copie na hora: ele aparece uma vez só.

Na mesma tela, o exemplo de uso mostra o **endereço da API**. Copie também.

</Step>
<Step>

### Guarde o token no servidor [#guarde-o-token-no-servidor]

Coloque o endereço e o token em variáveis de ambiente do servidor que vai chamar a API:

```bash
export PROJECT_API_URL="cole aqui o endereço do painel"
export PROJECT_TOKEN="prj_live_..."
```

<Callout type="warn">
  Nunca coloque o token em site, aplicativo ou programa que roda no computador do cliente: qualquer um conseguiria ler o token e gastar o seu saldo.
</Callout>

</Step>
<Step>

### Teste com `/me` [#teste-com-me]

Se voltar a sua conta e as permissões do token, está tudo certo:

```bash title="cURL"
curl -sS "$PROJECT_API_URL/me" \
  -H "Authorization: Bearer $PROJECT_TOKEN" \
  -H "Accept: application/json"
```

```js title="JavaScript"
const res = await fetch(`${process.env.PROJECT_API_URL}/me`, {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.PROJECT_TOKEN}`,
    Accept: "application/json",
  },
});
const json = await res.json();
if (!json.success) throw new Error(json.message);
console.log(json.data);
```

```php title="PHP"
<?php
$ch = curl_init(getenv("PROJECT_API_URL") . "/me");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PROJECT_TOKEN"),
        "Accept: application/json",
    ],
]);
$json = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!$json["success"]) {
    throw new Exception($json["message"]);
}
print_r($json["data"]);
```

```python title="Python"
import os, requests

res = requests.get(
    f"{os.environ['PROJECT_API_URL']}/me",
    headers={
        "Authorization": f"Bearer {os.environ['PROJECT_TOKEN']}",
        "Accept": "application/json",
    },
    timeout=30,
)
data = res.json()
if not data["success"]:
    raise Exception(data["message"])
print(data["data"])
```

</Step>
<Step>

### Gere a sua primeira key [#gere-a-sua-primeira-key]

Pegue um `product_id` em [Listar produtos](/docs/reference/products/list) e gere uma key:

```bash title="cURL"
curl -sS -X POST "$PROJECT_API_URL/keys" \
  -H "Authorization: Bearer $PROJECT_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3f1c9e2a-7b4d-4c1e-9a0f-5d2b8c6e1a47" \
  -d '{"product_id":1001,"quantity":1,"days":30}'
```

```js title="JavaScript"
// Um id por pedido. Guarde e reuse se precisar repetir a chamada.
const requestId = crypto.randomUUID();

const res = await fetch(`${process.env.PROJECT_API_URL}/keys`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PROJECT_TOKEN}`,
    Accept: "application/json",
    "Content-Type": "application/json",
    "Idempotency-Key": requestId,
  },
  body: JSON.stringify({ "product_id": 1001, "quantity": 1, "days": 30 }),
});
const json = await res.json();
if (!json.success) throw new Error(json.message);
console.log(json.data);
```

```php title="PHP"
<?php
// Um id por pedido. Guarde e reuse se precisar repetir a chamada.
$requestId = bin2hex(random_bytes(16));

$ch = curl_init(getenv("PROJECT_API_URL") . "/keys");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PROJECT_TOKEN"),
        "Accept: application/json",
        "Content-Type: application/json",
        "Idempotency-Key: " . $requestId,
    ],
    CURLOPT_POSTFIELDS => json_encode(["product_id" => 1001, "quantity" => 1, "days" => 30]),
]);
$json = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!$json["success"]) {
    throw new Exception($json["message"]);
}
print_r($json["data"]);
```

```python title="Python"
import os, uuid, requests

# Um id por pedido. Guarde e reuse se precisar repetir a chamada.
request_id = str(uuid.uuid4())

res = requests.post(
    f"{os.environ['PROJECT_API_URL']}/keys",
    headers={
        "Authorization": f"Bearer {os.environ['PROJECT_TOKEN']}",
        "Accept": "application/json",
        "Idempotency-Key": request_id,
    },
    json={"product_id": 1001, "quantity": 1, "days": 30},
    timeout=30,
)
data = res.json()
if not data["success"]:
    raise Exception(data["message"])
print(data["data"])
```

O cabeçalho `Idempotency-Key` é o que impede cobrar duas vezes se a conexão cair. [Entenda a idempotência](/docs/guides/idempotency).

</Step>
</Steps>

## E depois? [#e-depois]

- Veja o [fluxo de venda completo](/docs/guides/sales-flow), do catálogo à entrega da key.
- Receba avisos no seu sistema com os [webhooks](/docs/guides/webhooks).
- Prefere testar sem escrever código? Use a [coleção do Postman](/docs/postman).


---

# Postman (/docs/postman)

Use a coleção quando quiser:

- explorar as rotas e ver as respostas de verdade;
- conferir o token e as permissões dele;
- reproduzir um erro antes de mexer no seu sistema.

<Steps>
<Step>

### Baixe a coleção [#baixe-a-coleção]

<a href="/project-api.postman_collection.json" download className="not-prose inline-flex items-center rounded-lg bg-fd-primary px-4 py-2 text-sm font-medium text-white dark:text-fd-primary-foreground no-underline hover:opacity-90">
  Baixar a coleção do Postman
</a>

O mesmo arquivo funciona no Insomnia.

</Step>
<Step>

### Importe [#importe]

No Postman, clique em **Import** e escolha o arquivo baixado. A coleção aparece com todas as rotas.

</Step>
<Step>

### Preencha as variáveis [#preencha-as-variáveis]

Abra a coleção, vá em **Variables** e preencha:

| Variável | Valor |
| --- | --- |
| `baseUrl` | o endereço da API, que aparece no painel em **API › Tokens** |
| `token` | o seu token (`prj_live_...`) |

<Callout type="warn">
  Não salve o token num workspace público nem mande a coleção preenchida para ninguém.
</Callout>

</Step>
<Step>

### Faça a primeira chamada [#faça-a-primeira-chamada]

Abra **Quem sou eu** e clique em **Send**. Voltou a sua conta: está pronto.

</Step>
</Steps>

<Callout title="Gerar keys pelo Postman">
  Em **Gerar keys**, a `Idempotency-Key` já vai com um código novo a cada envio. Para repetir o **mesmo** pedido, copie o código usado e fixe no cabeçalho. Lembre que gerar keys gasta o seu saldo de verdade.
</Callout>


---

# Autenticação (/docs/security/authentication)

```http
Authorization: Bearer prj_live_...
```

## Cabeçalhos [#cabeçalhos]

| Cabeçalho | Valor | Obrigatório |
| --- | --- | --- |
| `Authorization` | `Bearer prj_live_...` | Sim |
| `Accept` | `application/json` | Recomendado |
| `Content-Type` | `application/json` | Sim, quando a chamada tem corpo (POST) |
| `Idempotency-Key` | um código único por pedido (UUID) | Sim, em [Gerar keys](/docs/reference/keys/generate) |

## O token [#o-token]

- Começa com `prj_live_` e tem 49 caracteres.
- Só vale no cabeçalho `Authorization`. Não mande o token no endereço (`?token=`) nem no corpo.
- Pode ter validade (30 dias, 90 dias ou 1 ano) ou não ter. Você escolhe ao criar.
- Cada conta pode ter até 10 tokens ativos.
- Token apagado ou vencido responde `401`. Crie outro no painel.

## Respostas de autenticação [#respostas-de-autenticação]

| HTTP | Quando |
| --- | --- |
| `401` | Cabeçalho ausente, token inválido, apagado ou vencido. |
| `403` | O token é válido, mas não tem a [permissão](/docs/security/permissions) daquela rota. |

```json title="403 — falta permissão"
{
  "success": false,
  "message": "Este token não tem permissão para esta ação (precisa de \"keys:generate\").",
  "errors": { "required_scope": "keys:generate" }
}
```

<Callout type="warn" title="Se o token vazar">
  Apague o token no painel: ele para de funcionar na hora. Depois crie outro e troque no seu servidor. Não compartilhe tokens em repositórios, imagens ou conversas.
</Callout>


---

# Permissões (/docs/security/permissions)

Ao criar um token, você escolhe o que ele pode fazer. Use o mínimo que o seu sistema precisa: um bot que só consulta não precisa poder gerar keys.

| Permissão | Código | O que libera | Rotas |
| --- | --- | --- | --- |
| Ver catálogo | `products:read` | Lista de mods liberados para revenda e o preço de cada um em créditos. | `GET /products`, `GET /products/{product_id}` |
| Ver saldo | `credits:read` | Saldo atual e extrato de créditos. | `GET /credits`, `GET /credits/transactions` |
| Ver keys | `keys:read` | Suas keys: situação, HWID e tempo restante. | `GET /keys` |
| Gerar keys | `keys:generate` | Cria keys novas. Desconta créditos do saldo. | `POST /keys` |
| Resetar HWID | `keys:reset-hwid` | Libera uma key para ser usada em outro computador. Não gasta créditos. | `POST /keys/{user_id}/reset-hwid` |

A rota [Quem sou eu](/docs/reference/me) funciona com qualquer token e mostra as permissões dele em `auth.scopes`.

## Quando falta permissão [#quando-falta-permissão]

A rota responde `403`, e `errors.required_scope` diz qual permissão faltou. Crie um token com ela no painel — não dá para mudar as permissões de um token que já existe.

<Callout title="O que nenhum token faz">
  Criar outros tokens, configurar webhooks e comprar créditos são coisas só do painel. Um token, mesmo com todas as permissões, não chega nelas. Se um token vazar, o estrago fica limitado ao que ele pode.
</Callout>

## Combinações comuns [#combinações-comuns]

| Sistema | Permissões |
| --- | --- |
| Loja que vende e entrega a key | Ver catálogo, Gerar keys |
| Bot de suporte | Ver keys, Resetar HWID |
| Painel financeiro | Ver saldo |


---

# Boas práticas (/docs/security/best-practices)

## Tokens [#tokens]

| Faça | Por quê |
| --- | --- |
| Use o token só no servidor. | No computador do cliente, qualquer um leria o token e gastaria o seu saldo. |
| Um token por sistema, com um nome que diga onde ele está. | Se um vazar, você apaga só ele. |
| Marque só as permissões necessárias. | Um token de consulta que vaza não gasta crédito. |
| Prefira tokens com validade. | Um token esquecido deixa de funcionar sozinho. |

## Gerar keys [#gerar-keys]

| Faça | Por quê |
| --- | --- |
| Crie uma `Idempotency-Key` por venda e guarde junto dela. | Repetir depois de um erro de rede devolve as mesmas keys, sem cobrar de novo. |
| Confira `available` antes de gerar. | Produto indisponível não gera key. |
| Mande `days` com uma das durações de `plans`. | Duração que o produto não tem é recusada. |
| Guarde `generation_id`, `user_id` e `key_code`. | São o que liga o extrato e o suporte às keys que você entregou. |

## Chamadas [#chamadas]

| Faça | Por quê |
| --- | --- |
| Use um tempo limite (timeout) de uns 30 segundos. | Sem ele, uma conexão travada prende o seu sistema. |
| Em erro de rede ou `500`, repita com a **mesma** `Idempotency-Key`. | A API reconhece a repetição e não gera de novo. |
| Em `429`, espere os segundos de `Retry-After`. | Insistir antes disso só gera mais `429`. |
| Guarde a lista de produtos por alguns minutos. | O catálogo muda pouco, e cada chamada conta no [limite](/docs/rate-limits). |
| Consulte keys específicas com `user_ids[]`. | É mais rápido que trazer a lista inteira. |

<Callout>
  Toda chamada fica registrada do nosso lado, com data, IP e resultado.
</Callout>


---

# Visão geral (/docs/reference)

Cada rota tem uma página com os parâmetros, o exemplo de chamada em cURL, JavaScript, PHP e Python, a resposta e os erros. A linguagem que você escolher num exemplo vale para todas as páginas.

| Método | Caminho | O que faz | Permissão |
| --- | --- | --- | --- |
| `GET` | `/me` | Quem sou eu | qualquer token |
| `GET` | `/products` | Listar produtos | **Ver catálogo** (`products:read`) |
| `GET` | `/products/{product_id}` | Ver um produto | **Ver catálogo** (`products:read`) |
| `GET` | `/credits` | Ver saldo | **Ver saldo** (`credits:read`) |
| `GET` | `/credits/transactions` | Extrato de créditos | **Ver saldo** (`credits:read`) |
| `GET` | `/keys` | Listar suas keys | **Ver keys** (`keys:read`) |
| `POST` | `/keys` | Gerar keys | **Gerar keys** (`keys:generate`) — gasta créditos |
| `POST` | `/keys/{user_id}/reset-hwid` | Resetar HWID | **Resetar HWID** (`keys:reset-hwid`) |

Todos os caminhos são relativos ao [endereço da API](/docs#endereço-da-api), e toda chamada leva o [token](/docs/security/authentication) no cabeçalho `Authorization`.


---

# Quem sou eu (/docs/reference/me)

`GET /me` · Permissão do token: qualquer token


Confirma que o token funciona e diz o que ele pode fazer. Use para conferir o token quando o seu sistema iniciar.

## Resposta [#resposta]

| Campo | O que é |
| --- | --- |
| `user.user_id` | O id da sua conta. |
| `user.user_login` | O seu usuário. |
| `user.user_email` | O e-mail da conta. |
| `balance` | O seu saldo em créditos. Só vem se o token tiver a permissão **Ver saldo**. |
| `auth.type` | Sempre `api_token` numa chamada com token. |
| `auth.scopes` | As [permissões](/docs/security/permissions) deste token. |

## Erros [#erros]

| HTTP | Quando |
| --- | --- |
| `401` | Token ausente, inválido, apagado ou vencido. |
| `429` | Passou do [limite de chamadas](/docs/rate-limits). |


```bash title="cURL"
curl -sS "$PROJECT_API_URL/me" \
  -H "Authorization: Bearer $PROJECT_TOKEN" \
  -H "Accept: application/json"
```

```js title="JavaScript"
const res = await fetch(`${process.env.PROJECT_API_URL}/me`, {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.PROJECT_TOKEN}`,
    Accept: "application/json",
  },
});
const json = await res.json();
if (!json.success) throw new Error(json.message);
console.log(json.data);
```

```php title="PHP"
<?php
$ch = curl_init(getenv("PROJECT_API_URL") . "/me");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PROJECT_TOKEN"),
        "Accept: application/json",
    ],
]);
$json = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!$json["success"]) {
    throw new Exception($json["message"]);
}
print_r($json["data"]);
```

```python title="Python"
import os, requests

res = requests.get(
    f"{os.environ['PROJECT_API_URL']}/me",
    headers={
        "Authorization": f"Bearer {os.environ['PROJECT_TOKEN']}",
        "Accept": "application/json",
    },
    timeout=30,
)
data = res.json()
if not data["success"]:
    raise Exception(data["message"])
print(data["data"])
```

```json title="Resposta · 200"
{
  "success": true,
  "message": "Dados do revendedor.",
  "data": {
    "user": {
      "user_id": 12,
      "user_login": "minhaloja",
      "user_email": "contato@minhaloja.com"
    },
    "balance": 480,
    "auth": {
      "type": "api_token",
      "scopes": ["products:read", "credits:read", "keys:read", "keys:generate", "keys:reset-hwid"]
    }
  }
}
```



---

# Listar produtos (/docs/reference/products/list)

`GET /products` · Permissão do token: **Ver catálogo** (`products:read`)


Use o `product_id` daqui para [gerar keys](/docs/reference/keys/generate). Os preços já são os da **sua conta** — o mesmo valor que a geração cobra.

## Parâmetros de busca (query) [#parâmetros-de-busca-query]

| Parâmetro | Tipo | Obrigatório | Descrição |
| --- | --- | --- | --- |
| `search` | texto | Não | Filtra pelo nome do produto. Até 100 caracteres. |

## Resposta [#resposta]

| Campo | O que é |
| --- | --- |
| `product_id` | O id do produto. É o que você manda para gerar keys. |
| `title` | O nome do produto. |
| `platform` | A plataforma do mod (ex.: Windows). |
| `credit_cost` | O preço, em créditos, de uma key de `days` dias. |
| `days` | A duração padrão: 30 dias, ou a maior que o mod tiver, se ele não tiver 30. |
| `plans` | As durações à venda, cada uma com o seu preço. Mande o `days` escolhido ao gerar. |
| `status.label` | A situação do mod, em texto (ex.: funcional, manutenção). |
| `available` | `true` quando dá para gerar key agora. |
| `stock` | Estoque: `status` (ex.: `instock`, `outofstock`), `quantity` e se o estoque é controlado (`managed`). Só `instock` deixa gerar. |
| `image` | A imagem do produto. |
| `total` | Quantos produtos vieram. |

<Callout title="Antes de gerar">
  Só gere keys de produtos com `available: true`. Um mod desativado ou sem estoque vem com `available: false`, e a geração é recusada. A situação de todos os mods também aparece na página de [status](/status).
</Callout>

## Erros [#erros]

| HTTP | Quando |
| --- | --- |
| `401` | Token ausente, inválido, apagado ou vencido. |
| `403` | O token não tem a permissão **Ver catálogo**. |
| `422` | `search` com mais de 100 caracteres. |
| `429` | Passou do [limite de chamadas](/docs/rate-limits). |


```bash title="cURL"
curl -sS "$PROJECT_API_URL/products" \
  -H "Authorization: Bearer $PROJECT_TOKEN" \
  -H "Accept: application/json"
```

```js title="JavaScript"
const res = await fetch(`${process.env.PROJECT_API_URL}/products`, {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.PROJECT_TOKEN}`,
    Accept: "application/json",
  },
});
const json = await res.json();
if (!json.success) throw new Error(json.message);
console.log(json.data);
```

```php title="PHP"
<?php
$ch = curl_init(getenv("PROJECT_API_URL") . "/products");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PROJECT_TOKEN"),
        "Accept: application/json",
    ],
]);
$json = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!$json["success"]) {
    throw new Exception($json["message"]);
}
print_r($json["data"]);
```

```python title="Python"
import os, requests

res = requests.get(
    f"{os.environ['PROJECT_API_URL']}/products",
    headers={
        "Authorization": f"Bearer {os.environ['PROJECT_TOKEN']}",
        "Accept": "application/json",
    },
    timeout=30,
)
data = res.json()
if not data["success"]:
    raise Exception(data["message"])
print(data["data"])
```

```json title="Resposta · 200"
{
  "success": true,
  "message": "Produtos de revenda.",
  "data": {
    "products": [
      {
        "product_id": 1001,
        "slug": "mod-exemplo",
        "title": "MOD EXEMPLO",
        "duration": "30 Dias",
        "platform": "Windows",
        "credit_cost": 18,
        "days": 30,
        "plans": [
          { "days": 7, "credit_cost": 8 },
          { "days": 15, "credit_cost": 12 },
          { "days": 30, "credit_cost": 18 }
        ],
        "status": { "id": 1, "label": "funcional" },
        "available": true,
        "stock": { "status": "instock", "quantity": 1000, "managed": false },
        "image": "https://exemplo.com/imagem.png"
      }
    ],
    "total": 1
  }
}
```



---

# Ver um produto (/docs/reference/products/get)

`GET /products/{product_id}` · Permissão do token: **Ver catálogo** (`products:read`)


## Parâmetros no endereço [#parâmetros-no-endereço]

| Parâmetro | Tipo | Obrigatório | Descrição |
| --- | --- | --- | --- |
| `product_id` | número | Sim | O `product_id` da [lista de produtos](/docs/reference/products/list). |

## Resposta [#resposta]

Os campos de `product` são os mesmos de [Listar produtos](/docs/reference/products/list#resposta).

## Erros [#erros]

| HTTP | Quando |
| --- | --- |
| `401` | Token ausente, inválido, apagado ou vencido. |
| `403` | O token não tem a permissão **Ver catálogo**. |
| `404` | O produto não existe ou não está liberado para revenda. |
| `429` | Passou do [limite de chamadas](/docs/rate-limits). |


```bash title="cURL"
curl -sS "$PROJECT_API_URL/products/1001" \
  -H "Authorization: Bearer $PROJECT_TOKEN" \
  -H "Accept: application/json"
```

```js title="JavaScript"
const res = await fetch(`${process.env.PROJECT_API_URL}/products/1001`, {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.PROJECT_TOKEN}`,
    Accept: "application/json",
  },
});
const json = await res.json();
if (!json.success) throw new Error(json.message);
console.log(json.data);
```

```php title="PHP"
<?php
$ch = curl_init(getenv("PROJECT_API_URL") . "/products/1001");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PROJECT_TOKEN"),
        "Accept: application/json",
    ],
]);
$json = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!$json["success"]) {
    throw new Exception($json["message"]);
}
print_r($json["data"]);
```

```python title="Python"
import os, requests

res = requests.get(
    f"{os.environ['PROJECT_API_URL']}/products/1001",
    headers={
        "Authorization": f"Bearer {os.environ['PROJECT_TOKEN']}",
        "Accept": "application/json",
    },
    timeout=30,
)
data = res.json()
if not data["success"]:
    raise Exception(data["message"])
print(data["data"])
```

```json title="Resposta · 200"
{
  "success": true,
  "message": "Produto de revenda.",
  "data": {
    "product": {
      "product_id": 1001,
      "title": "MOD EXEMPLO",
      "credit_cost": 18,
      "days": 30,
      "plans": [
        { "days": 7, "credit_cost": 8 },
        { "days": 30, "credit_cost": 18 }
      ],
      "available": true
    }
  }
}
```



---

# Ver saldo (/docs/reference/credits/balance)

`GET /credits` · Permissão do token: **Ver saldo** (`credits:read`)


## Resposta [#resposta]

| Campo | O que é |
| --- | --- |
| `balance` | O seu saldo, em créditos. |

Para ver de onde veio e para onde foi cada crédito, use o [extrato](/docs/reference/credits/transactions).

## Erros [#erros]

| HTTP | Quando |
| --- | --- |
| `401` | Token ausente, inválido, apagado ou vencido. |
| `403` | O token não tem a permissão **Ver saldo**. |
| `429` | Passou do [limite de chamadas](/docs/rate-limits). |


```bash title="cURL"
curl -sS "$PROJECT_API_URL/credits" \
  -H "Authorization: Bearer $PROJECT_TOKEN" \
  -H "Accept: application/json"
```

```js title="JavaScript"
const res = await fetch(`${process.env.PROJECT_API_URL}/credits`, {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.PROJECT_TOKEN}`,
    Accept: "application/json",
  },
});
const json = await res.json();
if (!json.success) throw new Error(json.message);
console.log(json.data);
```

```php title="PHP"
<?php
$ch = curl_init(getenv("PROJECT_API_URL") . "/credits");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PROJECT_TOKEN"),
        "Accept: application/json",
    ],
]);
$json = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!$json["success"]) {
    throw new Exception($json["message"]);
}
print_r($json["data"]);
```

```python title="Python"
import os, requests

res = requests.get(
    f"{os.environ['PROJECT_API_URL']}/credits",
    headers={
        "Authorization": f"Bearer {os.environ['PROJECT_TOKEN']}",
        "Accept": "application/json",
    },
    timeout=30,
)
data = res.json()
if not data["success"]:
    raise Exception(data["message"])
print(data["data"])
```

```json title="Resposta · 200"
{
  "success": true,
  "message": "Saldo carregado.",
  "data": { "balance": 480 }
}
```



---

# Extrato de créditos (/docs/reference/credits/transactions)

`GET /credits/transactions` · Permissão do token: **Ver saldo** (`credits:read`)


## Parâmetros de busca (query) [#parâmetros-de-busca-query]

| Parâmetro | Tipo | Obrigatório | Descrição |
| --- | --- | --- | --- |
| `limit` | número | Não | Quantos movimentos trazer, de 1 a 200. Padrão: 50. |
| `offset` | número | Não | Quantos pular, para paginar. Padrão: 0. |

Para paginar, some `limit` ao `offset` a cada página até vir uma lista menor que o `limit`.

## Resposta [#resposta]

| Campo | O que é |
| --- | --- |
| `amount` | O valor do movimento. Negativo é saída; positivo é entrada. |
| `balance_after` | O saldo logo depois deste movimento. |
| `type` | O tipo do movimento (tabela abaixo). |
| `reference_type` / `reference_id` | A que o movimento se refere (tabela abaixo). |
| `notes` | Observação, quando houver. |
| `created_at` | Quando aconteceu. |
| `balance` | O seu saldo atual. |

| `type` | O que é | `reference_id` |
| --- | --- | --- |
| `key_generation` | Você gerou keys. | O `generation_id` devolvido na geração. |
| `refund` | Estorno de uma geração que falhou. | O `generation_id` da geração. |
| `purchase` | Compra de créditos. | O número do pedido de créditos. |
| `manual_adjust` | Ajuste feito pela equipe. | — |

## Erros [#erros]

| HTTP | Quando |
| --- | --- |
| `401` | Token ausente, inválido, apagado ou vencido. |
| `403` | O token não tem a permissão **Ver saldo**. |
| `422` | `limit` fora de 1 a 200, ou `offset` negativo. |
| `429` | Passou do [limite de chamadas](/docs/rate-limits). |


```bash title="cURL"
curl -sS "$PROJECT_API_URL/credits/transactions?limit=20" \
  -H "Authorization: Bearer $PROJECT_TOKEN" \
  -H "Accept: application/json"
```

```js title="JavaScript"
const res = await fetch(`${process.env.PROJECT_API_URL}/credits/transactions?limit=20`, {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.PROJECT_TOKEN}`,
    Accept: "application/json",
  },
});
const json = await res.json();
if (!json.success) throw new Error(json.message);
console.log(json.data);
```

```php title="PHP"
<?php
$ch = curl_init(getenv("PROJECT_API_URL") . "/credits/transactions?limit=20");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PROJECT_TOKEN"),
        "Accept: application/json",
    ],
]);
$json = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!$json["success"]) {
    throw new Exception($json["message"]);
}
print_r($json["data"]);
```

```python title="Python"
import os, requests

res = requests.get(
    f"{os.environ['PROJECT_API_URL']}/credits/transactions?limit=20",
    headers={
        "Authorization": f"Bearer {os.environ['PROJECT_TOKEN']}",
        "Accept": "application/json",
    },
    timeout=30,
)
data = res.json()
if not data["success"]:
    raise Exception(data["message"])
print(data["data"])
```

```json title="Resposta · 200"
{
  "success": true,
  "message": "Extrato carregado.",
  "data": {
    "transactions": [
      {
        "id": 931,
        "amount": -18,
        "balance_after": 480,
        "type": "key_generation",
        "reference_type": "generation",
        "reference_id": 57,
        "notes": null,
        "created_at": "2026-09-27 21:14:05"
      },
      {
        "id": 930,
        "amount": 200,
        "balance_after": 498,
        "type": "purchase",
        "reference_type": "order",
        "reference_id": 88120,
        "notes": null,
        "created_at": "2026-09-27 20:02:41"
      }
    ],
    "balance": 480
  }
}
```



---

# Listar suas keys (/docs/reference/keys/list)

`GET /keys` · Permissão do token: **Ver keys** (`keys:read`)


## Parâmetros de busca (query) [#parâmetros-de-busca-query]

| Parâmetro | Tipo | Obrigatório | Descrição |
| --- | --- | --- | --- |
| `user_ids[]` | lista de números | Não | Traz só estas keys (o `user_id` de cada uma). Até 200. |

Sem `user_ids[]`, vêm todas as suas keys. Para acompanhar keys específicas, repita o parâmetro: `/keys?user_ids[]=204496&user_ids[]=204497`.

## Resposta [#resposta]

| Campo | O que é |
| --- | --- |
| `user_id` | O id da key na API. É ele que o [reset de HWID](/docs/reference/keys/reset-hwid) usa. |
| `key_code` | A key que o cliente digita. |
| `product_id` / `product_name` | O produto da key. |
| `activated` | `true` depois que o cliente ativou a key. |
| `status` | A situação da key (tabela abaixo). |
| `hwid_status` | A situação do HWID (tabela abaixo). |
| `access_end` | Quando o acesso termina, em segundos desde 1970 (Unix). `0` quando não há acesso. |
| `seconds_remaining` | Quanto tempo falta, em segundos. `0` se a key ainda não foi ativada. |
| `created_at` | Quando a key foi gerada. |
| `credits_spent` | Quanto a key custou quando foi gerada. `null` se não houver registro. |
| `days` | A duração comprada, em dias. |

| `status` | Significa |
| --- | --- |
| `DISPONIVEL` | Gerada, ainda não ativada pelo cliente. |
| `ATIVA` | Em uso. |
| `SEM_ACESSO` | A key não tem mais acesso. |

| `hwid_status` | Significa |
| --- | --- |
| `HWID_ATIVO` | Presa ao computador do cliente. |
| `HWID_RESETADO` | Livre para ser usada em outro computador. |
| `N/A` | A key ainda não foi ativada. |

## Erros [#erros]

| HTTP | Quando |
| --- | --- |
| `401` | Token ausente, inválido, apagado ou vencido. |
| `403` | O token não tem a permissão **Ver keys**. |
| `422` | Mais de 200 ids em `user_ids[]`, ou id que não é número. |
| `429` | Passou do [limite de chamadas](/docs/rate-limits). |


```bash title="cURL"
curl -sS "$PROJECT_API_URL/keys" \
  -H "Authorization: Bearer $PROJECT_TOKEN" \
  -H "Accept: application/json"
```

```js title="JavaScript"
const res = await fetch(`${process.env.PROJECT_API_URL}/keys`, {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.PROJECT_TOKEN}`,
    Accept: "application/json",
  },
});
const json = await res.json();
if (!json.success) throw new Error(json.message);
console.log(json.data);
```

```php title="PHP"
<?php
$ch = curl_init(getenv("PROJECT_API_URL") . "/keys");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PROJECT_TOKEN"),
        "Accept: application/json",
    ],
]);
$json = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!$json["success"]) {
    throw new Exception($json["message"]);
}
print_r($json["data"]);
```

```python title="Python"
import os, requests

res = requests.get(
    f"{os.environ['PROJECT_API_URL']}/keys",
    headers={
        "Authorization": f"Bearer {os.environ['PROJECT_TOKEN']}",
        "Accept": "application/json",
    },
    timeout=30,
)
data = res.json()
if not data["success"]:
    raise Exception(data["message"])
print(data["data"])
```

```json title="Resposta · 200"
{
  "success": true,
  "message": "Keys carregadas.",
  "data": {
    "keys": [
      {
        "user_id": 204496,
        "key_code": "0a1b2c3d-4e5f6a7b-8c9d0e1f",
        "product_id": 1001,
        "product_name": "MOD EXEMPLO",
        "activated": true,
        "status": "ATIVA",
        "hwid_status": "HWID_ATIVO",
        "access_end": 1793105851,
        "seconds_remaining": 2591200,
        "created_at": "2026-09-27 21:14:05",
        "credits_spent": 18,
        "days": 30
      }
    ]
  }
}
```



---

# Gerar keys (/docs/reference/keys/generate)

`POST /keys` · Permissão do token: **Gerar keys** (`keys:generate`) — gasta créditos


Cobra o preço da duração escolhida vezes a quantidade. Sem saldo suficiente, nada é gerado nem cobrado.

<Callout type="warn" title="Idempotency-Key obrigatória">
  Toda chamada leva o cabeçalho `Idempotency-Key`, um código único por pedido. Repetindo com o mesmo código, você recebe as mesmas keys sem pagar de novo. [Por quê?](/docs/guides/idempotency)
</Callout>

## Cabeçalhos [#cabeçalhos]

| Cabeçalho | Obrigatório | Descrição |
| --- | --- | --- |
| `Idempotency-Key` | Sim | Um código único por pedido (um UUID serve). Até 100 caracteres: letras, números e `_ - : .` |
| `Content-Type` | Sim | `application/json` |

## Corpo (JSON) [#corpo-json]

| Campo | Tipo | Obrigatório | Descrição |
| --- | --- | --- | --- |
| `product_id` | número | Sim | O produto, da [lista de produtos](/docs/reference/products/list). |
| `quantity` | número | Sim | Quantas keys gerar, de 1 a 50. |
| `days` | número | Não | A duração, entre os `plans` do produto. Sem ele, 30 dias. Se o produto não tiver plano de 30 dias, `days` é obrigatório. |

O preço é sempre calculado do nosso lado, pela duração e pela quantidade. Qualquer outro campo no corpo é ignorado.

## Resposta [#resposta]

| Campo | O que é |
| --- | --- |
| `generation_id` | O id deste lote. Aparece no [extrato](/docs/reference/credits/transactions) como `reference_id`. |
| `keys[].key_code` | A key para entregar ao cliente. |
| `keys[].user_id` | O id da key na API (para [consultar](/docs/reference/keys/list) e [resetar HWID](/docs/reference/keys/reset-hwid)). |
| `days` | A duração das keys geradas. |
| `credits_spent` | Quanto foi cobrado no total. |
| `balance` | O seu saldo depois da cobrança. |

### Repetição do mesmo pedido [#repetição-do-mesmo-pedido]

Repetindo com a mesma `Idempotency-Key` e o mesmo corpo, a resposta traz **as mesmas keys** e não cobra de novo. Ela vem com `"idempotent_replay": true` em `data` e o cabeçalho `Idempotent-Replayed: true`.

## Erros [#erros]

| HTTP | Quando | O que fazer |
| --- | --- | --- |
| `402` | Saldo insuficiente. Nada foi gerado nem cobrado. | `errors.balance` traz o seu saldo. Compre créditos e tente de novo. |
| `403` | O token não tem a permissão **Gerar keys**. | Crie um token com ela. |
| `409` | A `Idempotency-Key` já foi usada num pedido diferente, ou o primeiro pedido ainda está rodando. | Pedido novo: use um código novo. Mesmo pedido: espere alguns segundos e repita. |
| `422` | Dados inválidos, `Idempotency-Key` ausente ou fora do formato, produto indisponível, ou duração que o produto não tem. | A `message` diz o que corrigir. Duração errada traz as opções em `errors.days`. |
| `429` | Passou do [limite](/docs/rate-limits). | Espere os segundos de `Retry-After` e tente de novo. |
| `500` | Falha do nosso lado ao gerar. | Os créditos voltam para o saldo (`errors.credits_refunded` diz quanto). Repita com a mesma `Idempotency-Key`. Se a mensagem falar em estorno parcial, ou se continuar falhando, fale com o suporte. |


```bash title="cURL"
curl -sS -X POST "$PROJECT_API_URL/keys" \
  -H "Authorization: Bearer $PROJECT_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3f1c9e2a-7b4d-4c1e-9a0f-5d2b8c6e1a47" \
  -d '{"product_id":1001,"quantity":1,"days":30}'
```

```js title="JavaScript"
// Um id por pedido. Guarde e reuse se precisar repetir a chamada.
const requestId = crypto.randomUUID();

const res = await fetch(`${process.env.PROJECT_API_URL}/keys`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PROJECT_TOKEN}`,
    Accept: "application/json",
    "Content-Type": "application/json",
    "Idempotency-Key": requestId,
  },
  body: JSON.stringify({ "product_id": 1001, "quantity": 1, "days": 30 }),
});
const json = await res.json();
if (!json.success) throw new Error(json.message);
console.log(json.data);
```

```php title="PHP"
<?php
// Um id por pedido. Guarde e reuse se precisar repetir a chamada.
$requestId = bin2hex(random_bytes(16));

$ch = curl_init(getenv("PROJECT_API_URL") . "/keys");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PROJECT_TOKEN"),
        "Accept: application/json",
        "Content-Type: application/json",
        "Idempotency-Key: " . $requestId,
    ],
    CURLOPT_POSTFIELDS => json_encode(["product_id" => 1001, "quantity" => 1, "days" => 30]),
]);
$json = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!$json["success"]) {
    throw new Exception($json["message"]);
}
print_r($json["data"]);
```

```python title="Python"
import os, uuid, requests

# Um id por pedido. Guarde e reuse se precisar repetir a chamada.
request_id = str(uuid.uuid4())

res = requests.post(
    f"{os.environ['PROJECT_API_URL']}/keys",
    headers={
        "Authorization": f"Bearer {os.environ['PROJECT_TOKEN']}",
        "Accept": "application/json",
        "Idempotency-Key": request_id,
    },
    json={"product_id": 1001, "quantity": 1, "days": 30},
    timeout=30,
)
data = res.json()
if not data["success"]:
    raise Exception(data["message"])
print(data["data"])
```

```json title="Resposta · 200"
{
  "success": true,
  "message": "Keys geradas.",
  "data": {
    "generation_id": 57,
    "keys": [
      { "key_code": "0a1b2c3d-4e5f6a7b-8c9d0e1f", "user_id": 204496 }
    ],
    "days": 30,
    "credits_spent": 18,
    "balance": 480
  }
}
```

```json title="Resposta · 402 — sem saldo"
{
  "success": false,
  "message": "Créditos insuficientes.",
  "errors": { "balance": 12 }
}
```

```json title="Resposta · 422 — duração que o produto não tem"
{
  "success": false,
  "message": "MOD EXEMPLO não tem key de 60 dias. Durações: 7, 15 e 30 dias.",
  "errors": { "days": [7, 15, 30] }
}
```



---

# Resetar HWID (/docs/reference/keys/reset-hwid)

`POST /keys/{user_id}/reset-hwid` · Permissão do token: **Resetar HWID** (`keys:reset-hwid`)


Quando o cliente ativa a key, ela fica presa ao computador dele (HWID). Se ele trocar de PC ou formatar, resete o HWID: a key volta a funcionar na próxima máquina em que for usada. **Não gasta créditos.**

## Parâmetros no endereço [#parâmetros-no-endereço]

| Parâmetro | Tipo | Obrigatório | Descrição |
| --- | --- | --- | --- |
| `user_id` | número | Sim | O `user_id` da key — vem na [geração](/docs/reference/keys/generate) e na [lista de keys](/docs/reference/keys/list). |

A chamada não tem corpo.

## Resposta [#resposta]

Depois do reset, a key aparece com `hwid_status: "HWID_RESETADO"` na [lista de keys](/docs/reference/keys/list).

## Erros [#erros]

| HTTP | Quando |
| --- | --- |
| `401` | Token ausente, inválido, apagado ou vencido. |
| `403` | O token não tem a permissão **Resetar HWID**, ou a key não é sua. |
| `404` | A key não tem acesso para resetar. |
| `429` | Passou do [limite](/docs/rate-limits). Resetar HWID entra no limite de ações por conta. |


```bash title="cURL"
curl -sS -X POST "$PROJECT_API_URL/keys/204496/reset-hwid" \
  -H "Authorization: Bearer $PROJECT_TOKEN" \
  -H "Accept: application/json"
```

```js title="JavaScript"
const res = await fetch(`${process.env.PROJECT_API_URL}/keys/204496/reset-hwid`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PROJECT_TOKEN}`,
    Accept: "application/json",
  },
});
const json = await res.json();
if (!json.success) throw new Error(json.message);
console.log(json.data);
```

```php title="PHP"
<?php
$ch = curl_init(getenv("PROJECT_API_URL") . "/keys/204496/reset-hwid");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PROJECT_TOKEN"),
        "Accept: application/json",
    ],
]);
$json = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!$json["success"]) {
    throw new Exception($json["message"]);
}
print_r($json["data"]);
```

```python title="Python"
import os, requests

res = requests.post(
    f"{os.environ['PROJECT_API_URL']}/keys/204496/reset-hwid",
    headers={
        "Authorization": f"Bearer {os.environ['PROJECT_TOKEN']}",
        "Accept": "application/json",
    },
    timeout=30,
)
data = res.json()
if not data["success"]:
    raise Exception(data["message"])
print(data["data"])
```

```json title="Resposta · 200"
{
  "success": true,
  "message": "HWID resetado.",
  "data": []
}
```



---

# Fluxo de venda (/docs/guides/sales-flow)

Este é o caminho que uma loja ou um bot faz para vender e entregar keys sem ninguém abrir o painel.

<Steps>
<Step>

### Monte a vitrine [#monte-a-vitrine]

Busque os produtos com [Listar produtos](/docs/reference/products/list) e guarde a lista por alguns minutos. Mostre só os que têm `available: true`, com as durações de `plans`.

</Step>
<Step>

### Registre a venda com um código [#registre-a-venda-com-um-código]

Quando o cliente pagar, crie uma `Idempotency-Key` (um UUID) e guarde **junto do pedido**, antes de chamar a API. Esse código é o que garante que a venda não seja cobrada duas vezes.

</Step>
<Step>

### Gere a key [#gere-a-key]

Chame [Gerar keys](/docs/reference/keys/generate) com o `product_id`, a `quantity` e os `days` escolhidos.

- **Deu certo:** guarde `generation_id`, e de cada key o `user_id` e o `key_code`.
- **Erro de rede, timeout ou `500`:** repita com a **mesma** `Idempotency-Key`. Se a primeira tinha dado certo, você recebe as mesmas keys sem nova cobrança.
- **`402`:** falta saldo. Nada foi cobrado; avise que a entrega vai atrasar e compre créditos.
- **`422`:** o produto ficou indisponível ou a duração não existe. Não repita: confira a vitrine.

</Step>
<Step>

### Entregue ao cliente [#entregue-ao-cliente]

Mande o `key_code` ao cliente. A key fica com `status: DISPONIVEL` até ele ativar.

</Step>
<Step>

### Atenda o suporte [#atenda-o-suporte]

- **"Troquei de PC":** chame [Resetar HWID](/docs/reference/keys/reset-hwid) com o `user_id` da key.
- **"Minha key não funciona":** consulte a key com [Listar suas keys](/docs/reference/keys/list) e `user_ids[]` — veja `status`, `hwid_status` e `seconds_remaining`.

</Step>
<Step>

### Confira as contas [#confira-as-contas]

No [extrato](/docs/reference/credits/transactions), cada geração aparece com `type: key_generation` e `reference_id` igual ao `generation_id`. É assim que cada saída de créditos se liga à venda que a causou.

</Step>
</Steps>

## Receba avisos em vez de perguntar [#receba-avisos-em-vez-de-perguntar]

Com os [webhooks](/docs/guides/webhooks), o seu sistema recebe um aviso a cada key gerada, HWID resetado ou mudança de saldo — inclusive as feitas pelo painel.


---

# Idempotência (/docs/guides/idempotency)

Se a conexão cair no meio de "gerar keys", o seu sistema não sabe se as keys saíram. Repetir o pedido sem proteção geraria e cobraria de novo.

Por isso [Gerar keys](/docs/reference/keys/generate) exige o cabeçalho `Idempotency-Key`: um código único que você cria para cada pedido. Repetindo com o mesmo código, a API devolve **as mesmas keys** e não cobra outra vez.

```http
Idempotency-Key: 3f1c9e2a-7b4d-4c1e-9a0f-5d2b8c6e1a47
```

## O código [#o-código]

- Um UUID serve, e é o mais simples (`crypto.randomUUID()`, `uuid.uuid4()`).
- Até 100 caracteres: letras, números e `_ - : .` — sem espaço nem acento.
- Um código por **venda**, criado antes de chamar a API e guardado junto dela.

## O que acontece ao repetir [#o-que-acontece-ao-repetir]

| Situação | Resposta |
| --- | --- |
| Mesmo código e mesmo pedido, e o primeiro já terminou | As **mesmas** keys, sem cobrar, com `"idempotent_replay": true` e o cabeçalho `Idempotent-Replayed: true`. |
| Mesmo código, e o primeiro ainda está rodando | `409` — espere alguns segundos e repita. |
| Mesmo código com pedido diferente (outro produto, quantidade ou duração) | `409` — use um código novo para um pedido novo. |
| O primeiro falhou (sem saldo, dados inválidos, erro) | Nada foi cobrado; o código fica livre para tentar de novo. |

<Callout type="warn">
  Reuse o código só para repetir **o mesmo pedido**. Nunca crie um código novo para repetir: aí a API entende que é outra venda e cobra de novo.
</Callout>

## Exemplo de repetição segura [#exemplo-de-repetição-segura]

```js title="JavaScript"
async function generateKeys(order) {
  // O código nasce com o pedido e fica salvo nele.
  order.idempotencyKey ??= crypto.randomUUID();
  await saveOrder(order);

  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      const res = await fetch(`${process.env.PROJECT_API_URL}/keys`, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.PROJECT_TOKEN}`,
          'Content-Type': 'application/json',
          Accept: 'application/json',
          'Idempotency-Key': order.idempotencyKey,
        },
        body: JSON.stringify({ product_id: order.productId, quantity: order.quantity, days: order.days }),
        signal: AbortSignal.timeout(30_000),
      });
      // 5xx e 409 (ainda rodando): espera e repete com o MESMO código.
      if (res.status >= 500 || res.status === 409) throw new Error(`HTTP ${res.status}`);
      return await res.json();
    } catch (error) {
      if (attempt === 3) throw error;
      await new Promise((resolve) => setTimeout(resolve, attempt * 2000));
    }
  }
}
```


---

# Webhooks (/docs/guides/webhooks)

Em vez de perguntar à API de tempos em tempos, o seu sistema pode **receber** os avisos: a cada evento, o nosso servidor faz um `POST` com um JSON assinado para a URL que você cadastrar.

## Como cadastrar [#como-cadastrar]

No painel, em **Notificações e logs**, cada canal aceita uma URL:

- **URL de webhook do Discord:** o canal recebe a mensagem pronta, formatada para o Discord.
- **Qualquer outra URL `https`:** o seu sistema recebe o JSON descrito nesta página.

Na mesma tela ficam o botão **Testar**, o histórico de envios e o **segredo** de cada canal, usado para [validar a assinatura](/docs/guides/webhooks/signature).

## Canais [#canais]

| Canal | Quando avisa | Eventos |
| --- | --- | --- |
| `keys` | Você gerou keys (pela API ou pelo painel). | `key.generated` |
| `hwid` | Um HWID foi resetado. | `key.hwid_reset` |
| `credits` | O seu saldo mudou: compra ou ajuste. | `credits.purchased`, `credits.added`, `credits.removed` |
| `mods` | Saiu change-log de um mod da revenda. | `changelog.published` |
| `games` | O jogo de um mod da revenda foi atualizado. | `game.updated` |

Nos canais `mods` e `games` você pode escolher, no painel, de quais mods ou jogos quer receber. Sem escolha, recebe de todos.

## Requisitos da URL [#requisitos-da-url]

- `https`, na porta padrão (443), num endereço público. Endereço de rede interna é recusado.
- Sem usuário e senha na URL. Para saber que o aviso veio de nós, [valide a assinatura](/docs/guides/webhooks/signature).
- Responda `2xx` em até 5 segundos. O ideal é `204` sem corpo; uma resposta acima de 1 MB conta como falha.
- Redirecionamento não é seguido: cadastre a URL final.

## Cabeçalhos [#cabeçalhos]

| Cabeçalho | O que é |
| --- | --- |
| `Content-Type` | `application/json` |
| `X-Project-Event` | O evento (o mesmo do campo `event`). |
| `X-Project-Delivery` | O id do aviso (o mesmo do campo `id`). Igual numa repetição. |
| `X-Project-Timestamp` | Quando o envio foi feito, em segundos desde 1970 (Unix). |
| `X-Project-Signature-256` | `sha256=` seguido do HMAC-SHA256, em hex, de `"{timestamp}.{corpo}"` com o segredo do canal. |

## Corpo [#corpo]

Todo aviso tem o mesmo envelope; o que muda é o `data` de cada evento.

```json
{
  "id": "6f1c2a4e-9d3b-4e7a-8c51-2b0d9f7a1e33",
  "event": "key.generated",
  "channel": "keys",
  "version": 1,
  "created_at": "2026-10-15T14:03:00-03:00",
  "data": {
    "generation_id": 57,
    "product": { "id": 1001, "title": "MOD EXEMPLO" },
    "quantity": 1,
    "days": 30,
    "keys": [{ "id": 204496, "code": "0a1b2c3d…c9d0e1f" }],
    "credits": { "spent": 18, "balance": 480 },
    "reseller": { "id": 12, "login": "minhaloja" }
  }
}
```

| Campo | O que é |
| --- | --- |
| `id` | O id único do aviso. Use para ignorar repetições. |
| `event` | O evento (tabela abaixo). |
| `channel` | O canal que recebeu. |
| `version` | A versão do formato do envelope. Hoje, `1`. |
| `created_at` | Quando o evento aconteceu (ISO 8601). |
| `data` | Os dados do evento. |

## Eventos [#eventos]

| Evento | Campos em `data` |
| --- | --- |
| `key.generated` | `generation_id`, `product {id, title}`, `quantity`, `days`, `keys [{id, code}]`, `credits {spent, balance}`, `reseller {id, login}` |
| `key.hwid_reset` | `key {id, code}`, `reseller {id, login}` |
| `credits.purchased` | `order_id`, `credits`, `balance`, `reseller {id, login}` |
| `credits.added` | `amount`, `balance`, `reseller {id, login}` |
| `credits.removed` | `amount`, `balance`, `reason`, `reseller {id, login}` |
| `changelog.published` | `product {id, title, image}`, `changelog {id, items [], text_pt, text_en}` |
| `game.updated` | `game {appid, name, version, link, image}`, `products [{id, title}]` |
| `webhook.test` | `message`, `reseller {id, login}` — enviado pelo botão **Testar** do painel. |

<Callout>
  Nos avisos, as keys vêm **mascaradas** (o começo e o fim do código). A key inteira sai pela API: [Listar suas keys](/docs/reference/keys/list), com o `id` de `keys[]` em `user_ids[]`.
</Callout>

## Respostas e falhas [#respostas-e-falhas]

| A sua resposta | O que acontece |
| --- | --- |
| `2xx` | Entregue. |
| `3xx` ou `4xx` (menos `429`) | Falha. O aviso não é reenviado. |
| `5xx`, `429` ou nenhuma resposta em 5 segundos | Falha. O aviso pode ser reenviado algumas vezes, com o mesmo `id`. |

Toda falha fica no histórico de envios do painel.

<Callout type="warn">
  Depois de 10 falhas seguidas, o canal é desativado. Corrija o seu servidor e salve a URL de novo no painel para reativar.
</Callout>

## Próximo passo [#próximo-passo]

[Valide a assinatura](/docs/guides/webhooks/signature) de todo aviso antes de confiar nele.


---

# Validar a assinatura (/docs/guides/webhooks/signature)

Qualquer um que descubra a sua URL pode mandar um `POST` para ela. A assinatura é o que separa um aviso nosso de um falso.

## Como funciona [#como-funciona]

1. Pegue o segredo do canal no painel, em **Notificações e logs** (**Mostrar** / **Gerar outro**). Guarde no servidor, como `PROJECT_WEBHOOK_SECRET`.
2. Calcule o HMAC-SHA256 de `"{X-Project-Timestamp}.{corpo cru}"` com o segredo, em hex, e ponha `sha256=` na frente.
3. Compare com `X-Project-Signature-256` em **tempo constante**.
4. Recuse avisos com mais de 5 minutos (`X-Project-Timestamp`), para ninguém reaproveitar um aviso antigo.
5. Guarde o `id` de cada aviso e ignore um que já processou.

<Callout type="warn" title="Use o corpo cru">
  A assinatura é sobre os bytes exatos que chegaram. Converter para JSON e de volta para texto muda espaços e a ordem das chaves, e a assinatura deixa de bater — é o erro mais comum de quem integra webhook.
</Callout>

## Exemplos [#exemplos]

<Tabs groupId="language" persist items={['JavaScript', 'PHP', 'Python']}>
<Tab value="JavaScript">

```js title="Node.js (Express)"
const crypto = require('crypto');
const express = require('express');
const app = express();

// O corpo CRU (express.raw): a assinatura é sobre o texto exato que chegou.
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.get('X-Project-Timestamp') || '';
  const received = req.get('X-Project-Signature-256') || '';
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.PROJECT_WEBHOOK_SECRET)
    .update(timestamp + '.' + req.body)
    .digest('hex');

  const valid = received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  const recent = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
  if (!valid || !recent) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  // event.id é o mesmo numa repetição: ignore o que já processou.
  res.sendStatus(204);
});
```

</Tab>
<Tab value="PHP">

```php
<?php
$body = file_get_contents('php://input'); // o corpo cru
$timestamp = $_SERVER['HTTP_X_PROJECT_TIMESTAMP'] ?? '';
$received = $_SERVER['HTTP_X_PROJECT_SIGNATURE_256'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, getenv('PROJECT_WEBHOOK_SECRET'));

if (! hash_equals($expected, $received) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);
// $event['id'] é o mesmo numa repetição: ignore o que já processou.
http_response_code(204);
```

</Tab>
<Tab value="Python">

```python title="Python (Flask)"
import hashlib, hmac, os, time
from flask import Flask, abort, request

app = Flask(__name__)

@app.post('/webhook')
def webhook():
    body = request.get_data()  # o corpo cru
    timestamp = request.headers.get('X-Project-Timestamp', '')
    received = request.headers.get('X-Project-Signature-256', '')
    secret = os.environ['PROJECT_WEBHOOK_SECRET'].encode()
    expected = 'sha256=' + hmac.new(secret, timestamp.encode() + b'.' + body, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, received) or abs(time.time() - int(timestamp or 0)) > 300:
        abort(401)

    event = request.get_json()
    # event['id'] é o mesmo numa repetição: ignore o que já processou.
    return '', 204
```

</Tab>
</Tabs>

## Trocar o segredo [#trocar-o-segredo]

Em **Notificações e logs**, **Gerar outro** cria um segredo novo para o canal. Atualize `PROJECT_WEBHOOK_SECRET` no seu servidor logo em seguida: a partir daí, os avisos chegam assinados com o segredo novo.


---

# Erros (/docs/errors)

```json title="Exemplo — saldo insuficiente (402)"
{
  "success": false,
  "message": "Créditos insuficientes.",
  "errors": { "balance": 12 }
}
```

- **Decida pelo código HTTP.** Ele é o que diz o tipo do erro.
- **Mostre ou registre a `message`.** Ela explica o que aconteceu. Quase sempre vem em português; os detalhes de campo em `errors` podem vir em inglês.
- **Leia `errors` para os detalhes.** Pode vir vazio, e pode trazer campos além dos descritos aqui (como um `status_code` numérico, que serve só para o suporte).

## Códigos HTTP [#códigos-http]

| HTTP | Significa | O que fazer |
| --- | --- | --- |
| `400` | Pedido mal formado. | Confira o JSON enviado. |
| `401` | Token ausente, inválido, apagado ou vencido. | Confira o cabeçalho `Authorization`. Se o token foi apagado, crie outro no painel. |
| `402` | Saldo insuficiente para gerar as keys. | Nada foi cobrado. `errors.balance` traz o seu saldo; compre créditos e tente de novo. |
| `403` | O token não tem a permissão da rota, ou a key não é sua. | `errors.required_scope` diz a permissão que falta. Crie um token com ela. |
| `404` | Rota, produto ou key não encontrado. | Confira o id e o endereço. |
| `409` | `Idempotency-Key` repetida com pedido diferente, ou o primeiro pedido ainda está rodando. | Pedido novo: use um código novo. Mesmo pedido: espere alguns segundos e repita. |
| `422` | Dados inválidos ou faltando. | A `message` diz o que corrigir; em erro de campo, `errors` traz a lista por campo. |
| `429` | Limite de chamadas atingido. | Espere os segundos do cabeçalho `Retry-After` e tente de novo. Veja os [limites](/docs/rate-limits). |
| `500` | Erro do nosso lado. | Repita com a mesma `Idempotency-Key`. Se continuar, fale com o suporte. |

## Erro de campo (422) [#erro-de-campo-422]

Quando um campo vem errado, `errors` traz cada campo com a lista de problemas dele:

```json
{
  "success": false,
  "message": "Dados inválidos.",
  "errors": {
    "quantity": ["The quantity field must not be greater than 50."]
  }
}
```

## Repetir ou não repetir [#repetir-ou-não-repetir]

| Repita | Não repita |
| --- | --- |
| Erro de rede, timeout, `500` — com a mesma `Idempotency-Key`. | `400`, `401`, `402`, `403`, `404`, `422` — corrija antes. |
| `409` de pedido ainda rodando — depois de alguns segundos. | `409` de código usado em outro pedido — crie um código novo. |
| `429` — depois de esperar. | |


---

# Limites (/docs/rate-limits)

| Limite | Valor |
| --- | --- |
| Chamadas por token | 120 por minuto, somando todas as rotas |
| Ações por conta ([gerar keys](/docs/reference/keys/generate) e [resetar HWID](/docs/reference/keys/reset-hwid)) | 30 por minuto, somando painel e todos os tokens |
| Keys por chamada de geração | 50 |
| Movimentos por página do [extrato](/docs/reference/credits/transactions) | 200 |
| Keys por consulta com `user_ids[]` | 200 |
| Tokens ativos por conta | 10 |

## Quando passa do limite [#quando-passa-do-limite]

A resposta é `429`, com o cabeçalho `Retry-After` dizendo quantos segundos esperar. A `message` diz o mesmo tempo:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 55
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
```

```json
{
  "success": false,
  "message": "Muitas requisições. Tente de novo em 55 segundo(s).",
  "errors": []
}
```

As respostas que deram certo trazem `X-RateLimit-Limit` (o limite) e `X-RateLimit-Remaining` (quanto ainda resta) do limite mais perto de estourar. Use o `Remaining` para diminuir o ritmo antes de chegar ao `429`.

## Como não chegar ao limite [#como-não-chegar-ao-limite]

- Guarde a lista de produtos por alguns minutos em vez de buscar a cada venda.
- Consulte só as keys que interessam, com `user_ids[]`.
- Prefira [webhooks](/docs/guides/webhooks) a perguntar à API de tempos em tempos.
- Em lote grande, gere até 50 keys por chamada em vez de uma por vez.


---

# Perguntas frequentes (/docs/faq)

<Accordions>
<Accordion title="Existe ambiente de teste?">

Não. Toda chamada usa a sua conta e o seu saldo de verdade. Para testar sem gastar, use as rotas de consulta ([Quem sou eu](/docs/reference/me), [Listar produtos](/docs/reference/products/list), [Ver saldo](/docs/reference/credits/balance)) e gere keys só quando for entregar.

</Accordion>
<Accordion title="Onde encontro o endereço da API?">

No painel, em **API › Tokens**, no exemplo de uso.

</Accordion>
<Accordion title="Posso usar o token no meu site ou aplicativo?">

Não. O token fica só no seu servidor. No navegador ou no computador do cliente, qualquer pessoa conseguiria ler o token e gastar o seu saldo.

</Accordion>
<Accordion title="O meu token vazou. E agora?">

Apague o token no painel: ele para de funcionar na hora. Crie outro e troque no seu servidor.

</Accordion>
<Accordion title="A chamada de gerar keys deu timeout. Fui cobrado?">

Talvez. Repita o pedido com a **mesma** `Idempotency-Key`: se a primeira chamada deu certo, você recebe as mesmas keys sem nova cobrança; se não deu, a geração acontece agora. Veja [Idempotência](/docs/guides/idempotency).

</Accordion>
<Accordion title="A key gerada já está valendo?">

Ela está pronta para entregar, com `status: DISPONIVEL`. Quando o cliente ativa, ela passa para `ATIVA`, e `seconds_remaining` em [Listar suas keys](/docs/reference/keys/list) mostra quanto tempo falta.

</Accordion>
<Accordion title="Resetar HWID custa créditos?">

Não. O reset é gratuito, mas entra no limite de 30 ações por minuto da conta.

</Accordion>
<Accordion title="Por que recebi 403?">

Ou o token não tem a permissão daquela rota (`errors.required_scope` diz qual), ou a key não é sua. Veja [Permissões](/docs/security/permissions).

</Accordion>
<Accordion title="Dá para mudar as permissões de um token?">

Não. Crie um token novo com as permissões certas e apague o antigo.

</Accordion>
</Accordions>

