Resumo: Monte express.raw({ type: 'application/json' }) na sua rota /webhook antes de qualquer middleware JSON; essa decisão determina se cada verificação de assinatura do Stripe vai funcionar ou não. Remova o '+' inicial do telefone do assinante e acrescente @s.whatsapp.net. Faça POST em /groups/{id}/participants no evento checkout.session.completed; DELETE em customer.subscription.deleted. O Google Sheets suporta até 50 membros. O SQLite cuida do restante. Todo o código está neste guia.
Quem usa grupos pagos de WhatsApp em 2026 (e por que a gestão manual não escala)
Personal trainers, provedores de sinais de cripto, consultores imobiliários e professores particulares para concursos estão operando grupos pagos de WhatsApp em escala; quase todos gerenciam membros em uma planilha que inevitavelmente quebra depois de 50 assinantes.
O mercado está consolidado em quatro verticais: fitness ($9,99--$140/mês), sinais de cripto ($49--$149/mês no safetrading.today), professores para concursos na Nigéria e África do Sul (₦1.500--R149/mês) e grupos de varejo de luxo como a britânica Love Luxe, onde o vazamento de links de convite é igualmente ativo. Projetos open source como dhryvyad/wahooks (MIT, 2026) confirmam o interesse dos desenvolvedores em soluções auto-gerenciadas além do SaaS comercial.
A dor operacional está quantificada. A CommuniPass, que cobra entre $29 e $299/mês como SaaS exatamente para esse problema, documenta erros consistentes a partir de aproximadamente 50 membros, quando cruzar datas de pagamento com nomes de exibição do WhatsApp ultrapassa a tolerância humana de erro. O custo de tempo é aproximadamente 8 horas por semana, ou $20.800 por ano a $50/hora.
O problema de vazamento de receita é separado e, na prática, mais grave. Os grupos perdem entre 15% e 20% da receita de assinaturas com membros expirados que permanecem no grupo depois que o pagamento vence. O motivo persiste por ser social: o WhatsApp envia uma notificação visível para todo o grupo quando alguém é removido pelo app. Como um operador disse diretamente: "Os membros ficam no grupo depois que a assinatura expira porque é constrangedor removê-los manualmente." A chamada DELETE via API abordada neste guia remove membros sem qualquer notificação visível para os outros participantes.
Por que a abordagem com Zapier e link de convite quebra assim que alguém compartilha o link
Você pode primeiro tentar o Zapier ou o Make: evento de pagamento do Stripe → buscar link de convite do grupo → enviar por e-mail ou SMS. Funciona até que um assinante compartilhe o link.
Como a NothingApps documentou exatamente esse modo de falha: "Você vende uma vaga no seu grupo VIP. Em menos de uma hora, cinco membros fantasmas entraram porque o comprador compartilhou o link em um fórum público." Um link de convite estático enviado a todos os assinantes cria um vazamento composto sem mecanismo para fechá-lo. O fluxo do Zapier também não oferece forma de remover membros quando as assinaturas expiram — ele reage ao pagamento bem-sucedido, mas não tem capacidade de DELETE na associação ao grupo, nem gatilho para o encerramento da assinatura.
A abordagem deste guia adiciona e remove membros individuais por número de telefone, não por um link compartilhável. A rotação de link de convite continua disponível como fallback apenas para o caso extremo em que as configurações de privacidade de um assinante bloqueiem a adição direta via API, tratado na seção de links de convite abaixo.
O sistema completo: Stripe Checkout → Webhook → Whapi.Cloud
Cinco componentes, dois eventos do Stripe, um token da Whapi.Cloud. Todo o ciclo de vida da associação cabe em um único arquivo Express com um módulo de banco de dados e duas rotas de API.
Fluxo de execução:
-
Criação da sessão de Checkout: O assinante conclui a página de pagamento hospedada pelo Stripe. O número de telefone do WhatsApp é capturado em
metadata.whatsapp_phonena criação da sessão, não na confirmação do pagamento. -
checkout.session.completed: O Stripe entrega esse evento em segundos após o pagamento bem-sucedido. Seu handler lê o telefone dos metadados, converte para JID do WhatsApp e chamaPOST /groups/{id}/participants. O assinante está no grupo. -
Associação ativa: Seu banco de dados registra o telefone, o ID de assinatura do Stripe e o ID do grupo. Um cron job diário verifica registros que o webhook pode ter perdido.
-
customer.subscription.deleted: Dispara no cancelamento ou quando o Stripe esgota as tentativas de pagamento. Seu handler chamaDELETE /groups/{id}/participantscom o JID do membro. -
Remoção silenciosa: A chamada DELETE remove o membro sem nenhuma notificação no grupo do WhatsApp. O problema de atrito social desaparece.
customer.subscription.deleted é o único evento do Stripe que garante a remoção por expiração. Um erro comum de configuração é rotear a remoção para invoice.payment_failed. Esse evento dispara a cada tentativa falha (até 4 por padrão), não quando o Stripe encerra definitivamente a assinatura. A assinatura pode ser recuperada após uma nova tentativa. Use customer.subscription.deleted.
Como capturar o telefone do WhatsApp do assinante no Stripe Checkout
Capture o número de telefone do WhatsApp do assinante na criação da sessão. Você não conseguirá recuperá-lo de forma confiável do Stripe depois sem criar um sistema de busca de perfil de cliente separado.
Adicione uma etapa de coleta de telefone no seu frontend antes do redirecionamento para o Stripe. Passe o valor para seu backend ao criar a Checkout Session via campo metadata do Stripe:
// server.js -- create a Stripe Checkout Session with phone metadata
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/create-checkout', async (req, res) => {
const { phone } = req.body; // e.g. "+12025551234" from your pre-checkout form
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: process.env.STRIPE_PRICE_ID, quantity: 1 }],
// Store WhatsApp phone here -- webhook reads it from event.data.object.metadata.whatsapp_phone
metadata: { whatsapp_phone: phone },
success_url: `${process.env.BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.BASE_URL}/cancel`,
});
res.json({ url: session.url });
});
Se whatsapp_phone estiver ausente dos metadados da sessão quando checkout.session.completed disparar, seu webhook handler não conseguirá adicionar o membro. Valide esse campo no formulário de checkout. Rejeite envios com menos de 10 dígitos e exija o prefixo internacional para que a conversão de E.164 para JID funcione corretamente na próxima etapa.
O objeto de sessão na conclusão também contém o ID de subscription. Salve-o junto com o número de telefone no banco de dados; ele é a chave de busca quando customer.subscription.deleted disparar mais tarde. Sem esse par, o evento de remoção não consegue identificar qual número de telefone remover.
Por que as assinaturas de webhook do Stripe falham — e a solução com raw-body
Toda verificação de assinatura do Stripe falha silenciosamente se express.json() for executado antes do seu webhook handler. A correção é uma linha na posição certa — mas normalmente custa horas para diagnosticar na primeira vez.
Times que registram express.json() globalmente e depois adicionam uma rota /webhook descobrem que stripe.webhooks.constructEvent() lança "No signatures found matching the expected signature for payload" em toda chamada, mesmo com o segredo correto. O erro não dá nenhuma pista. A causa: express.json() já re-serializou o corpo da requisição, alterando sua representação em bytes o suficiente para invalidar a assinatura HMAC-SHA256 que o Stripe calculou sobre os bytes brutos originais.
A regra do raw-body primeiro: monte express.raw({ type: 'application/json' }) na rota /webhook antes de registrar express.json() globalmente. Middleware específico de rota no Express é executado primeiro, então a rota de webhook recebe um Buffer antes que o parser global o toque.
// server.js -- middleware registration order is non-negotiable
// without express.raw() here, stripe.webhooks.constructEvent throws 400
// even when STRIPE_WEBHOOK_SECRET is correct and matches your dashboard
app.use('/webhook', express.raw({ type: 'application/json' }), webhookHandler);
// All other routes get parsed JSON normally
app.use(express.json());
app.use('/create-checkout', checkoutRouter);
Com a ordem de rotas correta, o webhook handler verifica a assinatura e roteia os eventos para a lógica de negócio correspondente:
async function webhookHandler(req, res) {
const sig = req.headers['stripe-signature'];
let event;
try {
// req.body is a raw Buffer here -- not a parsed object
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Idempotency gate: Stripe delivers each event at least once.
// A duplicate checkout.session.completed adds the member twice; a duplicate
// customer.subscription.deleted removes a still-active subscriber.
if (await db.hasProcessedEvent(event.id)) {
return res.status(200).send('Already processed');
}
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutCompleted(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionDeleted(event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
await db.markEventProcessed(event.id);
res.status(200).send('OK');
}
A verificação de idempotência depende de uma tabela processed_events que registra cada ID de evento do Stripe após o processamento bem-sucedido. O schema dessa tabela está incluído na seção de banco de dados abaixo.
Normalização de número de telefone: duas operações que evitam 90% das falhas
Remova o '+', acrescente '@s.whatsapp.net'. Duas operações de string, zero ambiguidade. Todas as outras falhas em integrações de Stripe + Whapi.Cloud têm origem na omissão de um desses passos.
O WhatsApp identifica contatos por JID (Jabber ID), não por número de telefone. O formato é {country_code}{national_number}@s.whatsapp.net. O Stripe armazena números no formato E.164 com um '+' inicial. A conversão é determinística:
// Converts E.164 phone → WhatsApp JID
// Input: "+12025551234" → Output: "[email protected]"
// Input: "12025551234" → Output: "[email protected]" (no + present)
function toWhatsAppJID(e164Phone) {
const digits = String(e164Phone).replace(/^\+/, '');
return `${digits}@s.whatsapp.net`;
}
// Optional: validate before using
function isValidJID(jid) {
return /^\d{7,15}@s\.whatsapp\.net$/.test(jid);
}
Times passam horas depurando por que toda chamada a addGroupParticipant falha com um erro genérico, apenas para descobrir que o JID continha um '+' ou um espaço de um campo de input não limpo. Essa é a falha silenciosa mais frequente em novas integrações de Stripe + Whapi.Cloud. Chame isValidJID() antes de cada chamada de API e registre um erro explícito se retornar false.
Dois casos extremos para tratar no formulário de checkout: (1) números com espaços ou hífens: remova todos os caracteres não numéricos antes de tirar o '+'; (2) números sem código de país: rejeite inputs com menos de 10 dígitos e solicite o formato internacional.
Adicionar ao pagar, remover ao expirar
Dois handlers de eventos do Stripe, quatro chamadas HTTP para a Whapi.Cloud. O caminho de adição no pagamento roda no checkout; o de remoção na expiração roda quando a assinatura é encerrada.
O handler de checkout lê o telefone dos metadados da sessão, adiciona o membro e salva o registro para rastreamento de expiração:
async function handleCheckoutCompleted(session) {
const phone = session.metadata?.whatsapp_phone;
if (!phone) {
console.error('No whatsapp_phone in session metadata -- member cannot be added automatically');
return;
}
const groupId = process.env.WHAPI_GROUP_ID;
await addGroupMember(groupId, phone);
await db.saveMember({
phone,
stripeSubscriptionId: session.subscription,
stripeCustomerId: session.customer,
groupId,
});
console.log(`Member added: ${phone} (subscription: ${session.subscription})`);
}
async function handleSubscriptionDeleted(subscription) {
const member = await db.getMemberBySubscriptionId(subscription.id);
if (!member) {
// Already removed manually, or was never successfully added
console.warn(`No member found for subscription ${subscription.id}`);
return;
}
await removeGroupMember(member.groupId, member.phone);
await db.removeMember(subscription.id);
console.log(`Member removed: ${member.phone} (subscription: ${subscription.id})`);
}
As chamadas à API da Whapi.Cloud são requisições HTTP diretas para a API de Grupos. A chamada de adição envia o JID do assinante como participante:
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// POST https://gate.whapi.cloud/groups/{GroupID}/participants
async function addGroupMember(groupId, phone) {
const jid = toWhatsAppJID(phone);
const response = await fetch(
`https://gate.whapi.cloud/groups/${groupId}/participants`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.WHAPI_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ participants: [jid] }),
}
);
const data = await response.json();
if (!response.ok) {
// 403 means the subscriber's privacy settings block direct group addition
// Fall back to sending them a rotated invite link via DM
if (response.status === 403) {
console.warn(`Direct add blocked for ${phone}, sending invite link`);
return sendInviteLink(groupId, phone);
}
throw new Error(`addGroupMember failed (${response.status}): ${JSON.stringify(data)}`);
}
// Sleep >250ms between consecutive adds -- prevents IQErrorRateOverlimit
// which fires after ~4 rapid additions and requires a channel-level retry
await sleep(300);
return data;
}
// DELETE https://gate.whapi.cloud/groups/{GroupID}/participants
async function removeGroupMember(groupId, phone) {
const jid = toWhatsAppJID(phone);
const response = await fetch(
`https://gate.whapi.cloud/groups/${groupId}/participants`,
{
method: 'DELETE',
headers: {
'Authorization': `Bearer ${process.env.WHAPI_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ participants: [jid] }),
}
);
// This DELETE is silent: no WhatsApp group notification is sent to other members
return response.json();
}
A chamada DELETE de removeGroupMember não gera nenhuma notificação no grupo. A mensagem "X foi removido pelo admin" no WhatsApp só aparece quando um administrador usa o app manualmente. A remoção via API com DELETE /groups/{id}/participants é invisível para os outros membros. O assinante simplesmente desaparece da lista de participantes. Todo o atrito social — o constrangimento de remover alguém manualmente — desaparece junto.
Limites de taxa, idempotência e o fallback 403
Três modos de falha que qualquer deploy em produção vai encontrar, cada um com uma solução determinística. Coloque os três no código antes do primeiro pagamento de assinante em produção.
IQErrorRateOverlimit é o erro do servidor do WhatsApp que dispara quando você chama addParticipants rápido demais. Um desenvolvedor registrou em uma issue do GitHub com precisão: "Erro desconhecido (internamente IQErrorRateOverlimit) ao chamar group.addParticipants() — ocorre tipicamente após 4 membros adicionados com sucesso com os valores de sleep padrão." A correção no addGroupMember() acima é await sleep(300) após cada adição individual. Para migrações em lote de 50+ assinantes existentes de uma vez, aumente o sleep para 500ms e processe em grupos de 10 com uma pausa de 3 segundos entre os lotes.
O módulo de banco de dados abaixo implementa tanto a verificação de idempotência quanto o armazenamento dos registros de membros. Usa better-sqlite3 para acesso síncrono ao SQLite, simplificando o fluxo assíncrono:
// db.js -- SQLite member store with idempotency
const Database = require('better-sqlite3');
const db = new Database('members.db');
db.exec(`
CREATE TABLE IF NOT EXISTS members (
id INTEGER PRIMARY KEY AUTOINCREMENT,
phone TEXT NOT NULL,
stripe_subscription_id TEXT UNIQUE NOT NULL,
stripe_customer_id TEXT NOT NULL,
group_id TEXT NOT NULL,
created_at INTEGER DEFAULT (unixepoch())
);
CREATE TABLE IF NOT EXISTS processed_events (
event_id TEXT PRIMARY KEY,
processed_at INTEGER DEFAULT (unixepoch())
);
`);
module.exports = {
hasProcessedEvent: (id) =>
!!db.prepare('SELECT 1 FROM processed_events WHERE event_id = ?').get(id),
markEventProcessed: (id) =>
db.prepare('INSERT OR IGNORE INTO processed_events (event_id) VALUES (?)').run(id),
saveMember: ({ phone, stripeSubscriptionId, stripeCustomerId, groupId }) =>
db.prepare(`
INSERT OR REPLACE INTO members (phone, stripe_subscription_id, stripe_customer_id, group_id)
VALUES (?, ?, ?, ?)
`).run(phone, stripeSubscriptionId, stripeCustomerId, groupId),
getMemberBySubscriptionId: (sid) =>
db.prepare('SELECT * FROM members WHERE stripe_subscription_id = ?').get(sid),
removeMember: (sid) =>
db.prepare('DELETE FROM members WHERE stripe_subscription_id = ?').run(sid),
};
Um 403 de addGroupParticipant significa que as configurações de privacidade do WhatsApp do assinante bloqueiam adições ao grupo pelo servidor. O único caminho: envie diretamente um link de convite rotacionado para esse assinante. Essa é uma configuração de dispositivo que você não pode substituir. Veja a referência de API para adicionar novo membro ao grupo para o formato exato do erro. O fallback é tratado por sendInviteLink() na próxima seção.
Rotação de link de convite: fechando a entrada de membros não pagantes
Sempre que enviar um link de convite para alguém, revogue-o imediatamente e gere um novo. Um link sem rotação pode encher seu grupo de membros não pagantes em menos de uma hora.
O mecanismo: DELETE /groups/{id}/invite invalida o código de convite atual instantaneamente. GET /groups/{id}/invite gera e retorna um novo código. O novo link vai apenas para o assinante, via mensagem direta no WhatsApp. Qualquer código que existia antes do DELETE (de um teste anterior, um assinante passado ou uma captura de tela vazada) para de funcionar imediatamente.
// DELETE https://gate.whapi.cloud/groups/{GroupID}/invite (revoke current code)
// GET https://gate.whapi.cloud/groups/{GroupID}/invite (get new code)
// POST https://gate.whapi.cloud/messages/text (send link via DM)
async function sendInviteLink(groupId, phone) {
// Step 1: Revoke -- any previously distributed link stops working now
await fetch(`https://gate.whapi.cloud/groups/${groupId}/invite`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${process.env.WHAPI_TOKEN}` },
});
// Step 2: Get fresh invite code
const res = await fetch(`https://gate.whapi.cloud/groups/${groupId}/invite`, {
method: 'GET',
headers: { 'Authorization': `Bearer ${process.env.WHAPI_TOKEN}` },
});
const { invite_code } = await res.json();
const inviteLink = `https://chat.whatsapp.com/${invite_code}`;
// Step 3: Send the unique link to this subscriber only via DM
await fetch('https://gate.whapi.cloud/messages/text', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.WHAPI_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: toWhatsAppJID(phone),
body: `Your subscription is confirmed! Join your private group here:\n${inviteLink}\n\nThis link is personalized -- please do not share it.`,
}),
});
console.log(`Invite link rotated and sent to ${phone}`);
}
Há uma troca: cada rotação invalida todos os códigos anteriores, incluindo os enviados para assinantes que ainda não clicaram. Mantenha a adição direta via API como caminho principal. Use a rotação apenas como fallback para o erro 403, não no onboarding padrão.
Projetos que pulam a rotação e entregam uma URL de convite estática relatam o padrão de membros fantasmas em poucos dias após a primeira promoção pública. Cada membro não pagante consome conteúdo, ocupa um dos 1.024 slots do grupo e reduz a exclusividade percebida pelos assinantes pagantes. O vazamento de receita é permanente — não há como identificar retroativamente qual membro entrou por um link compartilhado versus uma assinatura paga sem rastreamento de participantes no nível da API.
O que armazenar no banco de dados (e em qual escala)
Google Sheets suporta 50 membros; SQLite suporta 50.000. Escolha antes do lançamento, não depois que a linha 51 da sua planilha começar a gerar erros de busca.
Tabela de decisão:
| Número de membros | Armazenamento | Tempo de configuração | Idempotência | Migre quando |
|---|---|---|---|---|
| ≤50 membros | Google Sheets + Apps Script | ~30 min | Verificação manual de duplicatas | Erros aparecem nas buscas |
| 51--500 membros | SQLite (baseado em arquivo, servidor único) | ~2 horas | Tabela processed_events |
Você fizer deploy em vários servidores |
| 500+ membros | PostgreSQL ou MySQL | ~4 horas + hospedagem | processed_events + indexado |
Nunca — suporta facilmente grupos com os 1.024 membros completos |
Para o nível Google Sheets, um app web do Google Apps Script recebe requisições POST do seu servidor webhook e adiciona ou atualiza linhas. Faça o deploy como app web público que seu servidor Express chama em vez de um banco de dados local:
// Google Apps Script -- deploy as a web app at script.google.com
// Your Express server calls this URL with { action, phone, subscriptionId }
function doPost(e) {
const data = JSON.parse(e.postData.contents);
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Members');
if (data.action === 'add') {
sheet.appendRow([
data.phone,
data.subscriptionId,
data.customerId,
'active',
new Date().toISOString(),
]);
} else if (data.action === 'remove') {
const rows = sheet.getDataRange().getValues();
for (let i = 1; i < rows.length; i++) {
if (rows[i][1] === data.subscriptionId) {
sheet.getRange(i + 1, 4).setValue('removed');
break;
}
}
}
return ContentService.createTextOutput('OK');
}
O schema do SQLite da seção de limites de taxa cobre o nível de 51 a 500 sem modificações. Para o nível 500+, migre para o PostgreSQL: os nomes de colunas se transferem diretamente, SERIAL substitui INTEGER PRIMARY KEY AUTOINCREMENT e unixepoch() vira EXTRACT(EPOCH FROM NOW()). Quando seu grupo atingir o teto de 1.024 membros, adicione uma consulta GROUP BY group_id à sua lógica de roteamento de membros para abranger grupos paralelos. Mais de 3.000 empresas usam a Whapi.Cloud em produção hoje, incluindo operadores de comunidades pagas que escalaram além dos 1.024 membros. Quando suas assinaturas chegarem às centenas, a última coisa que você precisa é de uma atualização de protocolo do WhatsApp quebrando o gerenciamento de grupos às 3h da manhã — a Whapi.Cloud absorve essas mudanças de protocolo, mantendo /groups/{id}/participants estável enquanto a camada de sessão subjacente é atualizada de forma transparente.
O caminho sem código: trigger do Stripe no n8n e o nó HTTP da Whapi.Cloud
O n8n elimina completamente o servidor Express: um nó Stripe Trigger, um nó Set para o formato do telefone e um nó HTTP Request para a Whapi.Cloud via n8n. Zero código de backend customizado para o caminho principal.
Fluxo para checkout.session.completed:
-
Nó Stripe Trigger: configure para receber
checkout.session.completed. O n8n fornece uma URL de webhook para registrar no seu painel do Stripe. -
Nó Set (transformação do telefone): campo de expressão:
{{ $json.data.object.metadata.whatsapp_phone.replace(/^\+/, '') + '@s.whatsapp.net' }}. Essa é a normalização completa em uma única expressão. -
Nó HTTP Request: Método: POST, URL:
https://gate.whapi.cloud/groups/YOUR_GROUP_ID/participants, Autenticação: credencial genérica (Bearer token), Body (JSON):{"participants": ["{{ $json.jid }}"]}.
Para o caminho de remoção, duplique o fluxo e mude o Stripe Trigger para customer.subscription.deleted e o método HTTP para DELETE. A troca: o n8n não tem idempotência nativa. Uma retentativa do Stripe entrega o evento duas vezes, adicionando ou removendo o membro duas vezes. Para comunidades com menos de 50 assinantes ativos onde o volume de retentativas é desprezível, isso é aceitável. Acima desse limite, o caminho Node.js + SQLite trata a deduplicação de forma confiável via tabela processed_events.
O que este sistema não cobre (e quais são os próximos passos)
Assinaturas de múltiplos níveis ficam de fora deste guia. Rotear assinantes Silver e Gold para grupos diferentes exige uma tabela de lookup de Product IDs do Stripe e IDs de grupo por nível que depende totalmente da sua estrutura de preços.
Também fora do escopo: períodos de carência após o cancelamento, gerenciamento de períodos de teste em que membros entram antes da primeira cobrança, e o limite de 8 membros por grupo da API oficial do WhatsApp Business (que requer mais de 100.000 mensagens diárias para acesso ao gerenciamento de grupos e não se aplica à implementação de sessão web da Whapi.Cloud). Para lógica de carência de assinatura, adicione uma coluna grace_days à sua tabela de membros e ajuste a verificação do cron de expiração conforme necessário.









