SubSovereign
All guides

Uw abonnees migreren naar SubSovereign

Komt u van RevenueCat, Adapty, Qonversion of uw eigen backend? Uw bestaande abonnees gaan mee. Deze gids behandelt de import zelf en — net zo belangrijk — de omschakeling, want daar zit het werkelijke risico.

Lees dit eerst. De import is veilig en herhaalbaar. De omschakeling is het moment dat planning vraagt, want zolang u uw storemeldingen niet naar ons omleidt, worden verlengingen nog steeds bij uw oude aanbieder afgeleverd. Plan §4 voordat u §2 uitvoert.


De twee helften van een migratie — lees dit voordat u er een plant

Elke migratie kent twee mechanismen, en het onderscheid ertussen is het verschil tussen een soepele verhuizing en een belofte die u niet kunt nakomen.

Hervalidatie aan de clientzijde is de GARANTIE. Zodra uw app onze SDK meelevert, stuurt het apparaat van elke abonnee bij de eerstvolgende opening zijn actuele bon opnieuw. De identificatie komt van Apple of Google zelf: actueel, correct en volledig. Het werkt voor elke store, vereist van niemand een export en kan geen verouderde identificatie meedragen. Elke abonnee die uw app opent, wordt per constructie correct gemigreerd.

De import is de VERSNELLER. Die bestaat zodat uw abonnees niet buitengesloten raken voordat ze de app weer openen, en zodat uw rapportage op dag één niet leeg is. Hij kopieert wat uw vorige aanbieder wist.

Waarom allebei: abonnees openen apps in sterk uiteenlopende tempo's. Wie uw app zes weken niet start, zou met alleen hervalidatie zes weken lang onrechthebbend lijken. De import geeft die persoon vanaf de eerste minuut continuïteit; de hervalidatie maakt het gezaghebbend.

🚩 Kunt u er maar één houden, houd dan de hervalidatie. Een import die voor een bepaalde store geen bruikbare identificatie kan leveren (zie hieronder) is voor die abonnees geen migratie, maar een registratie die stilletjes verloopt.

Wat uw vorige aanbieder u werkelijk kan geven

De import heeft originalTransactionId nodig, en wat uw aanbieder aanlevert verschilt per store. Controleer de uwe voordat u erop plant:

Aanbieder Apple Google
RevenueCat ✅ De data-export bevat original_transaction_id. ⚠️ De export geeft een Order ID, en dat is niet het purchase token — een andere identificatie, waarop een verlengingsmelding niet matcht. Hun v2-API is slechter: die toont alleen de laatste transactie-id, en die verandert bij elke verlenging.
Adapty / Qonversion Controleer de export op Apple's original transaction ID voordat u plant. Controleer of u het purchase token krijgt of een order-/transactieverwijzing. Bevestig het, ga er niet van uit.
Superwall ⚠️ Bevestig dit vóór u plant. Zij draaien een eigen laag voor rechten, aankopen en bonvalidatie, dus de identificatie bestaat aan hun kant — de open vraag is of de export u Apple's original_transaction_id geeft of alleen Superwalls eigen abonnementsverwijzing. ⚠️ Dezelfde vraag, hetzelfde antwoord: stel schriftelijk vast of u het purchase token krijgt of een order-/transactieverwijzing, voordat u erop plant.
Uw eigen backend Wat u bij aankoop hebt opgeslagen — meestal het juiste. U hebt het purchase token hoogstwaarschijnlijk al, omdat u het nodig had om te valideren.
Stripe n.v.t. n.v.t. — wij lezen uw Stripe-account rechtstreeks, geen export nodig. Zie §3b.

🚩 Een overstap vanaf RevenueCat importeert dus netjes voor Apple en kan Google-abonnees niet met een bruikbare identificatie importeren. Dat kunnen wij niet wegprogrammeren: de identificatie die wij nodig hebben, zit simpelweg niet in de gegevens die u kunt verkrijgen. Die abonnees komen over via hervalidatie aan de clientzijde — lever de SDK uit, leid de meldingen om (§4), en elk van hen wordt bij de eerstvolgende opening van de app correct gemigreerd. Importeer de Apple-kant voor de continuïteit en laat Google zichzelf herstellen.


