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

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 /ping deixou de responder o texto pong e 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 campo status; e mande Accept: application/json ou */* — quem fixa Accept: text/plain (sonda de monitoramento é o caso típico) passa a receber 406.

O campo que importa é o storeName, e o anexo explica por quê: o tenantId que 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

  1. O que é uma integradora homologada
  2. O contrato em uma página
  3. Autenticação
  4. Ingestão de catálogo, de ponta a ponta
  5. Quarentena e de/para
  6. O que a API devolve, e os erros
  7. Webhooks de saída
  8. Limites
  9. Sandbox
  10. O que a plataforma ainda NÃO oferece
  11. Checklist de homologação
  12. 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ê

  1. 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.
  2. Um contato técnico e um canal para incidentes.
  3. O checklist da seção 11 inteiro verde.

O que a Revo entrega

  1. Este manual e o anexo api-integracao.md.
  2. O endereço da API do ambiente onde você vai trabalhar.
  3. 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

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

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:

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 productId e variants[].skuId. Eles são os únicos ids nossos que você recebe, e são o que permite ler os webhooks product.updated e stock.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:

  1. o status HTTP;
  2. o campo code dos erros de item, dentro do envelope 200;
  3. o campo code do 404 de loja (tenant_not_found), o único code fora 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.placed e, com o e-mail certo, order.paid logo 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 productId e o skuId que 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


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 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 é text e 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:

Você recebe de volta, na mesma mensagem:

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:

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:

  1. 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á.
  2. Pelas rotas de vitrine, direto do seu servidor, mandando X-Tenant-Id: {a sua loja} em POST /api/v1/storefront/cart/items, POST /api/v1/storefront/cart/shipping-quotes e POST /api/v1/storefront/checkout. Elas resolvem a loja pelo Host, e só consultam o X-Tenant-Id quando 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 aprovadoorder.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:

  1. Nenhum endereço de sandbox separado (explicado acima). A distinção é o X-Tenant-Id e o prefixo da chave.
  2. Nenhum autoatendimento público para criar a loja. O pedido é humano. Peça com um dia de antecedência.
  3. Nenhuma rota para LER um pedido. Você consegue fazer nascer um order.paid no 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.
  4. 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.

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:

Com isso conseguimos reproduzir o caso sem uma troca de mensagens.

Para webhook, mande o X-Revo-Event-Id em vez do batchId.