SubSovereign
All guides

Migrare i tuoi abbonati su SubSovereign

Arrivi da RevenueCat, Adapty, Qonversion o dal tuo backend? I tuoi abbonati attuali vengono con te. Questa guida copre l'importazione vera e propria e — cosa altrettanto importante — il passaggio, che è la parte che comporta davvero un rischio.

Leggi prima questo. L'importazione è sicura e ripetibile. Il passaggio è il momento che richiede pianificazione, perché finché non reindirizzi le notifiche degli store verso di noi, i rinnovi continuano ad arrivare al tuo vecchio fornitore. Pianifica il §4 prima di eseguire il §2.


Le due metà di una migrazione — leggile prima di pianificarne una

Ogni migrazione ha due meccanismi, e distinguerli è la differenza tra un trasloco senza intoppi e una promessa che non puoi mantenere.

La riconvalida lato client è la GARANZIA. Non appena la tua app include il nostro SDK, il dispositivo di ogni abbonato reinvia la ricevuta in corso alla successiva apertura. L'identificativo arriva da Apple o Google stessi: aggiornato, corretto e completo. Funziona per ogni store, non richiede l'export di nessuno e non può trasportare un identificativo obsoleto. Ogni abbonato che apre la tua app viene migrato correttamente, per costruzione.

L'importazione è l'ACCELERATORE. Esiste perché i tuoi abbonati non restino bloccati prima di riaprire l'app, e perché i tuoi report del primo giorno non siano vuoti. Copia ciò che sapeva il tuo fornitore precedente.

Perché entrambi: gli abbonati aprono le app con frequenze molto diverse. Chi non avvia la tua app per sei settimane, con la sola riconvalida, passerebbe sei settimane apparendo senza diritti. L'importazione gli dà continuità dal primo minuto; la riconvalida la rende autorevole.

🚩 Se puoi tenerne solo uno, tieni la riconvalida. Un'importazione che non riesce a fornire un identificativo utilizzabile per un certo store (vedi sotto) non è una migrazione per quegli abbonati: è un record che scade in silenzio.

Cosa può darti davvero il tuo fornitore precedente

L'importazione ha bisogno di originalTransactionId, e ciò che il fornitore consegna varia da store a store. Verifica il tuo prima di costruirci sopra un piano:

Fornitore Apple Google
RevenueCat ✅ L'export dei dati contiene original_transaction_id. ⚠️ L'export fornisce un Order ID, che non è il purchase token: un identificativo diverso, su cui una notifica di rinnovo non corrisponderà. La loro API v2 è peggio: espone solo l'identificativo di transazione più recente, che cambia a ogni rinnovo.
Adapty / Qonversion Verifica nell'export la presenza dell'original transaction ID di Apple prima di pianificare. Verifica se ottieni il purchase token o un riferimento a ordine/transazione. Confermalo, non darlo per scontato.
Superwall ⚠️ Da confermare prima di pianificare. Gestiscono un proprio livello di diritti, acquisti e validazione delle ricevute, quindi l'identificativo esiste dalla loro parte: la domanda aperta è se l'export ti consegni l'original_transaction_id di Apple o solo il riferimento di abbonamento di Superwall. ⚠️ Stessa domanda, stessa risposta: stabilisci per iscritto se ottieni il purchase token o un riferimento a ordine/transazione, prima di costruirci sopra un piano.
Il tuo backend Quello che hai salvato all'acquisto: di solito la cosa giusta. Molto probabilmente hai già il purchase token, perché ti serviva per validare.
Stripe n/d n/d — leggiamo direttamente il tuo account Stripe, nessun export necessario. Vedi il §3b.

🚩 Quindi una migrazione da RevenueCat importa pulitamente per Apple e non può importare gli abbonati Google con un identificativo utilizzabile. Non è qualcosa che possiamo aggirare con il codice: l'identificativo che ci serve non è presente nei dati che riesci a ottenere. Quegli abbonati arrivano tramite la riconvalida lato client: pubblica l'SDK, reindirizza le notifiche (§4) e ciascuno verrà migrato correttamente alla successiva apertura dell'app. Importa il lato Apple per la continuità e lascia che Google si sistemi da solo.


