SubSovereign
All guides

Migracja subskrybentów do SubSovereign

Przechodzisz z RevenueCat, Adapty, Qonversion albo z własnego backendu? Twoi dotychczasowi subskrybenci idą z tobą. Ten przewodnik opisuje sam import oraz — co równie ważne — przełączenie, czyli tę część, która naprawdę niesie ryzyko.

Przeczytaj to najpierw. Import jest bezpieczny i powtarzalny. To przełączenie wymaga planowania, ponieważ dopóki nie przekierujesz powiadomień ze sklepów do nas, odnowienia nadal trafiają do poprzedniego dostawcy. Zaplanuj §4, zanim wykonasz §2.


Dwie połowy migracji — przeczytaj, zanim ją zaplanujesz

Każda migracja opiera się na dwóch mechanizmach, a ich rozróżnienie decyduje o tym, czy przeprowadzka będzie gładka, czy zamieni się w obietnicę nie do dotrzymania.

Ponowna walidacja po stronie klienta to GWARANCJA. Gdy tylko twoja aplikacja zacznie dostarczać nasz SDK, urządzenie każdego subskrybenta przy najbliższym uruchomieniu ponownie prześle swój aktualny paragon. Identyfikator pochodzi od samego Apple lub Google — aktualny, poprawny i kompletny. Działa dla każdego sklepu, nie wymaga od nikogo eksportu i nie może przenieść nieaktualnego identyfikatora. Każdy subskrybent, który otworzy twoją aplikację, zostaje zmigrowany poprawnie — z samej konstrukcji.

Import to PRZYSPIESZACZ. Istnieje po to, by twoi subskrybenci nie zostali odcięci zanim ponownie otworzą aplikację, i żeby raporty z pierwszego dnia nie były puste. Kopiuje to, co wiedział twój poprzedni dostawca.

Dlaczego oba: subskrybenci otwierają aplikacje w bardzo różnym tempie. Ktoś, kto nie uruchomi twojej aplikacji przez sześć tygodni, przy samej ponownej walidacji przez sześć tygodni wyglądałby na osobę bez uprawnień. Import daje mu ciągłość od pierwszej minuty; ponowna walidacja czyni ją wiążącą.

🚩 Jeśli możesz zachować tylko jedno, zachowaj ponowną walidację. Import, który dla danego sklepu nie potrafi dostarczyć dopasowywalnego identyfikatora (patrz niżej), nie jest migracją dla tych subskrybentów — to zapis, który po cichu wygasa.

Co twój poprzedni dostawca naprawdę może ci przekazać

Import potrzebuje originalTransactionId, a to, co przekazuje dostawca, różni się w zależności od sklepu. Sprawdź swojego, zanim zbudujesz na tym plan:

Dostawca Apple Google
RevenueCat ✅ Eksport danych zawiera original_transaction_id. ⚠️ Eksport daje Order ID, który nie jest purchase tokenem — to inny identyfikator, do którego powiadomienie o odnowieniu nie pasuje. Ich API v2 jest gorsze: udostępnia wyłącznie najnowszy identyfikator transakcji, który zmienia się przy każdym odnowieniu.
Adapty / Qonversion Sprawdź w eksporcie obecność original transaction ID od Apple, zanim zaczniesz planować. Sprawdź, czy otrzymujesz purchase token, czy odniesienie do zamówienia/transakcji. Potwierdź to, nie zakładaj.
Superwall ⚠️ Potwierdź przed planowaniem. Prowadzą własną warstwę uprawnień, zakupów i weryfikacji paragonów, więc identyfikator istnieje po ich stronie — otwarte pytanie brzmi, czy eksport przekaże ci original_transaction_id od Apple, czy tylko własne odniesienie subskrypcji Superwall. ⚠️ To samo pytanie, ta sama odpowiedź: ustal na piśmie, czy otrzymujesz purchase token, czy odniesienie do zamówienia/transakcji, zanim na tym oprzesz plan.
Twój własny backend To, co zapisałeś przy zakupie — zwykle właściwa wartość. Najprawdopodobniej masz już purchase token, bo był ci potrzebny do walidacji.
Stripe nie dotyczy nie dotyczy — czytamy twoje konto Stripe bezpośrednio, eksport nie jest potrzebny. Zobacz §3b.