1. Wat u nodig hebt

  • Uw abonnee-export, met één regel per actief toegangsrecht (zie het formaat hieronder).
  • Een productkoppeling. Elke product-ID in uw export moet eerst aan een van uw toegangsniveaus zijn gekoppeld — Dashboard → uw app → producten. 🚩 De import gaat niet raden welk niveau hij moet toekennen. Een niet-gekoppeld product wordt geweigerd en gemeld, want raden zou een omweg zijn om precies die bescherming te passeren die voorkomt dat een klant een niveau claimt dat hij niet heeft gekocht.
  • Een beheerderslogin voor de organisatie die eigenaar is van de app. Viewers kunnen niet importeren: het kent betaalde toegang in bulk toe.

2. Het formaat

Eén JSON-object per toegangsrecht van een abonnee:

{
  "userId": "user_12345",
  "email": "person@example.com",
  "store": "apple",
  "productId": "pro_monthly",
  "originalTransactionId": "1000000987654321",
  "expiresAt": "2027-01-01T00:00:00Z",
  "willRenew": true,
  "status": "active"
}
Veld Verplicht Toelichting
userId ja Uw eigen gebruikersidentificatie — dezelfde die uw app aan onze SDK doorgeeft.
store ja apple · google · amazon · roku · stripe
productId ja Moet in uw productkoppeling voorkomen, anders wordt de regel geweigerd.
originalTransactionId ja 🚩 Zie hieronder. Apple's original transaction ID, Google's purchase token, of het abonnements-ID van de aanbieder.
status ja active · trialing · grace_period · expired · lifetime · cancelled · refunded · billing_failed
expiresAt nee ISO 8601. Weglaten bij lifetime.
amazonUserId voor Amazon 🚩 Amazons eigen gebruikersidentificatie uit hun SDK — niet de gebruikersidentificatie van uw app. Amazon zoekt een bon op via (die id, receiptId), dus zonder die identificatie zou het toegangsrecht nooit opnieuw geverifieerd kunnen worden. Amazon-regels zonder deze waarde worden geweigerd.
email, willRenew nee

🚩 originalTransactionId is het veld dat bepaalt of dit werkt

Het is niet alleen een sleutel voor ons — het is de manier waarop een verlenging de abonnee maanden later terugvindt. Wanneer Apple of Google een servermelding voor een verlenging stuurt, wordt er juist op die identificatie gematcht.

Een regel die zonder dat veld is geïmporteerd, veroorzaakt de ergste soort storing: op dag één lijkt alles correct, en vervolgens verliest de abonnee bij zijn volgende verlenging stilletjes de toegang, zonder dat er ergens een fout wordt gemeld.

Daarom weigeren wij die regels in plaats van ze te accepteren. Zonder dat veld importeren is geen kortere weg, maar een uitgestelde storing.

🚩 Niet elke aanbieder kan u dit veld voor elke store geven — zie de tabel hierboven. Waar uw export het werkelijk niet kan leveren (RevenueCat + Google is het bekende geval), omzeil dat dan niet door een waarde te verzinnen: elke tijdelijke waarde veroorzaakt precies de hier beschreven stille verlengingsstoring. Importeer de stores waarvoor u echte identificaties hebt en laat hervalidatie aan de clientzijde de rest dragen.

3. Uitvoeren — altijd eerst een proefrun

*Deze paragraaf beschrijft de CSV-/exportroute, gebruikt voor Apple, Google, Amazon en Roku. Voor Stripe gaat u door naar §3b — wij lezen uw account rechtstreeks en er valt niets te bouwen.*

De eenvoudigste weg is via het dashboard: Subscriber Migration (Insights → Customers) → From an export. Plak de export als CSV of JSON, of laad het bestand. De pagina controleert eerst de regels en schrijft niets tot u erom vraagt; ze verdeelt grote bestanden voor u in batches; en ze somt elke geweigerde regel met regelnummer op, zodat u ze kunt corrigeren in het bestand dat u werkelijk hebt. De API hieronder doet precies hetzelfde, als u liever scriptt.

🚩 Deze regels zijn een bewering, geen bevestiging. Een export is wat uw vorige aanbieder u heeft overhandigd; geïmporteerde regels komen dus ongeverifieerd binnen en sluiten aan in de hercontrolewachtrij (§6), waar wij ze daarna tegen de echte store toetsen. Regels die rechtstreeks uit Stripe worden gelezen (§3b) zijn anders: die komen al bevestigd binnen.

De proefrun is de standaard. U moet expliciet vragen om iets weg te schrijven.

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": [ … tot 1000 regels … ] }'

U krijgt een rapport terug:

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

Lees deze drie getallen voordat u vastlegt:

  • missingTransactionIds — regels die zich in het niets zouden verlengen. Corrigeer de export, ga niet verder.
  • unmappedProducts — koppel ze en voer opnieuw uit.
  • newUsers — hoeveel abonnees dit gaat aanmaken. Lijkt dat getal verkeerd, dan is uw veld userId waarschijnlijk niet de juiste identificatie.

