Venty API — Catálogo & Estoque para ERP

API máquina-a-máquina para o seu ERP criar/atualizar produtos e dar entrada e baixa de estoque (in/out) numa loja Venty. Autenticação OAuth2, exemplos prontos em Go, Rust e Node.js. Este documento é auto-suficiente e propositalmente redundante — cada endpoint repete o essencial para você (ou um agente) entender isolado.

Base URL. Produção https://api.venty.me (chaves vk_live_…) · Homologação/teste https://api.hml.venty.me (chaves vk_test_…). Todos os caminhos abaixo são relativos à base escolhida; o token vale para o host onde foi emitido.
Regras de ouro (valem em TODO endpoint):
  1. Bearer sempre. Envie Authorization: Bearer <access_token>.
  2. O tenant (a loja) vem do TOKEN — nunca do corpo nem da URL. Não existe tenant_id nos paths.
  3. Estoque é SET absoluto, não delta. Entrada/baixa = read-modify-write (GET → calcula → PUT).
  4. Escopos. Escrever produto = catalog:write; escrever estoque = inventory:write; ler = catalog:read.

Fluxo de autenticação

No backoffice da loja, o dono cria uma chave de API (client_id + client_secret — o segredo aparece uma única vez) com os escopos mínimos. Seu ERP troca esse par por um access_token Bearer de ~10 min e o usa nas chamadas. Renove quando faltar < 60s para expirar.

Fluxo: client_credentials → access_token (JWT) → Authorization: Bearer. Não há X-API-Key em request; o segredo só vai ao endpoint de token.

Modelo de estoque

ModoOnde viveDisponívelO que você envia no PUT
globalUm número na loja (stock/reserved)stock - reserved{"stock": N}
per_locationPor origem (loja/depósito) em inventory_levelsΣ(on_hand - reserved){"levels":[{"origin_id":"…","on_hand":N}]}
Trava anti-venda-a-descoberto: você NÃO consegue baixar o estoque abaixo do que já está reservado por pedidos em aberto → 409 estoque_abaixo_do_reservado.
manage_stock=false = produto não rastreado (sempre disponível, available=null). Definir stock_mode liga manage_stock=true.

Emitir token de máquina

POST /api/v2/oauth/token · sem auth prévia

Troca client_id + client_secret por um access_token Bearer (~10 min). Aceita form-urlencoded (padrão OAuth) ou JSON. Os escopos efetivos vêm do grant da chave.

Corpo (form)

CampoTipo
grant_typestringobrigatório = client_credentials
client_idstringobrigatório prefixo da chave
client_secretstringobrigatório segredo (1x)
scopestringopcional (informativo)

Resposta 200

access_tokenstringJWT Bearer
token_typestringBearer
expires_inintsegundos (~600)
scopestringescopos concedidos
# form-urlencoded (padrão OAuth)
curl -X POST https://api.venty.me/api/v2/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=vk_live_ab12cd34 \
  -d client_secret=$VENTY_SECRET \
  -d 'scope=catalog:read inventory:write'
package main
import ("net/http"; "net/url"; "io"; "fmt")
func token() string {
  form := url.Values{"grant_type":{"client_credentials"},
    "client_id":{"vk_live_ab12cd34"}, "client_secret":{secret}}
  r, _ := http.PostForm("https://api.venty.me/api/v2/oauth/token", form)
  defer r.Body.Close(); b, _ := io.ReadAll(r.Body)
  // parse {"access_token": "..."} → devolve o token
  return parseAccessToken(b)
}
// reqwest = { version = "0.12", features=["json"] }
let res = client.post("https://api.venty.me/api/v2/oauth/token")
    .form(&[("grant_type","client_credentials"),
            ("client_id","vk_live_ab12cd34"),
            ("client_secret",&secret)])
    .send().await?.json::<Token>().await?;
let access = res.access_token;
const body = new URLSearchParams({
  grant_type:'client_credentials',
  client_id:'vk_live_ab12cd34', client_secret:process.env.VENTY_SECRET });