🚩 Przejście z RevenueCat importuje się więc czysto dla Apple i nie pozwala zaimportować subskrybentów Google z dopasowywalnym identyfikatorem. Nie da się tego obejść kodem: identyfikatora, którego potrzebujemy, po prostu nie ma w danych, do których masz dostęp. Ci subskrybenci przechodzą przez ponowną walidację po stronie klienta — wypuść SDK, przekieruj powiadomienia (§4), a każdy z nich zostanie poprawnie zmigrowany przy następnym otwarciu aplikacji. Zaimportuj stronę Apple dla ciągłości i pozwól Google naprawić się samo.


1. Czego potrzebujesz

  • Eksportu subskrybentów, z jednym wierszem na każde aktywne uprawnienie (format poniżej).
  • Mapowania produktów. Każdy identyfikator produktu z eksportu musi być najpierw powiązany z jednym z twoich poziomów dostępu — Panel → twoja aplikacja → produkty. 🚩 Import nie będzie zgadywał, jaki poziom przyznać. Niepowiązany produkt zostaje odrzucony i zgłoszony, ponieważ zgadywanie byłoby obejściem dokładnie tego zabezpieczenia, które nie pozwala klientowi rościć sobie prawa do poziomu, którego nie kupił.
  • Konta administratora organizacji będącej właścicielem aplikacji. Przeglądający (viewers) nie mogą importować: operacja przyznaje płatny dostęp masowo.

2. Format

Jeden obiekt JSON na każde uprawnienie subskrybenta:

{
  "userId": "user_12345",
  "email": "person@example.com",
  "store": "apple",
  "productId": "pro_monthly",
  "originalTransactionId": "1000000987654321",
  "expiresAt": "2027-01-01T00:00:00Z",
  "willRenew": true,
  "status": "active"
}
Pole Wymagane Uwagi
userId tak Twój własny identyfikator użytkownika — ten sam, który aplikacja przekazuje naszemu SDK.
store tak apple · google · amazon · roku · stripe
productId tak Musi istnieć w twoim mapowaniu produktów, inaczej wiersz zostaje odrzucony.
originalTransactionId tak 🚩 Patrz niżej. Original transaction ID od Apple, purchase token od Google albo identyfikator subskrypcji u dostawcy.
status tak active · trialing · grace_period · expired · lifetime · cancelled · refunded · billing_failed
expiresAt nie ISO 8601. Pomiń dla lifetime.
amazonUserId dla Amazona 🚩 Własny identyfikator użytkownika Amazona, z ich SDK — nie identyfikator użytkownika twojej aplikacji. Amazon wyszukuje paragon po parze (ten identyfikator, receiptId), więc bez niego uprawnienia nigdy nie dałoby się zweryfikować ponownie. Wiersze Amazona bez tej wartości są odrzucane.
email, willRenew nie

🚩 originalTransactionId to pole, które decyduje, czy to zadziała

To nie jest tylko klucz dla nas — to sposób, w jaki odnowienie odnajduje subskrybenta wiele miesięcy później. Gdy Apple lub Google wysyła powiadomienie serwerowe o odnowieniu, dopasowanie następuje właśnie po tym identyfikatorze.

Wiersz zaimportowany bez niego powoduje najgorszy rodzaj awarii: pierwszego dnia wszystko wygląda poprawnie, a potem subskrybent po cichu traci dostęp przy kolejnym odnowieniu, bez żadnego błędu zgłoszonego gdziekolwiek.

Dlatego odrzucamy takie wiersze, zamiast je przyjmować. Import bez tego pola nie jest skrótem, tylko odroczoną awarią.

