Documentação técnica

Tudo o que dá para fazer na Vitrina por código: as duas chaves da sua conta, a referência completa das rotas, as opções do widget e como fazer um vídeo aparecer só na página do produto certo.

Nada aqui é obrigatório. Quem instala pela Nuvemshop ou colando a linha do painel não precisa ler esta página. Ela existe para quem quer integrar com o próprio sistema ou ajustar o encaixe no tema.

Nesta página

Começar

O endereço da API pública é https://api.vitrinavideos.com. Todas as rotas descritas aqui vivem sob esse host, respondem application/json em UTF-8 e não têm versão além do prefixo /v1.

As duas chaves: pública e secreta

Sua conta tem duas chaves e elas fazem coisas opostas. Trocar uma pela outra é o erro mais caro que dá para cometer aqui, então vale guardar a diferença antes de qualquer coisa:

 Chave públicaChave secreta de servidor
Formatopk_…sk_…
Onde viveNo HTML da sua loja, à vista de todo mundoSó no seu servidor, em variável de ambiente
Serve paraLer os vídeos já publicados daquela contaCriar vídeo a partir de uma URL
Como viajaNo caminho da URL das rotas de embedNo cabeçalho X-Api-Key
Exige plano pagoNãoSim, a partir do Loja
Onde encontrarPasso “Instalar” da Vitrine, no painelIntegrações, no painel

Chave pública pk_…

É a chave que identifica a sua conta para o widget. Ela vai na tag do script que o painel gera e aparece no caminho de toda rota de embed.

Por que ela pode ficar visível

Porque ela não abre nada que já não esteja público. Com a chave pública só é possível ler, e só os vídeos com status ready daquela conta — os mesmos que qualquer visitante já vê na sua loja. Não dá para enviar vídeo, editar, apagar, ver métricas nem chegar a dado de conta. É o mesmo raciocínio de uma chave de mapa ou de um identificador de analytics: ela é um crachá, não uma senha.

A forma ns_<id da loja>

Quem instala pela loja de aplicativos da Nuvemshop nunca vê uma pk_. O script publicado na vitrine de apps é o mesmo arquivo para todas as lojas, então não há como embutir uma chave própria nele. Nesse caso o widget lê o id da loja do global que a Nuvemshop injeta na página e envia ns_1234567 no lugar da chave. Todas as rotas públicas aceitam as duas formas, indistintamente — inclusive o campo pk da rota de eventos.

Depois do prefixo ns_ só entram dígitos: ns_abc responde 404. Aceitar o id da loja não afrouxa nada — ele também é público por natureza (está no HTML da loja) e dá acesso exatamente ao mesmo conjunto.

Chave secreta de servidor (X-Api-Key)

Você gera essa chave em Integrações, no painel. Ela começa com sk_ e é mostrada uma única vez — gerar de novo invalida a anterior.

A API de integração é recurso do plano Loja. Isso vale nas duas pontas: gerar a chave exige plano pago, e uma chave já gerada para de funcionar se a assinatura terminar. Nesse caso a resposta é 402, não 401 — a chave continua válida e volta a funcionar assim que você assinar.

Ela existe para o seu sistema criar vídeos sozinho, sem ninguém abrir o painel e arrastar arquivo. O caso concreto que motivou isso: produto novo entra no catálogo, o seu ERP/PIM (ou um script de linha de comando, ou uma automação do tipo Zapier/n8n) já tem o vídeo do fornecedor num endereço público, e chama a Vitrina passando essa URL. O vídeo entra na fila de conversão e aparece publicado no bloco poucos minutos depois, com o produto já vinculado. A rota é uma só: POST /v1/videos/from-url.

Esta chave nunca pode ir para o navegador. Não cole no tema da loja, não ponha em HTML, não deixe em arquivo JavaScript servido ao cliente, não mande em print de suporte. Quem tiver essa chave cria vídeos na sua conta e consome a cota do seu plano. Ela vive só no servidor, em variável de ambiente.
Para instalar na loja você usa a chave pública (pk_…), que é outra coisa e está descrita acima. Se desconfiar que vazou, gere uma nova em Integrações: a antiga para de funcionar na hora.

Formato de erro e CORS

Erro tratado sempre volta com o mesmo corpo — um objeto com a chave detail e uma mensagem curta em português, pronta para mostrar a uma pessoa:

{"detail": "Chave desconhecida"}

A exceção é o 422, gerado pela validação de corpo/parâmetros: nele detail é uma lista de objetos apontando qual campo faltou ou veio no tipo errado. Ele aparece em qualquer rota com corpo obrigatório e não está repetido rota a rota abaixo.

CódigoQuando acontece
400Pedido malformado no sentido do negócio (falta um parâmetro que só faz sentido em conjunto, URL fora de http(s), tipo de imagem inválido).
401Sessão inválida (Bearer) ou X-Api-Key ausente/inválida.
402Limite do plano atingido, ou recurso que exige plano pago.
403O recurso existe mas é de outra conta/loja.
404Chave, vídeo, bloco, vitrine ou assinatura inexistente.
409Conflito de estado (slug repetido, assinatura já ativa, vídeo já na fila).
413Arquivo acima do teto aceito.
422Validação de corpo/parâmetro.
502/503Um terceiro (Stripe, Nuvemshop) recusou, ou não está configurado neste ambiente.

CORS: a API espelha a origem de quem chamou e aceita credenciais, então as rotas públicas podem ser chamadas direto do navegador de qualquer domínio. Isso existe porque o widget registra eventos com navigator.sendBeacon, que envia em modo credentials=include — e o navegador recusa curinga nesse modo. A autenticação é por cabeçalho, nunca por cookie, então espelhar não abre brecha de CSRF.

Rotas públicas

São as rotas que o widget usa. Elas respondem com a chave pública no caminho — sem autenticação, sem cabeçalho — e devolvem só o que já é público. Em toda rota abaixo, {chave} pode ser uma pk_… ou um ns_<id da loja>. Se você quiser montar a experiência por conta própria em vez de usar o nosso widget, é daqui que os dados saem.

Quatro delas devolvem vídeos, e o objeto de vídeo é sempre o mesmo:

{
  "id": "66b1c2d3e4f5a60718293a4b",
  "title": "Batom X — como aplicar",
  "hls": "https://cdn.vitrinavideos.com/<conta>/<video>/hls/master.m3u8",
  "poster": "https://cdn.vitrinavideos.com/<conta>/<video>/poster.jpg",
  "preview": "https://cdn.vitrinavideos.com/<conta>/<video>/preview.mp4",
  "duration_s": 18.4,
  "product": {
    "title": "Batom X",
    "url": "https://sualoja.com.br/produtos/batom-x",
    "price": "R$ 59,90"
  }
}

Os três campos de product são strings e vêm vazias quando o vídeo não tem produto vinculado. preview também vem vazio quando o worker ainda não gerou a prévia curta. Só vídeos com status ready aparecem nestas rotas.

