HubflyBriefing de implementaçãoManual da integradoraReferência da API

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

  1. O contrato em uma página
  2. Ambientes e chaves
  3. Autenticação
  4. Os quatro conceitos que explicam tudo
  5. Rota: cadastro de produtos
  6. Rota: preço
  7. Rota: estoque
  8. Rotas de diagnóstico
  9. O envelope de resposta
  10. Códigos de erro e o que fazer em cada um
  11. O de/para de categorias, marcas e grade
  12. Ritmo de sincronização recomendado
  13. Limites
  14. Checklist da primeira integração

1. O contrato em uma página

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

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, chave revo_test_, um provedor de pagamento de mentira (que torna os quatro webhooks de pedido exercitáveis) e um POST /sandbox/reset que a própria integradora dispara. O que continua NÃO existindo: endereço de sandbox separado (a loja é distinguida pelo X-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

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 attributes nã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" }
]

Regras:

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:

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 (e 429, se um dia aparecer) com espera crescente. Não re-tente 400, 401, 403 e 409 — 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:

  1. 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.
  2. "OFERTA" é campanha, não categoria. Um produto que sai da promoção mudaria de categoria — e a URL dele mudaria junto.
  3. O mesmo código apareceu com nomes diferentes ao longo do tempo (o código 3 de 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

  1. Você manda o produto com category.code e category.label.
  2. 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.
  3. O lojista abre Integrações → De/para e liga cada código da sua origem a uma categoria da loja dele. O label que você mandou é o que ele lê.
  4. 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:

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:

  1. Sempre Idempotency-Key. Custa nada e resolve o "não sei se entrou".
  2. 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.
  3. Ordem: cadastro → preço → estoque. Preço e estoque de uma variação nova antes do cadastro são recusados.
  4. Olhe o summary. Se rejeitado > 0 num ciclo, alguém precisa ver — leve para a sua tela de pendências. sem_mudanca alto é o comportamento saudável, não um problema.
  5. 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.
  6. 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.

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:

Com essas quatro coisas conseguimos reproduzir o caso sem uma troca de mensagens.