🚩 Nie każdy dostawca poda ci to pole dla każdego sklepu — patrz tabela powyżej. Tam, gdzie twój eksport naprawdę nie jest w stanie go dostarczyć (RevenueCat + Google to znany przypadek), nie obchodź tego, wymyślając wartość: każdy wypełniacz powoduje dokładnie tę cichą awarię odnowienia, którą opisano wyżej. Zaimportuj te sklepy, dla których masz prawdziwe identyfikatory, a resztę zostaw ponownej walidacji po stronie klienta.

3. Uruchomienie — zawsze najpierw próba

Ta sekcja opisuje ścieżkę CSV / eksport, używaną dla Apple, Google, Amazona i Roku. Dla Stripe przejdź do §3b — czytamy twoje konto bezpośrednio i nie ma tu nic do budowania.

Najprościej zrobić to w panelu: Subscriber Migration (Insights → Customers) → From an export. Wklej eksport jako CSV lub JSON albo wczytaj plik. Strona najpierw sprawdza wiersze i nic nie zapisuje, dopóki o to nie poprosisz; sama dzieli duże pliki na paczki; i wypisuje każdy odrzucony wiersz wraz z numerem wiersza, żebyś mógł poprawić go w pliku, który faktycznie masz. API poniżej robi dokładnie to samo, jeśli wolisz skrypt.

🚩 Te wiersze są deklaracją, nie potwierdzeniem. Eksport to jest to, co przekazał ci poprzedni dostawca, więc zaimportowane wiersze trafiają niezweryfikowane i dołączają do kolejki ponownego sprawdzenia (§6), gdzie później konfrontujemy je z prawdziwym sklepem. Wiersze czytane bezpośrednio ze Stripe (§3b) są inne — przychodzą już potwierdzone.

Próba jest ustawieniem domyślnym. Musisz wyraźnie poprosić, żeby cokolwiek zostało zapisane.

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

W odpowiedzi dostajesz raport:

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

Przeczytaj te trzy liczby, zanim zatwierdzisz:

  • missingTransactionIds — wiersze, które odnowiłyby się w próżnię. Popraw eksport, nie kontynuuj.
  • unmappedProducts — powiąż je i uruchom ponownie.
  • newUsers — ilu subskrybentów zostanie utworzonych. Jeśli liczba wygląda źle, twoje pole userId prawdopodobnie nie jest właściwym identyfikatorem.

Gdy raport jest czysty, dodaj "dryRun": false i wyślij go ponownie.

Paczki i wznawianie

Wysyłaj najwyżej 1000 wierszy na żądanie; duża migracja to wiele paczek. Import jest idempotentny — ponowne wysłanie tej samej paczki aktualizuje zamiast duplikować, więc skrypt, który padnie w połowie, można po prostu uruchomić od początku. Błędny wiersz zostaje zgłoszony i pominięty; nigdy nie odrzuca dobrych wierszy obok siebie.

Co zostanie odrzucone i dlaczego

Kod Znaczenie
missing_transaction_id Patrz wyżej. Ten najważniejszy.
unmapped_product Najpierw powiąż produkt; nie będziemy zgadywać poziomu.
transaction_owned_by_another_user Ta transakcja sklepowa należy już do innego konta w tej aplikacji. Jedna transakcja, jeden właściciel — zwykle scalone konto albo błędne dane w eksporcie.
invalid_store / invalid_status / invalid_expiry Nieprawidłowo sformułowana wartość.

3b. Stripe — czytamy twoje konto bezpośrednio

Stripe to jedyny sklep, który potrafi odpowiedzieć na pytanie „kim są wszyscy moi subskrybenci?". Apple, Google, Amazon i Roku odpowiadają wyłącznie na „czy ten paragon jest wciąż ważny?", po jednym paragonie naraz. Dla Stripe nie ma więc żadnego eksportu do zamówienia ani CSV do zbudowania: Subscriber Migration w panelu (Insights → Customers) przechodzi twoje konto i je importuje.

