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.
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.
- Bearer sempre. Envie
Authorization: Bearer <access_token>. - O tenant (a loja) vem do TOKEN — nunca do corpo nem da URL. Não existe
tenant_idnos paths. - Estoque é SET absoluto, não delta. Entrada/baixa = read-modify-write (GET → calcula → PUT).
- 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.
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
| Modo | Onde vive | Disponível | O que você envia no PUT |
|---|---|---|---|
global | Um número na loja (stock/reserved) | stock - reserved | {"stock": N} |
per_location | Por origem (loja/depósito) em inventory_levels | Σ(on_hand - reserved) | {"levels":[{"origin_id":"…","on_hand":N}]} |
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
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)
| Campo | Tipo | |
|---|---|---|
grant_type | string | obrigatório = client_credentials |
client_id | string | obrigatório prefixo da chave |
client_secret | string | obrigatório segredo (1x) |
scope | string | opcional (informativo) |
Resposta 200
access_token | string | JWT Bearer |
token_type | string | Bearer |
expires_in | int | segundos (~600) |
scope | string | escopos 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
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
| Campo | Tipo | |
|---|---|---|
stock_mode | enum | global|per_location |
stock/reserved/available | int | totais globais |
levels[] | array | por 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
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.
reserved → 409 estoque_abaixo_do_reservado (com origin_id no per_location). Nada é alterado.Corpo
stock | int | novo on-hand global (modo global) |
levels[].origin_id | uuid | obrigatório por linha |
levels[].on_hand | int | novo on-hand absoluto na origem |
stock_mode | enum | opcional; 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
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)
name | string | obrig. |
slug | string | obrig. único |
sku | string | obrig. único |
price | number | R$ (obrig. p/ simple) |
stock | int | estoque inicial (global) |
manage_stock | bool | default 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
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
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
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
| HTTP | Quando | Corpo |
|---|---|---|
| 400 | Corpo/campo inválido | {"error","code":"invalid_input","details":{campo:constraint}} |
| 401/403 | Sem token / escopo insuficiente | problem+json {status,title,detail,trace_id} |
| 404 | Produto não existe na loja | {"error":"product not found"} |
| 409 | Estoque abaixo do reservado | {"error":"estoque_abaixo_do_reservado","origin_id":"…"} |
| 429 | Rate limit | — |
api.call) com linhagem máquina→dono. Guarde o trace_id das respostas de erro para suporte.