GET /v1/embed/{chave} sem autenticação

Configuração da vitrine: nome da loja e a lista de blocos configurados, cada um com seu formato, título, opções de tema e a primeira página de vídeos (12 por bloco).

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
chavepathstringsimpk_… ou ns_<store_id>.
produtoquerystringnão""Endereço da página que está pedindo. Comparado depois de normalizar: sem http(s)://, sem www., sem query string, sem fragmento e sem barra final.

Quando produto vem preenchido, os blocos marcados como “vídeo do produto da página” (theme.match_product) já saem filtrados do servidor — o navegador não baixa o catálogo inteiro e o HTML da loja não expõe vídeos de outros produtos. Se o bloco é de produto e o parâmetro não veio, ele volta com a lista vazia de propósito.

Requisição

curl "https://api.vitrinavideos.com/v1/embed/pk_suachave?produto=sualoja.com.br/produtos/batom-x"

Resposta 200

{
  "store": "Sua Loja",
  "placements": [
    {
      "id": "66a9f0e1d2c3b40516273849",
      "kind": "carousel",
      "slug": "home",
      "title": "Nossos vídeos",
      "theme": {"pagina": "home", "autoplay": false},
      "videos": [ /* até 12 objetos de vídeo */ ],
      "total": 37,
      "tem_mais": true,
      "por_produto": false
    }
  ]
}
CampoTipoDescrição
storestringNome da loja, como cadastrado na conta.
placements[].idstringId do bloco. É o que você manda em placement_id ao registrar evento.
placements[].kindstringFormato: carousel, slider, stories, feed, bubble ou hero.
placements[].slugstringIdentificador do bloco. É o que vai em data-shorts e no parâmetro slug da paginação.
placements[].titlestringTítulo exibido acima da fileira.
placements[].themeobjetoOpções do bloco. Chaves em As chaves de theme. Vem {} quando nada foi configurado.
placements[].videoslistaPrimeira página, no máximo 12 objetos de vídeo.
placements[].totalinteiroQuantos vídeos o bloco tem ao todo, já aplicado o filtro de produto.
placements[].tem_maisbooleanotrue quando total passa de 12.
placements[].por_produtobooleanoReflete theme.match_product: o bloco só mostra vídeos do produto da página.

Ordem dos vídeos

Depende do modo de seleção do bloco. manual devolve a sequência exata escolhida no painel; smart ordena por conversão (CTR suavizado dos últimos 14 dias, contando aberturas e cliques no produto contra impressões); qualquer outro valor — o padrão gravado é recent — devolve do mais novo para o mais antigo.

Erros

CódigoSignificado
404Chave desconhecida — não existe conta com essa pk_, ou o ns_ não termina em dígitos, ou nenhuma conta tem esse store_id.

GET /v1/embed/{chave}/pagina sem autenticação

Próxima página de vídeos de um bloco, para carregar conforme o cliente arrasta. É deslocamento e não cursor porque a ordem depende do modo escolhido e precisa ser estável entre chamadas.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
chavepathstringsimpk_… ou ns_<store_id>.
slugquerystringsimIdentificador do bloco.
skipqueryinteironão0Quantos vídeos pular.
limitqueryinteironão12Quantos trazer. Preso entre 1 e 48: valor fora disso é ajustado, não recusado.

Requisição

curl "https://api.vitrinavideos.com/v1/embed/pk_suachave/pagina?slug=home&skip=12&limit=12"

Resposta 200

{
  "videos": [ /* objetos de vídeo */ ],
  "total": 37,
  "tem_mais": true
}

total é o tamanho da lista inteira do bloco (não da fatia). tem_mais é skip + limit < total. Esta rota não aplica o filtro de produto: match_product só vale na rota de configuração.

Erros

CódigoSignificado
404Chave desconhecida.
404Bloco não encontrado — não há bloco vivo com esse slug na conta.
422slug ausente.

GET /v1/embed/{chave}/video/{video_id} sem autenticação

Um vídeo só, pelo id — para quem monta a própria página e quer inserir um vídeo específico.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
chavepathstringsimpk_… ou ns_<store_id>.
video_idpathstringsimO id devolvido nas outras rotas (24 caracteres hexadecimais).

Requisição

curl "https://api.vitrinavideos.com/v1/embed/pk_suachave/video/66b1c2d3e4f5a60718293a4b"

Resposta 200

Um objeto de vídeo, sem invólucro — exatamente o formato mostrado no início desta seção.

Erros

CódigoSignificado
404Chave desconhecida.
404Vídeo não encontrado — id inválido, vídeo de outra conta, apagado, ou ainda não ready.

GET /v1/embed/{chave}/produto sem autenticação

Os vídeos de um produto, sem baixar o catálogo. É a rota certa para a página de produto de um site feito à mão.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
chavepathstringsimpk_… ou ns_<store_id>.
urlquerystringnão*""Endereço da página do produto. Normalizado antes de comparar: ignora http/https, www., query string e barra final.
skuquerystringnão*""SKU do produto. Casamento exato. Tem precedência: informando os dois, só o SKU é usado.
limitqueryinteironão12Máximo de vídeos devolvidos.

* Pelo menos um entre url e sku é obrigatório.

Requisição

curl "https://api.vitrinavideos.com/v1/embed/pk_suachave/produto?url=https://sualoja.com.br/produtos/batom-x"

Resposta 200

{
  "videos": [ /* objetos de vídeo */ ],
  "total": 2
}

Aqui total é o tamanho da lista devolvida, já cortada por limit — não o total do catálogo.

O casamento por sku é exato e estável; o casamento por url quebra se você mudar o endereço do produto. Use SKU quando tiver. Repare que hoje nem o painel nem a rota de integração expõem um campo para escrever o SKU — na prática, o casamento em uso é o por URL.

Erros

CódigoSignificado
400Informe url ou sku do produto — nenhum dos dois veio.
404Chave desconhecida.

POST /v1/e sem autenticação

Ingestão de evento. É como o widget registra o que aconteceu, e é o que alimenta as métricas do painel, a contagem de views do mês e a ordenação por conversão.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
pkbodystringsimChave pública pk_… ou ns_<store_id>.
typebodystringsimTipo do evento. Truncado em 30 caracteres.
video_idbodystringnão""Vídeo a que o evento se refere.
placement_idbodystringnão""Bloco em que aconteceu.
session_idbodystringnão""Identificador anônimo da sessão do widget. Truncado em 64 caracteres.
video_idsbodylista de stringnão[]Só na impressão em lote: os vídeos exibidos naquela renderização. No máximo 50 itens, cada um truncado em 32 caracteres.
Origin/RefererheaderstringnãoNão é parâmetro seu: o servidor extrai o host de um dos dois e guarda (120 caracteres) para provar ao lojista que o widget está rodando.