const r = await fetch('https://api.venty.me/api/v2/oauth/token',{method:'POST',body});
const { access_token } = await r.json();
{"access_token":"eyJ…","token_type":"Bearer","expires_in":600,"scope":"catalog:read inventory:write"}

Ler estoque do produto catalog:read

GET /api/v2/catalog/products/{id}/inventory

Passo 1 do in/out. Retorna o modo de estoque, os totais globais e uma linha por origem ativa. Use o on_hand/available atuais para calcular o novo total antes do PUT. Lembre: Bearer e o tenant vem do token.

Resposta 200

CampoTipo
stock_modeenumglobal|per_location
stock/reserved/availableinttotais globais
levels[]arraypor origem: origin_id, origin_name, origin_type, on_hand, reserved, available
curl https://api.venty.me/api/v2/catalog/products/$PID/inventory \
  -H "Authorization: Bearer $TOKEN"
req, _ := http.NewRequest("GET",
  base+"/api/v2/catalog/products/"+pid+"/inventory", nil)
req.Header.Set("Authorization", "Bearer "+token)
res, _ := http.DefaultClient.Do(req) // → 200 Inventory JSON
let inv = client.get(format!("{base}/api/v2/catalog/products/{pid}/inventory"))
    .bearer_auth(&token).send().await?.json::<Inventory>().await?;
const inv = await (await fetch(
  `${base}/api/v2/catalog/products/${pid}/inventory`,
  { headers:{ Authorization:`Bearer ${token}` } })).json();
{"stock_mode":"per_location","manage_stock":true,"stock":0,"reserved":0,"available":0,
 "levels":[{"origin_id":"eae8…","origin_name":"Bonsucesso","origin_type":"store","on_hand":40,"reserved":3,"available":37}]}

Definir estoque — entrada/saída inventory:write

PUT /api/v2/catalog/products/{id}/inventory

