SubSovereign
All guides

Migrer vos abonnés vers SubSovereign

Vous quittez RevenueCat, Adapty, Qonversion ou votre propre backend ? Vos abonnés existants vous suivent. Ce guide couvre l'import lui-même et — tout aussi important — la bascule, qui est la partie qui comporte réellement un risque.

À lire en premier. L'import est sûr et répétable. C'est la bascule qui demande de la préparation, car tant que vous n'avez pas redirigé les notifications de vos boutiques vers nous, les renouvellements continuent d'être livrés à votre ancien fournisseur. Planifiez le §4 avant d'exécuter le §2.


Les deux moitiés d'une migration — à lire avant d'en planifier une

Toute migration repose sur deux mécanismes, et les distinguer fait la différence entre un déplacement sans heurt et une promesse intenable.

La revalidation côté client est la GARANTIE. Dès que votre application embarque notre SDK, l'appareil de chaque abonné renvoie son reçu en cours à la prochaine ouverture. L'identifiant vient d'Apple ou de Google eux-mêmes — à jour, correct et complet. Cela fonctionne pour toutes les boutiques, ne nécessite l'export de personne, et ne peut pas transporter un identifiant périmé. Chaque abonné qui ouvre votre application est migré correctement, par construction.

L'import est l'ACCÉLÉRATEUR. Il existe pour que vos abonnés ne se retrouvent pas bloqués avant leur prochaine ouverture de l'application, et pour que vos rapports du premier jour ne soient pas vides. Il copie ce que votre fournisseur précédent savait.

Pourquoi les deux : les abonnés ouvrent les applications à des rythmes très variables. Quelqu'un qui ne lance pas votre application pendant six semaines passerait, avec la seule revalidation, six semaines à paraître sans droits. L'import lui donne la continuité dès la première minute ; la revalidation la rend faisant autorité.

🚩 Si vous ne pouvez en garder qu'un, gardez la revalidation. Un import incapable de fournir un identifiant exploitable pour une boutique donnée (voir ci-dessous) n'est pas une migration pour ces abonnés — c'est un enregistrement qui expire en silence.

Ce que votre fournisseur précédent peut réellement vous donner

L'import a besoin de originalTransactionId, et ce que votre fournisseur transmet varie selon la boutique. Vérifiez le vôtre avant de bâtir un plan dessus :

Fournisseur Apple Google
RevenueCat ✅ L'export de données contient original_transaction_id. ⚠️ L'export fournit un Order ID, qui n'est pas le purchase token — un identifiant différent, sur lequel une notification de renouvellement ne correspondra pas. Leur API v2 est pire : elle n'expose que l'identifiant de transaction le plus récent, qui change à chaque renouvellement.
Adapty / Qonversion Vérifiez dans l'export la présence de l'original transaction ID d'Apple avant de planifier. Vérifiez si vous obtenez le purchase token ou une référence de commande/transaction. Confirmez-le, ne le supposez pas.
Superwall ⚠️ À confirmer avant de planifier. Ils exploitent leur propre couche d'abonnements, d'achats et de validation de reçus : l'identifiant existe donc chez eux — la question ouverte est de savoir si l'export vous remet l'original_transaction_id d'Apple ou seulement la référence d'abonnement propre à Superwall. ⚠️ Même question, même réponse : établissez par écrit si vous obtenez le purchase token ou une référence de commande/transaction, avant de bâtir un plan dessus.
Votre propre backend Ce que vous avez stocké à l'achat — en général la bonne valeur. Vous détenez très probablement déjà le purchase token, puisqu'il vous fallait le valider.
Stripe s.o. s.o. — nous lisons directement votre compte Stripe, aucun export nécessaire. Voir le §3b.

🚩 Ainsi, une migration depuis RevenueCat s'importe proprement pour Apple et ne peut pas importer les abonnés Google avec un identifiant exploitable. Ce n'est pas quelque chose que nous pouvons contourner par du code : l'identifiant dont nous avons besoin n'est pas présent dans les données auxquelles vous avez accès. Ces abonnés arrivent par la revalidation côté client — publiez le SDK, redirigez les notifications (§4), et chacun est migré correctement à sa prochaine ouverture de l'application. Importez le côté Apple pour la continuité, et laissez Google se réparer tout seul.