Tipos usados hoje

TipoQuandoEntra em
impressionUm evento por renderização, com a lista exibida em video_ids.Views do mês, denominador do CTR e do score inteligente.
openO cliente abriu o player.Métrica por vídeo e score inteligente.
playO vídeo tocou.Métrica por vídeo.
cta_clickO cliente clicou para ir ao produto.Numerador do CTR; pesa o dobro no score inteligente.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/e \
  -H "Content-Type: application/json" \
  -d '{"pk":"pk_suachave","type":"open","video_id":"66b1c2d3e4f5a60718293a4b",
       "placement_id":"66a9f0e1d2c3b40516273849","session_id":"a1b2c3d4"}'

Resposta 200

{"ok": true}

Erros

CódigoSignificado
200 com {"ok": false}A chave não existe (ou o ns_ é malformado). É de propósito que não vira erro: telemetria nunca deve derrubar a página da loja.
422pk ou type ausente no corpo.

GET /v1/storefront/{handle} sem autenticação

Dados públicos de uma vitrine hospedada — a página vitrinavideos.com/@sualoja. Útil se você quiser exibir a mesma vitrine em outro lugar.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
handlepathstringsimApelido da loja. O @ inicial, espaços e maiúsculas são ignorados.

Requisição

curl "https://api.vitrinavideos.com/v1/storefront/sualoja"

Resposta 200

{
  "handle": "sualoja",
  "store": "Sua Loja",
  "bio": "Maquiagem de verdade, testada por gente de verdade.",
  "cor": "#0b7a6b",
  "capa": "https://cdn.vitrinavideos.com/<conta>/vitrine/capa-1754500000.webp",
  "avatar": "https://cdn.vitrinavideos.com/<conta>/vitrine/avatar-1754500000.webp",
  "links": [{"titulo": "Comprar no site", "url": "https://sualoja.com.br"}],
  "public_key": "pk_suachave",
  "videos": [ /* até 60 objetos de vídeo, do mais novo ao mais antigo */ ]
}

Nada de e-mail, telefone, CNPJ ou chave secreta sai daqui. links tem no máximo 6 itens; capa e avatar vêm vazios quando não foram enviados; cor é #0b7a6b por padrão.

Erros

CódigoSignificado
404Vitrine não encontrada — nenhuma conta tem esse apelido.

GET /healthz sem autenticação

Sonda de disponibilidade, para o seu monitoramento. Não recebe parâmetro nenhum e não toca no banco.

Parâmetros

Nenhum.

Requisição

curl https://api.vitrinavideos.com/healthz

Resposta 200

{"ok": true}

Erros

Nenhum previsto. Qualquer coisa diferente de 200 significa que a API não está no ar.

Integração por X-Api-Key

Uma rota só, e é a que importa para automação: criar vídeo a partir de uma URL. A chave vai no cabeçalho X-Api-Key e nunca na URL. Para gerar a chave, veja POST /v1/videos/api-key — é o painel que a emite.

POST /v1/videos/from-url X-Api-Key

Cria um vídeo a partir de um arquivo já hospedado num endereço público. O worker baixa, converte para HLS, gera pôster e prévia, e publica no bloco. A resposta é imediata: o processamento acontece depois.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
X-Api-KeyheaderstringsimChave secreta sk_… da conta.
urlbodystringsimEndereço http:// ou https:// do arquivo de vídeo. Guardado com no máximo 1000 caracteres.
titlebodystringnãonome do arquivoNome do vídeo no painel. Vazio, usamos o último trecho da url (até 80 caracteres).
product_titlebodystringnãoNome do produto exibido no card e no player. Truncado em 500 caracteres.
product_urlbodystringnãoLink do produto. É por ele que o vídeo casa com a página do produto. Truncado em 500.
product_pricebodystringnãoPreço como texto, do jeito que você quer exibir. Truncado em 500.

Campos de produto vazios ou ausentes são simplesmente ignorados — não apagam nada, porque o vídeo está sendo criado agora. Qualquer outra chave enviada no corpo é descartada em silêncio.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/videos/from-url \
  -H "X-Api-Key: sk_suachavesecreta" \
  -H "Content-Type: application/json" \
  -d '{
        "url": "https://cdn.suamarca.com/videos/batom-x.mp4",
        "title": "Batom X — como aplicar",
        "product_title": "Batom X",
        "product_url": "https://sualoja.com.br/produtos/batom-x",
        "product_price": "R$ 59,90"
      }'

Resposta 200

{"video_id": "66b1c2d3e4f5a60718293a4b", "status": "processing"}

Guarde o video_id: é com ele que o vídeo aparece nas rotas de embed assim que o status virar ready. A conversão costuma levar de 1 a 3 minutos. Esta rota não devolve o progresso — quem acompanha isso é o painel.

Erros

CódigoSignificado
401X-Api-Key ausente ou inválida — o cabeçalho não veio ou não começa com sk_.
401Chave de API inválida — o formato está certo mas nenhuma conta tem essa chave (foi regerada, por exemplo).
402A conta não está num plano pago. A chave continua válida e volta a funcionar quando a assinatura voltar.
402Seu plano publica N peças e todas estão no ar. … A mensagem traz o número, a saída grátis (marcar uma peça como vendida ou escondê-la) e o plano seguinte.
400Informe a URL http(s) do arquivo de vídeourl vazia ou com outro esquema.

Nuvemshop: OAuth e webhooks

Estas rotas não são chamadas pelo seu código: quem chama é o navegador do lojista, no fluxo de autorização, e os servidores da Nuvemshop. Estão documentadas porque aparecem na configuração do app parceiro e porque o comportamento delas explica o que acontece quando alguém instala a Vitrina pela loja de aplicativos.

Sem NUVEMSHOP_CLIENT_ID e NUVEMSHOP_CLIENT_SECRET configurados, todas respondem 503 e nada mais no produto quebra.

GET /v1/nuvemshop/callback sem autenticação

Volta da autorização — e também o destino do botão “Configurar” no admin da Nuvemshop, que chega aqui sem code. Nunca devolve JSON de erro: qualquer falha vira um redirecionamento para uma tela do painel com a mensagem.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
codequerystringnão""Código de uso único da autorização. Sem ele, redireciona para a tela de conexão.
statequerystringnão""Id da conta Vitrina que iniciou a autorização. Ausente quando a instalação veio pela vitrine de apps.

Requisição

GET https://api.vitrinavideos.com/v1/nuvemshop/callback?code=abc123&state=66a9f0e1d2c3b40516273849

Respostas

SituaçãoResposta
Sem code307/app/connect/?instalar=nuvemshop
state é uma conta existenteLoja vinculada; 307/app/integrations/?nuvemshop=ok
Sem conta (veio do marketplace)Autorização guardada como vínculo pendente por 30 minutos; 307/app/connect/?vinculo=<nonce>
A Nuvemshop recusou o code307/app/connect/?instalar=nuvemshop&erro=<motivo>

