Venty API — Administração completa para ERP
API máquina-a-máquina para o seu ERP controlar 100% das ações administrativas de uma loja Venty: produtos (todos os campos), estoque multi-local, localizações, transferências, categorias, marcas, atributos, variações, clientes (PF/PJ) e pedidos. Autenticação OAuth2, exemplos em Go, Rust e Node.js. Auto-suficiente e redundante — cada endpoint entende-se isolado. Mapa completo em Superfície completa · contrato em /openapi.yaml.
https://api.venty.me (chaves venty_ci_live_…) ·
Homologação/teste https://api.hml.venty.me (chaves venty_ci_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 escolhendo um escopo base
(ERP/Pedidos, Catálogo & estoque, Fiscal, Somente leitura ou Acesso total). Gera um par:
CI (client_id, público, 64 chars, prefixo venty_ci_live_/venty_ci_test_) e
CS (client_secret, secreto, 256 chars, prefixo venty_cs_live_/venty_cs_test_) —
o CS aparece uma única vez. 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. O id (UUID) da chave é só para gerir/revogar — não é o client_id.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.Com N lojas, qual atende o pedido?
Não existe uma “loja selecionada” fixa. A origem é resolvida por item, no momento da compra:
| Cenário | Quem escolhe a loja | Regra | Como ler na API |
|---|---|---|---|
Entrega (delivery) |
Backend (automático) | Prefere a origem que o cliente indicou na opção de frete; se ela não cobre a quantidade, cai para a de maior disponível que cobre. ORDER BY (origin=preferida) DESC, (on_hand-reserved) DESC. Itens diferentes podem sair de lojas diferentes. |
items[].stock_origin_id + stock_origin_name |
Retirada (pickup) |
Cliente | A loja é a que o cliente escolheu na opção de retirada. | fulfillment.pickup_location |
| Crossdock | Cliente (loja A) + backend (supplier B) | Cliente retira na loja A; o backend acha a loja B da malha com estoque e transfere A←B. Retirada agendada. | fulfillment.is_crossdock, scheduled_for, pickup_location |
| Estoque global | — | Sem conceito de loja: pool único. | stock_origin_id = null |
Tudo no detalhe GET /api/v2/orders/{id}:
{
"fulfillment": { "type": "pickup", "is_crossdock": false, "scheduled_for": null,
"pickup_location": { "name": "Loja Centro", "address": "Rua X, 3", "instructions": "" } },
"items": [
{ "product_id": "…", "sku": "DW00180", "quantity": 2,
"stock_origin_id": "7937a17e-…", "stock_origin_name": "Loja Alcântara", "stock_status": "reserved" }
]
}
fulfillment.pickup_location; entrega → items[].stock_origin_id/name.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 CI (64 chars, venty_ci_live_/venty_ci_test_) |
client_secret | string | obrigatório CS (256 chars, venty_cs_…, mostrado 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=venty_ci_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":{"venty_ci_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","venty_ci_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:'venty_ci_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 — TODOS os campos
Obrigatórios: name, slug, sku, price (simple/variation). Além destes, o produto aceita o conjunto completo:
| Preço/comercial | ||
price, promotional_price, cost_price | number | venda, promo, custo (margem) |
tax_rate | number | % imposto (fiscal) |
| Descrição/mídia | ||
short_description, description, content, banner_image, image_url, external_image_url | string | textos (HTML) + imagens |
| Logística | ||
weight, length, width, height | number | peso (kg) + dimensões (cm) |
| Estoque/estado | ||
stock, stock_status, manage_stock, low_stock_threshold, active, show_product | — | estoque inicial + flags |
| Classificação/relacionamento | ||
category_id, brand_id, product_group_id | uuid | categoria/marca/grupo |
| Tipo/variação/extras | ||
product_type, parent_product_id, variation_attributes, product_attributes | — | simple/parent/variation + eixos |
is_virtual, is_downloadable, download_file, custom_fields | — | virtual/digital + campos livres |
ProductWrite).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}
Localizações (lojas/depósitos) inventory:read/write
Uma localização é uma loja ou depósito. O `id` da localização é o `origin_id` usado nos níveis de estoque (multi-local). Crie as localizações aqui e depois escreva o estoque por localização no PUT de inventário. Bearer + tenant do token.
Campos (criar/atualizar)
name | string | obrig. |
type | enum | store|warehouse |
cep | string | 8 díg — geocoda no save |
address,number,city,state… | string | endereço |
pickup_enabled | bool | retirada (só store) |
own_delivery_enabled | bool | frete próprio (só store) |
# criar loja com retirada curl -X POST https://api.venty.me/api/v2/inventory/locations \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"name":"Loja Centro","type":"store","cep":"20221030","city":"Rio de Janeiro","state":"RJ","pickup_enabled":true,"pickup_ready_hours":24}' # → {"id":"<location id = origin_id>","geocoded":true}
body := `{"name":"Loja Centro","type":"store","cep":"20221030","pickup_enabled":true}` req, _ := http.NewRequest("POST", base+"/api/v2/inventory/locations", strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) // 201 {id, geocoded}
const r = await fetch(`${base}/api/v2/inventory/locations`,{ method:'POST', headers:{ Authorization:`Bearer ${token}`, 'Content-Type':'application/json' }, body: JSON.stringify({ name:'Loja Centro', type:'store', cep:'20221030', pickup_enabled:true }) }); const { id } = await r.json(); // id = origin_id p/ os níveis de estoque
Clientes (PF/PJ) customers:read/write
Cadastro completo de clientes, PF ou PJ. No create, a senha de login é gerada (o cliente pode redefinir depois) — o ERP não precisa dela. Bearer + tenant do token.
Campos
email | string | obrig. no create |
name, phone | string | contato |
person_type | enum | pf|pj |
document | string | CPF (11) / CNPJ (14) díg |
legal_name, trade_name | string | razão social / fantasia |
state_registration | string | inscrição estadual |
birth_date | date | YYYY-MM-DD |
# criar cliente PJ curl -X POST https://api.venty.me/api/v2/customers \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"email":"compras@acme.com","name":"ACME","person_type":"pj","document":"12345678000199","legal_name":"ACME LTDA","trade_name":"ACME"}' # → {"id":"…","email":"compras@acme.com"}; GET/PUT /api/v2/customers/{id}
body := `{"email":"compras@acme.com","person_type":"pj","document":"12345678000199","legal_name":"ACME LTDA"}` req, _ := http.NewRequest("POST", base+"/api/v2/customers", strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) // 201 {id, email}
const r = await fetch(`${base}/api/v2/customers`,{ method:'POST', headers:{ Authorization:`Bearer ${token}`, 'Content-Type':'application/json' }, body: JSON.stringify({ email:'compras@acme.com', person_type:'pj', document:'12345678000199', legal_name:'ACME LTDA' }) });
Mídia / imagens — referência externa (não re-suba a cada update)
Toda imagem tem duas identidades: o id (UUID do Venty) e o
external_id (a sua chave do ERP). Suba os bytes uma vez; depois
referencie por external_id/id/URL — reordenar e atualizar não re-sobem imagem.
- Suba a imagem em
POST /api/v2/catalog/media— por bytes (multipartfile) ou por URL pública ({url, external_id}; o Venty baixa e re-hospeda). Retorna{id, url, checksum, deduped}. Dedup por sha256: os mesmos bytes viram um só asset (deduped:true). - Declare a galeria em
PUT /api/v2/catalog/products/{id}/gallerycom a lista final (porexternal_id). O Venty faz upsert + reordena + define capa + remove os ausentes numa call. - Reordenar/atualizar = reenviar o mesmo PUT com os mesmos
external_id/checksumna nova ordem. Fast-path:checksumigual ao gravado ⇒ a URL não é re-baixada.
Sobe bytes OU URL. external_id opcional = sua referência ao asset. Só imagem raster (PNG/JPEG/WEBP/GIF); a URL passa por fetch protegido contra SSRF (só http(s) para IP público).
# por URL (o Venty baixa e re-hospeda + deduplica) curl -X POST https://api.venty.me/api/v2/catalog/media \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"url":"https://erp.exemplo/img/1001.jpg","external_id":"MATNR-1001"}' # → {"id":"…","url":"/media/…","checksum":"…","deduped":false}
Sincroniza a galeria inteira de forma declarativa. rehost (default true) baixa URLs http(s);
prune (default true) remove as imagens ERP-owned ausentes. Só mexe em itens com external_id
— imagens adicionadas por humanos ficam intactas.
curl -X PUT https://api.venty.me/api/v2/catalog/products/$PID/gallery \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"items":[
{"external_id":"MATNR-1001","url":"https://erp/1001.jpg","checksum":"","is_main":true},
{"external_id":"MATNR-1002","media_id":""}
]}'
# → {"synced":2,"pruned":0}
Superfície completa — todos os recursos via API
100% das ações administrativas seguras via token de máquina. O contrato exaustivo (todos os campos, todos os endpoints) está em /openapi.yaml e /llms.txt.
| Recurso | Escopo | Endpoints |
|---|---|---|
| Produtos (todos os campos + variações) | catalog:read/write | GET/POST/PUT /catalog/products[/:id] |
| Estoque (global + multi-local) | catalog:read / inventory:write | GET/PUT /catalog/products/:id/inventory |
| Localizações (lojas/depósitos) | inventory:read/write | GET/POST/PUT/DELETE /inventory/locations |
| Transferências (malha + crossdock) | inventory:read/write | /inventory/transfer-routes, /inventory/transfers/:id/{dispatch,receive,cancel} |
| Categorias | catalog:read/write | GET/POST/PUT/DELETE /catalog/categories |
| Marcas | catalog:read/write | GET/POST/PUT/DELETE /catalog/brands |
| Atributos + valores | catalog:read/write | /catalog/attributes, /catalog/attributes/values |
| Variações (grupos + termos) | catalog:read/write | /catalog/variation-groups, /catalog/variation-terms |
| Grupos de produto | catalog:read/write | /catalog/product-groups[/:id/attributes] |
| Mídia/galeria | catalog:read/write | /catalog/products/:id/gallery |
| Venda-casada | catalog:read/write | /catalog/cross-sell/pools |
| Clientes (PF/PJ) | customers:read/write | GET/POST/PUT /customers[/:id] |
| Pedidos (status/fulfillment) | orders:read/write | GET /orders[/:id], PATCH status, POST ship, fiscal-document |
| Relatórios | reports:read | /reports/abc, /reports/sales |
| Exports (bulk) | por dataset | /exports[/:id/download] |
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.