1. Ce qu'il vous faut

  • Votre export d'abonnés, contenant une ligne par droit d'accès actif (voir le format ci-dessous).
  • Une table de correspondance produits. Chaque identifiant de produit de votre export doit d'abord être associé à l'un de vos niveaux d'accès — Tableau de bord → votre application → produits. 🚩 L'import ne devinera pas quel niveau accorder. Un produit non associé est refusé et signalé, car deviner serait un moyen de contourner la protection même qui empêche un client de revendiquer un niveau qu'il n'a pas acheté.
  • Un accès administrateur pour l'organisation propriétaire de l'application. Les lecteurs (viewers) ne peuvent pas importer : cela accorde un accès payant en masse.

2. Le format

Un objet JSON par droit d'accès d'abonné :

{
  "userId": "user_12345",
  "email": "person@example.com",
  "store": "apple",
  "productId": "pro_monthly",
  "originalTransactionId": "1000000987654321",
  "expiresAt": "2027-01-01T00:00:00Z",
  "willRenew": true,
  "status": "active"
}
Champ Obligatoire Remarques
userId oui Votre propre identifiant d'utilisateur — le même que celui que votre application transmet à notre SDK.
store oui apple · google · amazon · roku · stripe
productId oui Doit exister dans votre table de correspondance produits, sinon la ligne est refusée.
originalTransactionId oui 🚩 Voir ci-dessous. L'original transaction ID d'Apple, le purchase token de Google, ou l'identifiant d'abonnement du fournisseur.
status oui active · trialing · grace_period · expired · lifetime · cancelled · refunded · billing_failed
expiresAt non ISO 8601. À omettre pour lifetime.
amazonUserId pour Amazon 🚩 L'identifiant utilisateur propre à Amazon, issu de leur SDK — et non l'identifiant utilisateur de votre application. Amazon retrouve un reçu par (cet identifiant, receiptId) ; sans lui, le droit d'accès ne pourrait jamais être revérifié. Les lignes Amazon qui en sont dépourvues sont refusées.
email, willRenew non

🚩 originalTransactionId est le champ qui décide si tout cela fonctionne

Ce n'est pas seulement une clé pour nous — c'est ainsi qu'un renouvellement retrouve l'abonné des mois plus tard. Lorsqu'Apple ou Google envoie une notification serveur pour un renouvellement, c'est sur cet identifiant que la correspondance se fait.

Une ligne importée sans lui produit le pire type de défaillance : tout paraît correct le premier jour, puis l'abonné perd silencieusement son accès à son prochain renouvellement, sans qu'aucune erreur ne soit signalée nulle part.

Nous refusons donc ces lignes plutôt que de les accepter. Importer sans lui n'est pas un raccourci, c'est une panne différée.

🚩 Tous les fournisseurs ne peuvent pas vous donner ce champ pour toutes les boutiques — voir le tableau ci-dessus. Là où votre export ne peut réellement pas le fournir (RevenueCat + Google est le cas connu), ne contournez pas le problème en inventant une valeur : n'importe quel substitut produit exactement la défaillance silencieuse de renouvellement décrite ici. Importez les boutiques pour lesquelles vous avez de vrais identifiants, et laissez la revalidation côté client se charger du reste.

3. Lancez-le — toujours une simulation d'abord

Cette section décrit la voie CSV / export, utilisée pour Apple, Google, Amazon et Roku. Pour Stripe, passez au §3b — nous lisons votre compte directement et il n'y a rien à construire.

Le plus simple est de passer par le tableau de bord : Subscriber Migration (Insights → Customers) → From an export. Collez l'export en CSV ou en JSON, ou chargez le fichier. La page vérifie d'abord les lignes et n'écrit rien tant que vous ne le demandez pas ; elle découpe les gros fichiers en lots pour vous ; et elle liste chaque ligne refusée par numéro de ligne, afin que vous puissiez les corriger dans le fichier que vous avez réellement. L'API ci-dessous fait exactement la même chose si vous préférez scripter.

🚩 Ces lignes sont une déclaration, pas une confirmation. Un export est ce que votre fournisseur précédent vous a remis ; les lignes importées arrivent donc non vérifiées et rejoignent la file de revérification (§6), où nous les confrontons ensuite à la vraie boutique. Les lignes lues directement depuis Stripe (§3b) sont différentes — elles arrivent déjà confirmées.

La simulation est le comportement par défaut. Vous devez demander explicitement à écrire quoi que ce soit.

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": [ … au maximum 1000 lignes … ] }'

Vous recevez un rapport :

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

Lisez ces trois nombres avant de valider :

  • missingTransactionIds — des lignes qui se renouvelleraient dans le vide. Corrigez l'export, ne continuez pas.
  • unmappedProducts — associez-les, puis relancez.
  • newUsers — combien d'abonnés cela va créer. Si le chiffre semble faux, votre champ userId n'est probablement pas le bon identifiant.