Is het rapport schoon, voeg dan "dryRun": false toe en stuur het opnieuw.

Batches en hervatten

Stuur maximaal 1000 regels per verzoek; een grote migratie bestaat uit veel batches. De import is idempotent — dezelfde batch opnieuw sturen werkt bij in plaats van te dupliceren, zodat een script dat halverwege afbreekt eenvoudig opnieuw vanaf het begin kan lopen. Een foutieve regel wordt gemeld en overgeslagen; hij gooit nooit de goede regels ernaast weg.

Wat er wordt geweigerd, en waarom

Code Betekenis
missing_transaction_id Zie hierboven. De belangrijke.
unmapped_product Koppel eerst het product; wij raden geen niveau.
transaction_owned_by_another_user Die storetransactie hoort in deze app al bij een ander account. Eén transactie, één eigenaar — meestal een samengevoegd account of foutieve gegevens in de export.
invalid_store / invalid_status / invalid_expiry Onjuist opgemaakte waarde.

3b. Stripe — wij lezen uw account rechtstreeks

Stripe is de enige store die "wie zijn al mijn abonnees?" kan beantwoorden. Apple, Google, Amazon en Roku beantwoorden alleen "is deze bon nog geldig?", één bon tegelijk. Voor Stripe is er dus geen export aan te vragen en geen CSV te bouwen: Subscriber Migration in het dashboard (Insights → Customers) doorloopt uw account en importeert het.

Voordat u begint moeten twee dingen op orde zijn, en de pagina zegt het als dat niet zo is:

  1. Uw geheime Stripe-sleutel, opgeslagen onder Apps → store credentials. Er is geen terugval op platformniveau en die komt er ook nooit — zonder uw sleutel faalt de import door zich te sluiten, in plaats van andermans gegevens te lezen.
  2. Uw producten gekoppeld aan niveaus. Koppel de Price (price_…) of het Product (prod_…); beide werken. Niet-gekoppelde producten worden geweigerd in plaats van geraden, en dat is correct gedrag, geen fout.

Vertel ons vervolgens waar de gebruikersidentificatie van uw app in Stripe staat — abonnementsmetadata, klantmetadata, of de Stripe-klantidentificatie zelf. 🚩 Er is geen standaard en wij gaan niet raden. Stripe kent de klant als cus_…; alleen u weet welke van uw gebruikers dat is, en ernaast zitten faalt niet luidruchtig — het geeft de betaalde toegang van de ene abonnee aan het account van iemand anders.

Bekijk eerst het voorbeeld. Het voorbeeld verandert niets en toont u precies wat er zou gebeuren: hoeveel abonnementen zijn gelezen, hoeveel er zouden worden geïmporteerd, en elk abonnement dat zou worden geweigerd met de reden in gewone taal. Lees de weigeringen voordat u importeert: dat is de lijst met dingen die u moet oplossen, geen ruis.

Grote accounts worden automatisch over meerdere verzoeken doorlopen; de pagina doseert zichzelf om binnen de snelheidslimiet te blijven en toont de voortgang.

Wat er wordt geweigerd, en waarom dat de juiste antwoorden zijn

Code Betekenis
never_paid Het abonnement staat op incomplete — er is nooit een eerste betaling voltooid. Importeren zou betaalde toegang geven aan iemand die nooit heeft betaald.
access_expired De status geeft toegang, maar de factureringsperiode is al afgelopen. Toegang wordt hier door de status bepaald, dus importeren zou onbeperkt toegang geven.
ambiguous_product Onderdelen van één abonnement wijzen naar twee verschillende niveaus. Wij weigeren in plaats van te kiezen — het hoogste nemen zou een niveauverhoging zijn die via de migratieroute binnenkomt, en het eerste nemen zou willekeurig zijn.
no_user_id Geen app-gebruikersidentificatie op de plek waar u ons liet kijken. Zie hierboven: dit is de gevaarlijke om te raden.
unmapped_product Koppel het product en voer opnieuw uit.

🚩 Producten die wel zijn gezien maar niet gekoppeld, worden gemeld ook al hebben ze niets geblokkeerd. Vandaag blokkeren ze niets, maar zodra u er één koppelt, wordt elk abonnement dat het naast een ander gekoppeld product draagt dubbelzinnig en wordt het voortaan geweigerd. Beter nu weten dan bij de tweede run.