Erros

CódigoSignificado
503Integração Nuvemshop ainda não configurada neste ambiente.

GET /v1/nuvemshop/install-url sem autenticação

Endereço da tela de autorização da Nuvemshop, sem exigir sessão — quem chega pelo “Configurar” do admin ainda não tem conta aqui, e a tela de conexão precisa montar o botão antes de qualquer login.

Parâmetros

Nenhum.

Requisição

curl https://api.vitrinavideos.com/v1/nuvemshop/install-url

Resposta 200

{"url": "https://www.nuvemshop.com.br/apps/<app_id>/authorize"}

Erros

CódigoSignificado
503Integração não configurada neste ambiente.

GET /v1/nuvemshop/vinculo/{nonce} sem autenticação

Dados públicos de uma autorização pendente, para a tela de boas-vindas dizer “conectando Fulana Store” antes de a pessoa criar conta. O nonce já é o segredo: quem o tem acabou de autorizar o app.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
noncepathstringsimToken de uso único devolvido no redirecionamento do callback. Vale 30 minutos.

Requisição

curl https://api.vitrinavideos.com/v1/nuvemshop/vinculo/8f3a...

Resposta 200

{"loja": "Sua Loja", "plataforma": "nuvemshop", "store_id": 1234567}

O access_token da loja nunca sai por aqui.

Erros

CódigoSignificado
404Autorização expirada — instale o app de novo pela Nuvemshop. Também é a resposta para nonce inexistente.

POST /v1/nuvemshop/webhooks/store-redact
POST /v1/nuvemshop/webhooks/customers-redact
POST /v1/nuvemshop/webhooks/customers-data-request HMAC do app

Os três webhooks de privacidade que a Nuvemshop exige para homologar um app. Chegam sem sessão e são assinados com o client secret.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
x-linkedstore-hmac-sha256headerstringsimHMAC-SHA256 do corpo cru com o client secret, em base64. Comparado em tempo constante.
store_idbodyinteironãoSó em store-redact: a loja que desinstalou. Sem ele, a resposta é {"ok": true} e nada é apagado.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/nuvemshop/webhooks/store-redact \
  -H "x-linkedstore-hmac-sha256: <assinatura base64>" \
  -H "Content-Type: application/json" \
  -d '{"store_id": 1234567}'

Respostas 200

// store-redact — apaga o vínculo e o catálogo importado; os vídeos ficam
{"ok": true, "contas": 1}

// customers-redact
{"ok": true, "detail": "Nenhum dado pessoal de comprador é armazenado"}

// customers-data-request
{"ok": true, "data": {}, "detail": "Nenhum dado pessoal de comprador é armazenado"}

Em store-redact saem o token da loja, os produtos importados com source: "nuvemshop" e os vínculos pendentes daquela loja. Os vídeos e a conta Vitrina não são tocados: são do lojista, não da plataforma.

Erros

CódigoSignificado
401Assinatura inválida.
503Integração não configurada neste ambiente (sem client secret não há como verificar assinatura).

O widget no seu site

A tag do script

A chave pública identifica a conta para o widget. Ela vai na tag que o painel gera:

<script type="module" async src="https://vitrinavideos.com/widget.js"
        data-key="pk_suachave"
        data-api="https://api.vitrinavideos.com"></script>

Cada bloco entra no HTML como uma âncora com o slug do bloco. O atributo data-shorts-carousel é aceito como sinônimo de data-shorts, e uma âncora sem valor cai no slug home:

<div data-shorts="home"></div>

Se o bloco tiver theme.selector, o widget cria a âncora sozinho e você não precisa colar nada no tema — é assim que funciona a instalação em um clique.

Para exibir uma vitrine hospedada em outra página, a tag aceita data-vitrine="sualoja" no lugar de data-key; nesse caso data-api é opcional.

Opções de layout

O widget monta cada bloco dentro de um Shadow DOM. Isso é o que impede o tema da loja de quebrar o player e o player de vazar estilo na loja — mas também significa que o seu CSS não alcança o que está dentro. As custom properties abaixo atravessam essa fronteira por design: são a porta oficial de ajuste.

<div data-shorts="home"
     style="--sw-pad:0 24px; --sw-max:640px; --sw-gap:16px"></div>
VariávelChave no painelPadrãoO que controla
--sw-padpad0Respiro interno da seção. Aceita qualquer valor de padding.
--sw-maxmaxnoneLargura máxima da seção — para encaixar na coluna de conteúdo do tema.
--sw-alignalign0Vai em margin-inline. Use auto para centralizar dentro do --sw-max.
--sw-gapgap10pxEspaço entre os cards da fileira.
--sw-cardcardmin(34vw,150px)Largura do card 9:16 no carrossel e no feed.

As mesmas cinco existem como chaves do theme do bloco, com os nomes curtos da coluna do meio; o widget as traduz nas variáveis. Isso importa em um caso específico: quando o bloco é inserido automaticamente por seletor, quem instalou em um clique não tem nenhum elemento no tema onde escrever style="--sw-pad:…" — então o único caminho é pelo tema do bloco.

Se você não definir max, pad nem align, o widget mede a página e alinha a seção com a coluna de conteúdo do tema sozinho — ele procura a borda esquerda mais repetida entre os blocos reais da página, e só aceita o resultado se ela se repetir pelo menos duas vezes. Qualquer escolha explícita sua desliga essa medição.

As chaves de theme

O objeto theme do bloco vem inteiro na resposta de GET /v1/embed/{chave}. Além das cinco de layout, o widget lê estas:

ChaveValoresO que faz
paginatodas (padrão), home, produto, colecao Em que tipo de página o bloco aparece. Na Nuvemshop usamos o template informado pela plataforma (LS.template); fora dela, uma heurística de caminho. Página que não cai em nenhum dos três é outra.
match_producttrue / false Bloco de produto: só mostra os vídeos vinculados ao produto da página atual. Filtrado no servidor, a partir do parâmetro produto.
show_productfalse esconde Oculta nome, preço e link do produto em todos os formatos e no player.
roundedfalse deixa reto Cantos arredondados dos cards. false zera o raio.
autoplayfalse desliga O carrossel andar sozinho. Ele já pausa ao passar o mouse ou tocar; false desliga de vez.
ver_todosfalse esconde O botão “Ver todos os N vídeos” abaixo da fileira, que abre o catálogo do bloco em grade.
storytrue / false Modo história: os vídeos do bloco viram capítulos em sequência, com barra de progresso e avanço automático, em vez de vitrines independentes.
selectorseletor CSS Inserção automática: sem âncora no tema, o widget cria o <div> em relação ao primeiro elemento que casar. Seletor inválido ou sem correspondência é ignorado em silêncio.
insertafter (padrão), before, start, end Onde a âncora automática entra em relação ao selector: depois, antes, como primeiro filho ou como último filho.