Lorsque le rapport est propre, ajoutez "dryRun": false et renvoyez-le.

Lots et reprise

Envoyez au maximum 1000 lignes par requête ; une grande migration se fait en plusieurs lots. L'import est idempotent — renvoyer le même lot met à jour au lieu de dupliquer, si bien qu'un script interrompu en cours de route peut simplement être relancé depuis le début. Une ligne incorrecte est signalée et ignorée ; elle ne fait jamais perdre les bonnes lignes qui l'entourent.

Ce qu'il refusera, et pourquoi

Code Signification
missing_transaction_id Voir ci-dessus. Le point important.
unmapped_product Associez d'abord le produit ; nous ne devinerons pas un niveau.
transaction_owned_by_another_user Cette transaction de boutique appartient déjà à un autre compte dans cette application. Une transaction, un propriétaire — généralement un compte fusionné ou des données erronées dans l'export.
invalid_store / invalid_status / invalid_expiry Valeur mal formée.

3b. Stripe — nous lisons directement votre compte

Stripe est la seule boutique capable de répondre à « qui sont tous mes abonnés ? ». Apple, Google, Amazon et Roku ne répondent qu'à « ce reçu est-il toujours valable ? », un reçu à la fois. Pour Stripe, il n'y a donc aucun export à demander ni aucun CSV à construire : Subscriber Migration dans le tableau de bord (Insights → Customers) parcourt votre compte et l'importe.

Avant de commencer, deux choses doivent être en place, et la page vous le signale si ce n'est pas le cas :

  1. Votre clé secrète Stripe, enregistrée dans Apps → store credentials. Il n'existe aucun repli au niveau de la plateforme et il n'en existera jamais — sans votre clé, l'import échoue en se fermant plutôt que de lire les données de quelqu'un d'autre.
  2. Vos produits associés à des niveaux. Associez soit le Price (price_…), soit le Product (prod_…) ; les deux fonctionnent. Les produits non associés sont refusés plutôt que devinés, ce qui est le comportement correct et non un défaut.

Indiquez-nous ensuite où se trouve l'identifiant utilisateur de votre application dans Stripe — métadonnées d'abonnement, métadonnées de client, ou l'identifiant client Stripe lui-même. 🚩 Il n'y a pas de valeur par défaut et nous ne devinerons pas. Stripe connaît le client sous la forme cus_… ; vous seul savez de quel utilisateur il s'agit, et se tromper n'échoue pas bruyamment — cela accorde l'accès payant d'un abonné au compte de quelqu'un d'autre.

Prévisualisez d'abord. L'aperçu ne modifie rien et vous montre exactement ce qui se produirait : combien d'abonnements ont été lus, combien seraient importés, et chacun de ceux qui seraient refusés avec la raison en langage clair. Lisez les refus avant d'importer — c'est la liste des choses à corriger, pas du bruit.

Les gros comptes sont parcourus automatiquement en plusieurs requêtes ; la page adapte son rythme pour rester dans la limite de débit et affiche sa progression.

Ce qu'il refuse, et pourquoi ce sont les bonnes réponses

Code Signification
never_paid L'abonnement est incomplete — il n'a jamais abouti à un premier paiement. L'importer accorderait un accès payant à quelqu'un qui n'a jamais payé.
access_expired Le statut accorde l'accès mais la période de facturation est déjà terminée. Ici l'accès est déterminé par le statut, donc l'importer accorderait un accès indéfini.
ambiguous_product Les articles d'un même abonnement correspondent à deux niveaux différents. Nous refusons plutôt que de choisir — retenir le plus élevé serait une montée en gamme arrivant par la voie de la migration, et retenir le premier serait arbitraire.
no_user_id Aucun identifiant utilisateur d'application là où vous nous avez dit de regarder. Voir ci-dessus : c'est celui qu'il est dangereux de deviner.
unmapped_product Associez le produit, puis relancez.

🚩 Les produits vus mais non associés sont signalés même lorsqu'ils n'ont rien bloqué. Ils ne bloquent rien aujourd'hui, mais dès que vous en associez un, tout abonnement qui le porte aux côtés d'un autre produit associé devient ambigu et commence à être refusé. Mieux vaut le savoir maintenant qu'à la deuxième exécution.

Pourquoi les lignes Stripe arrivent déjà vérifiées

