Desenvolvedores

Estoque por API

Venda os itens que ficam no seu próprio sistema, sem copiar nada para a loja.

Com o estoque por API, as contas, chaves e licenças continuam no seu sistema. A loja pergunta quanto você tem, reserva os itens na hora em que o pedido nasce e devolve se o Pix vencer. Quem compra recebe exatamente o que a sua API entregou, e o mesmo item nunca sai duas vezes, mesmo que você venda também no seu site, num bot ou em outra loja.

Se preferir mandar as linhas para a loja guardar, use a rota de Estoque da API pública.

Como ligar#

  1. Cadastre a API#

    Em Integrações > Estoque por API, clique em Cadastrar API, dê um nome e informe a URL do seu sistema. Em produção ela precisa ser https, na porta 443, e apontar para um endereço público.

  2. Guarde o segredo#

    O segredo aparece uma vez só. O seu sistema usa ele para conferir que cada chamada veio da Promisse. Se perder, gere outro em Novo segredo: o antigo para de valer na hora.

    Quando sai da equipe quem viu o segredo atual ou apontou a URL, a API ganha o selo Trocar segredo e o dono recebe um aviso. A troca não é automática, para as vendas não pararem sem você saber: confira se a URL ainda é do seu sistema, gere um novo segredo e atualize o seu sistema.

  3. Teste a conexão#

    Testar manda a ação ping e mostra o código e o tempo da resposta.

  4. Ligue o produto#

    No editor do produto, no cartão Estoque, clique em Ligar a uma API, escolha a API e informe o ID do produto no seu sistema. A loja chama stock com esse ID na hora e só liga se a sua API reconhecer o produto.

Depois de ligado, o produto passa a ter entrega automática e mostra a quantidade da sua API, atualizada a cada minuto e a cada venda. Ele não aceita variações nem linhas de estoque da loja, e vende até 100 unidades por pedido. Produto entregue no jogo e produto que já usa o estoque de um bot não podem ser ligados.

A requisição#

Toda chamada é um POST em JSON na URL cadastrada, com a ação no campo action.

Cabeçalho Conteúdo
content-type application/json
user-agent PromisseStore-Stock/1.0
x-promisse-action A mesma ação do corpo
x-promisse-timestamp Hora do envio, em segundos desde 1970
x-promisse-signature HMAC-SHA256 em hexadecimal de timestamp.corpo, com o segredo como chave

A loja espera a resposta por até 8 segundos, não segue redirecionamento e lê no máximo 64 KB (2 MB na reserve). A resposta precisa ser JSON.

Validar a assinatura#

Junte o timestamp, um ponto e os bytes exatos do corpo, antes de converter o JSON. Recuse chamadas com mais de 5 minutos: assim uma chamada copiada não vale de novo.

estoque.js
import crypto from 'node:crypto';
import http from 'node:http';

const SECRET = process.env.PROMISSE_STOCK_SECRET;
if (!SECRET) throw new Error('Defina PROMISSE_STOCK_SECRET');

// Troque por um banco. Cada item tem o texto entregue e a referência de quem reservou.
const stock = {
  licenca: [
    { text: 'AAAA-BBBB-CCCC', reference: null },
    { text: 'DDDD-EEEE-FFFF', reference: null },
  ],
};

function isValid(rawBody, headers) {
  const timestamp = String(headers['x-promisse-timestamp'] ?? '');
  if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = crypto.createHmac('sha256', SECRET).update(`${timestamp}.`).update(rawBody).digest('hex');
  const received = String(headers['x-promisse-signature'] ?? '');
  return expected.length === received.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}

function readBody(req) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    req.on('data', (chunk) => chunks.push(chunk));
    req.on('end', () => resolve(Buffer.concat(chunks)));
    req.on('error', reject);
  });
}

const free = (items) => items.filter((item) => !item.reference);

function handle(call) {
  const items = stock[call.product];
  switch (call.action) {
    case 'ping':
      return [200, { ok: true }];
    case 'stock':
      return [200, { products: call.products.filter((id) => stock[id]).map((id) => ({ id, stock: free(stock[id]).length })) }];
    case 'reserve': {
      if (!items) return [404, { error: 'Produto não existe' }];
      // A mesma referência devolve os mesmos itens: a loja pode repetir a chamada.
      const mine = items.filter((item) => item.reference === call.reference);
      const take = free(items).slice(0, Math.max(0, call.quantity - mine.length));
      if (mine.length + take.length < call.quantity) return [409, { error: 'Sem estoque' }];
      for (const item of take) item.reference = call.reference;
      return [200, { items: [...mine, ...take].slice(0, call.quantity).map((item) => item.text) }];
    }
    case 'release': {
      const mine = (items ?? []).filter((item) => item.reference === call.reference);
      for (const item of mine) item.reference = null;
      return [200, { released: mine.length }];
    }
    case 'confirm':
      // Reservado e pago: marque como vendido e guarde o pedido e o e-mail, se precisar.
      return [200, { ok: true }];
    default:
      return [200, { ok: true }];
  }
}

