Manual da integradora — plataforma Revo
Versão 3 · 2026-08-25 · documento enviado à integradora
Este manual é para a empresa que vai escrever a integração entre um sistema (ERP, PDV, gestor de estoque, marketplace) e uma loja da plataforma Revo. Ele cobre o processo de homologação, a autenticação, o que a API aceita e devolve, o caminho de volta (webhooks), os limites e o que ainda não existe.
Este manual vai junto com um anexo:
api-integracao.md, a referência campo a campo das três rotas de escrita. Este documento é o mapa e o contrato de trabalho; o anexo é o dicionário. Onde os dois divergirem, vale este — ele foi conferido contra o código em 2026-08-25.O que mudou na versão 3:
GET /pingdeixou de responder o textoponge passou a responder um JSON com o nome da loja, o ambiente, o prefixo da chave e os escopos. Duas consequências práticas: se você lia a resposta como texto, passe a ler o campostatus; e mandeAccept: application/jsonou*/*— quem fixaAccept: text/plain(sonda de monitoramento é o caso típico) passa a receber406.O campo que importa é o
storeName, e o anexo explica por quê: otenantIdque volta é eco do header que você mandou, então ele bate mesmo quando o conector está apontado para a loja errada. O nome é o único dado que você não forneceu — é com ele que se descobre o engano antes do primeiro lote.O que mudou na versão 2: o sandbox passou a existir. A versão 1 dizia, com todas as letras, que o mecanismo estava pronto e o ambiente não. Agora há loja de sandbox provisionável com catálogo de demonstração, chave
revo_test_, pagamento de mentira (que destrava os quatro webhooks de pedido) e um "recomeçar do zero" que você mesmo dispara. A seção 9 foi reescrita inteira. O que continua não existindo está listado lá e na seção 10 — sem promessa nova.
Sumário
- O que é uma integradora homologada
- O contrato em uma página
- Autenticação
- Ingestão de catálogo, de ponta a ponta
- Quarentena e de/para
- O que a API devolve, e os erros
- Webhooks de saída
- Limites
- Sandbox
- O que a plataforma ainda NÃO oferece
- Checklist de homologação
- Suporte
1. O que é uma integradora homologada
Integradora é a empresa que escreve a integração. Ela não é o lojista e não é a Revo: ela é quem mantém o código que conversa com esta API, geralmente para vários lojistas ao mesmo tempo.
Homologar significa que a Revo exercitou o caminho inteiro com aquela empresa e sabe dizer até onde ele foi provado. A situação aparece no painel do lojista, em Integrações → Integradoras, junto com duas listas: o que já foi provado e o que ainda não foi. Uma integradora homologada para catálogo e ainda não exercitada em webhooks aparece assim, com todas as letras — o lojista precisa saber o que pode prometer.
Hoje há uma integradora homologada: a Vetor Sistemas (ERP), homologada para cadastro de produtos, preço, estoque e leitura do de/para e da quarentena.
O que a Revo pede de você
- Um nome de origem (
origin): um slug curto, minúsculo e estável para sempre (vetor,bling,tiny). Ele identifica o vocabulário do seu sistema, e o de/para de cada loja é por origem. Trocá-lo depois equivale a apresentar um sistema novo: todo o de/para volta a zero. - Um contato técnico e um canal para incidentes.
- O checklist da seção 11 inteiro verde.
O que a Revo entrega
- Este manual e o anexo
api-integracao.md. - O endereço da API do ambiente onde você vai trabalhar.
- O cartão da sua empresa no painel dos lojistas, com a situação e o que foi provado.
A credencial não vem da Revo. Quem emite a chave é o lojista, no painel dele — ver seção 3. É assim de propósito: a chave dá acesso de escrita ao catálogo da loja, e a decisão de dar esse acesso é dele.
2. O contrato em uma página
- Base:
{URL_DA_API}/api/v1/integration. Em desenvolvimento local éhttp://localhost:4380; em produção a Revo informa o endereço junto com o ambiente (para a primeira loja no ar éhttps://api.up3esportes.com.br). - Autenticação: header
X-Api-Key(chave escopada) + headerX-Tenant-Id(identificador da loja). - Três canais de escrita, duas rotas de diagnóstico:
| O quê | Rota | Escopo | Frequência típica |
|---|---|---|---|
| Cadastro | POST /catalog/products |
catalog:write |
quando o produto muda |
| Preço | POST /catalog/prices |
price:write |
a cada 5–15 min |
| Estoque | POST /catalog/stock |
stock:write |
a cada 5–15 min |
| O que falta traduzir | GET /mappings |
mapping:read |
sob demanda |
| O que está preso | GET /quarantine |
mapping:read |
sob demanda |
| Quem sou eu | GET /ping |
nenhum (só autenticar) | uma vez, ao subir |
| Recomeçar o sandbox | POST /sandbox/reset |
catalog:write |
quando quiser, só em sandbox |
- Tudo é lote, e a resposta traz o resultado de cada item. Um item ruim
não derruba o lote: a rota responde
200com aquele item marcado. - Você identifica os registros pelo id que eles têm no SEU sistema
(
externalId). A chave real é a trinca(loja, origem, externalId). Você nunca precisa guardar id nosso — exceto se quiser consumir webhooks, ver a seção 7. - A ordem importa: cadastro apresenta as variações; preço e estoque de uma variação que o cadastro nunca mandou são recusados.
Uma chamada mínima e completa:
curl -X POST https://api.up3esportes.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: vetor-estoque-2026-08-24T14:05:00Z' \
-d '{
"origin": "vetor",
"items": [
{ "externalId": "9000312700002", "balance": 3 },
{ "externalId": "9000312700019", "balance": 0 }
]
}'
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 ponha a chave em query string, em log de aplicação ou em URL.
Como o lojista emite a chave
No painel dele: Integrações → Integradoras → (sua empresa) → Configurar →
Emitir chave. A tela já vem com os escopos que a sua integração usa marcados,
e mostra, na mesma página, o X-Tenant-Id que ele precisa te passar.
O segredo em claro aparece uma única vez, na resposta da criação. A plataforma guarda apenas o SHA-256; não há como recuperá-lo. Se perder, o lojista revoga e emite outra.
Escopos
| Escopo | Libera |
|---|---|
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. O escopo existe no vocabulário da API, mas nenhuma rota o exige — ver seção 10. Não peça o que não usa. |
Peça só o que for usar. Um incidente com uma chave que só escreve preço não pode apagar o catálogo.
O que dá errado, e com que status
Esta tabela foi conferida no código da autenticação, e ela é mais precisa que a intuição — em particular, loja desconhecida é 404, não 401:
| Situação | Status | Corpo |
|---|---|---|
X-Tenant-Id ausente, malformado ou de loja inexistente |
404 |
application/problem+json com "code": "tenant_not_found" |
X-Api-Key ausente |
401 |
corpo de erro padrão do servidor (não é problem+json) |
X-Api-Key desconhecida ou revogada |
401 |
idem |
Chave válida, mas de outra loja que não a do X-Tenant-Id |
401 |
idem |
| Chave de sandbox apontada a uma loja de produção (ou o contrário) | 401 |
idem |
| Chave sem o escopo da rota | 403 |
corpo de erro padrão do servidor |
O code do 404 é estável — programe contra ele. Ele é o que distingue "esta
LOJA não existe" (você recebeu o X-Tenant-Id errado) de "este RECURSO não
existe".
Nunca re-tente 401, 403 e 404: são definitivos, e a mesma requisição vai falhar de novo para sempre. Ver a regra completa de retry na seção 6.
Ambiente
O ambiente é da loja, não da chave: a chave herda o ambiente da loja em que é emitida, e o banco recusa qualquer outra combinação. O prefixo mostra qual é:
| Produção | Sandbox | |
|---|---|---|
| Prefixo | revo_live_… |
revo_test_… |
| Contratos e validações | — | exatamente os mesmos |
Uma chave de sandbox não alcança a loja de produção e uma chave de produção não
alcança a de sandbox: as duas combinações são 401, e isso é garantido no
banco, não por validação de aplicação. Comece pela seção 9: a
sua loja de sandbox nasce com catálogo, com chave e com pagamento de teste.
Ao pedir suporte, mande o prefixo (revo_live_R0hZa1B3) — nunca a chave
inteira.
4. Ingestão de catálogo, de ponta a ponta
O caminho completo, do primeiro envio ao produto na vitrine:
1. você: POST /catalog/products (cadastro: identidade, grade, fotos)
↓
2. plataforma: cria/atualiza o produto e os SKUs
cria SOZINHA as linhas pendentes do de/para
enfileira o download das fotos
↓
3. resposta: 200, com result por item:
criado | atualizado | sem_mudanca | em_quarentena | rejeitado
↓
4. lojista: painel → Integrações → De/para → liga cada termo seu ao dele
↓
5. plataforma: solta SOZINHA os produtos presos por aqueles termos
↓
6. você: POST /catalog/prices e POST /catalog/stock
↓
7. vitrine: o produto está à venda
Três coisas que essa sequência esconde e que mudam o seu código:
A 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 voltam
com variacao_desconhecida — e é um erro de item, não do lote.
Você não precisa saber o que mudou. Mande o estado completo do que está
marcado para exportar. Um item idêntico ao que já está lá responde
sem_mudanca e nada é escrito — nenhum evento, nenhuma reindexação. É o
resultado esperado da maioria dos itens de um ciclo saudável, não um problema.
(O hash é canônico: a ordem das chaves dentro de attributes não o afeta.)
Idempotência em dois níveis. A de conteúdo é a de cima. A de transporte é o
header opcional Idempotency-Key, que resolve o "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. Use algo estável e único por lote
(vetor-produtos-2026-08-24T14:05:00Z). Mande sempre — custa nada e é o que
torna seguro re-tentar um 5xx.
Estoque é saldo absoluto, e vira lançamento
balance é o saldo absoluto, inteiro ≥ 0 — não é diferença, não é
movimento. A plataforma calcula a diferença e lança um movimento no livro de
estoque; ela nunca sobrescreve a quantidade. É o que mantém "por que este SKU
tem 3?" respondível seis meses depois.
Duas consequências práticas:
- Estoque por filial: decida com o lojista quais filiais alimentam a loja e mande a soma delas. Num ERP que serviu de caso de teste, a filial chamada "e-commerce" tinha saldo zero em todas as linhas e o estoque vivo estava em outra — somar a errada põe a loja inteira como esgotada.
- Reservas: se o saldo enviado for menor que o número de unidades já
reservadas por pedidos em aberto, o item é recusado com
saldo_abaixo_do_reservadoe o saldo não muda. A plataforma não baixa estoque debaixo de um pedido.
Fotos
Você manda a URL; a plataforma baixa o arquivo para o storage dela. O
download é assíncrono: a resposta diz quantas fotos foram enfileiradas
(images.queued), não quantas já estão no ar. Há re-tentativa com espera
crescente (1min, 5min, 30min, 2h) e depois disso o job morre e a falha aparece
no painel do lojista. Uma foto que falha não reprova o produto, e reenviar a
mesma URL não baixa de novo.
alt é obrigatório. É requisito de produto da plataforma: loja com imagem
sem texto alternativo é inacessível a leitor de tela e penalizada em busca. Se o
seu cadastro não tem esse dado, derive algo útil ("{nome do produto}, {cor}").
"foto", "imagem" ou o nome do arquivo não resolvem.
Retirar um produto da loja
Mande active: false no produto. Não pare simplesmente de enviá-lo:
ausência não é sinal, e a plataforma não apaga nada por dedução. Mandar todas as
variações como active: false é recusado com sem_variacao_ativa — é quase
sempre um engano de quem quis arquivar o produto.
5. Quarentena e de/para
Por que ele existe
As categorias do seu sistema nasceram para relatório fiscal e controle de estoque; as da loja nasceram para navegação e SEO. A plataforma não adota as suas e não cria categoria a partir dos seus dados. O que existe é um de/para por loja: código da sua origem → categoria daquela loja. Quem preenche é o lojista.
O de/para liga pelo código, nunca pelo rótulo. No ERP que motivou esta API o mesmo código de nível 3 aparece como "CORRIDA", "CORRIDA E FITNESS" e "FITNESS" ao longo do tempo — ligar por nome criaria três categorias para o mesmo galho e remapear pelo nome mais recente moveria produtos em silêncio.
O que você tem de 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 lê 1/6/5 e não
sabe o que é.
"category": { "code": "1/6/5", "label": "FEMININO > LEGGING > FITNESS" },
"brand": { "code": "ALTO GIRO", "label": "ALTO GIRO" }
O formato do code é seu — a plataforma o trata como texto opaco. Pode ser
um id ("857"), o caminho concatenado ("1/6/5") ou o que fizer sentido
("DEP01-GRU06"). Escolha um formato e não mude: mudar o formato equivale a
inventar categorias novas, e tudo volta para a quarentena.
Se a sua classificação tem mais eixos do que uma árvore (gênero × peça ×
modalidade), mande o caminho completo concatenado como code e o caminho
legível como label. Assim o lojista mapeia "FEMININO > LEGGING > FITNESS" e
"MASCULINO > LEGGING > FITNESS" para a mesma categoria "Leggings", e o gênero
vira filtro na loja em vez de pasta.
O que acontece com o que não está mapeado
O produto não é recusado: ele entra em quarentena. Ou seja: o produto existe, o lojista o vê no painel marcado como preso, e ele não aparece na loja. Quando o lojista mapear o último termo pendente daquele produto, ele sobe sozinho — você não reenvia nada.
Isso vale para categoria, para o nome dos eixos de grade (tamanho → "Tamanho")
e para os valores (2 → "P"). Marca é diferente: marca não mapeada não
segura o produto — ele entra sem marca.
O lojista também pode marcar um termo como ignorado ("OFERTA" é campanha,
não categoria). A partir daí os produtos daquela categoria passam a ser
rejeitados com categoria_ignorada em vez de ficarem presos para sempre.
Vale a pena parar de enviá-los.
Como você diagnostica sozinho
Não peça ao lojista para "olhar o painel". Estas duas rotas (escopo
mapping:read) respondem à pergunta "por que este produto não apareceu?" sem
abrir chamado:
GET /api/v1/integration/mappings?origin=vetor&status=pending
GET /api/v1/integration/quarantine?origin=vetor
GET /mappings aceita kind (category, brand, option, option_value) e
status (pending, mapped, ignored). Cada linha traz code, label,
status, seenCount, firstSeenAt e lastSeenAt.
GET /quarantine lista externalId, productId, reasons[] (exatamente o que
falta) e as datas. Lista vazia = nada preso.
Leve isso para a sua tela. A sua tela de sincronização deve mostrar quantos itens ficaram em quarentena e por quê; é a diferença entre o lojista saber que precisa mapear e ele achar que a integração está quebrada.
6. O que a API devolve, e os erros
O envelope das rotas de escrita
As três rotas de escrita respondem 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."
}
]
}
]
}
Guarde
productIdevariants[].skuId. Eles são os únicos ids nossos que você recebe, e são o que permite ler os webhooksproduct.updatedestock.changed— que trazem só o id da plataforma. Sem eles guardados, esses dois eventos são ilegíveis para você.
Os cinco desfechos de result:
| 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. |
em_quarentena |
Entrou, mas não está na loja: falta traduzir algo (veja pending). |
Avise o lojista. Não reenvie depois. |
rejeitado |
Não entrou (veja errors). |
Corrija e reenvie o item. |
Erros de item
Todo erro 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 |
|---|---|
campo_obrigatorio |
Um campo exigido veio vazio (veja field). |
url_invalida |
URL de imagem que não é http/https, sem host ou malformada. |
limite_excedido |
Mais de 20 imagens no produto. |
grade_ausente |
Mais de uma variação e nenhum eixo que as distinga. |
grade_incompleta |
Uma variação não tem valor para todos os eixos do produto. |
grade_invalida |
Alguma opção veio sem group ou sem code. |
de_para_ambiguo |
Dois valores do mesmo eixo foram mapeados para o mesmo rótulo. |
sem_variacao_ativa |
Todas as variações vieram active: false. |
categoria_ignorada |
O lojista marcou essa categoria da sua origem como "não trazer". |
eixo_ignorado / valor_de_grade_ignorado |
O mesmo, para eixo ou valor de grade. |
variacao_desconhecida |
Preço/estoque de uma variação que o cadastro nunca apresentou. |
preco_invalido |
listPrice ausente/negativo, ou salePrice maior que listPrice. |
saldo_invalido |
balance ausente ou negativo. |
saldo_abaixo_do_reservado |
O saldo enviado é menor que o já reservado por pedidos em aberto. |
erro_interno |
Falha inesperada nossa, naquele item. Re-tente o item. |
Erros de requisição, e o que RFC 9457 significa aqui
A plataforma usa RFC 9457 (application/problem+json) — mas é preciso ser
exato sobre o que isso te dá, porque o contrário custa tempo de depuração:
| Status | Quando | Corpo |
|---|---|---|
400 |
Erro do lote: origin fora do padrão, items vazio, mais de 500 itens, campo acima do tamanho máximo. |
Erro padrão do servidor ({"timestamp","status","error","path"}), não problem+json. |
400 |
Validação interna da rota de mapeamentos. | problem+json, title: "Bad Request". |
404 |
X-Tenant-Id ausente ou de loja inexistente. |
problem+json com code: "tenant_not_found". |
404 |
Recurso inexistente (endpoint de webhook, no painel). | problem+json, title: "Not Found". |
409 |
Idempotency-Key repetida com corpo diferente. |
problem+json, title: "Conflict", detail em pt-BR explicando. |
401 / 403 |
Ver a tabela da seção 3. | Erro padrão do servidor. |
5xx |
Problema nosso. | Varia. |
Não programe contra o title. Nas rotas de integração ele é a frase padrão
do HTTP ("Bad Request", "Not Found", "Conflict") — não é um código de
domínio, e mudá-lo não é considerado quebra de contrato. Os identificadores
estáveis são, nesta ordem:
- o status HTTP;
- o campo
codedos erros de item, dentro do envelope200; - o campo
codedo 404 de loja (tenant_not_found), o únicocodefora do envelope.
Também não programe contra detail: é prosa em pt-BR e melhora com o tempo.
Regra de retry
Re-tente 5xx (e 429, se um dia aparecer) com espera crescente. Não
re-tente 400, 401, 403, 404 e 409 — são definitivos. 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.
Re-tentar 5xx é seguro desde que você mande Idempotency-Key: o lote não é
processado duas vezes.
7. Webhooks de saída
O caminho de volta existe: a loja avisa o seu servidor quando algo acontece nela. Quem cadastra o endereço é o lojista, no painel — Integrações → Integradoras → (sua empresa) → Configurar → Cadastrar endereço. Você informa a ele a URL e quais eventos quer.
Os seis eventos
type |
Quando | data |
|---|---|---|
order.placed |
Pedido criado. | orderId, orderNumber |
order.paid |
Pagamento aprovado. | orderId, orderNumber, grandTotal |
order.status_changed |
Pedido mudou de situação. | orderId, orderNumber, fromStatus, toStatus |
order.cancelled |
Pedido cancelado. | orderId, orderNumber, reason |
product.updated |
Produto alterado. | productId |
stock.changed |
Saldo/reserva de um SKU mudou. | skuId, quantity, reserved |
Os quatro eventos de pedido são exercitáveis no sandbox — é o que o pagamento de mentira da seção 9 resolve: uma compra na vitrine da sua loja de sandbox faz nascer
order.placede, com o e-mail certo,order.paidlogo em seguida.O que continua faltando é a mão inversa: eles carregam o id do pedido NA PLATAFORMA e não existe rota de integração que leia um pedido — você recebe o aviso e não tem como buscar o conteúdo. Ver a seção 10.
Os dois de catálogo são utilizáveis desde já, se você tiver guardado o
productIde oskuIdque a resposta da ingestão devolveu.
O corpo
O mesmo para todos os eventos:
{
"id": "7c9f2e10-3a5b-4c8d-9e1f-2b3c4d5e6f70",
"type": "order.paid",
"occurredAt": "2026-08-24T14:07:11.482-03:00",
"data": { "orderId": "…", "orderNumber": "REVO-2026-1001", "grandTotal": 499.80 }
}
id é a chave de idempotência do seu lado. O mesmo evento vai com o mesmo
id para todos os endpoints inscritos nele, e uma entrega pode se repetir (o
seu servidor pode ter recebido e caído antes de responder 2xx). Guarde os
id já processados e descarte repetição.
Os headers
Content-Type: application/json
X-Revo-Event-Id: 7c9f2e10-3a5b-4c8d-9e1f-2b3c4d5e6f70
X-Revo-Signature: 3f8a… (hex)
Como validar a assinatura
O segredo é gerado no servidor no formato revo_whsec_ + 32 caracteres
hexadecimais, e aparece uma única vez, quando o lojista cadastra o endereço.
Ele é guardado cifrado do nosso lado e nunca volta numa leitura.
X-Revo-Signature é o HMAC-SHA256 do corpo exato recebido, em hexadecimal
minúsculo, com o segredo como chave. "Corpo exato" é literal: calcule sobre os
bytes crus da requisição, antes de qualquer parse. Reserializar o JSON muda
os bytes e a assinatura não bate.
import hmac, hashlib
def assinatura_confere(corpo_bruto: bytes, cabecalho: str, segredo: str) -> bool:
esperado = hmac.new(segredo.encode(), corpo_bruto, hashlib.sha256).hexdigest()
# Comparação em tempo constante: `==` vaza, pelo tempo, quantos
# caracteres iniciais bateram.
return hmac.compare_digest(esperado, cabecalho)
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(segredo.getBytes(UTF_8), "HmacSHA256"));
String esperado = HexFormat.of().formatHex(mac.doFinal(corpoBruto));
boolean ok = MessageDigest.isEqual(
esperado.getBytes(UTF_8), cabecalho.getBytes(UTF_8));
Recuse o que não bate. Um endpoint que aceita corpo sem conferir assinatura é uma rota pública que escreve no seu ERP.
Entrega, re-tentativa e desistência
- Responda
2xxpara confirmar. Qualquer outro status conta como falha. - Timeout de 10 segundos (conexão e leitura). Responda rápido e processe depois: gastar 30 segundos escrevendo no seu banco antes de responder produz falha e re-tentativa de algo que você já processou.
- Falhou, a próxima tentativa é em 1min, 5min, 30min, 2h e 6h. Esgotadas as
cinco, a entrega vira
deade não é mais tentada — ela fica registrada com o último erro, e o lojista a vê no painel. - A ordem não é garantida. Trate cada evento como um fato datado
(
occurredAt), não como uma sequência. - Não há assinatura de timestamp nem janela de validade no header. Se você
precisa recusar replay antigo, use
occurredAte o seu próprio critério.
8. Limites
Todos conferidos no código:
| Limite | Valor | O que acontece ao estourar |
|---|---|---|
| Itens por lote (qualquer canal) | 500 | 400 no lote inteiro |
| Imagens por produto | 20 | item rejeitado com limite_excedido |
| Tamanho de cada imagem | 5 MiB (5 × 1024 × 1024 bytes) | a foto falha; o produto não é reprovado |
| Tipos de imagem aceitos | image/jpeg, image/png, image/webp, image/avif |
a foto falha (image/jpg e image/pjpeg são aceitos como jpeg) |
| Esquema da URL de imagem | só http e https |
url_invalida |
| Redirecionamento na URL de imagem | não é seguido | a foto falha — mande a URL final |
| Host da URL de imagem | precisa ser público | a foto falha (loopback, faixas privadas, link-local, IPv6 fc00::/7 e CGNAT 100.64/10 são recusados) |
| Timeout do download de imagem | 20s | a foto entra na re-tentativa |
origin |
minúsculas, dígitos, - e _ ([a-z0-9]+([-_][a-z0-9]+)*) |
400 no lote |
category.code, brand.code, options[].code |
até 200 caracteres | 400 no lote |
category.label, brand.label, options[].label |
até 300 caracteres | 400 no lote |
options[].group |
até 60 caracteres | 400 no lote |
externalId (produto e variação) |
sem limite declarado no código | — |
Sobre a última linha: documentação anterior falava em "200 caracteres para
externalId". Não é verdade — a coluna étexte não há validação de tamanho. Mesmo assim, use identificadores curtos: eles entram em índice composto e em toda chave de deduplicação.
Não há limite de requisições por minuto implementado hoje nas rotas de integração. Isso não é permissão para chamar item a item: mande lotes.
9. Sandbox
O desenho: uma loja de sandbox POR INTEGRADORA
Não é uma por lojista. Você escreve UM código que serve vários clientes, e o que precisa exercitar é o contrato — não o catálogo de um cliente específico. Uma loja por lojista multiplicaria lojas de mentira, faria você depender de cada cliente provisionar a dele antes de poder testar, e te deixaria sem lugar para trabalhar enquanto nenhum cliente tivesse pedido.
A loja de sandbox é sua: o OWNER dela é o seu contato técnico, você entra no painel dela, você mapeia o de/para, você suja e você limpa. Isso resolve o problema mais chato da homologação — o de/para é metade do fluxo de ingestão e, numa loja de cliente, só o lojista consegue preenchê-lo. Na sua, você mesmo preenche.
O que ela tem no primeiro minuto
| Endereço da API | o mesmo da produção (ver seção 2) |
| Catálogo | 30 produtos, 183 variações, com preço, estoque, fotos, coleções e filtros |
| Frete | tabela de faixas por região já configurada — a loja cota sozinha |
| Chave de API | uma revo_test_… com catalog:write, price:write, stock:write e mapping:read |
| Pagamento | um provedor de mentira, que só existe em sandbox (ver abaixo) |
| Painel | um convite de OWNER para o seu contato técnico |
O catálogo é exatamente o mesmo que a plataforma usa como loja de demonstração — o mesmo produto, o mesmo id, a mesma grade. Homologar contra um catálogo que não existe em lugar nenhum é homologar contra uma loja que não existe.
Como pedir
Escreva para o canal de suporte da Revo pedindo uma loja de sandbox, com:
- o nome da sua empresa e o
originque você vai usar (ver seção 1); - o e-mail do contato técnico que vai administrar a loja.
Você recebe de volta, na mesma mensagem:
- o
X-Tenant-Idda sua loja; - a chave
revo_test_…(ela aparece uma única vez; guarde-a); - o link do convite para o seu contato técnico definir a senha e entrar no painel da loja;
- o endereço da vitrine da sua loja, quando ele já estiver publicado (ver logo abaixo — dá para fechar um pedido de teste sem ele).
Não há autoatendimento público: o pedido continua sendo humano. O que mudou é que ele agora é uma ação de um minuto do nosso lado, e a loja sai pronta — antes, era um roteiro de SQL na mão, e por isso a versão 1 deste manual não podia prometer sandbox nenhum.
Não há endereço de sandbox separado
Isto não mudou, e é importante: a loja de sandbox vive na MESMA API e no mesmo
endereço da produção. Não existe sandbox.api.revo.…. As rotas de integração
resolvem a loja pelo header X-Tenant-Id, não pelo domínio.
O que separa os dois mundos é o par (loja, chave), e o banco o garante:
- a chave herda o ambiente da loja, e nenhuma outra combinação é aceita (constraint + gatilho);
- o prefixo (
revo_live_×revo_test_) tem de bater com o ambiente, e isso é conferido a cada requisição; - chave de sandbox contra loja de produção:
401. O contrário:401.
Ou seja: você não precisa de um endereço diferente para ter certeza de que não
encostou em dado real. Precisa acertar o X-Tenant-Id — e errá-lo dá 404, não
uma escrita silenciosa na loja errada.
Pagamento de mentira: como fechar um pedido
Este é o pedaço que destrava os quatro webhooks de pedido. Na loja de sandbox, o checkout usa um provedor de pagamento que não fala com gateway nenhum: ele decide o desfecho na hora, pelas regras abaixo, e o pedido percorre exatamente o mesmo caminho de um pedido de verdade — reserva de estoque, máquina de estados, histórico e webhook.
Como disparar uma compra. Duas formas, e as duas percorrem o mesmo caminho:
- Pela vitrine da sua loja de sandbox, num navegador. É a forma que mais se parece com o que o comprador faz. Depende do endereço da loja estar publicado (DNS), então confirme com o suporte se o seu já está.
- Pelas rotas de vitrine, direto do seu servidor, mandando
X-Tenant-Id: {a sua loja}emPOST /api/v1/storefront/cart/items,POST /api/v1/storefront/cart/shipping-quotesePOST /api/v1/storefront/checkout. Elas resolvem a loja pelo Host, e só consultam oX-Tenant-Idquando o Host não pertence a loja nenhuma — que é exatamente o seu caso chamando de fora. Não precisa de DNS e não precisa de navegador.
Estas rotas de vitrine são públicas (é assim que um visitante anônimo compra) e não usam a sua
X-Api-Key. Elas não fazem parte do contrato de integração: estão aqui como ferramenta de teste, para você provocar os eventos de pedido.
As regras do desfecho, na ordem:
| Se… | desfecho |
|---|---|
o e-mail do pagador começa com recusa |
recusado — o pedido vai para payment_failed |
o e-mail do pagador começa com pendente |
aguardando pagamento |
os centavos do total são ,01 |
recusado |
os centavos do total são ,02 |
aguardando pagamento |
| qualquer outro caso | aprovado — order.paid sai em seguida |
São dois eixos porque são dois jeitos de testar: pela vitrine, o e-mail é o campo que você digita livremente; pela API, o e-mail costuma vir fixado e o que você controla é o valor.
Pix e boleto devolvem um "meio de pagamento" declaradamente falso
(SANDBOX-SEM-VALOR-NAO-PAGUE-…, https://sandbox.invalid/boleto/…). Ele existe
para a tela de pagamento não ficar vazia — não tente pagá-lo.
Este provedor não existe em loja de produção, e não é uma questão de configuração: o banco recusa a linha que o habilita em qualquer loja
live, por gatilho. Não há como um teste seu aprovar uma compra de verdade.
Recomeçar do zero
Sandbox sem reset vira, em duas semanas, um ambiente tão sujo quanto produção — e a partir daí você para de confiar no que lê nele. Por isso a limpeza é sua, e não um pedido de suporte:
curl -X POST https://{URL_DA_API}/api/v1/integration/sandbox/reset \
-H 'X-Api-Key: revo_test_…' \
-H 'X-Tenant-Id: {a sua loja de sandbox}'
Escopo: catalog:write (quem já pode reescrever o catálogo inteiro já pode
destruí-lo item a item; não inventamos um escopo novo só para isto). Sem corpo,
sem confirmação.
Resposta 200:
{ "tabelasAfetadas": 12, "linhasApagadas": 1287, "duracaoMs": 940 }
O que some: produtos, preços e estoque que você enviou, o de/para, a quarentena, os lotes recebidos, pedidos, carrinhos, clientes de teste, cupons, métricas e as entregas de webhook. Em seguida o catálogo de demonstração é reaplicado inteiro, na mesma transação — a loja nunca fica vazia entre uma coisa e outra.
tabelasAfetadas é quantas tabelas tinham alguma coisa, não quantas foram
varridas — linhasApagadas: 0 é o sinal de que não havia nada para limpar.
O que fica: a sua chave de API, o endereço de webhook que você cadastrou (o segredo dele aparece uma vez só e não voltaria), os usuários da loja e o domínio. Um reset que revogasse a credencial obrigaria você a pedir outra a cada rodada.
O que volta ao padrão: o tema da vitrine e os dados da loja (razão social, CNPJ, endereço de origem) — a loja de sandbox nasce com um perfil de mentira, declaradamente de ninguém, e o reset o repõe.
Numa loja de produção esta rota responde 409 com
"code": "not_a_sandbox_store" — e, na prática, você nem chega lá: uma chave
revo_test_ não alcança loja live. As duas barreiras são independentes de
propósito.
O que continua NÃO existindo
Seja qual for o seu plano, ele precisa considerar:
- Nenhum endereço de sandbox separado (explicado acima). A distinção é o
X-Tenant-Ide o prefixo da chave. - Nenhum autoatendimento público para criar a loja. O pedido é humano. Peça com um dia de antecedência.
- Nenhuma rota para LER um pedido. Você consegue fazer nascer um
order.paidno sandbox — e é uma novidade grande —, mas o evento carrega o id do pedido NA PLATAFORMA e não há rota de integração que devolva o conteúdo dele. Ver seção 10. - Nenhum endereço público para este manual. Ele continua indo por anexo.
10. O que a plataforma ainda NÃO oferece
Lista honesta, para você não desenhar em cima do que não existe:
| O que falta | Consequência para você |
|---|---|
Nenhuma rota de leitura de pedidos. O escopo orders:read existe no vocabulário, mas nenhuma rota o exige. |
Você recebe order.paid com o orderId e não tem como buscar o pedido. Um ERP que precise faturar a venda ainda depende de exportação manual. |
| Nenhuma rota de leitura de clientes. | O mesmo, para CRM. |
| Os webhooks de pedido carregam o id da plataforma, não o seu. | Mesmo com a rota de leitura pronta, o casamento com o seu pedido dependerá do que a leitura devolver. |
| Nenhum evento de webhook para o resultado de um lote. | O acompanhamento da ingestão é síncrono, na resposta do POST. Guarde-a. |
| Nenhum limite de taxa publicado. | Não existe hoje; não conte com ele nem abuse da ausência. |
| Sandbox autoatendido (ver seção 9). | A loja de sandbox EXISTE e vem pronta, mas criá-la ainda é um pedido humano à Revo — não há cadastro público. Depois de criada, você é autônomo (inclusive para recomeçar do zero). |
| Endereço de sandbox separado. | Não existe, e não vai existir tão cedo: o isolamento é por loja + chave, garantido no banco. Acerte o X-Tenant-Id. |
Quando algum desses itens sair, ele vira uma versão nova deste manual e uma linha nova na lista "já foi provado" do seu cartão no painel do lojista.
11. Checklist de homologação
Todo item verde antes de tocar em produção. Faça na sua loja de sandbox (seção 9) — ela já vem com catálogo, frete e pagamento de teste.
- [ ] Você tem a chave
revo_test_…e oX-Tenant-Idda MESMA loja. - [ ] O seu contato técnico aceitou o convite e entra no painel da loja — é lá que você mesmo preenche o de/para.
- [ ]
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 algumlabelvier vazio, corrija agora: é o que o lojista vai ler. - [ ] Você mapeou categoria, eixos e valores no painel da sua loja de sandbox.
- [ ] Reenviar o mesmo lote responde
result: "sem_mudanca". - [ ] O produto aparece na loja, 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; com corpo diferente devolve409. - [ ] Um lote de 500 itens roda dentro do seu tempo limite.
- [ ] Chave errada devolve
401e o seu código não re-tenta. - [ ]
X-Tenant-Iderrado devolve404comcode: "tenant_not_found"e o seu código não re-tenta. - [ ] Sua tela de sincronização mostra
criado/atualizado/sem_mudanca/em_quarentena/rejeitadoe amessagede cada rejeição. - [ ] Se você for consumir webhooks: o seu endpoint recusa corpo com
assinatura inválida, responde
2xxem menos de 10s e descartaidrepetido. - [ ] Se você for consumir os eventos de PEDIDO: uma compra na vitrine da sua
loja de sandbox gerou
order.placedeorder.paidno seu endpoint; e uma compra com e-mail começando emrecusaNÃO gerouorder.paid. - [ ]
POST /sandbox/resetdevolveu a loja ao estado inicial e a sua chave continuou valendo — rode isto ANTES da carga final, para provar que o ambiente está limpo do que você testou.
Só então: 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.
Ritmo recomendado depois de no ar
| O quê | Sugestão |
|---|---|
| Cadastro | 1× por hora, ou por gatilho de alteração. Lotes de 100–200. |
| Preço | a cada 5–15 minutos. Lotes de até 500. |
| Estoque | a cada 5–15 minutos. Lotes de até 500. |
E olhe o summary: se rejeitado > 0 num ciclo, alguém precisa ver.
sem_mudanca alto é o comportamento saudável, não um problema.
12. Suporte
Ao abrir um chamado, mande sempre as quatro coisas:
- o
batchIdda resposta; - o prefixo da chave (
revo_live_R0hZa1B3) — nunca a chave inteira; - o
externalIddo item; - o corpo que você enviou.
Com isso conseguimos reproduzir o caso sem uma troca de mensagens.
Para webhook, mande o X-Revo-Event-Id em vez do batchId.