Migrar os seus assinantes para o SubSovereign
Vem do RevenueCat, da Adapty, da Qonversion ou do seu próprio backend? Os seus assinantes atuais vêm consigo. Este guia cobre a importação em si e — igualmente importante — a transição, que é a parte que realmente comporta risco.
Leia isto primeiro. A importação é segura e repetível. A transição é o momento que exige planeamento, porque enquanto não redirecionar as notificações das lojas para nós, as renovações continuam a ser entregues ao seu fornecedor anterior. Planeie o §4 antes de executar o §2.
As duas metades de uma migração — leia antes de planear uma
Toda a migração assenta em dois mecanismos, e distingui-los é a diferença entre uma mudança tranquila e uma promessa que não consegue cumprir.
A revalidação do lado do cliente é a GARANTIA. Assim que a sua aplicação incluir o nosso SDK, o dispositivo de cada assinante reenvia o recibo em vigor na próxima vez que a abrir. O identificador vem da própria Apple ou Google: atual, correto e completo. Funciona em todas as lojas, não precisa da exportação de ninguém e não pode transportar um identificador desatualizado. Todo o assinante que abra a sua aplicação é migrado corretamente, por construção.
A importação é o ACELERADOR. Existe para que os seus assinantes não fiquem bloqueados antes de voltarem a abrir a aplicação, e para que os seus relatórios do primeiro dia não apareçam vazios. Copia aquilo que o seu fornecedor anterior sabia.
Porquê ambos: os assinantes abrem aplicações a ritmos muito diferentes. Quem não abrir a sua aplicação durante seis semanas passaria, só com a revalidação, seis semanas a parecer sem direitos. A importação dá-lhe continuidade desde o primeiro minuto; a revalidação torna-a fidedigna.
🚩 Se só puder ficar com um, fique com a revalidação. Uma importação que não consiga fornecer um identificador correspondível para determinada loja (ver abaixo) não é uma migração para esses assinantes: é um registo que expira em silêncio.
O que o seu fornecedor anterior lhe consegue realmente dar
A importação precisa de originalTransactionId, e aquilo que cada fornecedor entrega varia consoante a loja. Verifique o seu antes de planear em função disso:
| Fornecedor | Apple | |
|---|---|---|
| RevenueCat | ✅ A exportação de dados contém original_transaction_id. |
⚠️ A exportação dá um Order ID, que não é o purchase token — um identificador diferente, com o qual uma notificação de renovação não fará correspondência. A API v2 é pior: expõe apenas o identificador de transação mais recente, que muda a cada renovação. |
| Adapty / Qonversion | Verifique na exportação a presença do original transaction ID da Apple antes de planear. | Verifique se recebe o purchase token ou uma referência de encomenda/transação. Confirme, não presuma. |
| Superwall | ⚠️ Confirme antes de planear. Operam a sua própria camada de direitos, compras e validação de recibos, pelo que o identificador existe do lado deles — a questão em aberto é se a exportação lhe entrega o original_transaction_id da Apple ou apenas a referência de subscrição da própria Superwall. |
⚠️ Mesma questão, mesma resposta: estabeleça por escrito se recebe o purchase token ou uma referência de encomenda/transação, antes de planear em função disso. |
| O seu próprio backend | Aquilo que guardou na compra — normalmente o correto. | Muito provavelmente já tem o purchase token, porque precisou dele para validar. |
| Stripe | n/a | n/a — lemos diretamente a sua conta Stripe, sem exportação. Ver o §3b. |
🚩 Assim, uma migração a partir do RevenueCat importa de forma limpa para a Apple e não consegue importar assinantes Google com um identificador correspondível. Não é algo que possamos contornar com código: o identificador de que precisamos não está presente nos dados a que consegue aceder. Esses assinantes chegam através da revalidação do lado do cliente — publique o SDK, redirecione as notificações (§4), e cada um será migrado corretamente na próxima vez que abrir a aplicação. Importe o lado Apple para dar continuidade e deixe a Google resolver-se sozinha.
1. Do que precisa
- A sua exportação de assinantes, com uma linha por cada direito de acesso ativo (ver o formato abaixo).
- Um mapa de produtos. Cada ID de produto da sua exportação tem de estar primeiro associado a um dos seus níveis de acesso — Painel → a sua aplicação → produtos. 🚩 A importação não vai adivinhar que nível conceder. Um produto não associado é recusado e reportado, porque adivinhar seria uma forma de contornar a mesma proteção que impede um cliente de reclamar um nível que não comprou.
- Um acesso de administrador da organização proprietária da aplicação. Os visualizadores (viewers) não podem importar: a operação concede acesso pago em massa.
2. O formato
Um objeto JSON por cada direito de acesso de assinante:
{
"userId": "user_12345",
"email": "person@example.com",
"store": "apple",
"productId": "pro_monthly",
"originalTransactionId": "1000000987654321",
"expiresAt": "2027-01-01T00:00:00Z",
"willRenew": true,
"status": "active"
}
| Campo | Obrigatório | Notas |
|---|---|---|
userId |
sim | O seu próprio identificador de utilizador — o mesmo que a sua aplicação passa ao nosso SDK. |
store |
sim | apple · google · amazon · roku · stripe |
productId |
sim | Tem de existir no seu mapa de produtos, caso contrário a linha é recusada. |
originalTransactionId |
sim | 🚩 Ver abaixo. O original transaction ID da Apple, o purchase token da Google ou o ID de subscrição do fornecedor. |
status |
sim | active · trialing · grace_period · expired · lifetime · cancelled · refunded · billing_failed |
expiresAt |
não | ISO 8601. Omitir para lifetime. |
amazonUserId |
para a Amazon | 🚩 O identificador de utilizador da própria Amazon, obtido do SDK deles — não o identificador de utilizador da sua aplicação. A Amazon procura um recibo por (esse id, receiptId), pelo que sem ele o direito de acesso nunca poderia ser reverificado. As linhas Amazon sem ele são recusadas. |
email, willRenew |
não |
🚩 originalTransactionId é o campo que decide se isto funciona
Não é apenas uma chave para nós — é a forma como uma renovação reencontra o assinante meses depois. Quando a Apple ou a Google envia uma notificação de servidor por uma renovação, é nesse identificador que a correspondência é feita.
Uma linha importada sem ele produz o pior tipo de falha: no primeiro dia tudo parece correto e depois o assinante perde o acesso em silêncio na renovação seguinte, sem que qualquer erro seja assinalado em lado nenhum.
Por isso recusamos essas linhas em vez de as aceitar. Importar sem ele não é um atalho, é uma falha adiada.
🚩 Nem todos os fornecedores conseguem dar-lhe este campo para todas as lojas — ver a tabela acima. Onde a sua exportação genuinamente não o consiga fornecer (RevenueCat + Google é o caso conhecido), não contorne o problema inventando um valor: qualquer valor de substituição produz exatamente a falha silenciosa de renovação aqui descrita. Importe as lojas para as quais tem identificadores reais e deixe a revalidação do lado do cliente tratar do resto.
3. Executar — sempre uma simulação primeiro
Esta secção descreve a via CSV / exportação, usada para Apple, Google, Amazon e Roku. Para a Stripe, salte para o §3b — lemos a sua conta diretamente e não há nada a construir.
A forma mais simples é no painel: Subscriber Migration (Insights → Customers) → From an export. Cole a exportação em CSV ou JSON, ou carregue o ficheiro. A página verifica primeiro as linhas e não escreve nada até que o peça; divide os ficheiros grandes em lotes por si; e lista cada linha recusada por número de linha, para que possa corrigi-las no ficheiro que realmente tem. A API abaixo faz exatamente o mesmo, se preferir automatizar.
🚩 Estas linhas são uma afirmação, não uma confirmação. Uma exportação é aquilo que o seu fornecedor anterior lhe entregou, pelo que as linhas importadas chegam não verificadas e entram na fila de reverificação (§6), onde depois as confrontamos com a loja real. As linhas lidas diretamente da Stripe (§3b) são diferentes: chegam já confirmadas.
A simulação é o comportamento por omissão. Tem de pedir explicitamente que algo seja escrito.
curl -X POST https://api.miimagineai.com/api/v1/apps/YOUR_APP_ID/import/subscribers \
-H "Authorization: Bearer $DASHBOARD_TOKEN" -H 'Content-Type: application/json' \
-d '{ "source": "revenuecat", "rows": [ … até 1000 linhas … ] }'
Recebe um relatório:
{
"dryRun": true, "received": 1000, "accepted": 987, "newUsers": 964,
"missingTransactionIds": 6,
"unmappedProducts": ["legacy_annual_v1"],
"rejected": [ { "index": 12, "code": "unmapped_product", "reason": "…" } ]
}
Leia estes três números antes de confirmar:
missingTransactionIds— linhas que renovariam para o vazio. Corrija a exportação, não prossiga.unmappedProducts— associe-os e volte a executar.newUsers— quantos assinantes isto vai criar. Se o número parecer errado, o seu campouserIdprovavelmente não é o identificador certo.
Quando o relatório estiver limpo, acrescente "dryRun": false e envie de novo.
Lotes e retoma
Envie no máximo 1000 linhas por pedido; uma migração grande são muitos lotes. A importação é idempotente — reenviar o mesmo lote atualiza em vez de duplicar, pelo que um script que morra a meio pode simplesmente voltar a correr desde o início. Uma linha incorreta é reportada e ignorada; nunca descarta as linhas boas ao seu lado.
O que vai recusar, e porquê
| Código | Significado |
|---|---|
missing_transaction_id |
Ver acima. O importante. |
unmapped_product |
Associe primeiro o produto; não vamos adivinhar um nível. |
transaction_owned_by_another_user |
Essa transação da loja já pertence a outra conta nesta aplicação. Uma transação, um proprietário — normalmente uma conta fundida ou dados errados na exportação. |
invalid_store / invalid_status / invalid_expiry |
Valor mal formado. |
3b. Stripe — lemos a sua conta diretamente
A Stripe é a única loja capaz de responder a «quem são todos os meus assinantes?». A Apple, a Google, a Amazon e a Roku respondem apenas a «este recibo continua válido?», um recibo de cada vez. Por isso, com a Stripe não há exportação a pedir nem CSV a construir: Subscriber Migration no painel (Insights → Customers) percorre a sua conta e importa-a.
Antes de começar, duas coisas têm de estar em ordem, e a página avisa-o se não estiverem:
- A sua chave secreta Stripe, guardada em Apps → store credentials. Não existe alternativa ao nível da plataforma nem alguma vez existirá — sem a sua chave a importação falha fechando-se, em vez de ler dados de outra pessoa.
- Os seus produtos associados a níveis. Associe o Price (
price_…) ou o Product (prod_…); ambos funcionam. Os produtos não associados são recusados em vez de adivinhados, o que é o comportamento correto e não um defeito.
Depois diga-nos onde está o identificador de utilizador da sua aplicação dentro da Stripe — metadados da subscrição, metadados do cliente, ou o próprio identificador de cliente da Stripe. 🚩 Não há valor por omissão e não vamos adivinhar. A Stripe conhece o cliente como cus_…; só o senhor sabe qual dos seus utilizadores é, e enganar-se não falha de forma ruidosa — concede o acesso pago de um assinante à conta de outra pessoa.
Pré-visualize primeiro. A pré-visualização não altera nada e mostra-lhe exatamente o que aconteceria: quantas subscrições foram lidas, quantas seriam importadas e cada uma das que seria recusada, com o motivo em linguagem clara. Leia as recusas antes de importar: são a lista de coisas a corrigir, não ruído.
As contas grandes são percorridas automaticamente ao longo de vários pedidos; a página regula o próprio ritmo para se manter dentro do limite de frequência e mostra o progresso à medida que avança.
O que recusa, e porque estas são as respostas certas
| Código | Significado |
|---|---|
never_paid |
A subscrição está incomplete — nunca concluiu um primeiro pagamento. Importá-la concederia acesso pago a alguém que nunca pagou. |
access_expired |
O estado concede acesso mas o período de faturação já terminou. Aqui o acesso é decidido pelo estado, pelo que importá-la concederia acesso indefinidamente. |
ambiguous_product |
Os itens de uma mesma subscrição remetem para dois níveis diferentes. Recusamos em vez de escolher — ficar com o mais alto seria uma subida de nível a entrar pela porta da migração, e ficar com o primeiro seria arbitrário. |
no_user_id |
Nenhum identificador de utilizador da aplicação onde nos disse para procurar. Ver acima: este é o perigoso de adivinhar. |
unmapped_product |
Associe o produto e volte a executar. |
🚩 Os produtos vistos mas não associados são reportados mesmo quando não bloquearam nada. Hoje não bloqueiam nada, mas assim que associar um, qualquer subscrição que o transporte ao lado de outro produto associado torna-se ambígua e passa a ser recusada. Melhor saber agora do que na segunda execução.
Porque é que as linhas Stripe chegam já verificadas
Uma linha importada conta normalmente como uma afirmação até a confirmarmos junto da loja (§6). As linhas Stripe são diferentes: foram lidas através da própria API da Stripe usando as suas credenciais, pelo que chegam já confirmadas pela loja. Isto é uma afirmação sobre a proveniência, não sobre a Stripe — qualquer futuro processador de pagamentos que leiamos diretamente comportar-se-á da mesma forma, e qualquer linha fornecida por uma pessoa continua a ser uma afirmação, independentemente da loja que nomeie.
Prefere a API? A página é um cliente de POST /apps/:appId/import/stripe; as mesmas opções são dryRun, userIdSource, userIdKey e startingAfter para a paginação.
4. 🚩 A transição — planeie-a antes de importar
A importação copia um estado. Não redireciona o futuro. Enquanto não reapontar as notificações de servidor das suas lojas para o SubSovereign, renovações, cancelamentos e reembolsos continuam a ir para o seu fornecedor anterior.
A sequência recomendada:
- Importe (este guia). Os seus direitos de acesso passam a existir nos dois sistemas.
- Corra em paralelo. Mantenha o fornecedor anterior ativo e compare. Ainda nada se moveu.
- Publique uma versão da aplicação com o nosso SDK. Os direitos são resolvidos por nós e, como importou os mesmos identificadores de transação, os assinantes existentes mantêm o acesso sem fazer nada.
- Reaponte as notificações das lojas para os nossos endpoints de webhook (Apple App Store Server Notifications, Google Real-Time Developer Notifications). É este o verdadeiro interruptor.
- Volte a executar a importação para tudo o que tenha mudado durante a janela. É idempotente — é precisamente para isso que este passo serve.
- Desative o fornecedor anterior assim que um ciclo completo de renovação tiver decorrido de forma limpa.
5. O que é um direito de acesso importado — e o que não é
Um direito de acesso importado é uma concessão de acesso pago feita com base na palavra do seu fornecedor anterior. Ainda não o verificámos nós próprios junto da Apple ou da Google.
Registamos isso com honestidade em vez de o esconder: as linhas importadas transportam imported_at e import_source, e o seu store_verified_at permanece vazio até o direito ter sido confirmado junto da loja real. Assim, «quais destes concedemos por confiança?» é uma pergunta com resposta exata, a qualquer momento.
🚩 Isto importa se algum número parecer errado. Direitos importados e verificados não são a mesma categoria de facto, e uma auditoria que os tratasse como idênticos seria enganadora.
6. Reverificar uma importação junto da loja
Depois de importados, pode pedir-nos que verifiquemos cada direito de acesso junto da loja real:
curl -X POST https://api.miimagineai.com/api/v1/apps/YOUR_APP_ID/import/verify \
-H "Authorization: Bearer $DASHBOARD_TOKEN" -H 'Content-Type: application/json' \
-d '{ "limit": 50 }'
Cada linha regressa com um de três resultados:
| Resultado | Significado |
|---|---|
| verified | A loja confirmou-a. Adotamos a validade e o indicador de renovação da loja em vez dos importados — é ela a autoridade. |
| mismatch | A loja contrariou a importação: nenhuma subscrição ativa, ou uma ativa para um produto diferente. |
| unverifiable | Não conseguimos obter uma resposta utilizável — um limite de frequência, um problema de credenciais, um erro transitório. Isto não é uma contradição. |
🚩 Um mismatch nunca revoga o acesso. Nada muda no direito de acesso; apenas as colunas de verificação são escritas, e a divergência é apresentada para que uma pessoa a avalie. «A loja não o confirmou» e «este assinante não tem direito» são afirmações diferentes, e a API de uma loja pode devolver a primeira por razões que nada têm a ver com o seu cliente. Não estamos dispostos a cancelar um assinante pagante com essa base — muito menos um que acabou de migrar para nós. Ver DECISIONS.md #70.
Execute em lotes (50 por omissão, 200 no máximo) — as APIs das lojas têm limites de frequência e estas chamadas usam as suas credenciais de loja. GET /apps/YOUR_APP_ID/import/status mostra o retrato atual:
{ "imported": 4820, "verified": 4776, "awaiting": 44, "mismatch": 11, "unverifiable": 33 }
Ainda não existe um agendamento automático para isto, de propósito — preferimos não usar as suas credenciais de loja sem supervisão, num temporizador que não pediu. Execute quando lhe convier; é idempotente e retomável.