O formato stories já se comporta como sequência sem precisar de story. A chave serve para dar esse comportamento a um bloco de outro formato. Blocos com selector só são inseridos se tiverem pelo menos um vídeo e se pagina casar com a página atual.

Vincular vídeo a produto

Vincular é dizer qual produto aquele vídeo mostra. É isso que faz aparecer o nome, o preço e o botão que leva à página do produto — e é isso que permite ao bloco saber que ele pertence àquela página.

  • Pelo painel: abra o vídeo e escolha o produto. Se você importou o catálogo da Nuvemshop, o campo autocompleta com imagem e preço. Também dá para colar o link do produto: lemos os dados da própria página.
  • Pela API de integração: mande product_title, product_url e product_price no POST /v1/videos/from-url.
  • Depois, pelo painel: os mesmos três campos são editáveis por PATCH /v1/videos/{id}.

O campo que importa para o casamento automático é o product_url: ele precisa ser o endereço real da página do produto na sua loja.

Fazer a seção aparecer só na página daquele produto

  1. Vincule os vídeos aos respectivos produtos.
  2. Crie um bloco e marque a opção de vídeo do produto da página (match_product). Vale a pena marcar também pagina: produto, para o bloco nem tentar existir fora da página de produto.
  3. Coloque a âncora do bloco no template de produto do tema — o mesmo <div data-shorts="…"> serve para todos os produtos, porque quem escolhe o vídeo é o endereço da página, não o HTML.

A partir daí cada página de produto mostra só os vídeos daquele produto, e a página que não tiver vídeo nenhum simplesmente não mostra a seção — sem espaço vazio e sem título solto.

Apontar um produto diferente da página

Se você quiser um bloco “quem viu esse, viu aquele”, dá para dizer na própria âncora qual produto usar, em vez do produto da página:

<div data-shorts="relacionados"
     data-shorts-product="https://sualoja.com.br/produtos/outro-item"></div>

Com data-shorts-product vazio, o widget usa o endereço da página atual. O filtro é feito no navegador, sobre os vídeos que o bloco já trouxe.

Uso interno do painel

Estas rotas não fazem parte da integração. Elas exigem a sessão do painel (Authorization: Bearer <token>, obtido no login) e existem para o aplicativo em /app. Não há contrato de estabilidade: podem mudar sem aviso. Se você precisa automatizar algo por aqui, escreva antes — talvez a resposta seja uma rota nova na API de integração.

Toda rota desta seção responde 401 Sessão inválida quando o token está ausente, expirado ou não corresponde a nenhuma conta. Erros de validação de corpo saem como 422. Abaixo, só o que é específico de cada uma.

Vídeos

POST /v1/videos/presign Bearer

Cria o registro do vídeo e devolve por onde enviar o arquivo.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
filenamebodystringsimNome do arquivo. Vira o título quando title não vem.
titlebodystringnão""Título do vídeo no painel.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/videos/presign \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"filename":"batom-x.mp4","title":"Batom X"}'

Resposta 200

{
  "video_id": "66b1c2d3e4f5a60718293a4b",
  "via_api": false,
  "upload_url": "https://<storage>/...?X-Amz-Signature=..."
}

Com via_api: false, envie o arquivo com PUT direto na upload_url. Com true, o storage está atrás de um proxy com prefixo e a upload_url aponta para PUT /v1/videos/{id}/upload.

Erros

CódigoSignificado
402Seu plano publica N peças e todas estão no ar. … A mesma mensagem do envio por URL.

PUT /v1/videos/{video_id}/upload Bearer

Recebe o arquivo e grava no storage, quando não dá para pré-assinar.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
video_idpathstringsimId devolvido pelo presign.
Content-Typeheaderstringnãovideo/mp4Tipo gravado no objeto do storage.
(corpo)bodybináriosimO arquivo cru, sem multipart.

Requisição

curl -X PUT https://api.vitrinavideos.com/v1/videos/66b1.../upload \
  -H "Authorization: Bearer <token>" -H "Content-Type: video/mp4" \
  --data-binary @batom-x.mp4

Resposta 200

{"ok": true, "bytes": 8431920}

Erros

CódigoSignificado
400Arquivo vazio.
404Vídeo não encontrado — id inválido, de outra conta ou apagado.

POST /v1/videos/{video_id}/complete Bearer

Avisa que o upload terminou e põe o vídeo na fila de conversão.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
video_idpathstringsimId do vídeo.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/videos/66b1.../complete \
  -H "Authorization: Bearer <token>"

Resposta 200

{"status": "processing"}

Erros

CódigoSignificado
404Vídeo não encontrado.

POST /v1/videos/photos/presign Bearer

URLs assinadas para subir as fotos que virarão um vídeo gerado.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
countbodyinteironão1Quantas URLs gerar. Teto de 12.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/videos/photos/presign \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"count": 5}'

Resposta 200

{"photos": [{"key": "<conta>/photos/ab12cd34-0.jpg", "upload_url": "https://..."}]}

Cada URL espera Content-Type: image/jpeg.

Erros

Nenhum específico.

POST /v1/videos/from-photos Bearer

Cria um vídeo com movimento de câmera a partir das fotos já enviadas.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
photo_keysbodylista de stringsimAs key devolvidas pelo presign de fotos. Máximo 12; cada uma truncada em 300 caracteres.
titlebodystringnãoVídeo geradoTítulo no painel.
stylebodystringnãoimovelproduto ou imovel. Qualquer outro valor vira imovel.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/videos/from-photos \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"photo_keys":["<conta>/photos/ab12cd34-0.jpg"],"title":"Batom X","style":"produto"}'

Resposta 200

{"video_id": "66b1c2d3e4f5a60718293a4b", "status": "processing"}

Erros

CódigoSignificado
400Envie ao menos uma foto.
402Limite de vídeos do plano atingido.

POST /v1/videos/api-key Bearer

Gera (ou troca) a chave de integração sk_… da conta. Chamar de novo invalida a anterior na hora, e a chave só é exibida nesta resposta.

Parâmetros

Nenhum.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/videos/api-key \
  -H "Authorization: Bearer <token>"

Resposta 200

{"api_key": "sk_..."}

Erros

CódigoSignificado
402A conta não está num plano pago. A API de integração faz parte do plano Loja.

GET /v1/videos Bearer

A biblioteca da conta, com progresso de conversão e métricas por vídeo.

Parâmetros

Nenhum.

Requisição

curl https://api.vitrinavideos.com/v1/videos -H "Authorization: Bearer <token>"

Resposta 200