Zanim zaczniesz, dwie rzeczy muszą być na miejscu, a strona powie ci, jeśli ich nie ma:

  1. Twój tajny klucz Stripe, zapisany w Apps → store credentials. Nie ma żadnego zapasowego klucza na poziomie platformy i nigdy nie będzie — bez twojego klucza import kończy się zamknięciem, zamiast czytać cudze dane.
  2. Twoje produkty powiązane z poziomami. Powiąż albo Price (price_…), albo Product (prod_…); działa jedno i drugie. Niepowiązane produkty są odrzucane, a nie zgadywane — to zachowanie poprawne, a nie błąd.

Następnie powiedz nam, gdzie w Stripe znajduje się identyfikator użytkownika twojej aplikacji — w metadanych subskrypcji, metadanych klienta albo w samym identyfikatorze klienta Stripe. 🚩 Nie ma wartości domyślnej i nie będziemy zgadywać. Stripe zna klienta jako cus_…; tylko ty wiesz, który z twoich użytkowników to jest, a pomyłka nie kończy się głośnym błędem — przyznaje płatny dostęp jednego subskrybenta kontu zupełnie innej osoby.

Najpierw podgląd. Podgląd niczego nie zmienia i pokazuje dokładnie, co by się stało: ile subskrypcji odczytano, ile zostałoby zaimportowanych i każdą, która zostałaby odrzucona — wraz z powodem wyrażonym zwykłym językiem. Przeczytaj odrzucenia przed importem: to lista rzeczy do poprawienia, a nie szum.

Duże konta są przechodzone automatycznie w kilku żądaniach; strona sama reguluje tempo, by mieścić się w limicie zapytań, i pokazuje postęp.

Co odrzuca i dlaczego to są właściwe odpowiedzi

Kod Znaczenie
never_paid Subskrypcja ma status incomplete — nigdy nie doszło do pierwszej płatności. Zaimportowanie jej przyznałoby płatny dostęp komuś, kto nigdy nie zapłacił.
access_expired Status przyznaje dostęp, ale okres rozliczeniowy już się zakończył. Dostęp jest tu rozstrzygany przez status, więc import przyznałby dostęp bezterminowo.
ambiguous_product Pozycje jednej subskrypcji wskazują na dwa różne poziomy. Odrzucamy, zamiast wybierać — wzięcie wyższego byłoby podniesieniem poziomu wchodzącym tylnymi drzwiami migracji, a wzięcie pierwszego byłoby arbitralne.
no_user_id Brak identyfikatora użytkownika aplikacji tam, gdzie kazałeś nam szukać. Patrz wyżej: to ten, którego zgadywanie jest niebezpieczne.
unmapped_product Powiąż produkt i uruchom ponownie.

🚩 Produkty zauważone, ale niepowiązane są zgłaszane nawet wtedy, gdy niczego nie zablokowały. Dziś niczego nie blokują, ale w chwili, gdy powiążesz jeden z nich, każda subskrypcja niosąca go obok innego powiązanego produktu staje się niejednoznaczna i zaczyna być odrzucana. Lepiej dowiedzieć się teraz niż przy drugim przebiegu.

Dlaczego wiersze ze Stripe przychodzą już zweryfikowane

Zaimportowany wiersz normalnie liczy się jako deklaracja, dopóki nie potwierdzimy go w sklepie (§6). Wiersze ze Stripe są inne: odczytano je przez własne API Stripe przy użyciu twoich danych uwierzytelniających, więc trafiają już potwierdzone przez sklep. To stwierdzenie o pochodzeniu, nie o Stripe — każdy przyszły operator płatności, którego będziemy czytać bezpośrednio, zachowa się tak samo, a każdy wiersz dostarczony przez człowieka pozostaje deklaracją, niezależnie od tego, jaki sklep wskazuje.

Wolisz API? Strona jest klientem POST /apps/:appId/import/stripe; te same opcje to dryRun, userIdSource, userIdKey oraz startingAfter do stronicowania.

4. 🚩 Przełączenie — zaplanuj je, zanim zaimportujesz

