API de Integração Revo — guia do integrador
Versão 2 · atualizado em 2026-08-25
Este documento é a referência campo a campo das rotas de escrita. Ele
acompanha o manual-da-integradora.md, que é o documento enviado à integradora
e cobre o processo de homologação, os webhooks de saída, os erros e a verdade
sobre o sandbox.
Onde os dois divergirem, vale o manual — ele foi conferido contra o código em 2026-08-24. Os pontos já corrigidos lá estão marcados aqui.
Sumário
- O contrato em uma página
- Ambientes e chaves
- Autenticação
- Os quatro conceitos que explicam tudo
- Rota: cadastro de produtos
- Rota: preço
- Rota: estoque
- Rotas de diagnóstico
- O envelope de resposta
- Códigos de erro e o que fazer em cada um
- O de/para de categorias, marcas e grade
- Ritmo de sincronização recomendado
- Limites
- Checklist da primeira integração
1. O contrato em uma página
- Base:
{URL_DA_API}/api/v1/integration— a plataforma informa a URL base junto com a chave. Nos exemplos deste documento ela aparece comohttps://api.revo.com.br; em desenvolvimento local éhttp://localhost:4380. - Autenticação: header
X-Api-Key(chave escopada) + headerX-Tenant-Id(id da loja). - Três canais, três rotas:
| Canal | Rota | Frequência típica |
|---|---|---|
| Cadastro | POST /catalog/products |
quando o produto muda |
| Preço | POST /catalog/prices |
várias vezes ao dia |
| Estoque | POST /catalog/stock |
várias vezes ao dia |
- Tudo é lote, e a resposta traz o resultado de cada item. Um item ruim não derruba o lote.
- Tudo é idempotente: você manda o estado completo, sempre; nós decidimos se mudou. Não precisamos que você saiba "o que mudou desde ontem".
- Você identifica os registros pelo id que eles têm no SEU sistema
(
externalId). Nunca precisa guardar id nosso.
Uma chamada mínima, completa:
curl -X POST https://api.revo.com.br/api/v1/integration/catalog/stock \
-H 'X-Api-Key: revo_live_R0hZa1B3TnZ...' \
-H 'X-Tenant-Id: f721f819-0279-5e3b-acf1-979cfdc2c712' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: estoque-2026-08-15T14:05:00Z' \
-d '{
"origin": "vetor",
"items": [
{ "externalId": "9000312700002", "balance": 3 },
{ "externalId": "9000312700019", "balance": 0 }
]
}'
2. Ambientes e chaves
Dois ambientes, e a diferença é visível a olho nu
| Produção | Sandbox | |
|---|---|---|
| Prefixo da chave | revo_live_… |
revo_test_… |
| Loja | a loja real do lojista | uma loja de testes, com dados descartáveis |
| Contratos | — | exatamente os mesmos |
| Validações | — | exatamente as mesmas |
Detalhado no manual. O quadro acima descreve o MECANISMO. O AMBIENTE passou a existir na versão 2 do
manual-da-integradora.md: há loja de sandbox provisionável por integradora, com o catálogo de demonstração, chaverevo_test_, um provedor de pagamento de mentira (que torna os quatro webhooks de pedido exercitáveis) e umPOST /sandbox/resetque a própria integradora dispara. O que continua NÃO existindo: endereço de sandbox separado (a loja é distinguida peloX-Tenant-Id), cadastro público para criar a loja e rota para LER um pedido. Leia a seção "Sandbox" do manual antes de planejar o teste.
Uma chave de sandbox não alcança a loja de produção, e uma chave de produção
não alcança o sandbox: as duas combinações retornam 401. Isso é conferido
no banco (constraint + trigger) e a cada requisição — não depende de ninguém
lembrar.
O prefixo aparece nos nossos logs e na tela do lojista. Quando precisar de
suporte, mande o prefixo (revo_live_R0hZa1B3) — nunca a chave inteira.
Como obter a chave
Quem emite é o lojista, no painel da loja: Integrações → Integradoras →
(a sua empresa) → Configurar → Emitir chave. A tela já vem com os escopos que a
sua integração usa marcados, e mostra o X-Tenant-Id na mesma página.
A chave em claro aparece uma única vez, na tela de criação. Guardamos apenas o hash; não há como recuperá-la depois. Se perder, o lojista revoga e emite outra.
Na sua loja de sandbox a chave já vem emitida junto com a loja (ver o manual). Na loja do lojista, quem emite é ele. Comece pela de sandbox.
Escopos
Peça só o que for usar. Um incidente com uma chave que só escreve preço não pode apagar o catálogo.
| Escopo | Permite |
|---|---|
catalog:write |
POST /catalog/products |
price:write |
POST /catalog/prices |
stock:write |
POST /catalog/stock |
mapping:read |
GET /mappings, GET /quarantine |
orders:read |
nada, hoje — nenhuma rota exige este escopo (corrigido no manual) |
Chamar uma rota sem o escopo correspondente devolve 403.
3. Autenticação
Todo request precisa de dois headers:
X-Api-Key: revo_live_R0hZa1B3TnZQY2Z3RmpLbUxxTXhBdw
X-Tenant-Id: f721f819-0279-5e3b-acf1-979cfdc2c712
X-Api-Key— a chave que o lojista emitiu.X-Tenant-Id— o id da loja. O lojista encontra em Integrações → Integradoras → (a sua empresa) → Configurar. Ele precisa ser o da MESMA loja da chave; divergência é401. Ausente ou de loja inexistente é404comcode: "tenant_not_found"— não401.
Não há OAuth, não há refresh, a chave não expira. Ela vale até o lojista revogá-la.
Nunca coloque a chave em query string, em log de aplicação ou em URL.
4. Os quatro conceitos que explicam tudo
4.1 Identidade externa
Cada registro que você manda carrega o id que ele tem no seu sistema, no
campo externalId. A chave real é a trinca (loja, origem, externalId).
Consequência prática: você nunca guarda id nosso. Mandar o mesmo
externalId de novo atualiza o registro; nunca cria um segundo. Se você já
mantém uma tabela de/para com o id do parceiro (como faz com Bling), pode
aposentá-la para a Revo.
origin é o nome do seu sistema, em minúsculas, estável para sempre — para a
Vetor, "vetor". Ele existe porque uma loja pode receber dados de mais de uma
origem, e um mesmo código de produto pode significar coisas diferentes em cada
uma.
4.2 Idempotência em dois níveis
Nível conteúdo. Guardamos um hash do item. Se você reenviar o mesmo produto
sem nada mudar, a resposta é sem_mudanca e nada é escrito — nenhum evento,
nenhuma reindexação. Você pode reenviar o catálogo inteiro a cada ciclo sem
culpa; é para isso que o campo existe.
A ordem das chaves dentro de
attributesnão afeta o hash. Você pode serializar do jeito que for mais fácil.
Nível transporte. O header opcional Idempotency-Key protege contra o caso
"mandei, a conexão caiu, não sei se entrou". Reenviar o mesmo corpo com a
mesma chave devolve a resposta original, sem reprocessar. Reenviar um corpo
diferente com a mesma chave devolve 409 — é sinal de bug do lado de cá ou
de lá, e é melhor doer.
Use algo estável e único por lote, por exemplo
vetor-produtos-2026-08-15T14:05:00Z.
4.3 Taxonomia nossa, com de/para
As categorias do seu sistema são as categorias do seu sistema — nasceram para relatório fiscal e controle de estoque. As da loja nasceram para navegação e SEO. Não adotamos as suas, e também não criamos categoria a partir dos seus dados.
O que existe é um de/para por loja: código de categoria da sua origem → categoria da loja. Quem preenche é o lojista, no painel dele. Você não precisa saber nada da árvore dele.
O que você precisa fazer: mandar, junto com o código, o rótulo que aquilo tem no seu sistema. É o que aparece na tela do lojista para ele decidir. Sem o rótulo, ele vê "1/6/5" e não sabe o que é.
Produto cuja categoria (ou marca, ou valor de grade) ainda não foi mapeada não é recusado: ele entra em quarentena. Ou seja: o produto existe, o lojista o vê no painel marcado como "aguardando categoria", e ele não aparece na loja. Quando o lojista mapear, o produto sobe sozinho — você não precisa reenviar.
Detalhes e exemplos na seção 11.
4.4 Canais separados
Cadastro, preço e estoque são rotas diferentes de propósito. Cadastro muda pouco; preço e estoque mudam o dia inteiro. Se fossem a mesma rota, corrigir uma quantidade obrigaria você a reenviar nome, descrição, grade e fotos do produto inteiro — e é assim que sincronização vira lentidão.
Ordem obrigatória: o cadastro é quem apresenta os externalId das
variações. Preço e estoque de uma variação que o cadastro nunca mandou são
recusados com variacao_desconhecida.
5. Rota: cadastro de produtos
POST /api/v1/integration/catalog/products
Escopo: catalog:write
Corpo
{
"origin": "vetor",
"items": [ /* até 500 produtos */ ]
}
Um produto
Exemplo real: o produto 3127 da Up3 Esportes, com os dados como eles existem no ERP.
{
"externalId": "3127",
"name": "Legging Eterna Alto Giro Cós Sustentação Feminina",
"description": "<p>A Legging Eterna Cós Sustentação é perfeita para quem busca firmeza, conforto e performance em uma só peça.</p><p>84% POLIAMIDA / 16% ELASTANO · Tecnologia UV50</p>",
"active": true,
"category": { "code": "1/6/5", "label": "FEMININO > LEGGING > FITNESS" },
"brand": { "code": "ALTO GIRO", "label": "ALTO GIRO" },
"attributes": { "ncm": "61091000", "referencia": "101312" },
"seoTitle": "Legging Eterna Alto Giro Cós Sustentação Feminina",
"seoDescription": "Legging de alta compressão e cobertura, com proteção UV50.",
"variants": [
{
"externalId": "9000312700002",
"sku": "9000312700002",
"gtinEan": "9000312700002",
"options": [
{ "group": "tamanho", "code": "2", "label": "P" },
{ "group": "cor", "code": "1", "label": "PRETO" }
],
"weightG": 200, "heightCm": 4, "widthCm": 22, "lengthCm": 26,
"active": true
},
{
"externalId": "9000312700019",
"sku": "9000312700019",
"gtinEan": "9000312700019",
"options": [
{ "group": "tamanho", "code": "3", "label": "M" },
{ "group": "cor", "code": "1", "label": "PRETO" }
],
"weightG": 200, "heightCm": 4, "widthCm": 22, "lengthCm": 26,
"active": true
}
],
"images": [
{
"url": "https://storage.googleapis.com/vetorapp0.appspot.com/UPMD-6113/31271_1752524750344.jpg",
"alt": "Legging Eterna Alto Giro preta, vista frontal",
"sortOrder": 0,
"variantExternalId": "9000312700002"
}
]
}
Campos
Produto
| Campo | Obrig. | Notas |
|---|---|---|
externalId |
✔ | O id do produto no seu sistema. Estável para sempre. |
name |
✔ | Como deve aparecer na loja. Evite mandar "REF:xxx" e faixa de grade dentro do nome — o nome é o título da página. |
description |
Aceita HTML simples (<p>, <br>, <ul>, <strong>). |
|
active |
false arquiva o produto (some da loja, histórico preservado). Ausente = true. |
|
category.code |
✔ | O código da categoria no seu sistema. Ver seção 11. |
category.label |
Como você chama aquilo. Fortemente recomendado. | |
brand.code / brand.label |
Mesma lógica da categoria. Opcional. | |
attributes |
Objeto livre ({"ncm": "...", "referencia": "..."}). Fica guardado no produto. |
|
seoTitle, seoDescription |
Se você não mandar, a loja deriva do nome. | |
variants |
✔ | Pelo menos uma. Produto sem grade também tem uma variação. |
images |
Até 20. |
Não existe campo de slug/URL amigável: nós geramos, e o slug de um produto que já existe nunca muda — trocá-lo quebraria a URL indexada.
Variação
| Campo | Obrig. | Notas |
|---|---|---|
externalId |
✔ | Id da variação no seu sistema. É por ele que preço e estoque chegam. |
sku |
Código do SKU na loja. Ausente = usamos o externalId. Precisa ser único na loja. |
|
gtinEan |
O código de barras de fabricante, quando houver. Se o seu código de barras é interno (gerado por você, ex. prefixo 900), mande-o em externalId/sku e deixe gtinEan vazio — um EAN interno declarado como GTIN atrapalha a loja em buscadores e marketplaces. |
|
options |
✔ (se houver grade) | Os eixos da variação. Ver abaixo. |
weightG |
Peso em GRAMAS, inteiro. Usado para cotar frete. | |
heightCm, widthCm, lengthCm |
Em centímetros. Usados para cotar frete. | |
active |
false desativa aquela variação. O SKU nunca é apagado (carrega histórico de venda). |
Peso e dimensão importam. Sem eles a loja não consegue cotar frete e o checkout do consumidor trava. Se o seu cadastro tem esses campos zerados, resolva isso antes de ir para produção.
Eixos da grade (options)
Cada variação declara um valor para cada eixo:
"options": [
{ "group": "tamanho", "code": "2", "label": "P" },
{ "group": "cor", "code": "1", "label": "PRETO" }
]
group— o eixo. Use um nome curto e estável (tamanho,cor,voltagem).code— o valor no seu sistema. Pode ser um número, um código, o que for.label— como você chama aquele valor. Vira a sugestão para o lojista.
Regras:
- Todas as variações do produto precisam declarar os mesmos eixos. Faltando
um, o item é rejeitado com
grade_incompleta. - Se o produto tem mais de uma variação, ele precisa ter pelo menos um eixo —
senão não há como distingui-las (
grade_ausente). - Se a sua grade tem mais eixos que a loja precisa (por exemplo três códigos de cor para uma peça tricolor), combine-os em um eixo só do seu lado. O que distingue duas variações na loja é a combinação dos eixos que você mandar; dois registros seus que caiam na mesma combinação viram um só, e o estoque do segundo somem no do primeiro.
Imagens
| Campo | Obrig. | Notas |
|---|---|---|
url |
✔ | http ou https, público. Nós baixamos o arquivo para o nosso storage. |
alt |
✔ | Descrição da imagem em uma frase. É obrigatório. |
sortOrder |
Ordem na galeria. Ausente = ordem do array. | |
variantExternalId |
Associa a foto a uma variação (ex.: a foto da cor preta). |
Sobre o alt: é requisito de produto da plataforma, não preferência. Uma
loja com imagem sem texto alternativo é uma loja inacessível a leitor de tela e
penalizada em busca. Se o seu cadastro não tem esse dado, gere algo útil a partir
do que você tem — "{nome do produto}, {cor}" já resolve. "foto", "imagem"
ou o nome do arquivo não resolvem.
O download é assíncrono. A resposta diz quantas fotos foram enfileiradas
(images.queued), não quantas já estão no ar. Nós tentamos de novo com espera
crescente (1min, 5min, 30min, 2h) e desistimos depois disso — a falha aparece no
painel do lojista. Uma foto que falha não reprova o produto.
Reenviar a mesma URL não baixa a imagem de novo.
6. Rota: preço
POST /api/v1/integration/catalog/prices
Escopo: price:write
{
"origin": "vetor",
"items": [
{ "externalId": "9000312700002", "listPrice": "269.90", "salePrice": "199.99" },
{ "externalId": "9000312700019", "listPrice": "269.90" }
]
}
| Campo | Obrig. | Notas |
|---|---|---|
externalId |
✔ | O da variação. Preço é por variação. |
listPrice |
✔ | O preço cheio (o "DE"). |
salePrice |
O preço promocional (o "POR"). Precisa ser ≤ listPrice. |
|
currency |
Ausente = BRL. |
Como dizer "não tem promoção": omita salePrice, ou mande null, ou mande
0. Os três significam a mesma coisa e apagam a promoção anterior. (Tratamos 0
como ausência de propósito: sistemas costumam usar zero para "sem oferta", e
interpretá-lo como preço poria a variação a R$ 0,00 na vitrine.)
Preço à vista × a prazo: mande em listPrice/salePrice o preço de
tabela da loja. O parcelamento e o desconto à vista (Pix) são configuração da
loja, não do seu envio.
Se o preço não mudou, a resposta é sem_mudanca e nada é escrito.
7. Rota: estoque
POST /api/v1/integration/catalog/stock
Escopo: stock:write
{
"origin": "vetor",
"items": [
{ "externalId": "9000312700002", "balance": 3 },
{ "externalId": "9000312700019", "balance": 0 }
]
}
| Campo | Obrig. | Notas |
|---|---|---|
externalId |
✔ | O da variação. |
balance |
✔ | O saldo absoluto, inteiro ≥ 0. Não é diferença, não é movimento. |
Mande o saldo que o seu sistema tem hoje. Nós calculamos a diferença e lançamos um movimento no nosso livro de estoque — nunca sobrescrevemos a quantidade. Isso é o que mantém "por que este SKU tem 3?" respondível seis meses depois.
Reenviar o mesmo saldo responde sem_mudanca e não lança movimento nenhum.
Se o seu sistema tem estoque por filial, decida com o lojista qual(is) filial(is) alimentam a loja e mande a soma dela(s). Uma armadilha real: no ERP que serviu de caso de teste, a "loja do e-commerce" tinha saldo zero em todas as linhas — o estoque vivo estava em outra filial. Somar a filial errada põe a loja inteira como esgotada.
Reservas. Se o saldo que você mandar for menor que o número de unidades
já reservadas por pedidos em aberto na loja, o item é recusado
(saldo_abaixo_do_reservado) e o saldo não muda. Não baixamos estoque debaixo de
um pedido. Confira os pedidos pendentes daquela variação antes de reenviar.
8. Rotas de diagnóstico
Servem para você descobrir sozinho por que um produto não apareceu na loja.
As duas últimas exigem o escopo mapping:read; a primeira não exige escopo
nenhum.
Quem sou eu — confira a credencial sem escrever nada
GET /api/v1/integration/ping
{
"status": "pong",
"tenantId": "f721f819-0279-5e3b-acf1-979cfdc2c712",
"storeName": "Up3 Esportes",
"environment": "sandbox",
"keyPrefix": "revo_test_a1b2c3d4",
"scopes": ["catalog:write", "mapping:read", "price:write", "stock:write"]
}
| campo | para que serve |
|---|---|
status |
sempre pong. Existe para quem já checava esse texto. |
tenantId |
eco do X-Tenant-Id que você mandou. Serve para log, não para conferência. |
storeName |
o nome da loja. É o único campo que você não mandou. |
environment |
live ou sandbox. |
keyPrefix |
a parte pública da chave, para casar com o seu cofre. |
scopes |
o que esta chave pode fazer, em ordem alfabética. |
Confira o storeName, não o tenantId. O X-Tenant-Id é obrigatório e a
chave só é aceita se for daquela loja — então o UUID que volta é sempre o que
você digitou, e compará-lo com a sua configuração é comparar a configuração com
ela mesma. Quem apontou o conector do cliente A para o UUID da loja B recebe o
UUID da loja B de volta, passa no próprio teste, e o lote seguinte escreve o
catálogo de A dentro de B.
O nome muda a pergunta de "o UUID bate com o meu arquivo?" — que sempre bate — para "é esta a loja que eu queria?", que tem resposta de verdade.
O mesmo vale para environment: ele já está legível no prefixo da chave que
você tem na mão (revo_live_ × revo_test_). Devolvê-lo é conveniência de
leitura, não descoberta.
Os scopes são a outra informação nova. Escopo faltando só aparecia no primeiro
lote real: uma chave sem stock:write funciona para cadastro e preço e denuncia
a falta quando o estoque começa a rodar.
Mudou em 2026-08-25. Antes esta rota devolvia o texto puro
pong; agora devolve o JSON acima. Dois efeitos:
- se você lia a resposta como texto, passe a ler o campo
status;- mande
Accept: application/jsonou*/*. Um cliente que fixeAccept: text/plain— sonda de monitoramento é o caso típico — passa a receber406 Not Acceptableem vez de200.
O que está pendente de tradução
GET /api/v1/integration/mappings?origin=vetor&status=pending
[
{
"kind": "category", "group": "", "code": "1/6/5",
"label": "FEMININO > LEGGING > FITNESS",
"status": "pending", "targetId": null, "targetValue": null,
"seenCount": 47,
"firstSeenAt": "2026-08-15T13:40:11Z", "lastSeenAt": "2026-08-15T14:05:02Z"
}
]
Filtros: kind (category, brand, option, option_value) e status
(pending, mapped, ignored).
O que está preso, e por quê
GET /api/v1/integration/quarantine?origin=vetor
[
{
"externalId": "3127",
"productId": "0f1e2d3c-…",
"reasons": [
{ "kind": "category", "group": "", "code": "1/6/5", "label": "FEMININO > LEGGING > FITNESS" }
],
"createdAt": "2026-08-15T13:40:11Z",
"updatedAt": "2026-08-15T13:40:11Z"
}
]
Lista vazia = nada preso.
9. O envelope de resposta
Todas as três rotas de escrita respondem no mesmo formato, com HTTP 200 mesmo quando há itens rejeitados.
{
"batchId": "6b1a7c30-…",
"origin": "vetor",
"channel": "products",
"environment": "live",
"summary": {
"criado": 1, "atualizado": 0, "semMudanca": 0, "emQuarentena": 1, "rejeitado": 1
},
"items": [
{
"externalId": "3127",
"result": "em_quarentena",
"productId": "0f1e2d3c-…",
"skuId": null,
"variants": [
{ "externalId": "9000312700002", "skuId": "aa11…" },
{ "externalId": "9000312700019", "skuId": "bb22…" }
],
"images": { "queued": 1, "alreadyStored": 0 },
"pending": [
{ "kind": "category", "group": "", "code": "1/6/5", "label": "FEMININO > LEGGING > FITNESS" }
],
"errors": []
},
{
"externalId": "9999",
"result": "rejeitado",
"productId": null,
"variants": [], "images": null, "pending": [],
"errors": [
{
"code": "campo_obrigatorio",
"field": "name",
"message": "name é obrigatório.",
"hint": "Envie o nome do produto como ele deve aparecer na loja."
}
]
}
]
}
result — os cinco desfechos
| Valor | Significa | O que fazer |
|---|---|---|
criado |
Registro novo. | Nada. |
atualizado |
Já existia e algo mudou. | Nada. |
sem_mudanca |
Já existia e veio idêntico. Nada foi escrito. | Nada. Não é erro — é o resultado esperado da maioria dos itens de um ciclo. |
em_quarentena |
Entrou, mas não está na loja: falta traduzir algo. Veja pending. |
Avise o lojista para mapear. Você não precisa reenviar depois. |
rejeitado |
Não entrou. Veja errors. |
Corrija e reenvie o item. |
Status HTTP
| Status | Quando | O que fazer |
|---|---|---|
200 |
O lote foi processado. Veja summary e items. |
— |
400 |
Erro do lote (origem inválida, items vazio, mais de 500 itens). |
Corrija o envelope. Não re-tente igual. |
401 |
Chave ausente, inválida, revogada, ou ambiente/loja divergentes. | Confira X-Api-Key e X-Tenant-Id. Não re-tente igual. |
403 |
A chave não tem o escopo da rota. | Peça ao lojista uma chave com o escopo. |
409 |
Idempotency-Key repetida com corpo diferente. |
Use uma chave nova. |
5xx |
Problema nosso. | Re-tente com espera crescente. É seguro re-tentar: com Idempotency-Key, o lote não é processado duas vezes. |
Regra de retry: re-tente
5xx(e429, se um dia aparecer) com espera crescente. Não re-tente400,401,403e409— são erros definitivos e a mesma requisição vai falhar de novo para sempre. Programe isso agora: um erro de negócio devolvido como definitivo e re-tentado em laço é o jeito mais comum de uma integração virar um ataque involuntário ao próprio parceiro.
10. Códigos de erro e o que fazer em cada um
Todo erro de item traz code (estável, programe contra ele), field, message
(pt-BR, pode ir para a tela do seu usuário) e hint (o próximo passo).
code |
Significa | O que fazer |
|---|---|---|
campo_obrigatorio |
Um campo exigido veio vazio. Veja field. |
Preencha e reenvie o item. |
url_invalida |
A URL da imagem não é http/https, não tem host, ou está malformada. | Abra a URL num navegador anônimo: ela precisa ser pública. |
limite_excedido |
Mais de 20 imagens no produto. | Envie só as principais. |
grade_ausente |
Mais de uma variação e nenhum eixo que as distinga. | Envie options em cada variação. |
grade_incompleta |
Uma variação não tem valor para todos os eixos do produto. | Toda variação precisa de um valor em cada eixo. |
grade_invalida |
Alguma opção veio sem group ou sem code. |
Confira o formato: {"group":"tamanho","code":"2","label":"P"}. |
de_para_ambiguo |
Dois valores diferentes do mesmo eixo foram mapeados para o mesmo rótulo. | Peça ao lojista para ajustar o de/para: cada valor da origem precisa de um rótulo próprio. |
sem_variacao_ativa |
Todas as variações vieram active: false. |
Para tirar o produto da loja, use active: false no produto. |
categoria_ignorada |
O lojista marcou essa categoria da sua origem como "não trazer". | Se ela deveria entrar, fale com o lojista. Parar de enviar esses produtos economiza banda dos dois lados. |
eixo_ignorado / valor_de_grade_ignorado |
O mesmo, para eixo ou valor de grade. | Um eixo ignorado não pode compor uma variação vendável. |
variacao_desconhecida |
Preço/estoque de uma variação que o cadastro nunca apresentou. | Mande o produto por POST /catalog/products primeiro. |
preco_invalido |
listPrice ausente/negativo, ou salePrice maior que listPrice. |
listPrice é o DE, salePrice é o POR. |
saldo_invalido |
balance ausente ou negativo. |
Envie o saldo absoluto, inteiro, ≥ 0. |
saldo_abaixo_do_reservado |
O saldo enviado é menor que o já reservado por pedidos em aberto. | Confira os pedidos pendentes da variação. O saldo não foi alterado. |
erro_interno |
Falha inesperada do nosso lado, naquele item. | Re-tente o item. Se persistir, mande o externalId, o batchId e o prefixo da chave para o suporte. |
11. O de/para de categorias, marcas e grade
Por que ele existe
Um exemplo real, do ERP que serviu de caso de teste. As "categorias" de lá são três eixos independentes achatados em três níveis:
| Nível 1 | Nível 2 | Nível 3 |
|---|---|---|
| FEMININO, MASCULINO, UNISSEX, INFANTIL, JUVENIL, OFERTA | TOP, LEGGING, TÊNIS, CAMISETA, BERMUDA… | CORRIDA, FITNESS, CICLISMO, NATAÇÃO, GERAL… |
Três coisas nesse quadro explicam a decisão:
- O nível 1 é gênero, não categoria. Uma loja não navega por "FEMININO > LEGGING > FITNESS"; ela navega por "Leggings" e filtra por gênero e modalidade.
- "OFERTA" é campanha, não categoria. Um produto que sai da promoção mudaria de categoria — e a URL dele mudaria junto.
- O mesmo código apareceu com nomes diferentes ao longo do tempo (o código
3de nível 3 aparece como "CORRIDA", "CORRIDA E FITNESS" e "FITNESS"). Por isso o de/para é pelo código, nunca pelo nome: mapear por nome criaria três categorias para o mesmo galho, e remapear pelo nome mais recente moveria produtos silenciosamente.
Como funciona, na prática
- Você manda o produto com
category.codeecategory.label. - Se aquele código ainda não foi mapeado nessa loja, nós criamos a linha pendente sozinhos e o produto vai para quarentena. O lojista descobre o que existe do seu lado sem precisar perguntar.
- O lojista abre Integrações → De/para e liga cada código da sua origem a
uma categoria da loja dele. O
labelque você mandou é o que ele lê. - No instante em que ele salva, os produtos presos por aquele código sobem sozinhos. Você não reenvia nada.
O mesmo vale para marca, para o nome dos eixos (tamanho → "Tamanho") e para os
valores (2 → "P", 1 → "Preto").
O formato do category.code é seu
Nós tratamos o código como texto opaco. Use o que for estável no seu sistema:
- um id só:
"857" - o caminho concatenado:
"1/6/5" - o que fizer sentido:
"DEP01-GRU06"
Escolha um formato e não mude. Mudar o formato do código é o mesmo que inventar categorias novas: tudo volta para a quarentena.
Se a sua classificação tem mais eixos do que uma árvore (gênero × peça ×
modalidade, como no exemplo), a recomendação é: mande o caminho completo
concatenado como code, e o caminho legível como label. Assim o lojista
mapeia "FEMININO > LEGGING > FITNESS" para "Leggings" e "MASCULINO > LEGGING >
FITNESS" para a mesma "Leggings", sem perder informação — o gênero vira filtro na
loja, não pasta.
Quando o lojista não quer trazer alguma coisa
Ele pode marcar um código como ignorado. Nesse caso os produtos daquela
categoria passam a ser rejeitados com categoria_ignorada em vez de ficarem
para sempre na quarentena. Vale a pena parar de enviá-los.
Para marca é diferente: marca ignorada não segura o produto — ele entra sem marca.
12. Ritmo de sincronização recomendado
Você não precisa saber "o que mudou desde ontem". Mande o estado completo do que está marcado para exportar; nós decidimos o que mudou.
| O quê | Sugestão |
|---|---|
| Cadastro | 1× por hora, ou por gatilho de alteração. Lotes de 100–200 produtos. |
| Preço | a cada 5–15 minutos. Lotes de até 500. |
| Estoque | a cada 5–15 minutos. Lotes de até 500. |
Boas práticas que economizam muito dos dois lados:
- Sempre
Idempotency-Key. Custa nada e resolve o "não sei se entrou". - Lotes grandes, não um item por chamada. Um lote de 500 estoques é uma requisição; 500 requisições são 500 conexões e 500 vezes o custo de autenticação.
- Ordem: cadastro → preço → estoque. Preço e estoque de uma variação nova antes do cadastro são recusados.
- Olhe o
summary. Serejeitado> 0 num ciclo, alguém precisa ver — leve para a sua tela de pendências.sem_mudancaalto é o comportamento saudável, não um problema. - Produto que saiu de linha: mande
active: false. Não pare simplesmente de enviá-lo — ausência não é sinal, e nós não apagamos nada por dedução. - Não segure o ciclo por causa de foto. As imagens são baixadas depois; a resposta não espera por elas.
13. Limites
| Limite | Valor |
|---|---|
| Itens por lote | 500 |
| Imagens por produto | 20 |
| Tamanho de cada imagem | 5 MB |
| Formatos de imagem | JPEG, PNG, WebP, AVIF |
origin |
minúsculas, dígitos, - e _ |
externalId |
sem limite declarado — a coluna é text (corrigido no manual; a linha antiga dizia 200) |
label |
até 300 caracteres |
Só baixamos imagens de endereços públicos. URLs apontando para rede interna,
localhost ou faixas privadas são recusadas.
14. Checklist da primeira integração
Sandbox primeiro. Todo item abaixo deve estar verde antes de tocar em produção.
- [ ] Você tem a chave de sandbox (
revo_test_…) e oX-Tenant-Idda sua loja de sandbox (a Revo entrega os dois junto com a loja — ver o manual). - [ ]
GET /api/v1/integration/pingresponde200, e ostoreNameé o nome da loja que você queria. Não confira otenantId: ele é eco do header que você mandou, e bate mesmo quando o conector está apontado para a loja errada. - [ ] O
environmentdo/pingé o que você esperava. Se disserlivee você achava que estava em sandbox, pare aqui — o próximo passo escreveria no catálogo de um cliente. - [ ] Os
scopesdo/pingcobrem tudo o que a sua integração vai fazer. Faltarstock:writeaqui é barato; faltar no primeiro lote de estoque em produção, não. - [ ] Um lote de 1 produto com 2 variações responde
200eresult: "em_quarentena"(é o esperado: nada está mapeado ainda). - [ ]
GET /mappings?status=pendinglista a sua categoria, os seus eixos e os seus valores de grade, com os rótulos que você mandou (se algumlabelestiver vazio, corrija agora — é o que o lojista vai ler). - [ ] Categoria, eixos e valores mapeados no painel — no sandbox, por você mesmo; em produção, pelo lojista.
- [ ] Reenviar o mesmo lote responde
result: "sem_mudanca". - [ ] O produto aparece na loja de sandbox, com a categoria certa, a grade certa e a foto no ar.
- [ ] Preço entra e o "de/por" aparece certo na vitrine.
- [ ] Estoque entra; zerar o saldo tira a variação de venda.
- [ ] Um lote com um item propositalmente errado responde
200comrejeitado: 1e os outros itens processados. - [ ] O mesmo lote com a mesma
Idempotency-Keydevolve a mesma resposta. - [ ] Um lote de 500 itens roda dentro do seu tempo limite.
- [ ] Sua tela de sincronização mostra
criado/atualizado/sem_mudanca/em_quarentena/rejeitadoe omessagede cada rejeição.
Só então: lojista emite a chave de produção (revo_live_…), você troca a chave e
o X-Tenant-Id, e roda uma carga inicial completa antes de ligar o ciclo
periódico.
Suporte
Ao abrir um chamado, mande sempre:
- o
batchIdda resposta; - o prefixo da chave (
revo_live_R0hZa1B3) — nunca a chave inteira; - o
externalIddo item; - o corpo que você enviou.
Com essas quatro coisas conseguimos reproduzir o caso sem uma troca de mensagens.