SubSovereign
All guides

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 Google
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 campo userId provavelmente 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:

  1. 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.
  2. 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:

  1. Importe (este guia). Os seus direitos de acesso passam a existir nos dois sistemas.
  2. Corra em paralelo. Mantenha o fornecedor anterior ativo e compare. Ainda nada se moveu.
  3. 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.
  4. 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.
  5. Volte a executar a importação para tudo o que tenha mudado durante a janela. É idempotente — é precisamente para isso que este passo serve.
  6. 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.