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ública | Chave secreta de servidor |
| Formato | pk_… | sk_… |
| Onde vive | No HTML da sua loja, à vista de todo mundo | Só no seu servidor, em variável de ambiente |
| Serve para | Ler os vídeos já publicados daquela conta | Criar vídeo a partir de uma URL |
| Como viaja | No caminho da URL das rotas de embed | No cabeçalho X-Api-Key |
| Exige plano pago | Não | Sim, a partir do Loja |
| Onde encontrar | Passo “Instalar” da Vitrine, no painel | Integraçõ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ódigo | Quando acontece |
400 | Pedido 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). |
401 | Sessão inválida (Bearer) ou X-Api-Key ausente/inválida. |
402 | Limite do plano atingido, ou recurso que exige plano pago. |
403 | O recurso existe mas é de outra conta/loja. |
404 | Chave, vídeo, bloco, vitrine ou assinatura inexistente. |
409 | Conflito de estado (slug repetido, assinatura já ativa, vídeo já na fila). |
413 | Arquivo acima do teto aceito. |
422 | Validação de corpo/parâmetro. |
502/503 | Um 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
chave | path | string | sim | — | pk_… ou ns_<store_id>. |
produto | query | string | nã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
}
]
}
| Campo | Tipo | Descrição |
store | string | Nome da loja, como cadastrado na conta. |
placements[].id | string | Id do bloco. É o que você manda em placement_id ao registrar evento. |
placements[].kind | string | Formato: carousel, slider, stories, feed, bubble ou hero. |
placements[].slug | string | Identificador do bloco. É o que vai em data-shorts e no parâmetro slug da paginação. |
placements[].title | string | Título exibido acima da fileira. |
placements[].theme | objeto | Opções do bloco. Chaves em As chaves de theme. Vem {} quando nada foi configurado. |
placements[].videos | lista | Primeira página, no máximo 12 objetos de vídeo. |
placements[].total | inteiro | Quantos vídeos o bloco tem ao todo, já aplicado o filtro de produto. |
placements[].tem_mais | booleano | true quando total passa de 12. |
placements[].por_produto | booleano | Reflete 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ódigo | Significado |
404 | Chave 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
chave | path | string | sim | — | pk_… ou ns_<store_id>. |
slug | query | string | sim | — | Identificador do bloco. |
skip | query | inteiro | não | 0 | Quantos vídeos pular. |
limit | query | inteiro | não | 12 | Quantos 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ódigo | Significado |
404 | Chave desconhecida. |
404 | Bloco não encontrado — não há bloco vivo com esse slug na conta. |
422 | slug 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
chave | path | string | sim | — | pk_… ou ns_<store_id>. |
video_id | path | string | sim | — | O 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ódigo | Significado |
404 | Chave desconhecida. |
404 | Ví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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
chave | path | string | sim | — | pk_… ou ns_<store_id>. |
url | query | string | não* | "" | Endereço da página do produto. Normalizado antes de comparar: ignora http/https, www., query string e barra final. |
sku | query | string | não* | "" | SKU do produto. Casamento exato. Tem precedência: informando os dois, só o SKU é usado. |
limit | query | inteiro | não | 12 | Má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ódigo | Significado |
400 | Informe url ou sku do produto — nenhum dos dois veio. |
404 | Chave 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
pk | body | string | sim | — | Chave pública pk_… ou ns_<store_id>. |
type | body | string | sim | — | Tipo do evento. Truncado em 30 caracteres. |
video_id | body | string | não | "" | Vídeo a que o evento se refere. |
placement_id | body | string | não | "" | Bloco em que aconteceu. |
session_id | body | string | não | "" | Identificador anônimo da sessão do widget. Truncado em 64 caracteres. |
video_ids | body | lista de string | nã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/Referer | header | string | não | — | Nã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
| Tipo | Quando | Entra em |
impression | Um evento por renderização, com a lista exibida em video_ids. | Views do mês, denominador do CTR e do score inteligente. |
open | O cliente abriu o player. | Métrica por vídeo e score inteligente. |
play | O vídeo tocou. | Métrica por vídeo. |
cta_click | O 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
Erros
| Código | Significado |
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. |
422 | pk 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
handle | path | string | sim | — | Apelido 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ódigo | Significado |
404 | Vitrine 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
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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
X-Api-Key | header | string | sim | — | Chave secreta sk_… da conta. |
url | body | string | sim | — | Endereço http:// ou https:// do arquivo de vídeo. Guardado com no máximo 1000 caracteres. |
title | body | string | não | nome do arquivo | Nome do vídeo no painel. Vazio, usamos o último trecho da url (até 80 caracteres). |
product_title | body | string | não | — | Nome do produto exibido no card e no player. Truncado em 500 caracteres. |
product_url | body | string | não | — | Link do produto. É por ele que o vídeo casa com a página do produto. Truncado em 500. |
product_price | body | string | não | — | Preç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ódigo | Significado |
401 | X-Api-Key ausente ou inválida — o cabeçalho não veio ou não começa com sk_. |
401 | Chave de API inválida — o formato está certo mas nenhuma conta tem essa chave (foi regerada, por exemplo). |
402 | A conta não está num plano pago. A chave continua válida e volta a funcionar quando a assinatura voltar. |
402 | Seu 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. |
400 | Informe a URL http(s) do arquivo de vídeo — url 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
code | query | string | não | "" | Código de uso único da autorização. Sem ele, redireciona para a tela de conexão. |
state | query | string | nã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ção | Resposta |
Sem code | 307 → /app/connect/?instalar=nuvemshop |
state é uma conta existente | Loja 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 code | 307 → /app/connect/?instalar=nuvemshop&erro=<motivo> |
Erros
| Código | Significado |
503 | Integraçã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ódigo | Significado |
503 | Integraçã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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
nonce | path | string | sim | — | Token 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ódigo | Significado |
404 | Autorizaçã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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
x-linkedstore-hmac-sha256 | header | string | sim | — | HMAC-SHA256 do corpo cru com o client secret, em base64. Comparado em tempo constante. |
store_id | body | inteiro | não | — | Só 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ódigo | Significado |
401 | Assinatura inválida. |
503 | Integração não configurada neste ambiente (sem client secret não há como verificar assinatura). |
O widget no seu site
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ável | Chave no painel | Padrão | O que controla |
--sw-pad | pad | 0 | Respiro interno da seção. Aceita qualquer valor de padding. |
--sw-max | max | none | Largura máxima da seção — para encaixar na coluna de conteúdo do tema. |
--sw-align | align | 0 | Vai em margin-inline. Use auto para centralizar dentro do --sw-max. |
--sw-gap | gap | 10px | Espaço entre os cards da fileira. |
--sw-card | card | min(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:
| Chave | Valores | O que faz |
pagina | todas (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_product | true / 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_product | false esconde |
Oculta nome, preço e link do produto em todos os formatos e no player. |
rounded | false deixa reto |
Cantos arredondados dos cards. false zera o raio. |
autoplay | false desliga |
O carrossel andar sozinho. Ele já pausa ao passar o mouse ou tocar; false desliga de vez. |
ver_todos | false esconde |
O botão “Ver todos os N vídeos” abaixo da fileira, que abre o catálogo do bloco em grade. |
story | true / 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. |
selector | seletor 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. |
insert | after (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
- Vincule os vídeos aos respectivos produtos.
- 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.
- 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
filename | body | string | sim | — | Nome do arquivo. Vira o título quando title não vem. |
title | body | string | nã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ódigo | Significado |
402 | Seu 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
video_id | path | string | sim | — | Id devolvido pelo presign. |
Content-Type | header | string | não | video/mp4 | Tipo gravado no objeto do storage. |
| (corpo) | body | binário | sim | — | O 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ódigo | Significado |
400 | Arquivo vazio. |
404 | Ví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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
video_id | path | string | sim | — | Id do vídeo. |
Requisição
curl -X POST https://api.vitrinavideos.com/v1/videos/66b1.../complete \
-H "Authorization: Bearer <token>"
Resposta 200
Erros
| Código | Significado |
404 | Ví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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
count | body | inteiro | não | 1 | Quantas 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
photo_keys | body | lista de string | sim | — | As key devolvidas pelo presign de fotos. Máximo 12; cada uma truncada em 300 caracteres. |
title | body | string | não | Vídeo gerado | Título no painel. |
style | body | string | não | imovel | produto 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ódigo | Significado |
400 | Envie ao menos uma foto. |
402 | Limite 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
Erros
| Código | Significado |
402 | A 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
video_id | path | string | sim | — | Id do vídeo. |
title | body | string | não | — | Novo título. |
product_title | body | string | não | — | Nome do produto. |
product_url | body | string | não | — | Link do produto. |
product_price | body | string | não | — | Preç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
Erros
| Código | Significado |
404 | Ví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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
video_id | path | string | sim | — | Id do vídeo. |
Requisição
curl -X POST https://api.vitrinavideos.com/v1/videos/66b1.../retry \
-H "Authorization: Bearer <token>"
Resposta 200
Erros
| Código | Significado |
404 | Vídeo não encontrado. |
409 | Esse vídeo já está na fila de processamento — só vale para status ready ou error. |
400 | O 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
video_id | path | string | sim | — | Id do vídeo. |
Requisição
curl -X DELETE https://api.vitrinavideos.com/v1/videos/66b1... \
-H "Authorization: Bearer <token>"
Resposta 200
Erros
| Código | Significado |
404 | Ví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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
slug | body | string | sim | — | Identificador único na conta. É o que vai em data-shorts. |
kind | body | string | não | carousel | carousel, slider, stories, feed, bubble ou hero. |
title | body | string | não | "" | Título exibido acima da fileira. |
selection | body | string | não | recent | manual fixa a sequência de video_ids; smart ordena por conversão; qualquer outro valor é “mais recentes”. |
video_ids | body | lista de string | não | [] | Recorta quais vídeos entram. Lista vazia significa “todos os publicados”. |
theme | body | objeto | nã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ódigo | Significado |
409 | Você 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
placement_id | path | string | sim | — | Id do bloco. |
title | body | string | não | — | Novo título. |
kind | body | string | não | — | Novo formato. Trocar carrossel por stories não exige refazer a instalação no tema. |
selection | body | string | não | — | Novo modo de ordenação. |
video_ids | body | lista de string | não | — | Substitui a seleção inteira. |
theme | body | objeto | não | — | Substitui 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
Corpo sem nenhum campo preenchido também responde {"ok": true},
sem tocar em nada e sem verificar se o bloco existe.
Erros
| Código | Significado |
404 | Posicionamento 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
placement_id | path | string | sim | — | Id do bloco. |
video_ids | body | lista de string | nã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ódigo | Significado |
404 | Bloco 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
placement_id | path | string | sim | — | Id do bloco. |
video_id | path | string | sim | — | Id 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ódigo | Significado |
404 | Bloco 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
video_id | path | string | sim | — | Id do vídeo. |
placement_ids | body | lista de string | nã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ódigo | Significado |
404 | Vídeo não encontrado. |
DELETE /v1/placements/{placement_id} Bearer
Exclusão lógica do bloco. Os vídeos continuam na biblioteca.
Parâmetros
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
placement_id | path | string | sim | — | Id do bloco. |
Requisição
curl -X DELETE https://api.vitrinavideos.com/v1/placements/66a9... \
-H "Authorization: Bearer <token>"
Resposta 200
Erros
| Código | Significado |
404 | Posicionamento 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
q | query | string | não | "" | Busca por trecho do título, sem diferenciar maiúsculas. Vazio devolve o começo da lista. |
limit | query | inteiro | não | 8 | Quantos 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
plan | query | string | não | pro | inicial, starter ou pro. |
period | query | string | não | month | year 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ódigo | Significado |
400 | Escolha um dos planos pagos — Business é sob consulta. |
409 | A 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. |
503 | Pagamentos 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
session_id | query | string | sim | — | Id 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ódigo | Significado |
403 | Essa sessão de pagamento é de outra conta. |
404 | Sessão de pagamento não encontrada. |
503 | Pagamentos 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ódigo | Significado |
404 | Esta conta ainda não tem assinatura para gerenciar. |
502 | Não consegui abrir o portal de assinatura agora — o Stripe recusou. |
503 | Pagamentos 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
stripe-signature | header | string | sim | — | Assinatura do corpo cru com o segredo do webhook. |
| (corpo) | body | JSON | sim | — | Evento 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ódigo | Significado |
400 | Assinatura inválida ou Corpo do evento em formato inesperado — são causas diferentes e por isso mensagens diferentes. |
503 | Webhook 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
handle | path | string | sim | — | Apelido 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
handle | body | string | não | — | Apelido. String vazia desativa a vitrine. |
bio | body | string | não | — | Até 200 caracteres. |
cor | body | string | não | — | Cor de destaque. Até 9 caracteres. |
capa | body | string | não | — | URL da capa. Até 500 caracteres. |
avatar | body | string | não | — | URL da foto de perfil. Até 500 caracteres. |
links | body | lista de objeto | não | — | Até 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ódigo | Significado |
400 | Endereço inválido ou reservado. |
409 | Esse 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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
tipo | query | string | não | capa | capa (1600×500) ou avatar (200×200). |
| (corpo) | body | binário | sim | — | A 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ódigo | Significado |
400 | Tipo deve ser capa ou avatar, Arquivo vazio, ou Não consegui ler essa imagem. Envie JPG ou PNG. |
413 | Imagem 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ódigo | Significado |
503 | Integraçã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
| Nome | Onde | Tipo | Obrig. | Padrão | Descrição |
nonce | body | string | sim | — | Token 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ódigo | Significado |
400 | Autorizaçã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
Erros
| Código | Significado |
400 | Conecte sua loja Nuvemshop primeiro. |
503 | Integraçã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ódigo | Significado |
400 | Conecte sua loja Nuvemshop primeiro. |
403 | Sua loja não autorizou a Vitrina a instalar scripts. Desconecte e conecte de novo para conceder a permissão. |
502 | Nuvemshop recusou a instalação do script (…), com o código e o texto da recusa. |
503 | Integraçã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
Erros
| Código | Significado |
400 | Nenhuma 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