1. Cosa ti serve

  • Il tuo export degli abbonati, con una riga per ogni diritto di accesso attivo (vedi il formato sotto).
  • Una mappa dei prodotti. Ogni ID prodotto presente nel tuo export deve prima essere associato a uno dei tuoi livelli di accesso — Dashboard → la tua app → prodotti. 🚩 L'importazione non indovinerà quale livello concedere. Un prodotto non associato viene rifiutato e segnalato, perché indovinare sarebbe un modo per aggirare la stessa protezione che impedisce a un cliente di rivendicare un livello che non ha acquistato.
  • Un accesso da amministratore per l'organizzazione proprietaria dell'app. I viewer non possono importare: l'operazione concede accesso a pagamento in blocco.

2. Il formato

Un oggetto JSON per ogni diritto di accesso dell'abbonato:

{
  "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 Obbligatorio Note
userId Il tuo identificativo utente, lo stesso che la tua app passa al nostro SDK.
store apple · google · amazon · roku · stripe
productId Deve esistere nella tua mappa dei prodotti, altrimenti la riga viene rifiutata.
originalTransactionId 🚩 Vedi sotto. L'original transaction ID di Apple, il purchase token di Google o l'ID abbonamento del fornitore.
status active · trialing · grace_period · expired · lifetime · cancelled · refunded · billing_failed
expiresAt no ISO 8601. Omettilo per lifetime.
amazonUserId per Amazon 🚩 L'identificativo utente di Amazon, ottenuto dal loro SDK — non l'identificativo utente della tua app. Amazon cerca una ricevuta tramite (quell'id, receiptId), quindi senza di esso il diritto di accesso non potrebbe mai essere riverificato. Le righe Amazon che ne sono prive vengono rifiutate.
email, willRenew no

🚩 originalTransactionId è il campo che decide se tutto questo funziona

Non è solo una chiave per noi: è il modo in cui un rinnovo ritrova l'abbonato mesi dopo. Quando Apple o Google invia una notifica server per un rinnovo, la corrispondenza avviene proprio su quell'identificativo.

Una riga importata senza produce il peggior tipo di guasto: il primo giorno sembra tutto corretto, poi l'abbonato perde l'accesso in silenzio al rinnovo successivo, senza che venga sollevato alcun errore da nessuna parte.

Per questo rifiutiamo quelle righe invece di accettarle. Importare senza non è una scorciatoia, è un'interruzione rimandata.

🚩 Non tutti i fornitori possono darti questo campo per ogni store: vedi la tabella sopra. Dove il tuo export davvero non può fornirlo (RevenueCat + Google è il caso noto), non aggirare il problema inventando un valore: qualsiasi segnaposto produce esattamente il guasto silenzioso al rinnovo descritto qui. Importa gli store per cui hai identificativi reali e lascia che la riconvalida lato client si occupi del resto.

3. Eseguirla — sempre prima una simulazione

Questa sezione descrive il percorso CSV / export, usato per Apple, Google, Amazon e Roku. Per Stripe, salta al §3b: leggiamo il tuo account direttamente e non c'è nulla da costruire.

Il modo più semplice è dalla dashboard: Subscriber Migration (Insights → Customers) → From an export. Incolla l'export come CSV o JSON, oppure carica il file. La pagina controlla prima le righe e non scrive nulla finché non lo chiedi; suddivide per te i file grandi in lotti; ed elenca ogni riga rifiutata per numero di riga, così puoi correggerla nel file che hai davvero. L'API qui sotto fa esattamente la stessa cosa, se preferisci automatizzare.

🚩 Queste righe sono un'affermazione, non una conferma. Un export è ciò che ti ha consegnato il fornitore precedente, quindi le righe importate arrivano non verificate ed entrano nella coda di riverifica (§6), dove poi le confrontiamo con lo store reale. Le righe lette direttamente da Stripe (§3b) sono diverse: arrivano già confermate.

La simulazione è il comportamento predefinito. Devi chiedere esplicitamente di scrivere qualcosa.

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": [ … fino a 1000 righe … ] }'

Ricevi un report:

{
  "dryRun": true, "received": 1000, "accepted": 987, "newUsers": 964,
  "missingTransactionIds": 6,
  "unmappedProducts": ["legacy_annual_v1"],
  "rejected": [ { "index": 12, "code": "unmapped_product", "reason": "…" } ]
}

Leggi questi tre numeri prima di confermare:

  • missingTransactionIds: righe che si rinnoverebbero nel vuoto. Correggi l'export, non proseguire.
  • unmappedProducts: associali, poi riesegui.
  • newUsers: quanti abbonati verranno creati. Se il numero sembra sbagliato, probabilmente il tuo campo userId non è l'identificativo giusto.

Quando il report è pulito, aggiungi "dryRun": false e rinvialo.

Lotti e ripartenza

Invia al massimo 1000 righe per richiesta; una grande migrazione è fatta di molti lotti. L'importazione è idempotente: rinviare lo stesso lotto aggiorna invece di duplicare, così uno script che si interrompe a metà può semplicemente ripartire dall'inizio. Una riga errata viene segnalata e saltata; non fa mai perdere le righe buone accanto a lei.

Cosa rifiuterà, e perché

Codice Significato
missing_transaction_id Vedi sopra. Quello importante.
unmapped_product Associa prima il prodotto; non indovineremo un livello.
transaction_owned_by_another_user Quella transazione dello store appartiene già a un altro account in questa app. Una transazione, un proprietario: di solito un account unificato o dati errati nell'export.
invalid_store / invalid_status / invalid_expiry Valore malformato.

3b. Stripe — leggiamo direttamente il tuo account

Stripe è l'unico store in grado di rispondere a «chi sono tutti i miei abbonati?». Apple, Google, Amazon e Roku rispondono solo a «questa ricevuta è ancora valida?», una ricevuta alla volta. Per Stripe quindi non c'è nessun export da richiedere né alcun CSV da costruire: Subscriber Migration nella dashboard (Insights → Customers) percorre il tuo account e lo importa.

Prima di iniziare, due cose devono essere a posto, e la pagina te lo segnala se non lo sono:

  1. La tua chiave segreta Stripe, salvata in Apps → store credentials. Non esiste alcun ripiego a livello di piattaforma e non esisterà mai: senza la tua chiave l'importazione fallisce chiudendosi, invece di leggere i dati di qualcun altro.
  2. I tuoi prodotti associati ai livelli. Associa il Price (price_…) oppure il Product (prod_…); funzionano entrambi. I prodotti non associati vengono rifiutati invece che indovinati: è il comportamento corretto, non un difetto.

Poi indicaci dove si trova l'identificativo utente della tua app dentro Stripe: metadati dell'abbonamento, metadati del cliente o l'identificativo cliente di Stripe stesso. 🚩 Non c'è un valore predefinito e non lo indovineremo. Stripe conosce il cliente come cus_…; solo tu sai quale dei tuoi utenti sia, e sbagliare non fallisce rumorosamente: concede l'accesso a pagamento di un abbonato all'account di un'altra persona.

Prima l'anteprima. L'anteprima non modifica nulla e ti mostra esattamente cosa accadrebbe: quanti abbonamenti sono stati letti, quanti verrebbero importati e ognuno di quelli che verrebbe rifiutato, con la motivazione in linguaggio chiaro. Leggi i rifiuti prima di importare: sono l'elenco delle cose da correggere, non rumore.

Gli account grandi vengono percorsi automaticamente su più richieste; la pagina regola il proprio ritmo per restare entro il limite di frequenza e mostra l'avanzamento.

Cosa rifiuta, e perché queste sono le risposte giuste

Codice Significato
never_paid L'abbonamento è incomplete: non ha mai completato un primo pagamento. Importarlo concederebbe accesso a pagamento a qualcuno che non ha mai pagato.
access_expired Lo stato concede l'accesso ma il periodo di fatturazione è già terminato. Qui l'accesso è deciso dallo stato, quindi importarlo concederebbe accesso a tempo indeterminato.
ambiguous_product Gli elementi di un solo abbonamento rimandano a due livelli diversi. Rifiutiamo invece di scegliere: prendere il più alto sarebbe un avanzamento di livello che entra dalla porta della migrazione, e prendere il primo sarebbe arbitrario.
no_user_id Nessun identificativo utente dell'app dove ci hai detto di cercare. Vedi sopra: è quello pericoloso da indovinare.
unmapped_product Associa il prodotto, poi riesegui.

🚩 I prodotti visti ma non associati vengono segnalati anche quando non hanno bloccato nulla. Oggi non bloccano niente, ma nel momento in cui ne associ uno, qualsiasi abbonamento che lo porta insieme a un altro prodotto associato diventa ambiguo e inizia a essere rifiutato. Meglio saperlo ora che alla seconda esecuzione.

Perché le righe Stripe arrivano già verificate

Una riga importata conta normalmente come affermazione finché non la confermiamo con lo store (§6). Le righe Stripe sono diverse: sono state lette tramite l'API di Stripe stessa usando le tue credenziali, quindi arrivano già confermate dallo store. È un'affermazione sulla provenienza, non su Stripe: qualsiasi futuro processore di pagamento che leggeremo direttamente si comporterà allo stesso modo, e qualsiasi riga fornita da una persona resta un'affermazione, indipendentemente dallo store che nomina.

Preferisci l'API? La pagina è un client di POST /apps/:appId/import/stripe; le stesse opzioni sono dryRun, userIdSource, userIdKey e startingAfter per la paginazione.

4. 🚩 Il passaggio — pianificalo prima di importare

L'importazione copia uno stato. Non reindirizza il futuro. Finché non reindirizzi le notifiche server dei tuoi store verso SubSovereign, rinnovi, cancellazioni e rimborsi continuano ad arrivare al tuo vecchio fornitore.

La sequenza consigliata:

  1. Importa (questa guida). I tuoi diritti di accesso ora esistono in entrambi i sistemi.
  2. Fai girare in parallelo. Tieni attivo il vecchio fornitore e confronta. Non si è ancora spostato nulla.
  3. Pubblica una build dell'app che usa il nostro SDK. I diritti vengono risolti da noi e, poiché hai importato gli stessi identificativi di transazione, gli abbonati esistenti mantengono l'accesso senza fare nulla.
  4. Reindirizza le notifiche degli store ai nostri endpoint webhook (Apple App Store Server Notifications, Google Real-Time Developer Notifications). Questo è il vero interruttore.
  5. Riesegui l'importazione per tutto ciò che è cambiato durante la finestra. È idempotente: questo passaggio serve proprio a questo.
  6. Dismetti il vecchio fornitore una volta trascorso pulitamente un ciclo completo di rinnovo.

5. Cos'è un diritto di accesso importato — e cosa non è

Un diritto di accesso importato è una concessione di accesso a pagamento fatta sulla parola del tuo fornitore precedente. Non l'abbiamo ancora verificato noi stessi presso Apple o Google.

Lo registriamo onestamente invece di nasconderlo: le righe importate portano imported_at e import_source, e il loro store_verified_at resta vuoto finché il diritto non è stato confermato presso lo store reale. Così «quali di questi abbiamo concesso sulla fiducia?» è una domanda con una risposta esatta, in qualsiasi momento.

🚩 Questo conta se una cifra dovesse mai sembrare sbagliata. I diritti importati e quelli verificati non sono la stessa categoria di fatto, e un audit che li trattasse come identici sarebbe fuorviante.

6. Riverificare un'importazione presso lo store

Una volta importati, puoi farci controllare ogni diritto di accesso presso lo store reale:

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 }'

Ogni riga torna con uno di tre esiti:

Esito Significato
verified Lo store l'ha confermata. Adottiamo la scadenza e il flag di rinnovo dello store al posto di quelli importati: è lui l'autorità.
mismatch Lo store ha contraddetto l'importazione: nessun abbonamento attivo, oppure uno attivo per un prodotto diverso.
unverifiable Non siamo riusciti a ottenere una risposta utilizzabile: un limite di frequenza, un problema di credenziali, un errore temporaneo. Questa non è una contraddizione.

🚩 Un mismatch non revoca mai l'accesso. Del diritto di accesso non cambia nulla; vengono scritte solo le colonne di verifica, e la discrepanza viene esposta perché sia una persona a giudicarla. «Lo store non l'ha confermato» e «questo abbonato non ha diritto» sono affermazioni diverse, e l'API di uno store può restituire la prima per motivi che non hanno nulla a che vedere con il tuo cliente. Non siamo disposti a cancellare un abbonato pagante su questa base — tanto meno uno che ha appena migrato da noi. Vedi DECISIONS.md #70.

Eseguila a lotti (50 di default, massimo 200): le API degli store hanno limiti di frequenza e queste chiamate usano le tue credenziali. GET /apps/YOUR_APP_ID/import/status mostra la situazione corrente:

{ "imported": 4820, "verified": 4776, "awaiting": 44, "mismatch": 11, "unverifiable": 33 }

Non esiste ancora una pianificazione automatica per questo, di proposito: preferiamo non usare le tue credenziali dello store senza supervisione, su un timer che non hai chiesto. Eseguila quando ti conviene; è idempotente e riprendibile.