Waarom Stripe-regels al geverifieerd binnenkomen

Een geïmporteerde regel telt normaal als een bewering totdat wij hem bij de store bevestigen (§6). Stripe-regels zijn anders: ze zijn via Stripes eigen API gelezen met uw inloggegevens, dus komen ze al store-bevestigd binnen. Dat is een uitspraak over de herkomst, niet over Stripe — elke toekomstige betaalverwerker die wij rechtstreeks uitlezen zal zich net zo gedragen, en elke door een mens aangeleverde regel blijft een bewering, ongeacht welke store hij noemt.

Liever de API? De pagina is een client voor POST /apps/:appId/import/stripe; dezelfde opties zijn dryRun, userIdSource, userIdKey en startingAfter voor paginering.

4. 🚩 De omschakeling — plan die voordat u importeert

De import kopieert een toestand. Hij verlegt niet de toekomst. Zolang u de servermeldingen van uw stores niet naar SubSovereign omleidt, gaan verlengingen, opzeggingen en terugbetalingen nog steeds naar uw oude aanbieder.

De aanbevolen volgorde:

  1. Importeer (deze gids). Uw toegangsrechten bestaan nu in beide systemen.
  2. Draai parallel. Houd de oude aanbieder actief en vergelijk. Er is nog niets verschoven.
  3. Lever een app-build uit met onze SDK. Toegangsrechten worden door ons bepaald en omdat u dezelfde transactie-identificaties hebt geïmporteerd, behouden bestaande abonnees hun toegang zonder iets te doen.
  4. Leid de storemeldingen om naar onze webhook-endpoints (Apple App Store Server Notifications, Google Real-Time Developer Notifications). Dit is de eigenlijke omschakeling.
  5. Voer de import opnieuw uit voor alles wat tijdens het venster is gewijzigd. Hij is idempotent — daar is deze stap precies voor.
  6. Zet de oude aanbieder pas stop nadat een volledige verlengingscyclus schoon is verlopen.

5. Wat een geïmporteerd toegangsrecht is — en niet is

Een geïmporteerd toegangsrecht is een toekenning van betaalde toegang op gezag van uw vorige aanbieder. Wij hebben het nog niet zelf bij Apple of Google gecontroleerd.

Wij leggen dat eerlijk vast in plaats van het te verbergen: geïmporteerde regels dragen imported_at en import_source, en hun store_verified_at blijft leeg totdat het recht bij de echte store is bevestigd. Zo is "welke hiervan hebben wij op vertrouwen toegekend?" op elk moment een vraag met een exact antwoord.

🚩 Dit is van belang als een cijfer ooit verkeerd lijkt. Geïmporteerde en geverifieerde rechten zijn niet dezelfde soort feit, en een audit die ze als identiek behandelt, zou misleidend zijn.

6. Een import opnieuw verifiëren bij de store

Na de import kunt u ons elk toegangsrecht bij de echte store laten controleren:

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

Elke regel komt terug met een van drie uitkomsten:

Uitkomst Betekenis
verified De store heeft het bevestigd. Wij nemen de vervaldatum en verlengingsvlag van de store over in plaats van de geïmporteerde — die is gezaghebbend.
mismatch De store sprak de import tegen: geen actief abonnement, of een actief abonnement voor een ander product.
unverifiable Wij konden geen bruikbaar antwoord krijgen — een snelheidslimiet, een probleem met inloggegevens, een tijdelijke fout. Dit is geen tegenspraak.

🚩 Een mismatch trekt nooit toegang in. Aan het toegangsrecht verandert niets; alleen de verificatiekolommen worden geschreven, en de afwijking wordt aan een mens voorgelegd. "De store heeft het niet bevestigd" en "deze abonnee heeft geen recht" zijn verschillende uitspraken, en een store-API kan de eerste teruggeven om redenen die niets met uw klant te maken hebben. Op die basis zijn wij niet bereid een betalende abonnee op te zeggen — zeker niet iemand die net naar ons is gemigreerd. Zie DECISIONS.md #70.

Voer het in batches uit (standaard 50, maximaal 200) — store-API's kennen snelheidslimieten en deze aanroepen gebruiken uw store-inloggegevens. GET /apps/YOUR_APP_ID/import/status toont het actuele beeld:

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

Er is hiervoor bewust nog geen automatische planning — wij benaderen uw store-inloggegevens liever niet onbeheerd, op een timer waar u niet om hebt gevraagd. Voer het uit wanneer het u uitkomt; het is idempotent en hervatbaar.