[
  {
    "id": "66b1c2d3e4f5a60718293a4b",
    "title": "Batom X — como aplicar",
    "status": "ready",
    "duration_s": 18.4,
    "poster": "https://cdn.vitrinavideos.com/...",
    "hls": "https://cdn.vitrinavideos.com/...",
    "product_title": "Batom X",
    "product_url": "https://sualoja.com.br/produtos/batom-x",
    "product_price": "R$ 59,90",
    "error": "",
    "stage": "",
    "progress": 100,
    "metrics": {"impression": 1204, "open": 96, "play": 88, "cta_click": 31, "ctr": 2.6}
  }
]

Lista crua, do mais novo para o mais antigo, sem paginação. status é uploading, processing, ready ou error. ctr é a porcentagem de cta_click sobre impression, com uma casa decimal, e vale 0.0 quando não houve impressão.

Erros

Nenhum específico.

PATCH /v1/videos/{video_id} Bearer

Edita título e dados do produto vinculado.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
video_idpathstringsimId do vídeo.
titlebodystringnãoNovo título.
product_titlebodystringnãoNome do produto.
product_urlbodystringnãoLink do produto.
product_pricebodystringnãoPreço como texto.

Campo ausente ou null não é alterado. Para limpar um campo, mande string vazia.

Requisição

curl -X PATCH https://api.vitrinavideos.com/v1/videos/66b1... \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"product_url":"https://sualoja.com.br/produtos/batom-x"}'

Resposta 200

{"ok": true}

Erros

CódigoSignificado
404Vídeo não encontrado.

POST /v1/videos/{video_id}/retry Bearer

Reprocessa um vídeo, sem exigir novo upload. Zera error, stage e progress.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
video_idpathstringsimId do vídeo.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/videos/66b1.../retry \
  -H "Authorization: Bearer <token>"

Resposta 200

{"status": "processing"}

Erros

CódigoSignificado
404Vídeo não encontrado.
409Esse vídeo já está na fila de processamento — só vale para status ready ou error.
400O arquivo original não está mais disponível — envie de novo.

DELETE /v1/videos/{video_id} Bearer

Exclusão lógica: o vídeo some das listagens e do embed, mas o registro é marcado, não removido.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
video_idpathstringsimId do vídeo.

Requisição

curl -X DELETE https://api.vitrinavideos.com/v1/videos/66b1... \
  -H "Authorization: Bearer <token>"

Resposta 200

{"ok": true}

Erros

CódigoSignificado
404Vídeo não encontrado.

GET /v1/install/status Bearer

Confirma que o widget está rodando na loja. Qualquer evento registrado serve como prova de vida.

Parâmetros

Nenhum.

Requisição

curl https://api.vitrinavideos.com/v1/install/status -H "Authorization: Bearer <token>"

Resposta 200

{"installed": true, "origin": "sualoja.com.br",
 "last_seen": "2026-08-06T14:02:11.482000+00:00", "tipo": "impression"}

// nenhum evento ainda
{"installed": false}

origin vem do evento mais recente que tenha origem registrada, que nem sempre é o mais recente de todos.

Erros

Nenhum específico.

Blocos

GET /v1/placements Bearer

Os blocos da conta, do mais antigo para o mais novo.

Parâmetros

Nenhum.

Requisição

curl https://api.vitrinavideos.com/v1/placements -H "Authorization: Bearer <token>"

Resposta 200

[
  {
    "id": "66a9f0e1d2c3b40516273849",
    "kind": "carousel",
    "slug": "home",
    "title": "Nossos vídeos",
    "selection": "recent",
    "video_ids": [],
    "theme": {"pagina": "home"}
  }
]

Erros

Nenhum específico.

POST /v1/placements Bearer

Cria um bloco. Recriar um slug que já foi excluído reaproveita o registro antigo em vez de dar conflito.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
slugbodystringsimIdentificador único na conta. É o que vai em data-shorts.
kindbodystringnãocarouselcarousel, slider, stories, feed, bubble ou hero.
titlebodystringnão""Título exibido acima da fileira.
selectionbodystringnãorecentmanual fixa a sequência de video_ids; smart ordena por conversão; qualquer outro valor é “mais recentes”.
video_idsbodylista de stringnão[]Recorta quais vídeos entram. Lista vazia significa “todos os publicados”.
themebodyobjetonão{}Opções do bloco. Chaves em As chaves de theme.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/placements \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"slug":"produto","kind":"carousel","title":"Veja em vídeo",
       "theme":{"pagina":"produto","match_product":true}}'

Resposta 200

{"id": "66a9f0e1d2c3b40516273849"}

Erros

CódigoSignificado
409Você já tem um bloco com esse identificador. Edite o existente ou use outro nome.

PATCH /v1/placements/{placement_id} Bearer

Edita o bloco. O slug não é editável — trocar o identificador quebraria a âncora já colada no tema.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
placement_idpathstringsimId do bloco.
titlebodystringnãoNovo título.
kindbodystringnãoNovo formato. Trocar carrossel por stories não exige refazer a instalação no tema.
selectionbodystringnãoNovo modo de ordenação.
video_idsbodylista de stringnãoSubstitui a seleção inteira.
themebodyobjetonãoSubstitui o objeto inteiro, não mescla chave a chave.

Requisição

curl -X PATCH https://api.vitrinavideos.com/v1/placements/66a9... \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"selection":"smart"}'

Resposta 200

{"ok": true}

Corpo sem nenhum campo preenchido também responde {"ok": true}, sem tocar em nada e sem verificar se o bloco existe.

Erros

CódigoSignificado
404Posicionamento não encontrado.

POST /v1/placements/{placement_id}/videos Bearer

Soma vídeos a um bloco — é o “adicionar” e também o “copiar para”. Nada aqui reprocessa vídeo: o arquivo já está convertido, então aparecer em mais um lugar custa zero.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
placement_idpathstringsimId do bloco.
video_idsbodylista de stringnão[]Ids a somar. Só entram os que existem na conta e ainda não estavam na lista. O bloco guarda no máximo 50.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/placements/66a9.../videos \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"video_ids":["66b1c2d3e4f5a60718293a4b"]}'

Resposta 200

{"ok": true, "video_ids": ["66b1c2d3e4f5a60718293a4b"], "adicionados": 1}

Erros

CódigoSignificado
404Bloco não encontrado.

DELETE /v1/placements/{placement_id}/videos/{video_id} Bearer

Tira o vídeo do bloco. O vídeo em si continua na biblioteca.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
placement_idpathstringsimId do bloco.
video_idpathstringsimId do vídeo a remover da lista.

Requisição

curl -X DELETE https://api.vitrinavideos.com/v1/placements/66a9.../videos/66b1... \
  -H "Authorization: Bearer <token>"

Resposta 200

{"ok": true, "video_ids": []}

Erros

CódigoSignificado
404Bloco não encontrado.

PUT /v1/placements/videos/{video_id} Bearer

A visão contrária: olhando um vídeo, dizer em quais blocos ele aparece. Blocos fora da lista perdem o vídeo.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
video_idpathstringsimId do vídeo.
placement_idsbodylista de stringnão[]Blocos em que o vídeo deve aparecer. Lista vazia tira o vídeo de todos.

