# 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. | | # Perguntas frequentes (/docs/faq) 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. No painel, em **API › Tokens**, no exemplo de uso. 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. Apague o token no painel: ele para de funcionar na hora. Crie outro e troque no seu servidor. 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). 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. Não. O reset é gratuito, mas entra no limite de 30 ações por minuto da conta. 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). Não. Crie um token novo com as permissões certas e apague o antigo. # 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. | 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. ## 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)); } } } ``` # 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. ### 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`. ### 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. ### 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. ### Entregue ao cliente [#entregue-ao-cliente] Mande o `key_code` ao cliente. A key fica com `status: DISPONIVEL` até ele ativar. ### 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`. ### 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. ## 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. # 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. | 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[]`. ## 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. Depois de 10 falhas seguidas, o canal é desativado. Corrija o seu servidor e salve a URL de novo no painel para reativar. ## 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. 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. ## Exemplos [#exemplos] ```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); }); ``` ```php 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); ``` ```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 ``` ## 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. # 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. ## 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_..." ``` Toda chamada usa o seu saldo de verdade. Gere key só quando for entregar para um cliente. ## 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 } } ``` As respostas podem trazer campos além dos descritos aqui. Ignore os que o seu sistema não conhece. ## 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] # 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. ### Baixe a coleção [#baixe-a-coleção] Baixar a coleção do Postman O mesmo arquivo funciona no Insomnia. ### Importe [#importe] No Postman, clique em **Import** e escolha o arquivo baixado. A coleção aparece com todas as rotas. ### 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_...`) | Não salve o token num workspace público nem mande a coleção preenchida para ninguém. ### Faça a primeira chamada [#faça-a-primeira-chamada] Abra **Quem sou eu** e clique em **Send**. Voltou a sua conta: está pronto. 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. # Início rápido (/docs/quickstart) ### 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. ### 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_..." ``` 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. ### 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" 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"]) ``` ### 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" 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). ## 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). # 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. # 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" 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" 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 } } ``` # 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`. # 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. 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) ## 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" 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] } } ``` # 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" 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 } ] } } ``` # 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" 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": [] } ``` # 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" 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"] } } } ``` # 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" 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 } } } ``` # 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. | 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). ## 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" 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 } } ``` # 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" } } ``` 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. # 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. | Toda chamada fica registrada do nosso lado, com data, IP e resultado. # 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. 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. ## 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 | # Integre com IA (/docs/vibecode) Com o **guia de implementação**, um assistente de IA — Claude Code, Cursor, ChatGPT, Copilot, Windsurf — monta a integração com a API para você: o cliente HTTP, a geração de keys sem cobrança dupla, o reset de HWID e o webhook com a assinatura validada. Baixar o guia de implementação (.md) ## O que vem no guia [#o-que-vem-no-guia] - **Instruções para a IA:** o que ela está construindo e um plano em etapas. - **Regras que ela não pode quebrar:** o token só no servidor, uma Idempotency-Key por venda, nada de gerar key em teste, a assinatura do webhook validada. - **Lista para conferir antes de entregar.** - **A documentação inteira** — todas as rotas, campos, erros, limites e os exemplos em cURL, JavaScript, PHP e Python. O guia é gerado da própria documentação: quando uma página muda, ele muda junto. ## Como usar [#como-usar] ### Baixe o guia [#baixe-o-guia] Use o botão acima e salve o arquivo no seu projeto, por exemplo em `docs/project-api.md`. ### Configure o ambiente você mesmo [#configure-o-ambiente-você-mesmo] Crie o `.env` do projeto com o endereço e o token (os dois estão no painel, em **API › Tokens**): ```bash title=".env" PROJECT_API_URL=cole aqui o endereço do painel PROJECT_TOKEN=prj_live_... ``` A IA não precisa do token para escrever o código: o código lê de `PROJECT_TOKEN`. Token colado num chat fica no histórico da conversa. Se isso acontecer, apague o token no painel e crie outro. ### Peça o que você quer [#peça-o-que-você-quer] Diga à IA para ler o guia e descreva o sistema. Os [prompts prontos](/docs/vibecode/prompts) cobrem os casos mais comuns. ### Revise e teste com cuidado [#revise-e-teste-com-cuidado] - Leia o código antes de rodar. Confira que o token não aparece em lugar nenhum. - Teste primeiro com rotas de consulta: [Quem sou eu](/docs/reference/me) e [Listar produtos](/docs/reference/products/list). - Gere a primeira key de verdade com calma: **não existe ambiente de teste**, e ela gasta créditos. ## Deixe o guia sempre à mão da IA [#deixe-o-guia-sempre-à-mão-da-ia] | Ferramenta | Como | | --- | --- | | Claude Code | Salve o guia em `docs/project-api.md` e cite no `CLAUDE.md`: `Integração com a Project: siga docs/project-api.md`. | | Cursor | Salve em `.cursor/rules/project-api.md` para valer em toda conversa do projeto. | | ChatGPT, Claude.ai e outros chats | Anexe o arquivo na conversa antes de fazer o pedido. | ## Outras formas de dar a documentação à IA [#outras-formas-de-dar-a-documentação-à-ia] - **Uma página só:** o botão **Copiar Markdown**, no topo de cada página, copia a página com os exemplos. - **Tudo de uma vez, sem as instruções:** [`/llms-full.txt`](/llms-full.txt). - **Índice para ferramentas que leem `llms.txt`:** [`/llms.txt`](/llms.txt). # Prompts prontos (/docs/vibecode/prompts) Cada prompt considera que o [guia de implementação](/docs/vibecode) está no projeto em `docs/project-api.md`. Troque o que está entre colchetes pelo seu caso. Os prompts nunca levam o token. O código lê de `PROJECT_TOKEN`, que você mesmo põe no `.env`. ## Cliente da API [#cliente-da-api] O ponto de partida de qualquer integração. ```text Leia docs/project-api.md e siga as regras dele. Crie um cliente da API de revenda da Project em [Node.js/TypeScript | PHP | Python]: - uma função de requisição única (endereço de PROJECT_API_URL, token de PROJECT_TOKEN, timeout de 30 s, leitura de { success, message, data, errors }, erro com o status HTTP); - uma função para cada rota da Referência; - testes com um cliente HTTP falso, sem chamar a API de verdade. No fim, me diga como testar a conexão com GET /me. ``` ## Bot do Discord que entrega key [#bot-do-discord-que-entrega-key] ```text Leia docs/project-api.md e siga as regras dele. Crie um bot do Discord em [discord.js | discord.py] com o comando /key, só para quem tem o cargo [nome do cargo]: - /key produto: dias: - gera 1 key com POST /keys (Idempotency-Key criada e salva antes da chamada), e manda a key por mensagem privada para quem usou o comando; - em 402, responde que falta saldo; em outros erros, mostra a message da API; - guarda generation_id, user_id e key_code de cada key num arquivo [SQLite | JSON]. ``` ## Loja que entrega a key depois do pagamento [#loja-que-entrega-a-key-depois-do-pagamento] ```text Leia docs/project-api.md e siga as regras dele. Na minha loja em [WooCommerce | Next.js | Laravel | outra], quando um pedido for confirmado como pago: - crie uma Idempotency-Key, salve no pedido e chame POST /keys com o product_id e os days ligados ao produto da loja (crie esse mapeamento como configuração); - salve o key_code no pedido e mostre/envie ao cliente; - se a chamada falhar por rede, timeout ou 5xx, tente de novo com a MESMA Idempotency-Key; - se der 402 (sem saldo), marque o pedido como "aguardando key" e me avise. Nunca gere key duas vezes para o mesmo pedido. ``` ## Atendimento: consultar key e resetar HWID [#atendimento-consultar-key-e-resetar-hwid] ```text Leia docs/project-api.md e siga as regras dele. Crie uma tela [ou comando de bot] de atendimento que: - recebe o key_code que o cliente informa e acha o user_id dela no meu banco; - mostra status, hwid_status e o tempo restante (GET /keys com user_ids[]); - tem um botão "Resetar HWID" (POST /keys/{user_id}/reset-hwid) que pede confirmação. ``` ## Receber os webhooks [#receber-os-webhooks] ```text Leia docs/project-api.md e siga as regras dele. Crie um endpoint POST /webhooks/project em [Express | Laravel | Flask | outro] que: - valida X-Project-Signature-256 com PROJECT_WEBHOOK_SECRET sobre o corpo cru, em tempo constante, e recusa avisos com mais de 5 minutos; - ignora um id que já processou; - responde 204 na hora e trata o evento depois: [o que fazer com key.generated, key.hwid_reset e credits.*]; - tem testes com uma assinatura válida, uma errada e uma antiga. ``` ## Painel de saldo e extrato [#painel-de-saldo-e-extrato] ```text Leia docs/project-api.md e siga as regras dele. Crie uma página [no meu painel] que mostra o saldo (GET /credits) e o extrato (GET /credits/transactions) com paginação de 50 em 50, com o tipo de cada movimento em português. A chamada à API acontece no servidor, nunca no navegador. ``` # Errors (/en/docs/errors) ```json title="Example — not enough balance (402)" { "success": false, "message": "Créditos insuficientes.", "errors": { "balance": 12 } } ``` - **Decide by the HTTP code.** It is what tells you the kind of error. - **Show or log the `message`.** It explains what happened. It comes in Portuguese; field details in `errors` may come in English. - **Read `errors` for the details.** It may be empty, and it may include fields beyond the ones described here (such as a numeric `status_code`, which is for support only). ## HTTP codes [#http-codes] | HTTP | Meaning | What to do | | --- | --- | --- | | `400` | Malformed request. | Check the JSON you sent. | | `401` | Missing, invalid, deleted or expired token. | Check the `Authorization` header. If the token was deleted, create another one in the panel. | | `402` | Not enough balance to generate the keys. | Nothing was charged. `errors.balance` has your balance; buy credits and try again. | | `403` | The token lacks the endpoint's permission, or the key is not yours. | `errors.required_scope` says the missing permission. Create a token with it. | | `404` | Endpoint, product or key not found. | Check the id and the address. | | `409` | `Idempotency-Key` reused for a different request, or the first request is still running. | New request: use a new code. Same request: wait a few seconds and retry. | | `422` | Invalid or missing data. | The `message` says what to fix; on field errors, `errors` lists them per field. | | `429` | Rate limit reached. | Wait the seconds in the `Retry-After` header and try again. See the [limits](/en/docs/rate-limits). | | `500` | Error on our side. | Retry with the same `Idempotency-Key`. If it persists, contact support. | ## Field errors (422) [#field-errors-422] When a field is wrong, `errors` lists each field with its problems: ```json { "success": false, "message": "Dados inválidos.", "errors": { "quantity": ["The quantity field must not be greater than 50."] } } ``` ## Retry or not [#retry-or-not] | Retry | Do not retry | | --- | --- | | Network error, timeout, `500` — with the same `Idempotency-Key`. | `400`, `401`, `402`, `403`, `404`, `422` — fix it first. | | `409` for a request still running — after a few seconds. | `409` for a code used in another request — create a new code. | | `429` — after waiting. | | # FAQ (/en/docs/faq) No. Every call uses your real account and balance. To test without spending, use the read-only endpoints ([Who am I](/en/docs/reference/me), [List products](/en/docs/reference/products/list), [Get balance](/en/docs/reference/credits/balance)) and only generate keys when you are about to deliver them. In the panel, under **API › Tokens**, in the usage example. No. The token stays only on your server. In the browser or on the customer's computer, anyone could read the token and spend your balance. Delete the token in the panel: it stops working immediately. Create another one and replace it on your server. Maybe. Repeat the request with the **same** `Idempotency-Key`: if the first call worked, you get the same keys without a new charge; if it did not, the generation happens now. See [Idempotency](/en/docs/guides/idempotency). It is ready to deliver, with `status: DISPONIVEL`. When the customer activates it, it becomes `ATIVA`, and `seconds_remaining` in [List your keys](/en/docs/reference/keys/list) shows how much time is left. No. The reset is free, but it counts toward the account limit of 30 actions per minute. Either the token lacks the permission for that endpoint (`errors.required_scope` says which one), or the key is not yours. See [Permissions](/en/docs/security/permissions). The API answers in Portuguese. Decide by the HTTP status code and the fields documented here, and show or log the `message` as it comes. No. Create a new token with the right permissions and delete the old one. # Idempotency (/en/docs/guides/idempotency) If the connection drops in the middle of "generate keys", your system does not know whether the keys came out. Repeating the request without protection would generate and charge again. That is why [Generate keys](/en/docs/reference/keys/generate) requires the `Idempotency-Key` header: a unique code you create for each request. Repeating with the same code, the API returns **the same keys** and does not charge again. ```http Idempotency-Key: 3f1c9e2a-7b4d-4c1e-9a0f-5d2b8c6e1a47 ``` ## The code [#the-code] - A UUID works, and it is the simplest (`crypto.randomUUID()`, `uuid.uuid4()`). - Up to 100 characters: letters, digits and `_ - : .` — no spaces or accents. - One code per **sale**, created before calling the API and stored with it. ## What happens when you repeat [#what-happens-when-you-repeat] | Situation | Response | | --- | --- | | Same code and same request, and the first one has finished | The **same** keys, without a charge, with `"idempotent_replay": true` and the `Idempotent-Replayed: true` header. | | Same code, and the first one is still running | `409` — wait a few seconds and retry. | | Same code with a different request (another product, quantity or duration) | `409` — use a new code for a new request. | | The first one failed (no balance, invalid data, error) | Nothing was charged; the code is free to try again. | Reuse the code only to repeat **the same request**. Never create a new code to retry: the API then treats it as another sale and charges again. ## Safe retry example [#safe-retry-example] ```js title="JavaScript" async function generateKeys(order) { // The code is born with the order and stays stored in it. 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 and 409 (still running): wait and retry with the SAME code. 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)); } } } ``` # Sales flow (/en/docs/guides/sales-flow) This is the path a store or a bot follows to sell and deliver keys without anyone opening the panel. ### Build the storefront [#build-the-storefront] Fetch the products with [List products](/en/docs/reference/products/list) and cache the list for a few minutes. Show only the ones with `available: true`, with the durations from `plans`. ### Record the sale with a code [#record-the-sale-with-a-code] When the customer pays, create an `Idempotency-Key` (a UUID) and store it **with the order**, before calling the API. That code is what guarantees the sale is never charged twice. ### Generate the key [#generate-the-key] Call [Generate keys](/en/docs/reference/keys/generate) with the chosen `product_id`, `quantity` and `days`. - **It worked:** store `generation_id`, and for each key its `user_id` and `key_code`. - **Network error, timeout or `500`:** retry with the **same** `Idempotency-Key`. If the first one had worked, you get the same keys without a new charge. - **`402`:** balance too low. Nothing was charged; tell the customer delivery will be delayed and buy credits. - **`422`:** the product became unavailable or the duration does not exist. Do not retry: check the storefront. ### Deliver it to the customer [#deliver-it-to-the-customer] Send the `key_code` to the customer. The key stays at `status: DISPONIVEL` until they activate it. ### Handle support [#handle-support] - **"I changed my PC":** call [Reset HWID](/en/docs/reference/keys/reset-hwid) with the key's `user_id`. - **"My key does not work":** look the key up with [List your keys](/en/docs/reference/keys/list) and `user_ids[]` — check `status`, `hwid_status` and `seconds_remaining`. ### Reconcile [#reconcile] In the [statement](/en/docs/reference/credits/transactions), each generation shows up with `type: key_generation` and `reference_id` equal to the `generation_id`. That is how each credit spent links to the sale that caused it. ## Get notified instead of asking [#get-notified-instead-of-asking] With [webhooks](/en/docs/guides/webhooks), your system receives a notification for every key generated, HWID reset or balance change — including the ones made in the panel. # Webhooks (/en/docs/guides/webhooks) Instead of polling the API, your system can **receive** notifications: on every event, our server sends a `POST` with a signed JSON to the URL you register. ## How to register [#how-to-register] In the panel, under **Notificações e logs** (Notifications and logs), each channel accepts one URL: - **A Discord webhook URL:** the channel receives a ready-made message, formatted for Discord. - **Any other `https` URL:** your system receives the JSON described on this page. The same screen has the **Test** button, the delivery history and each channel's **secret**, used to [validate the signature](/en/docs/guides/webhooks/signature). ## Channels [#channels] | Channel | When it notifies | Events | | --- | --- | --- | | `keys` | You generated keys (through the API or the panel). | `key.generated` | | `hwid` | An HWID was reset. | `key.hwid_reset` | | `credits` | Your balance changed: purchase or adjustment. | `credits.purchased`, `credits.added`, `credits.removed` | | `mods` | A changelog was published for a reseller mod. | `changelog.published` | | `games` | The game of a reseller mod was updated. | `game.updated` | On the `mods` and `games` channels you can choose, in the panel, which mods or games you want to hear about. With no choice, you receive all of them. ## URL requirements [#url-requirements] - `https`, on the default port (443), at a public address. Internal network addresses are refused. - No username or password in the URL. To know the notification came from us, [validate the signature](/en/docs/guides/webhooks/signature). - Answer `2xx` within 5 seconds. Ideally `204` with no body; a response over 1 MB counts as a failure. - Redirects are not followed: register the final URL. ## Headers [#headers] | Header | What it is | | --- | --- | | `Content-Type` | `application/json` | | `X-Project-Event` | The event (same as the `event` field). | | `X-Project-Delivery` | The notification id (same as the `id` field). Identical on a retry. | | `X-Project-Timestamp` | When it was sent, in seconds since 1970 (Unix). | | `X-Project-Signature-256` | `sha256=` followed by the hex HMAC-SHA256 of `"{timestamp}.{body}"` with the channel secret. | ## Body [#body] Every notification has the same envelope; what changes is the `data` of each event. ```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": "mystore" } } } ``` | Field | What it is | | --- | --- | | `id` | The unique notification id. Use it to ignore repeats. | | `event` | The event (table below). | | `channel` | The channel that received it. | | `version` | The envelope format version. Currently `1`. | | `created_at` | When the event happened (ISO 8601). | | `data` | The event data. | ## Events [#events] | Event | Fields in `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}` — sent by the **Test** button in the panel. | In notifications, keys come **masked** (the start and end of the code). The full key comes from the API: [List your keys](/en/docs/reference/keys/list), with the `id` from `keys[]` in `user_ids[]`. ## Responses and failures [#responses-and-failures] | Your response | What happens | | --- | --- | | `2xx` | Delivered. | | `3xx` or `4xx` (except `429`) | Failure. The notification is not resent. | | `5xx`, `429` or no response within 5 seconds | Failure. The notification may be resent a few times, with the same `id`. | Every failure shows up in the delivery history in the panel. After 10 failures in a row, the channel is disabled. Fix your server and save the URL again in the panel to re-enable it. ## Next step [#next-step] [Validate the signature](/en/docs/guides/webhooks/signature) of every notification before trusting it. # Validate the signature (/en/docs/guides/webhooks/signature) Anyone who finds your URL can send a `POST` to it. The signature is what separates a real notification from a fake one. ## How it works [#how-it-works] 1. Get the channel secret in the panel, under **Notificações e logs** (**Show** / **Generate another**). Keep it on the server as `PROJECT_WEBHOOK_SECRET`. 2. Compute the HMAC-SHA256 of `"{X-Project-Timestamp}.{raw body}"` with the secret, in hex, and prefix it with `sha256=`. 3. Compare it with `X-Project-Signature-256` in **constant time**. 4. Reject notifications older than 5 minutes (`X-Project-Timestamp`), so no one can replay an old one. 5. Store the `id` of each notification and ignore one you have already processed. The signature covers the exact bytes that arrived. Parsing to JSON and turning it back into text changes spaces and key order, and the signature no longer matches — the most common mistake when integrating webhooks. ## Examples [#examples] ```js title="Node.js (Express)" const crypto = require('crypto'); const express = require('express'); const app = express(); // The RAW body (express.raw): the signature covers the exact text that arrived. 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 is the same on a retry: ignore what you already processed. res.sendStatus(204); }); ``` ```php 300) { http_response_code(401); exit; } $event = json_decode($body, true); // $event['id'] is the same on a retry: ignore what you already processed. http_response_code(204); ``` ```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() # the raw body 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'] is the same on a retry: ignore what you already processed. return '', 204 ``` ## Rotating the secret [#rotating-the-secret] Under **Notificações e logs**, **Generate another** creates a new secret for the channel. Update `PROJECT_WEBHOOK_SECRET` on your server right after: from then on, notifications are signed with the new secret. # Introduction (/en/docs) The reseller API lets your system do on its own what you do in the panel: browse the catalog and your balance, generate keys and reset HWIDs. A store, a Discord bot or an order system can deliver the key to the customer at the moment of sale, without anyone opening the panel. ## API address [#api-address] Every call starts with the same address. It is shown in the panel, under **API › Tokens**, in the usage example. The examples in this documentation read the address and the token from two environment variables: ```bash export PROJECT_API_URL="paste the address from the panel here" export PROJECT_TOKEN="prj_live_..." ``` Every call uses your real balance. Only generate a key when you are about to deliver it to a customer. ## Response format [#response-format] Every response is JSON and always has the same fields: | Field | What it is | | --- | --- | | `success` | `true` when it worked, `false` on error. | | `message` | A text to show or log. **The API messages are in Portuguese.** | | `data` | The response data (only on success). | | `errors` | The error details (only on error). | ```json title="Success" { "success": true, "message": "Saldo carregado.", "data": { "balance": 480 } } ``` ```json title="Error" { "success": false, "message": "Créditos insuficientes.", "errors": { "balance": 12 } } ``` Responses may include fields beyond the ones described here. Ignore the ones your system does not know. Always decide by the HTTP status code, not by the text of `message`. ## Dates, deadlines and amounts [#dates-deadlines-and-amounts] - **Dates** such as `created_at` come in the `YYYY-MM-DD HH:MM:SS` format. - **Deadlines** such as `access_end` come in seconds since 1970 (Unix). - **Credits** are always integers. ## Next steps [#next-steps] # Postman (/en/docs/postman) Use the collection when you want to: - explore the endpoints and see real responses; - check the token and its permissions; - reproduce an error before changing your system. ### Download the collection [#download-the-collection] Download the Postman collection The same file works in Insomnia. ### Import it [#import-it] In Postman, click **Import** and choose the downloaded file. The collection shows up with every endpoint. ### Fill in the variables [#fill-in-the-variables] Open the collection, go to **Variables** and fill in: | Variable | Value | | --- | --- | | `baseUrl` | the API address, shown in the panel under **API › Tokens** | | `token` | your token (`prj_live_...`) | Do not save the token in a public workspace or send the filled-in collection to anyone. ### Make the first call [#make-the-first-call] Open **Who am I** and click **Send**. If it returns your account, you are ready. In **Generate keys**, the `Idempotency-Key` gets a new value on every send. To repeat the **same** request, copy the value used and pin it in the header. Remember that generating keys spends your real balance. # Quickstart (/en/docs/quickstart) ### Create a token [#create-a-token] In the panel, under **API › Tokens**, create a token with the permissions your system needs. Copy it right away: it is shown only once. On the same screen, the usage example shows the **API address**. Copy it too. ### Keep the token on the server [#keep-the-token-on-the-server] Put the address and the token in environment variables on the server that will call the API: ```bash export PROJECT_API_URL="paste the address from the panel here" export PROJECT_TOKEN="prj_live_..." ``` Never put the token in a website, an app or a program that runs on the customer's computer: anyone could read the token and spend your balance. ### Test with `/me` [#test-with-me] If it returns your account and the token permissions, everything is set: ```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" 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"]) ``` ### Generate your first key [#generate-your-first-key] Take a `product_id` from [List products](/en/docs/reference/products/list) and generate a 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" // One id per request. Store it and reuse it if you need to retry the call. 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" 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 # One id per request. Store it and reuse it if you need to retry the call. 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"]) ``` The `Idempotency-Key` header is what prevents a double charge if the connection drops. [Understand idempotency](/en/docs/guides/idempotency). ## What next? [#what-next] - See the [complete sales flow](/en/docs/guides/sales-flow), from the catalog to delivering the key. - Receive notifications in your system with [webhooks](/en/docs/guides/webhooks). - Prefer to test without writing code? Use the [Postman collection](/en/docs/postman). # Rate limits (/en/docs/rate-limits) | Limit | Value | | --- | --- | | Calls per token | 120 per minute, across all endpoints | | Actions per account ([generate keys](/en/docs/reference/keys/generate) and [reset HWID](/en/docs/reference/keys/reset-hwid)) | 30 per minute, across the panel and every token | | Keys per generation call | 50 | | Transactions per [statement](/en/docs/reference/credits/transactions) page | 200 | | Keys per lookup with `user_ids[]` | 200 | | Active tokens per account | 10 | ## When you go over [#when-you-go-over] The response is `429`, with the `Retry-After` header saying how many seconds to wait. The `message` says the same time (in Portuguese): ```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": [] } ``` Successful responses carry `X-RateLimit-Limit` (the limit) and `X-RateLimit-Remaining` (what is left) for the limit closest to running out. Use `Remaining` to slow down before reaching `429`. ## How to stay under the limit [#how-to-stay-under-the-limit] - Cache the product list for a few minutes instead of fetching it on every sale. - Look up only the keys you need, with `user_ids[]`. - Prefer [webhooks](/en/docs/guides/webhooks) to polling the API. - For large batches, generate up to 50 keys per call instead of one at a time. # Get balance (/en/docs/reference/credits/balance) `GET /credits` · Token permission: **View balance** (`credits:read`) ## Response [#response] | Field | What it is | | --- | --- | | `balance` | Your balance, in credits. | To see where each credit came from and where it went, use the [statement](/en/docs/reference/credits/transactions). ## Errors [#errors] | HTTP | When | | --- | --- | | `401` | Missing, invalid, deleted or expired token. | | `403` | The token lacks the **View balance** permission. | | `429` | Over the [rate limit](/en/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" 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="Response · 200" { "success": true, "message": "Saldo carregado.", "data": { "balance": 480 } } ``` # Credit statement (/en/docs/reference/credits/transactions) `GET /credits/transactions` · Token permission: **View balance** (`credits:read`) ## Query parameters [#query-parameters] | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `limit` | number | No | How many transactions to return, from 1 to 200. Default: 50. | | `offset` | number | No | How many to skip, for pagination. Default: 0. | To paginate, add `limit` to `offset` on each page until a list shorter than `limit` comes back. ## Response [#response] | Field | What it is | | --- | --- | | `amount` | The transaction amount. Negative is money out; positive is money in. | | `balance_after` | The balance right after this transaction. | | `type` | The transaction type (table below). | | `reference_type` / `reference_id` | What the transaction refers to (table below). | | `notes` | A note, when there is one. | | `created_at` | When it happened. | | `balance` | Your current balance. | | `type` | What it is | `reference_id` | | --- | --- | --- | | `key_generation` | You generated keys. | The `generation_id` returned by the generation. | | `refund` | Refund of a failed generation. | The generation's `generation_id`. | | `purchase` | Credit purchase. | The credit order number. | | `manual_adjust` | Adjustment made by the team. | — | ## Errors [#errors] | HTTP | When | | --- | --- | | `401` | Missing, invalid, deleted or expired token. | | `403` | The token lacks the **View balance** permission. | | `422` | `limit` outside 1 to 200, or a negative `offset`. | | `429` | Over the [rate limit](/en/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" 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="Response · 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 } } ``` # Overview (/en/docs/reference) Each endpoint has a page with its parameters, a call example in cURL, JavaScript, PHP and Python, the response and the errors. The language you pick in one example applies to every page. | Method | Path | What it does | Permission | | --- | --- | --- | --- | | `GET` | `/me` | Who am I | any token | | `GET` | `/products` | List products | **View catalog** (`products:read`) | | `GET` | `/products/{product_id}` | Get a product | **View catalog** (`products:read`) | | `GET` | `/credits` | Get balance | **View balance** (`credits:read`) | | `GET` | `/credits/transactions` | Credit statement | **View balance** (`credits:read`) | | `GET` | `/keys` | List your keys | **View keys** (`keys:read`) | | `POST` | `/keys` | Generate keys | **Generate keys** (`keys:generate`) — spends credits | | `POST` | `/keys/{user_id}/reset-hwid` | Reset HWID | **Reset HWID** (`keys:reset-hwid`) | Every path is relative to the [API address](/en/docs#api-address), and every call carries the [token](/en/docs/security/authentication) in the `Authorization` header. # Generate keys (/en/docs/reference/keys/generate) `POST /keys` · Token permission: **Generate keys** (`keys:generate`) — spends credits Charges the price of the chosen duration times the quantity. Without enough balance, nothing is generated or charged. Every call carries the `Idempotency-Key` header, a unique code per request. Repeating with the same code, you get the same keys without paying again. [Why?](/en/docs/guides/idempotency) ## Headers [#headers] | Header | Required | Description | | --- | --- | --- | | `Idempotency-Key` | Yes | A unique code per request (a UUID works). Up to 100 characters: letters, digits and `_ - : .` | | `Content-Type` | Yes | `application/json` | ## Body (JSON) [#body-json] | Field | Type | Required | Description | | --- | --- | --- | --- | | `product_id` | number | Yes | The product, from the [product list](/en/docs/reference/products/list). | | `quantity` | number | Yes | How many keys to generate, from 1 to 50. | | `days` | number | No | The duration, among the product `plans`. Without it, 30 days. If the product has no 30-day plan, `days` is required. | The price is always calculated on our side, from the duration and the quantity. Any other field in the body is ignored. ## Response [#response] | Field | What it is | | --- | --- | | `generation_id` | The id of this batch. It shows up in the [statement](/en/docs/reference/credits/transactions) as `reference_id`. | | `keys[].key_code` | The key to deliver to the customer. | | `keys[].user_id` | The key id in the API (to [look it up](/en/docs/reference/keys/list) and [reset its HWID](/en/docs/reference/keys/reset-hwid)). | | `days` | The duration of the generated keys. | | `credits_spent` | The total charged. | | `balance` | Your balance after the charge. | ### Repeating the same request [#repeating-the-same-request] Repeating with the same `Idempotency-Key` and the same body, the response brings **the same keys** and does not charge again. It comes with `"idempotent_replay": true` in `data` and the `Idempotent-Replayed: true` header. ## Errors [#errors] | HTTP | When | What to do | | --- | --- | --- | | `402` | Not enough balance. Nothing was generated or charged. | `errors.balance` has your balance. Buy credits and try again. | | `403` | The token lacks the **Generate keys** permission. | Create a token with it. | | `409` | The `Idempotency-Key` was already used for a different request, or the first request is still running. | New request: use a new code. Same request: wait a few seconds and retry. | | `422` | Invalid data, missing or malformed `Idempotency-Key`, unavailable product, or a duration the product does not have. | The `message` says what to fix. A wrong duration lists the options in `errors.days`. | | `429` | Over the [limit](/en/docs/rate-limits). | Wait the seconds in `Retry-After` and try again. | | `500` | Failure on our side while generating. | The credits go back to your balance (`errors.credits_refunded` says how much). Retry with the same `Idempotency-Key`. If the message mentions a partial refund, or it keeps failing, contact support. | ```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" // One id per request. Store it and reuse it if you need to retry the call. 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" 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 # One id per request. Store it and reuse it if you need to retry the call. 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="Response · 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="Response · 402 — not enough balance" { "success": false, "message": "Créditos insuficientes.", "errors": { "balance": 12 } } ``` ```json title="Response · 422 — duration the product does not have" { "success": false, "message": "MOD EXEMPLO não tem key de 60 dias. Durações: 7, 15 e 30 dias.", "errors": { "days": [7, 15, 30] } } ``` # List your keys (/en/docs/reference/keys/list) `GET /keys` · Token permission: **View keys** (`keys:read`) ## Query parameters [#query-parameters] | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `user_ids[]` | list of numbers | No | Returns only these keys (the `user_id` of each one). Up to 200. | Without `user_ids[]`, all your keys come back. To track specific keys, repeat the parameter: `/keys?user_ids[]=204496&user_ids[]=204497`. ## Response [#response] | Field | What it is | | --- | --- | | `user_id` | The key id in the API. It is what the [HWID reset](/en/docs/reference/keys/reset-hwid) uses. | | `key_code` | The key the customer types. | | `product_id` / `product_name` | The key's product. | | `activated` | `true` after the customer activated the key. | | `status` | The key status (table below). | | `hwid_status` | The HWID status (table below). | | `access_end` | When access ends, in seconds since 1970 (Unix). `0` when there is no access. | | `seconds_remaining` | Time remaining, in seconds. `0` if the key has not been activated yet. | | `created_at` | When the key was generated. | | `credits_spent` | What the key cost when it was generated. `null` if there is no record. | | `days` | The purchased duration, in days. | | `status` | Meaning | | --- | --- | | `DISPONIVEL` | Generated, not yet activated by the customer. | | `ATIVA` | In use. | | `SEM_ACESSO` | The key no longer has access. | | `hwid_status` | Meaning | | --- | --- | | `HWID_ATIVO` | Bound to the customer's computer. | | `HWID_RESETADO` | Free to be used on another computer. | | `N/A` | The key has not been activated yet. | ## Errors [#errors] | HTTP | When | | --- | --- | | `401` | Missing, invalid, deleted or expired token. | | `403` | The token lacks the **View keys** permission. | | `422` | More than 200 ids in `user_ids[]`, or an id that is not a number. | | `429` | Over the [rate limit](/en/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" 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="Response · 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 } ] } } ``` # Reset HWID (/en/docs/reference/keys/reset-hwid) `POST /keys/{user_id}/reset-hwid` · Token permission: **Reset HWID** (`keys:reset-hwid`) When the customer activates the key, it gets bound to their computer (HWID). If they change PCs or reinstall, reset the HWID: the key works again on the next machine it is used on. **It costs no credits.** ## Path parameters [#path-parameters] | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `user_id` | number | Yes | The key's `user_id` — it comes in the [generation](/en/docs/reference/keys/generate) and in the [key list](/en/docs/reference/keys/list). | The call has no body. ## Response [#response] After the reset, the key shows `hwid_status: "HWID_RESETADO"` in the [key list](/en/docs/reference/keys/list). ## Errors [#errors] | HTTP | When | | --- | --- | | `401` | Missing, invalid, deleted or expired token. | | `403` | The token lacks the **Reset HWID** permission, or the key is not yours. | | `404` | The key has no access to reset. | | `429` | Over the [limit](/en/docs/rate-limits). Resetting HWID counts toward the per-account action limit. | ```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" 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="Response · 200" { "success": true, "message": "HWID resetado.", "data": [] } ``` # Who am I (/en/docs/reference/me) `GET /me` · Token permission: any token Confirms the token works and tells you what it can do. Use it to check the token when your system starts. ## Response [#response] | Field | What it is | | --- | --- | | `user.user_id` | Your account id. | | `user.user_login` | Your username. | | `user.user_email` | The account e-mail. | | `balance` | Your credit balance. Only present if the token has the **View balance** permission. | | `auth.type` | Always `api_token` on a call with a token. | | `auth.scopes` | The [permissions](/en/docs/security/permissions) of this token. | ## Errors [#errors] | HTTP | When | | --- | --- | | `401` | Missing, invalid, deleted or expired token. | | `429` | Over the [rate limit](/en/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" 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="Response · 200" { "success": true, "message": "Dados do revendedor.", "data": { "user": { "user_id": 12, "user_login": "mystore", "user_email": "contact@mystore.com" }, "balance": 480, "auth": { "type": "api_token", "scopes": ["products:read", "credits:read", "keys:read", "keys:generate", "keys:reset-hwid"] } } } ``` # Get a product (/en/docs/reference/products/get) `GET /products/{product_id}` · Token permission: **View catalog** (`products:read`) ## Path parameters [#path-parameters] | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `product_id` | number | Yes | The `product_id` from the [product list](/en/docs/reference/products/list). | ## Response [#response] The `product` fields are the same as in [List products](/en/docs/reference/products/list#response). ## Errors [#errors] | HTTP | When | | --- | --- | | `401` | Missing, invalid, deleted or expired token. | | `403` | The token lacks the **View catalog** permission. | | `404` | The product does not exist or is not available for resale. | | `429` | Over the [rate limit](/en/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" 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="Response · 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 } } } ``` # List products (/en/docs/reference/products/list) `GET /products` · Token permission: **View catalog** (`products:read`) Use the `product_id` from here to [generate keys](/en/docs/reference/keys/generate). Prices are already **your account's** — the same amount the generation charges. ## Query parameters [#query-parameters] | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `search` | text | No | Filters by product name. Up to 100 characters. | ## Response [#response] | Field | What it is | | --- | --- | | `product_id` | The product id. It is what you send to generate keys. | | `title` | The product name. | | `platform` | The mod platform (e.g. Windows). | | `credit_cost` | The price, in credits, of a key of `days` days. | | `days` | The default duration: 30 days, or the longest the mod has if it has no 30-day plan. | | `plans` | The durations on sale, each with its price. Send the chosen `days` when generating. | | `status.label` | The mod status, as text (e.g. funcional, manutenção). | | `available` | `true` when keys can be generated right now. | | `stock` | Stock: `status` (e.g. `instock`, `outofstock`), `quantity` and whether stock is managed (`managed`). Only `instock` allows generating. | | `image` | The product image. | | `total` | How many products came back. | Only generate keys for products with `available: true`. A disabled or out-of-stock mod comes with `available: false`, and the generation is refused. The status of every mod is also on the [status](/en/status) page. ## Errors [#errors] | HTTP | When | | --- | --- | | `401` | Missing, invalid, deleted or expired token. | | `403` | The token lacks the **View catalog** permission. | | `422` | `search` longer than 100 characters. | | `429` | Over the [rate limit](/en/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" 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="Response · 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 } } ``` # Authentication (/en/docs/security/authentication) ```http Authorization: Bearer prj_live_... ``` ## Headers [#headers] | Header | Value | Required | | --- | --- | --- | | `Authorization` | `Bearer prj_live_...` | Yes | | `Accept` | `application/json` | Recommended | | `Content-Type` | `application/json` | Yes, when the call has a body (POST) | | `Idempotency-Key` | a unique code per request (UUID) | Yes, on [Generate keys](/en/docs/reference/keys/generate) | ## The token [#the-token] - It starts with `prj_live_` and is 49 characters long. - It only works in the `Authorization` header. Do not send the token in the URL (`?token=`) or in the body. - It can have an expiration (30 days, 90 days or 1 year) or none. You choose when creating it. - Each account can have up to 10 active tokens. - A deleted or expired token gets `401`. Create another one in the panel. ## Authentication responses [#authentication-responses] | HTTP | When | | --- | --- | | `401` | Missing header, or an invalid, deleted or expired token. | | `403` | The token is valid, but lacks the [permission](/en/docs/security/permissions) for that endpoint. | ```json title="403 — missing permission" { "success": false, "message": "Este token não tem permissão para esta ação (precisa de \"keys:generate\").", "errors": { "required_scope": "keys:generate" } } ``` Delete the token in the panel: it stops working immediately. Then create another one and replace it on your server. Do not share tokens in repositories, screenshots or chats. # Best practices (/en/docs/security/best-practices) ## Tokens [#tokens] | Do | Why | | --- | --- | | Use the token only on the server. | On the customer's computer, anyone could read the token and spend your balance. | | One token per system, named after where it lives. | If one leaks, you delete only that one. | | Check only the permissions you need. | A leaked read-only token does not spend credits. | | Prefer tokens with an expiration. | A forgotten token stops working on its own. | ## Generating keys [#generating-keys] | Do | Why | | --- | --- | | Create one `Idempotency-Key` per sale and store it with the sale. | Retrying after a network error returns the same keys, without charging again. | | Check `available` before generating. | An unavailable product does not generate keys. | | Send `days` with one of the durations in `plans`. | A duration the product does not have is rejected. | | Store `generation_id`, `user_id` and `key_code`. | They link the statement and support to the keys you delivered. | ## Calls [#calls] | Do | Why | | --- | --- | | Use a timeout of about 30 seconds. | Without it, a stuck connection holds your system. | | On a network error or `500`, retry with the **same** `Idempotency-Key`. | The API recognizes the retry and does not generate again. | | On `429`, wait the seconds in `Retry-After`. | Insisting before that only causes more `429`. | | Cache the product list for a few minutes. | The catalog rarely changes, and every call counts toward the [limit](/en/docs/rate-limits). | | Look up specific keys with `user_ids[]`. | It is faster than fetching the whole list. | Every call is logged on our side, with date, IP and result. # Permissions (/en/docs/security/permissions) When creating a token, you choose what it can do. Grant the minimum your system needs: a bot that only reads does not need to generate keys. | Permission | Code | What it allows | Endpoints | | --- | --- | --- | --- | | View catalog | `products:read` | Mods available for resale and the price of each one in credits. | `GET /products`, `GET /products/{product_id}` | | View balance | `credits:read` | Current balance and credit statement. | `GET /credits`, `GET /credits/transactions` | | View keys | `keys:read` | Your keys: status, HWID and time remaining. | `GET /keys` | | Generate keys | `keys:generate` | Creates new keys. Deducts credits from your balance. | `POST /keys` | | Reset HWID | `keys:reset-hwid` | Frees a key to be used on another computer. Costs no credits. | `POST /keys/{user_id}/reset-hwid` | The [Who am I](/en/docs/reference/me) endpoint works with any token and shows its permissions in `auth.scopes`. ## When a permission is missing [#when-a-permission-is-missing] The endpoint answers `403`, and `errors.required_scope` says which permission is missing. Create a token with it in the panel — the permissions of an existing token cannot be changed. Creating other tokens, configuring webhooks and buying credits are panel-only. A token, even with every permission, cannot reach them. If a token leaks, the damage is limited to what it can do. ## Common combinations [#common-combinations] | System | Permissions | | --- | --- | | Store that sells and delivers the key | View catalog, Generate keys | | Support bot | View keys, Reset HWID | | Finance dashboard | View balance | # Build with AI (/en/docs/vibecode) With the **implementation guide**, an AI assistant — Claude Code, Cursor, ChatGPT, Copilot, Windsurf — builds the API integration for you: the HTTP client, key generation without double charges, HWID resets and the webhook with signature validation. Download the implementation guide (.md) ## What is in the guide [#what-is-in-the-guide] - **Instructions for the AI:** what it is building and a step-by-step plan. - **Rules it must not break:** token only on the server, one Idempotency-Key per sale, no key generation in tests, webhook signature validated. - **A checklist to go through before delivering.** - **The whole documentation** — every endpoint, field, error, limit and the examples in cURL, JavaScript, PHP and Python. The guide is generated from the documentation itself: when a page changes, the guide changes with it. ## How to use it [#how-to-use-it] ### Download the guide [#download-the-guide] Use the button above and save the file in your project, for example at `docs/project-api.md`. ### Set up the environment yourself [#set-up-the-environment-yourself] Create the project's `.env` with the address and the token (both are in the panel, under **API › Tokens**): ```bash title=".env" PROJECT_API_URL=paste the address from the panel here PROJECT_TOKEN=prj_live_... ``` The AI does not need the token to write the code: the code reads it from `PROJECT_TOKEN`. A token pasted in a chat stays in the conversation history. If that happens, delete the token in the panel and create another one. ### Ask for what you want [#ask-for-what-you-want] Tell the AI to read the guide and describe your system. The [ready-made prompts](/en/docs/vibecode/prompts) cover the most common cases. ### Review and test carefully [#review-and-test-carefully] - Read the code before running it. Check that the token appears nowhere. - Test first with read-only endpoints: [Who am I](/en/docs/reference/me) and [List products](/en/docs/reference/products/list). - Generate the first real key carefully: **there is no test environment**, and it spends credits. ## Keep the guide at hand for the AI [#keep-the-guide-at-hand-for-the-ai] | Tool | How | | --- | --- | | Claude Code | Save the guide at `docs/project-api.md` and mention it in `CLAUDE.md`: `Project integration: follow docs/project-api.md`. | | Cursor | Save it at `.cursor/rules/project-api.md` so it applies to every conversation in the project. | | ChatGPT, Claude.ai and other chats | Attach the file to the conversation before asking. | ## Other ways to give the documentation to the AI [#other-ways-to-give-the-documentation-to-the-ai] - **A single page:** the **Copy Markdown** button, at the top of every page, copies the page with its examples. - **Everything at once, without the instructions:** [`/llms-full.txt`](/llms-full.txt). - **Index for tools that read `llms.txt`:** [`/llms.txt`](/llms.txt). # Ready-made prompts (/en/docs/vibecode/prompts) Each prompt assumes the [implementation guide](/en/docs/vibecode) is in your project at `docs/project-api.md`. Replace what is in brackets with your case. The prompts never include the token. The code reads it from `PROJECT_TOKEN`, which you put in the `.env` yourself. ## API client [#api-client] The starting point of any integration. ```text Read docs/project-api.md and follow its rules. Create a client for the Project reseller API in [Node.js/TypeScript | PHP | Python]: - a single request function (address from PROJECT_API_URL, token from PROJECT_TOKEN, 30 s timeout, reads { success, message, data, errors }, error with the HTTP status); - one function for each endpoint in the Reference; - tests with a fake HTTP client, never calling the real API. At the end, tell me how to test the connection with GET /me. ``` ## Discord bot that delivers keys [#discord-bot-that-delivers-keys] ```text Read docs/project-api.md and follow its rules. Create a Discord bot in [discord.js | discord.py] with the /key command, only for members with the [role name] role: - /key product: days: - generates 1 key with POST /keys (Idempotency-Key created and stored before the call), and sends the key by direct message to whoever used the command; - on 402, replies that the balance is too low; on other errors, shows the API message; - stores generation_id, user_id and key_code of each key in a [SQLite | JSON] file. ``` ## Store that delivers the key after payment [#store-that-delivers-the-key-after-payment] ```text Read docs/project-api.md and follow its rules. In my store on [WooCommerce | Next.js | Laravel | other], when an order is confirmed as paid: - create an Idempotency-Key, store it on the order and call POST /keys with the product_id and days mapped to the store product (create that mapping as config); - store the key_code on the order and show/send it to the customer; - if the call fails with a network error, timeout or 5xx, retry with the SAME Idempotency-Key; - on 402 (no balance), mark the order as "waiting for key" and notify me. Never generate a key twice for the same order. ``` ## Support: look up a key and reset HWID [#support-look-up-a-key-and-reset-hwid] ```text Read docs/project-api.md and follow its rules. Create a support screen [or bot command] that: - receives the key_code the customer provides and finds its user_id in my database; - shows status, hwid_status and the time remaining (GET /keys with user_ids[]); - has a "Reset HWID" button (POST /keys/{user_id}/reset-hwid) that asks for confirmation. ``` ## Receive webhooks [#receive-webhooks] ```text Read docs/project-api.md and follow its rules. Create a POST /webhooks/project endpoint in [Express | Laravel | Flask | other] that: - validates X-Project-Signature-256 with PROJECT_WEBHOOK_SECRET over the raw body, in constant time, and rejects notifications older than 5 minutes; - ignores an id it has already processed; - answers 204 right away and handles the event afterwards: [what to do with key.generated, key.hwid_reset and credits.*]; - has tests with a valid signature, a wrong one and an old one. ``` ## Balance and statement page [#balance-and-statement-page] ```text Read docs/project-api.md and follow its rules. Create a page [in my panel] that shows the balance (GET /credits) and the statement (GET /credits/transactions) paginated 50 at a time, with each transaction type in plain English. The API call happens on the server, never in the browser. ```