Une ligne importée compte normalement comme une déclaration jusqu'à ce que nous la confirmions auprès de la boutique (§6). Les lignes Stripe sont différentes : elles ont été lues via l'API de Stripe elle-même en utilisant vos identifiants, elles arrivent donc déjà confirmées par la boutique. C'est une affirmation sur la provenance, pas sur Stripe — tout futur processeur que nous lirons directement se comportera de la même façon, et toute ligne fournie par un humain reste une déclaration, quelle que soit la boutique qu'elle nomme.

Vous préférez l'API ? La page est un client de POST /apps/:appId/import/stripe ; les mêmes options sont dryRun, userIdSource, userIdKey et startingAfter pour la pagination.

4. 🚩 La bascule — planifiez-la avant d'importer

L'import copie un état. Il ne redirige pas l'avenir. Tant que vous n'avez pas repointé les notifications serveur de vos boutiques vers SubSovereign, les renouvellements, annulations et remboursements continuent d'aller à votre ancien fournisseur.

La séquence recommandée :

  1. Importez (ce guide). Vos droits d'accès existent désormais dans les deux systèmes.
  2. Faites tourner en parallèle. Gardez l'ancien fournisseur actif et comparez. Rien n'a encore bougé.
  3. Publiez une version de l'application utilisant notre SDK. Les droits d'accès sont résolus chez nous et, comme vous avez importé les mêmes identifiants de transaction, les abonnés existants conservent leur accès sans aucune action de leur part.
  4. Repointez les notifications des boutiques vers nos points d'entrée webhook (Apple App Store Server Notifications, Google Real-Time Developer Notifications). C'est là que se fait réellement la bascule.
  5. Relancez l'import pour tout ce qui a changé pendant la fenêtre. Il est idempotent — c'est précisément à cela que sert cette étape.
  6. Retirez l'ancien fournisseur une fois qu'un cycle complet de renouvellement s'est déroulé proprement.

5. Ce qu'est un droit d'accès importé — et ce qu'il n'est pas

Un droit d'accès importé est un octroi d'accès payant fondé sur la parole de votre fournisseur précédent. Nous ne l'avons pas encore vérifié nous-mêmes auprès d'Apple ou de Google.

Nous le consignons honnêtement plutôt que de le masquer : les lignes importées portent imported_at et import_source, et leur store_verified_at reste vide jusqu'à ce que le droit d'accès ait été confirmé auprès de la vraie boutique. Ainsi, « lesquels avons-nous accordés sur parole ? » est une question qui a une réponse exacte, à tout moment.

🚩 Cela compte si un chiffre paraît un jour erroné. Les droits d'accès importés et vérifiés ne sont pas la même catégorie de fait, et un audit qui les traiterait comme identiques serait trompeur.

6. Revérifier un import auprès de la boutique

Une fois l'import effectué, vous pouvez nous demander de contrôler chaque droit d'accès auprès de la vraie boutique :

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

Chaque ligne revient avec l'un de trois résultats :

Résultat Signification
verified La boutique l'a confirmé. Nous adoptons la date d'expiration et l'indicateur de renouvellement de la boutique plutôt que ceux importés — elle fait autorité.
mismatch La boutique a contredit l'import : aucun abonnement actif, ou un abonnement actif pour un produit différent.
unverifiable Nous n'avons pas pu obtenir de réponse exploitable — une limite de débit, un problème d'identifiants, une erreur passagère. Ce n'est pas une contradiction.

🚩 Un mismatch ne révoque jamais l'accès. Rien ne change dans le droit d'accès ; seules les colonnes de vérification sont écrites, et l'écart est remonté pour qu'une personne en juge. « La boutique ne l'a pas confirmé » et « cet abonné n'a pas de droits » sont deux affirmations différentes, et une API de boutique peut renvoyer la première pour des raisons qui n'ont rien à voir avec votre client. Nous ne sommes pas prêts à résilier un abonné payant sur cette base — surtout un abonné qui vient de migrer chez nous. Voir DECISIONS.md #70.

Exécutez-la par lots (50 par défaut, 200 au maximum) — les API des boutiques sont limitées en débit et ces appels utilisent vos identifiants de boutique. GET /apps/YOUR_APP_ID/import/status donne l'état courant :

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

Il n'existe pas encore de planification automatique pour cela, volontairement — nous préférons ne pas solliciter vos identifiants de boutique sans surveillance, sur une minuterie que vous n'avez pas demandée. Lancez-la quand cela vous convient ; elle est idempotente et reprenable.