Passo 2 do in/out. Escreve o estoque absoluto (SET). Entrada = número maior; baixa = número menor. Modo global{"stock":N}; modo per_location{"levels":[{"origin_id,"on_hand":N}]} (só as origens enviadas mudam). Opcional stock_mode troca o modo. Bearer + tenant do token.

Baixar abaixo do reserved409 estoque_abaixo_do_reservado (com origin_id no per_location). Nada é alterado.

Corpo

stockintnovo on-hand global (modo global)
levels[].origin_iduuidobrigatório por linha
levels[].on_handintnovo on-hand absoluto na origem
stock_modeenumopcional; troca o modo
# ENTRADA por loja: Bonsucesso passa a 45 on_hand
curl -X PUT https://api.venty.me/api/v2/catalog/products/$PID/inventory \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"levels":[{"origin_id":"eae8154e-…","on_hand":45}]}'
// read-modify-write: leia (GET) → novo = atual + entrada → PUT
body := `{"levels":[{"origin_id":"eae8154e-…","on_hand":45}]}`
req, _ := http.NewRequest("PUT", base+"/api/v2/catalog/products/"+pid+"/inventory",
  strings.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req) // 200 = ok; 409 = abaixo do reservado
let res = client.put(format!("{base}/api/v2/catalog/products/{pid}/inventory"))
    .bearer_auth(&token)
    .json(&json!({"levels":[{"origin_id":oid,"on_hand":45}]}))
    .send().await?;
if res.status()==409 { /* abaixo do reservado */ }
const res = await fetch(`${base}/api/v2/catalog/products/${pid}/inventory`,{
  method:'PUT',
  headers:{ Authorization:`Bearer ${token}`, 'Content-Type':'application/json' },
  body: JSON.stringify({ levels:[{ origin_id:oid, on_hand:45 }] }) });
if (res.status===409) { /* estoque_abaixo_do_reservado */ }
# 200: devolve o estoque recalculado (mesmo shape do GET)
{"stock_mode":"per_location","levels":[{"origin_id":"eae8…","on_hand":45,"reserved":3,"available":42}]}

Criar produto catalog:write

POST /api/v2/catalog/products

Cria um produto na loja do token. Mínimo: name, slug, sku e (para simple) price. Não é idempotente — reenvio duplica; slug/sku repetido → 409. Para estoque por-loja, crie e depois use o PUT de estoque.

Corpo (principais)

namestringobrig.
slugstringobrig. único
skustringobrig. único
pricenumberR$ (obrig. p/ simple)
stockintestoque inicial (global)
manage_stockbooldefault true
curl -X POST https://api.venty.me/api/v2/catalog/products \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Placa ST 13","slug":"placa-st-13","sku":"PLST13","price":18.03,"stock":120}'
body := `{"name":"Placa ST 13","slug":"placa-st-13","sku":"PLST13","price":18.03,"stock":120}`
req, _ := http.NewRequest("POST", base+"/api/v2/catalog/products", strings.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req) // 201 {"id":"…"}; 409 slug/sku
let r = client.post(format!("{base}/api/v2/catalog/products"))
    .bearer_auth(&token)
    .json(&json!({"name":"Placa ST 13","slug":"placa-st-13",
        "sku":"PLST13","price":18.03,"stock":120}))
    .send().await?; // 201 { id }
const r = await fetch(`${base}/api/v2/catalog/products`,{ method:'POST',
  headers:{ Authorization:`Bearer ${token}`, 'Content-Type':'application/json' },
  body: JSON.stringify({ name:'Placa ST 13', slug:'placa-st-13',
    sku:'PLST13', price:18.03, stock:120 }) });
const { id } = await r.json(); // 201
{"id":"88cfc492-ad6d-405c-8bbf-d582b3e21b83"}

Listar produtos catalog:read

GET /api/v2/catalog/products?limit=50&cursor=…

Paginado por cursor opaco. Resposta: {"data":[Product…],"next_cursor":"…"|null}. Passe next_cursor em cursor para a próxima página. limit máx 250. Bearer + tenant do token.

curl "https://api.venty.me/api/v2/catalog/products?limit=100" -H "Authorization: Bearer $TOKEN"
let cursor, all=[];
do {
  const u = new URL(`${base}/api/v2/catalog/products`);
  u.searchParams.set('limit','250'); if(cursor) u.searchParams.set('cursor',cursor);
  const p = await (await fetch(u,{headers:{Authorization:`Bearer ${token}`}})).json();
  all.push(...p.data); cursor = p.next_cursor;
} while (cursor);

Ler produto catalog:read

GET /api/v2/catalog/products/{id}

Detalhe do produto (id, name, slug, sku, price, active, stock, stock_mode, product_type, …). 404 se não existir na loja. Bearer + tenant do token.

curl https://api.venty.me/api/v2/catalog/products/$PID -H "Authorization: Bearer $TOKEN"

Atualizar produto catalog:write

PUT /api/v2/catalog/products/{id}

Atualização parcial. Atenção: alguns campos são preservados quando omitidos (name, slug, sku, price, active, stock, manage_stock) e outros são zerados quando omitidos (promotional_price, weight, category_id, …) — envie o conjunto completo desses para não limpar sem querer.

curl -X PUT https://api.venty.me/api/v2/catalog/products/$PID \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"price":19.90,"active":true}'   # → {"ok":true}

Erros

HTTPQuandoCorpo
400Corpo/campo inválido{"error","code":"invalid_input","details":{campo:constraint}}
401/403Sem token / escopo insuficienteproblem+json {status,title,detail,trace_id}
404Produto não existe na loja{"error":"product not found"}
409Estoque abaixo do reservado{"error":"estoque_abaixo_do_reservado","origin_id":"…"}
429Rate limit
Todo request v2 é auditado (api.call) com linhagem máquina→dono. Guarde o trace_id das respostas de erro para suporte.