Requisição

curl -X PUT https://api.vitrinavideos.com/v1/placements/videos/66b1... \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"placement_ids":["66a9f0e1d2c3b40516273849"]}'

Resposta 200

{"ok": true, "adicionado": 1, "removido": 0}

Erros

CódigoSignificado
404Vídeo não encontrado.

DELETE /v1/placements/{placement_id} Bearer

Exclusão lógica do bloco. Os vídeos continuam na biblioteca.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
placement_idpathstringsimId do bloco.

Requisição

curl -X DELETE https://api.vitrinavideos.com/v1/placements/66a9... \
  -H "Authorization: Bearer <token>"

Resposta 200

{"ok": true}

Erros

CódigoSignificado
404Posicionamento não encontrado.

Produtos

GET /v1/products Bearer

Catálogo do lojista — alimenta o autocomplete ao vincular produto a vídeo. É preenchido pelo import da Nuvemshop e por todo produto importado por link.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
qquerystringnão""Busca por trecho do título, sem diferenciar maiúsculas. Vazio devolve o começo da lista.
limitqueryinteironão8Quantos trazer. Teto de 25.

Requisição

curl "https://api.vitrinavideos.com/v1/products?q=batom&limit=8" \
  -H "Authorization: Bearer <token>"

Resposta 200

[
  {
    "id": "66c0...",
    "title": "Batom X",
    "url": "https://sualoja.com.br/produtos/batom-x",
    "price": "59.90",
    "image": "https://cdn.sualoja.com.br/batom-x.jpg",
    "source": "nuvemshop"
  }
]

Ordenado por título. source é nuvemshop ou link.

Erros

Nenhum específico.

Plano e cobrança

GET /v1/billing/me Bearer

Plano atual, consumo e a escada inteira de planos, para o painel desenhar a tela sem tabela fixa no código.

Parâmetros

Nenhum.

Requisição

curl https://api.vitrinavideos.com/v1/billing/me -H "Authorization: Bearer <token>"

Resposta 200

{
  "store": "Sua Loja",
  "email": "voce@sualoja.com.br",
  "plan": "free",
  "plans": { "free": {"name": "Grátis", "video_limit": 3, "view_limit": null,
                      "price_month": "R$ 0", "price_year": "R$ 0", "order": 0,
                      "highlights": ["..."]}, "...": {} },
  "videos_used": 2,
  "video_limit": 3,
  "views_used": 1204,
  "view_limit": null,
  "over_views": false,
  "checkout_available": true
}

Nenhum plano limita exibição: view_limit é sempre null e over_views sempre false — os campos ficam no contrato para não quebrar telas antigas. video_limit é null no Business (sem teto). views_used conta as impressões do mês corrente.

Erros

Nenhum específico.

POST /v1/billing/checkout Bearer

Abre uma sessão de Checkout do Stripe para assinar um plano.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
planquerystringnãoproinicial, starter ou pro.
periodquerystringnãomonthyear usa o preço anual; qualquer outro valor cai no mensal.

Requisição

curl -X POST "https://api.vitrinavideos.com/v1/billing/checkout?plan=inicial&period=year" \
  -H "Authorization: Bearer <token>"

Resposta 200

{"url": "https://checkout.stripe.com/c/pay/..."}

Erros

CódigoSignificado
400Escolha um dos planos pagos — Business é sob consulta.
409A conta já tem assinatura ativa. Trocar de plano passa pelo portal, para o Stripe fazer o cálculo proporcional em vez de criar uma segunda assinatura.
503Pagamentos ainda não configurados neste ambiente.

POST /v1/billing/confirm Bearer

Confirma a assinatura na volta do checkout, sem esperar o webhook. O webhook continua sendo a fonte oficial; isto existe para o lojista não ficar esperando pelo que acabou de pagar.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
session_idquerystringsimId da sessão de Checkout, devolvido na URL de sucesso.

Requisição

curl -X POST "https://api.vitrinavideos.com/v1/billing/confirm?session_id=cs_live_..." \
  -H "Authorization: Bearer <token>"

Resposta 200

{"plan": "inicial", "paid": true}

// pagamento ainda não compensado
{"plan": "free", "paid": false}

Erros

CódigoSignificado
403Essa sessão de pagamento é de outra conta.
404Sessão de pagamento não encontrada.
503Pagamentos não configurados neste ambiente.

POST /v1/billing/portal Bearer

Abre o portal do Stripe: trocar cartão, mudar de plano, cancelar. Existe para o lojista não depender de ninguém para cancelar.

Parâmetros

Nenhum.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/billing/portal \
  -H "Authorization: Bearer <token>"

Resposta 200

{"url": "https://billing.stripe.com/p/session/..."}

Erros

CódigoSignificado
404Esta conta ainda não tem assinatura para gerenciar.
502Não consegui abrir o portal de assinatura agora — o Stripe recusou.
503Pagamentos não configurados neste ambiente.

POST /v1/billing/webhook Assinatura Stripe

Recebe os eventos do Stripe. Não tem sessão: a autenticação é a assinatura do cabeçalho stripe-signature, com tolerância de 300 segundos.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
stripe-signatureheaderstringsimAssinatura do corpo cru com o segredo do webhook.
(corpo)bodyJSONsimEvento do Stripe. Tratamos checkout.session.completed, customer.subscription.updated, customer.subscription.deleted e invoice.payment_failed; os demais são aceitos e ignorados.

Requisição

POST https://api.vitrinavideos.com/v1/billing/webhook
stripe-signature: t=...,v1=...
Content-Type: application/json

Resposta 200

{"ok": true}

// reentrega do mesmo evento
{"ok": true, "repetido": true}

Erros

CódigoSignificado
400Assinatura inválida ou Corpo do evento em formato inesperado — são causas diferentes e por isso mensagens diferentes.
503Webhook não configurado.

Vitrine hospedada

GET /v1/storefront/{handle}/available Bearer

Checa o apelido enquanto o lojista digita, antes de ele salvar. Nunca devolve erro: indisponível é um 200 com o motivo em texto.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
handlepathstringsimApelido desejado. De 3 a 30 caracteres: letras minúsculas, números, ponto, hífen ou _, começando por letra ou número.

Requisição

curl https://api.vitrinavideos.com/v1/storefront/sualoja/available \
  -H "Authorization: Bearer <token>"

Resposta 200

{"ok": true}

{"ok": false, "motivo": "Já está em uso por outra loja."}
{"ok": false, "motivo": "Esse endereço é reservado pelo sistema."}
{"ok": false, "motivo": "Use de 3 a 30 letras, números, ponto, hífen ou _."}

Erros

Nenhum específico.

GET /v1/my-storefront Bearer

A configuração da vitrine da conta logada, para a tela de edição.

Parâmetros

Nenhum.

Requisição

