# Venty ERP API — guia COMPLETO para agentes de IA > Auto-suficiente e redundante. Contrato machine-readable completo: /openapi.yaml (OpenAPI 3.1, todos os campos). > Objetivo: controlar 100% das ações administrativas de uma loja Venty via API. ## REGRAS DE OURO (todo endpoint) 1. Base: produção https://api.venty.me (chaves vk_live_) | homologação https://api.hml.venty.me (chaves vk_test_). 2. Autentique em POST /api/v2/oauth/token (client_credentials) e envie Authorization: Bearer (~10 min). 3. O TENANT vem do TOKEN — nunca do corpo/URL. Não há tenant_id nos paths v2. 4. Estoque é SET absoluto (não delta): entrada/baixa = GET nível → calcula → PUT nível. 5. Escopos (o dono da loja concede à chave): catalog:read/write, inventory:read/write, customers:read/write, orders:read/write, reports:read, fiscal:read. ## AUTENTICAÇÃO POST /api/v2/oauth/token — form: grant_type=client_credentials, client_id=, client_secret=. → {access_token, token_type:Bearer, expires_in≈600, scope}. ## PRODUTOS (catalog) — TODOS os campos GET /api/v2/catalog/products?limit&cursor (catalog:read) — lista paginada por cursor. POST /api/v2/catalog/products (catalog:write) — cria. Obrigatório: name, slug, sku, price (p/ simple/variation). GET /api/v2/catalog/products/{id} (catalog:read) PUT /api/v2/catalog/products/{id} (catalog:write) — parcial. Campos do produto (write): name, slug, sku, price, promotional_price, short_description, description(HTML), content, image_url, external_image_url, banner_image, active, show_product, stock, stock_status(in_stock|out_of_stock|pre_order), manage_stock, low_stock_threshold, weight(kg), length/width/height(cm), is_virtual, is_downloadable, download_file, product_type(simple|parent|variation), parent_product_id, product_group_id, category_id, brand_id, cost_price, tax_rate(0..100), custom_fields(objeto livre), variation_attributes({variation_group_id:variation_term_id}), product_attributes([{attribute_id, value, role:differentiation|grouping|informative}]). ATENÇÃO no PUT: promotional_price/short_description/weight/dimensões/category_id/brand_id/cost_price/tax_rate são ZERADOS se omitidos (reenvie); name/slug/sku/price/active/stock/manage_stock/product_type são preservados. ## ESTOQUE (multi-local) — in/out GET /api/v2/catalog/products/{id}/inventory (catalog:read) → {stock_mode, manage_stock, stock, reserved, available, levels:[{origin_id, origin_name, origin_type, on_hand, reserved, available}]}. PUT /api/v2/catalog/products/{id}/inventory (inventory:write) — SET absoluto. Global: {"stock":N}. Multi-local: {"levels":[{"origin_id":"","on_hand":N}]}. Opcional {"stock_mode":"global"|"per_location"}. Baixar abaixo do reserved → 409 estoque_abaixo_do_reservado. Modelo: stock_mode global (um número) ou per_location (por LOCALIZAÇÃO). available per_location = Σ(on_hand-reserved). O origin_id dos levels É o id de uma LOCALIZAÇÃO (abaixo). ## LOCALIZAÇÕES (lojas/depósitos) — a base do multi-local (inventory) GET /api/v2/inventory/locations (inventory:read) → {origins:[Location]}. POST /api/v2/inventory/locations (inventory:write) — cria (geocoda CEP). → {id, geocoded}. PUT /api/v2/inventory/locations/{id} (inventory:write) — full-replace. DELETE /api/v2/inventory/locations/{id} (inventory:write). Campos: name(req), type(store|warehouse), address, number, complement, district, city, state, cep(8díg), phone, responsible, service_radius_km, own_delivery_enabled(só store), own_delivery_pricing, external_carriers, pickup_enabled(só store), pickup_instructions, pickup_ready_hours, active, order. O id da localização = origin_id usado no PUT inventory levels. Warehouse: só transportadora (sem pickup/frete próprio). ## TRANSFERÊNCIAS / CROSSDOCK (inventory) GET /api/v2/inventory/transfer-routes (inventory:read) — malha (quem abastece quem). POST /api/v2/inventory/transfer-routes (inventory:write) — {from_origin_id, to_origin_id, lead_time_hours, enabled}. POST /api/v2/inventory/transfer-routes/suggest (inventory:write) — auto-sugere por proximidade. PUT/DELETE /api/v2/inventory/transfer-routes/{id} (inventory:write). GET /api/v2/inventory/transfers (inventory:read) → {transfers:[{id,status,from_name,to_name,order_number,eta,items}]}. POST /api/v2/inventory/transfers/{id}/dispatch (inventory:write) — draft→in_transit (tira estoque da origem; 409 se insuficiente). POST /api/v2/inventory/transfers/{id}/receive (inventory:write) — in_transit→received (põe no destino + re-aponta reserva). POST /api/v2/inventory/transfers/{id}/cancel (inventory:write) — draft→cancelled (libera reserva). ## CATEGORIAS (catalog) GET/POST /api/v2/catalog/categories ; PUT/DELETE /api/v2/catalog/categories/{id} (DELETE=soft). Campos: name(req), slug(req), description, image_url, parent_id, order, active. ## MARCAS (catalog) GET/POST /api/v2/catalog/brands ; PUT/DELETE /api/v2/catalog/brands/{id} (DELETE=soft). Campos: name(req), slug(req), logo_url, description, order, active. ## ATRIBUTOS + VALORES (catalog) — identidade de diferenciação GET/POST /api/v2/catalog/attributes ; PUT/DELETE /api/v2/catalog/attributes/{id}. Campos: name(req), slug(req), layout_type(color_swatch|size_button|radio|dropdown), is_filterable, order, active. GET /api/v2/catalog/attributes/values?attribute_id= ; POST /api/v2/catalog/attributes/values ; PUT/DELETE /api/v2/catalog/attributes/values/{id}. Campos: attribute_id(req), name(req), slug(req), value, hex(#RRGGBB), order, active. (value/hex no PUT: omitir = NULL.) ## VARIAÇÕES (catalog) — grupos/termos (vitrine) GET/POST /api/v2/catalog/variation-groups ; PUT/DELETE /api/v2/catalog/variation-groups/{id}. Campos: name(req), slug(req), display_type, show_in_listing, order, active. GET /api/v2/catalog/variation-terms?variation_group_id= ; POST /api/v2/catalog/variation-terms ; PUT/DELETE /api/v2/catalog/variation-terms/{id}. Campos: variation_group_id(req), name(req), slug(req), value, image, hex, order, active. ## GRUPOS DE PRODUTO (catalog) GET/POST /api/v2/catalog/product-groups ; PUT/DELETE /api/v2/catalog/product-groups/{id}. Campos: name(req), slug(req), description, grouping_attribute_id, active. GET/POST /api/v2/catalog/product-groups/{id}/attributes (liga variation_group ao grupo: body {attribute_id:}). ## MÍDIA / GALERIA (catalog) GET/POST /api/v2/catalog/products/{id}/gallery. POST body: media_type(image|video), url(req), poster_url, is_main, order. ## VENDA-CASADA (catalog) GET/POST /api/v2/catalog/cross-sell/pools ; PUT/DELETE .../{id}. Body: name, active, product_ids[]. ## CLIENTES (customers) — PF/PJ GET /api/v2/customers?updated_since&cursor&limit (customers:read) — lista + delta sync. POST /api/v2/customers (customers:write) — cria (senha CSPRNG gerada; o cliente redefine). Obrigatório email. → {id, email}. GET /api/v2/customers/{id} (customers:read) — perfil completo. PUT /api/v2/customers/{id} (customers:write) — parcial. Campos: email(req no create), name, phone, document(CPF 11 / CNPJ 14 díg), person_type(pf|pj), legal_name(razão social/nome), trade_name(fantasia), state_registration, birth_date(YYYY-MM-DD). ## PEDIDOS (orders) GET /api/v2/orders?updated_since&cursor&limit (orders:read) — lista + delta. GET /api/v2/orders/{id} (orders:read). PATCH /api/v2/orders/{id} (orders:write) — {status?, payment_status?, notes?}. PATCH /api/v2/orders/{id}/status (orders:write) — {status}. cancelled/refunded libera estoque; paid/completed faz commit. POST /api/v2/orders/{id}/ship (orders:write) — {carrier, service_code} (reserva envio; 501 se logistics não configurado). POST /api/v2/orders/{id}/fiscal-document (fiscal:read|orders:write) — anexa NFe emitida pelo ERP {access_key, status, ...}. Obs: NÃO existe "compra/procurement" (ordem de compra a fornecedor). Só pedidos de VENDA. "Fornecedor" interno = outra localização sua (crossdock). ## RELATÓRIOS (reports) GET /api/v2/reports/abc (reports:read) — curva ABC. GET /api/v2/reports/sales (reports:read). ## EXPORTS / FISCAL (bulk) POST /api/v2/exports {dataset:orders|fiscal|customers|products|inventory, format:ndjson|csv|json} → job assíncrono; GET /api/v2/exports/{id}[/download]. ## ERROS 400 {"error","code":"invalid_input","details":{campo:constraint}} | 401/403 problem+json {status,title,detail,trace_id} | 404 {"error"} | 409 {"error":"estoque_abaixo_do_reservado","origin_id"} ou slug/sku duplicado | 429 rate limit. Todo request v2 é auditado (api.call). ## FLUXO: criar produto VARIÁVEL (Cor×Tamanho) ponta-a-ponta 1. POST /catalog/variation-groups {name:"Cor",slug:"cor",display_type:"color",show_in_listing:true} e {name:"Tamanho",slug:"tamanho"} → vg_cor, vg_tam. 2. POST /catalog/variation-terms por opção: {variation_group_id:vg_cor,name:"Vermelho",slug:"vermelho",hex:"#FF0000"} → term ids. 3. POST /catalog/attributes {name:"Cor",slug:"cor",layout_type:"color_swatch"} + /catalog/attributes/values → attr ids (identidade do hash). 4. POST /catalog/products {product_type:"parent",name,slug,sku} (sem price) → parent_id. 5. Por combinação: POST /catalog/products {product_type:"variation",parent_product_id:parent_id,price,sku,slug,variation_attributes:{vg_cor:term_vermelho,vg_tam:term_P},product_attributes:[{attribute_id:attr_cor,value:"vermelho",role:"differentiation"},{attribute_id:attr_tam,value:"P",role:"differentiation"}]}. Combinação duplicada sob o mesmo parent → 409. ## FLUXO: entrada de estoque multi-local (+10 na loja Bonsucesso) inv = GET /catalog/products/{id}/inventory ; lvl = inv.levels.find(origin_id==LOC) ; novo = (lvl?lvl.on_hand:0)+10 ; PUT /catalog/products/{id}/inventory {"levels":[{"origin_id":LOC,"on_hand":novo}]} ; se 409 recarregue.