---
title: "Nosso primeiro app na Shopify App Store: Simple WhatsApp Button"
description: "Publicamos nosso primeiro app na Shopify App Store. Escolhemos de propósito o escopo mais simples possível para atravessar o processo inteiro: um botão flutuante de WhatsApp, gratuito, sem servidor e sem um único script na loja. O que aprendemos, incluindo duas recusas na revisão por um bug que não existia."
published: "2026-08-19"
tags: ["shopify", "ecommerce", "ferramenta", "app"]
source: "https://marciotoledo.com/blog/nosso-primeiro-app-na-shopify-app-store-simple-whatsapp-button"
---

# Nosso primeiro app na Shopify App Store: Simple WhatsApp Button

Publicar um app na Shopify App Store é um daqueles processos que você só entende depois de atravessar inteiro. Tem checklist de requisitos, revisão automatizada, revisão humana, política de privacidade, webhooks obrigatórios e uma quantidade de detalhe que não aparece em nenhum tutorial.

Então a decisão foi essa: em vez de tentar um app ambicioso e travar no meio do caminho, escolher o escopo mais simples que ainda entregasse valor real, e usar esse app pra percorrer o processo do começo ao fim. Aprender a estrada com um carro simples.

O resultado está no ar: [Simple WhatsApp Button](https://apps.shopify.com/simple-whatsapp-button-1?locale=pt-BR), gratuito, publicado em 11 de agosto de 2026.

## Por que um botão de WhatsApp

Simples não podia significar inútil. O app precisava resolver algo que praticamente toda loja brasileira precisa.

Botão de WhatsApp é exatamente isso. Qualquer loja que usa WhatsApp como canal de atendimento, que aqui é quase todas, precisa de um jeito de colocar aquele botão flutuante na loja. E as opções costumam ser ruins: ou você mexe no código do tema, ou instala um app pesado cheio de coisa que você não pediu.

A proposta é um botão flutuante configurável em mensagem, cor, tamanho e posição. Só isso, mas bem feito.

## O foco foi não precisar de desenvolvedor

Essa foi a régua do produto inteiro: o lojista tem que instalar, configurar e ver funcionando sozinho, sem abrir código e sem chamar ninguém.

Na prática isso virou algumas decisões concretas:

- **Um número de telefone e o app já está no ar.** Todo o resto tem default sensato. A configuração mínima é literalmente preencher um campo.
- **Uma única página de ajustes.** Nada de menu com abas, seções escondidas ou configuração espalhada entre o app e o editor de temas.
- **Prévia em tempo real.** O lojista vê exatamente como o botão vai ficar antes de salvar, com a cor, o tamanho e a animação que ele escolheu. Isso elimina o ciclo de salvar, abrir a loja em outra aba, olhar, voltar e ajustar.
- **Português e inglês desde o primeiro dia**, detectando o idioma do admin da Shopify.

Tem um detalhe de configuração que resume bem a filosofia: o bloco no editor de temas mostra **só** o liga/desliga, sem nenhuma outra opção. Foi de propósito. Se a mesma configuração existisse nos dois lugares, o lojista poderia configurar de um jeito no app e de outro no tema, e discordar de si mesmo. A configuração mora num lugar só.

O que é ajustável, para quem quer ajustar: cor do botão, cor do ícone, tamanho (de 32 a 96 px), proporção do ícone dentro do botão, posição em quatro cantos, animação (nenhuma, pulso ou salto), sombra e z-index.

Esse último parece detalhe de nerd mas resolve um problema real. Cabeçalhos fixos de tema costumam ficar em z-index 1000 e alguma coisa sempre passa por cima do botão. Em vez do app cravar um valor máximo e atropelar o tema de todo mundo, o default é modesto e o lojista aumenta se precisar.

## Zero script na loja

Esse era inegociável. App de loja tem fama de destruir performance, e com razão: muitos injetam JavaScript, carregam bibliotecas e adicionam requisições em toda página.

O widget do Simple WhatsApp Button **não tem uma única tag `<script>`**. É um app embed via Theme App Extension: um bloco Liquid de 5,5 KB e um CSS de 5,4 KB. Nada de JavaScript, nada de ScriptTag, nada de edição no código do tema, e zero código residual quando o lojista desinstala.

O botão é um link com CSS. Não precisa de JavaScript pra ser um link.

Dois detalhes de implementação que vieram dessa escolha:

- Todas as classes são prefixadas com `swb-`, pra não colidir com nada do tema da loja.
- Todas as medidas estão em `px`, nunca em `rem`. Tema que mexe no tamanho de fonte da raiz (um `html { font-size: 62.5% }`, por exemplo) encolheria o botão em mais de um terço sem ninguém entender por quê.

> **TIP**
>
> Se você vai avaliar um app pra sua loja, olhe o que ele injeta antes de olhar o preço. App que adiciona script em toda página cobra o custo na performance da loja inteira, não só na página onde o recurso aparece.

## Sem servidor, quase

A restrição técnica que mais moldou o projeto: o app não tem backend próprio. Sem SSR, sem banco de dados, sem sessão, sem OAuth manual, sem webhook de produto. Ele compila para arquivos estáticos e conversa com a Shopify direto do navegador, via Direct API access.

A persistência inteira é **um metafield** de app: um JSON com as configurações, guardado na própria instalação do app. O admin escreve nele e o bloco Liquid lê dele na hora de renderizar. Não tem cache, não tem segunda cópia pra divergir, e não precisa de nenhum escopo de acesso, porque um metafield na sua própria instalação é seu por definição. Quando o lojista desinstala, as configurações morrem junto.

Só que "sem servidor" acabou não sendo 100% verdade, e essa foi uma das descobertas do processo. Dois endpoints foram inevitáveis, ambos rodando como Cloudflare Pages Functions, fora do app:

**1. Os webhooks obrigatórios de compliance.** Todo app da App Store precisa responder aos webhooks de privacidade (dados do cliente, dados da loja), *mesmo que não colete dado nenhum*. Não existe isenção. O handler valida o HMAC e confirma o recebimento, e não tem trabalho de dado nenhum a fazer, porque o app não guarda dado de comprador.

**2. O endpoint de session token, que é a parte mais interessante.** Esse é o que eu tinha esquecido, e vale explicar porque é um problema que só aparece nesse desenho de app.

A revisão automatizada da Shopify tem uma checagem de "app embutido" que verifica se o app usa session tokens corretamente. E ela verifica **observando o tráfego**: espera ver uma requisição do frontend para a URL do próprio app levando um `Authorization: Bearer <JWT>`.

O problema é que esse app nunca faz essa requisição naturalmente. Como tudo vai do navegador direto para a Shopify via Direct API, nada nunca toca o domínio do app. A checagem não tem tráfego pra observar, então fica **vermelha para sempre** e bloqueia a submissão.

A solução foi um endpoint que existe unicamente pra satisfazer essa verificação: o app dispara um POST autenticado por carregamento, e o endpoint valida o token de verdade (assinatura HS256 fixa, `aud` contra o client ID, `exp`/`nbf` com margem de cinco segundos, e o `dest` conferido como host `.myshopify.com`). Toda rejeição devolve um 401 idêntico, pra um token forjado não descobrir qual checagem ele falhou.

E o mais importante: ele engole qualquer erro. A funcionalidade real do app não depende desse endpoint, então uma requisição bloqueada não pode custar nada ao lojista.

## A aprovação: recusado duas vezes por um bug que não existia

Aqui está a parte que ninguém conta.

Depois do checklist técnico cumprido, o app vai para revisão humana. Um testador do time da Shopify instala, testa e aprova ou recusa.

O nosso foi **recusado duas vezes** pelo mesmo motivo: segundo o testador, o botão não abria o WhatsApp.

Só que abria. Testamos em vários dispositivos, navegadores e lojas. O link estava correto, o número estava correto, o comportamento estava correto.

O que estava acontecendo era na ponta dele: alguma coisa na rede local ou na VPN que ele usava estava bloqueando links do WhatsApp. O app funcionava perfeitamente; o ambiente de teste é que barrava o destino.

Ao responder explicando isso, com detalhe do que tínhamos verificado, o app foi aprovado logo em seguida.

> **NOTE**
>
> A lição não é "o revisor errou". É que a revisão é feita por uma pessoa, num ambiente que você não controla, e que **responder com evidência é parte do processo**, não uma reclamação. Duas recusas parecem um beco sem saída e podem ser só uma VPN. Se você tem certeza do comportamento, documente o que testou e responda.

## É a versão 1, e o plano é crescer de graça primeiro

O app está no ar como primeira versão, e é assim que ele foi pensado. O roadmap já tem várias funcionalidades, e a estratégia é deliberada:

**Primeiro, mais recursos gratuitos.** Ampliar o que o app faz sem cobrar, para construir base de instalação e avaliações. Um app com zero review não convence ninguém, e isso não se resolve com preço.

> **NOTA**
>
> **Uma semana no ar:** 4 lojas instaladas e a primeira avaliação, 5 estrelas. E uma das lojas é de fora do Brasil, o que me deixou especialmente feliz: valida a decisão de lançar bilíngue desde o primeiro dia, em vez de tratar o inglês como um "depois".
>
> Número pequeno em termos absolutos, e é exatamente o que eu esperava de um app sem nenhuma divulgação, achado só por quem buscou "whatsapp" na App Store. O que interessa aqui não é o volume, é o sinal: quem instalou conseguiu configurar sozinho, sem me procurar. Era essa a hipótese do produto inteiro.

**Depois, algo que justifique cobrança.** Não uma versão capada do que já é grátis, mas uma funcionalidade realmente interessante, que resolva um problema maior.

E aí entra a segunda camada de aprendizado, que é a razão de tudo isso: cobrar significa mexer com a **API de cobrança** da Shopify, planos, assinaturas e todo o ciclo de faturamento. Ou seja, o mesmo movimento de novo, atravessar um processo desconhecido, mas dessa vez em cima de um app que já existe e já tem usuários.

Já sabemos até o caminho: quando for monetizar, o plano é usar o **Shopify App Pricing**, em que a Shopify hospeda a página de escolha de plano e processa a cobrança, e o app só consulta qual plano está ativo. A alternativa, a Billing API, exigiria escopo extra e um webhook de atualização de assinatura, e webhook precisa de servidor, o que desmontaria justamente o desenho sem backend que descrevi acima.

## A stack

Para quem quer os detalhes técnicos:

| Camada | Tecnologia |
| --- | --- |
| Admin do app | React 18 + React Router 7, SPA estática (`ssr: false`) |
| UI | Polaris web components + App Bridge |
| Widget da loja | Theme App Extension (Liquid + CSS, zero JS) |
| Persistência | Um metafield de app (JSON), sem banco |
| Endpoints | Cloudflare Pages Functions (compliance + session token) |
| Hospedagem | Cloudflare Pages |
| Escopos de acesso | Nenhum (`scopes = ""`) |

O app precisa de zero escopos de acesso. Não lê produto, não lê pedido, não lê cliente. A superfície de risco para o lojista é basicamente nula, e isso não foi um acidente: é consequência direta de manter o escopo pequeno.

Se você está construindo para Shopify, vale ler também o que escrevi sobre [como manter o contexto do projeto no CLAUDE.md](/blog/claude-md-contexto-projeto-claude-code) — esse app tem um dos arquivos de contexto mais densos que já escrevi, e boa parte das armadilhas acima está documentada lá justamente para não morder de novo.

## Resumo

- **[Simple WhatsApp Button](https://apps.shopify.com/simple-whatsapp-button-1?locale=pt-BR)**: gratuito, na Shopify App Store desde 11 de agosto de 2026.
- Escopo mínimo de propósito, para atravessar o processo inteiro de publicação.
- Configuração em segundos, sem desenvolvedor: um número de telefone e está no ar.
- Zero JavaScript no storefront, nenhum ScriptTag e nada residual após desinstalar.
- Sem backend, exceto dois endpoints obrigatórios: compliance e session token.
- Nenhum escopo de acesso: o app não lê produto, pedido nem cliente.
- Versão 1, com roadmap de recursos gratuitos antes de qualquer cobrança.
- Primeira semana no ar: 4 lojas instaladas, uma delas fora do Brasil, e uma avaliação 5 estrelas.

Se você tem loja Shopify e usa WhatsApp no atendimento, instala e me conta o que faltou. É exatamente esse feedback que vai definir o que entra na próxima versão.