Import kopiuje stan. Nie przekierowuje przyszłości. Dopóki nie przestawisz powiadomień serwerowych ze swoich sklepów na SubSovereign, odnowienia, anulowania i zwroty nadal trafiają do poprzedniego dostawcy.

Zalecana kolejność:

  1. Zaimportuj (ten przewodnik). Twoje uprawnienia istnieją teraz w obu systemach.
  2. Działaj równolegle. Zostaw poprzedniego dostawcę aktywnego i porównuj. Nic się jeszcze nie przeniosło.
  3. Wypuść wersję aplikacji z naszym SDK. Uprawnienia rozstrzygamy my, a ponieważ zaimportowałeś te same identyfikatory transakcji, dotychczasowi subskrybenci zachowują dostęp bez żadnego działania z ich strony.
  4. Przestaw powiadomienia sklepów na nasze punkty webhook (Apple App Store Server Notifications, Google Real-Time Developer Notifications). To jest właściwe przełączenie.
  5. Uruchom import ponownie dla wszystkiego, co zmieniło się w tym oknie czasowym. Jest idempotentny — dokładnie po to jest ten krok.
  6. Wyłącz poprzedniego dostawcę dopiero wtedy, gdy pełny cykl odnowień przebiegnie czysto.

5. Czym jest zaimportowane uprawnienie — a czym nie jest

Zaimportowane uprawnienie to przyznanie płatnego dostępu na słowo twojego poprzedniego dostawcy. Sami nie sprawdziliśmy go jeszcze u Apple ani Google.

Zapisujemy to uczciwie, zamiast ukrywać: zaimportowane wiersze niosą imported_at i import_source, a ich store_verified_at pozostaje puste, dopóki uprawnienie nie zostanie potwierdzone w prawdziwym sklepie. Dzięki temu pytanie „które z nich przyznaliśmy na zaufanie?" ma w każdej chwili dokładną odpowiedź.

🚩 To ma znaczenie, jeśli któraś liczba kiedykolwiek będzie wyglądać źle. Uprawnienia zaimportowane i zweryfikowane to nie ta sama kategoria faktu, a audyt traktujący je jednakowo byłby mylący.

6. Ponowna weryfikacja importu w sklepie

Po imporcie możesz zlecić nam sprawdzenie każdego uprawnienia w prawdziwym sklepie:

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

Każdy wiersz wraca z jednym z trzech wyników:

Wynik Znaczenie
verified Sklep potwierdził. Przyjmujemy datę wygaśnięcia i flagę odnowienia ze sklepu zamiast zaimportowanych — to on jest źródłem rozstrzygającym.
mismatch Sklep zaprzeczył importowi: brak aktywnej subskrypcji albo aktywna subskrypcja na inny produkt.
unverifiable Nie udało się uzyskać użytecznej odpowiedzi — limit zapytań, problem z danymi uwierzytelniającymi, błąd przejściowy. To nie jest zaprzeczenie.

🚩 Niezgodność nigdy nie odbiera dostępu. W uprawnieniu nie zmienia się nic; zapisywane są wyłącznie kolumny weryfikacyjne, a rozbieżność trafia do oceny człowieka. „Sklep tego nie potwierdził" i „ten subskrybent nie ma uprawnień" to dwa różne stwierdzenia, a API sklepu może zwrócić pierwsze z powodów niemających nic wspólnego z twoim klientem. Nie jesteśmy gotowi anulować płacącego subskrybenta na takiej podstawie — tym bardziej kogoś, kto właśnie do nas przeszedł. Zobacz DECISIONS.md #70.

Uruchamiaj w paczkach (domyślnie 50, maksymalnie 200) — API sklepów mają limity zapytań, a te wywołania używają twoich danych uwierzytelniających. GET /apps/YOUR_APP_ID/import/status pokazuje bieżący obraz:

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

Celowo nie ma jeszcze automatycznego harmonogramu — wolimy nie sięgać po twoje dane uwierzytelniające do sklepu bez nadzoru, na liczniku, o który nie prosiłeś. Uruchom to, kiedy ci wygodnie; jest idempotentne i wznawialne.