http.createServer(async (req, res) => {
  if (req.method !== 'POST' || req.url !== '/promisse/estoque') return res.writeHead(404).end();
  const rawBody = await readBody(req);
  if (!isValid(rawBody, req.headers)) return res.writeHead(401, { 'content-type': 'application/json' }).end('{"error":"Assinatura inválida"}');
  const [status, body] = handle(JSON.parse(rawBody));
  res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify(body));
}).listen(3000);

Para testar, rode com PROMISSE_STOCK_SECRET=seu_segredo node estoque.js num endereço público e cadastre https://seu-dominio/promisse/estoque.

Ações#

ping#

Enviada pelo botão Testar. Qualquer resposta 2xx conta como funcionando.

JSON
{ "action": "ping" }

stock#

A quantidade de cada produto. A loja chama a cada minuto com todos os IDs ligados, em lotes de até 200, e também na hora de ligar um produto.

JSON
{ "action": "stock", "products": ["licenca", "curso"] }
JSON
{ "products": [{ "id": "licenca", "stock": 12 }, { "id": "curso", "unlimited": true }] }
Campo Regra
stock Quantos itens livres há. Negativo, vazio ou texto conta como zero
unlimited true vende sem limite. A reserve continua sendo chamada a cada venda

ID que não vier na resposta fica esgotado no site.

reserve#

Chamada quando o pedido nasce, antes do pagamento. Separe os itens para aquela referência e devolva o conteúdo que o comprador vai receber.

JSON
{
  "action": "reserve",
  "product": "licenca",
  "quantity": 2,
  "reference": "4a8f0c1e-5b7d-4e8a-9c2f-1d3e5f7a9b0c",
  "order": { "id": "e2b1c4d6-8f0a-4b2c-9d4e-6f8a0b2c4d6e", "number": 100231 }
}
JSON
{ "items": ["AAAA-BBBB-CCCC", "DDDD-EEEE-FFFF"] }
Regra Por quê
Tudo ou nada Sem itens suficientes, responda 409 e não separe nada
Mesma reference, mesmos itens A loja pode repetir a chamada, por exemplo num pagamento que chega depois do Pix vencer
Exatamente quantity itens em items Cada item é um texto de até 10.000 caracteres
Item reservado não volta à venda Até chegar a release com a mesma referência

Num banco de dados, faça a reserva numa transação. Em Postgres:

reserva.sql
BEGIN;
UPDATE itens SET referencia = $1
 WHERE id IN (
   SELECT id FROM itens
    WHERE produto = $2 AND referencia IS NULL
    ORDER BY id LIMIT $3
    FOR UPDATE SKIP LOCKED)
RETURNING texto;
-- Se voltaram menos linhas que $3: ROLLBACK e responda 409. Senão: COMMIT.

release#

O pedido venceu ou foi cancelado: solte os itens daquela referência. Pode chegar mais de uma vez, e também para uma reserva que a sua API concluiu depois de a loja desistir de esperar.

JSON
{ "action": "release", "product": "licenca", "reference": "4a8f0c1e-5b7d-4e8a-9c2f-1d3e5f7a9b0c" }
JSON
{ "released": 2 }

404 também conta como devolvido.

confirm#

O pedido foi pago e entregue ao comprador. Use para marcar os itens como vendidos de vez ou para registrar o comprador. Se não precisar, responda 200 e ignore.

JSON
{
  "action": "confirm",
  "product": "licenca",
  "reference": "4a8f0c1e-5b7d-4e8a-9c2f-1d3e5f7a9b0c",
  "quantity": 2,
  "order": { "id": "e2b1c4d6-8f0a-4b2c-9d4e-6f8a0b2c4d6e", "number": 100231, "email": "cliente@email.com", "paid_at": "2026-11-03T18:10:00.000Z" }
}

Quando a sua API falha#

Ação O que acontece
stock O site mantém a última quantidade e tenta de novo no minuto seguinte
reserve O comprador vê "estoque indisponível, tente de novo". O pedido é cancelado e a loja manda release com a mesma referência
release A loja tenta de novo mais tarde
confirm A loja tenta até 5 vezes. A entrega ao comprador não depende disso

Em Integrações > Estoque por API, cada API mostra se está funcionando e o último erro, com o nome da ação em que ele aconteceu.