curl https://api.vitrinavideos.com/v1/my-storefront -H "Authorization: Bearer <token>"

Resposta 200

{
  "handle": "sualoja",
  "ativa": true,
  "bio": "Maquiagem de verdade.",
  "cor": "#0b7a6b",
  "capa": "https://cdn.vitrinavideos.com/...",
  "avatar": "https://cdn.vitrinavideos.com/...",
  "links": [{"titulo": "Comprar no site", "url": "https://sualoja.com.br"}]
}

ativa é apenas “tem apelido definido”.

Erros

Nenhum específico.

PATCH /v1/my-storefront Bearer

Salva apelido, textos, cor, imagens e links. Só os campos presentes no corpo são tocados.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
handlebodystringnãoApelido. String vazia desativa a vitrine.
biobodystringnãoAté 200 caracteres.
corbodystringnãoCor de destaque. Até 9 caracteres.
capabodystringnãoURL da capa. Até 500 caracteres.
avatarbodystringnãoURL da foto de perfil. Até 500 caracteres.
linksbodylista de objetonãoAté 6 itens de {titulo, url} (40 e 400 caracteres). Item sem url é descartado.

Requisição

curl -X PATCH https://api.vitrinavideos.com/v1/my-storefront \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"handle":"sualoja","bio":"Maquiagem de verdade.","cor":"#0b7a6b"}'

Resposta 200

{"ok": true, "handle": "sualoja"}

Erros

CódigoSignificado
400Endereço inválido ou reservado.
409Esse endereço já é de outra loja.

POST /v1/my-storefront/image Bearer

Recebe capa ou foto de perfil. A imagem passa pelo servidor, é recortada pelo centro na proporção certa e convertida em WebP antes de guardar — foto de celular tem 4 MB e viraria a parte mais pesada da página.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
tipoquerystringnãocapacapa (1600×500) ou avatar (200×200).
(corpo)bodybináriosimA imagem crua, sem multipart. Até 8 MB.

Requisição

curl -X POST "https://api.vitrinavideos.com/v1/my-storefront/image?tipo=avatar" \
  -H "Authorization: Bearer <token>" --data-binary @foto.jpg

Resposta 200

{"url": "https://cdn.vitrinavideos.com/<conta>/vitrine/avatar-1754500000.webp", "bytes": 9124}

O nome muda a cada envio para o cache antigo não sobreviver à troca. A URL já fica salva na vitrine — não é preciso um PATCH depois.

Erros

CódigoSignificado
400Tipo deve ser capa ou avatar, Arquivo vazio, ou Não consegui ler essa imagem. Envie JPG ou PNG.
413Imagem muito grande. Envie até 8 MB..

Nuvemshop

GET /v1/nuvemshop/connect Bearer

Monta a URL de autorização já com o id da conta em state, para o callback saber a quem vincular a loja.

Parâmetros

Nenhum.

Requisição

curl https://api.vitrinavideos.com/v1/nuvemshop/connect -H "Authorization: Bearer <token>"

Resposta 200

{"url": "https://www.nuvemshop.com.br/apps/<app_id>/authorize?state=66a9..."}

Erros

CódigoSignificado
503Integração não configurada neste ambiente.

POST /v1/nuvemshop/vincular Bearer

Liga à conta logada uma loja autorizada antes do login. Depois de vincular, tenta importar o catálogo e instalar o script — nenhum dos dois pode derrubar o vínculo.

Parâmetros

NomeOndeTipoObrig.PadrãoDescrição
noncebodystringsimToken do vínculo pendente. Consumido: só serve uma vez.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/nuvemshop/vincular \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"nonce":"8f3a..."}'

Resposta 200

{"ok": true, "store_id": 1234567, "loja": "Sua Loja", "produtos": 42, "script": true}

produtos é null se o import falhou; script é false se a loja não concedeu o escopo de scripts. Nos dois casos o vínculo continua de pé.

Erros

CódigoSignificado
400Autorização expirada — instale o app de novo pela Nuvemshop.

POST /v1/nuvemshop/import-products Bearer

Importa o catálogo da loja conectada para as sugestões de produto. Percorre até 20 páginas de 50 itens e faz upsert por id do produto na Nuvemshop.

Parâmetros

Nenhum.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/nuvemshop/import-products \
  -H "Authorization: Bearer <token>"

Resposta 200

{"imported": 42}

Erros

CódigoSignificado
400Conecte sua loja Nuvemshop primeiro.
503Integração não configurada neste ambiente.

POST /v1/nuvemshop/install-script Bearer

Registra o widget na loja pelo recurso scripts da Nuvemshop — o lojista não toca em código. O script entra com o evento onfirstinteraction, que é liberado para todos os apps.

Parâmetros

Nenhum.

Requisição

curl -X POST https://api.vitrinavideos.com/v1/nuvemshop/install-script \
  -H "Authorization: Bearer <token>"

Resposta 200

{"ok": true, "script_id": 98765}

// já estava instalado nesta loja — isso é sucesso, não falha
{"ok": true, "ja_instalado": true}

Erros

CódigoSignificado
400Conecte sua loja Nuvemshop primeiro.
403Sua loja não autorizou a Vitrina a instalar scripts. Desconecte e conecte de novo para conceder a permissão.
502Nuvemshop recusou a instalação do script (…), com o código e o texto da recusa.
503Integração não configurada, ou instalação automática ainda não liberada neste ambiente.

GET /v1/nuvemshop/status Bearer

Estado da integração. “Conectado” não é “instalado”: o script é outro recurso e, sem o escopo de escrita, ele nunca chega a existir.

Parâmetros

Nenhum.

Requisição

curl https://api.vitrinavideos.com/v1/nuvemshop/status -H "Authorization: Bearer <token>"

Resposta 200

{
  "configured": true,
  "connected": true,
  "store_id": 1234567,
  "store_url": "https://sualoja.com.br",
  "scope": "read_products,write_scripts",
  "script_instalado": true,
  "script_motivo": "",
  "products": 42
}

Quando script_instalado é false, script_motivo diz por quê: sem_permissao, indisponivel ou http_<código>. Sem loja conectada, os dois vêm como false e "".

Erros

Nenhum específico — a consulta à Nuvemshop falha em silêncio e vira script_motivo.

DELETE /v1/nuvemshop/desconectar Bearer

Desliga a loja da conta: tira o token, o catálogo importado e o script. Os vídeos ficam — são do lojista, não da plataforma. A remoção do script é melhor esforço.

Parâmetros

Nenhum.

Requisição

curl -X DELETE https://api.vitrinavideos.com/v1/nuvemshop/desconectar \
  -H "Authorization: Bearer <token>"

Resposta 200

{"ok": true}

Erros

CódigoSignificado
400Nenhuma loja conectada.

Ficou faltando algo?

Se você está integrando e esbarrou em algo que esta página não responde, escreva — quem responde é quem escreve o código.

Falar com a gente