Migrar tus suscriptores a SubSovereign
¿Vienes de RevenueCat, Adapty, Qonversion o de tu propio backend? Tus suscriptores actuales vienen contigo. Esta guía cubre la importación en sí y —igual de importante— el cambio de sistema, que es la parte que realmente conlleva riesgo.
Lee esto primero. La importación es segura y repetible. El cambio es el momento que exige planificación, porque mientras no redirijas las notificaciones de tus tiendas hacia nosotros, las renovaciones se siguen entregando a tu proveedor anterior. Planifica el §4 antes de ejecutar el §2.
Las dos mitades de una migración — léelo antes de planificar una
Toda migración tiene dos mecanismos, y distinguirlos marca la diferencia entre un traslado sin sobresaltos y una promesa que no puedes cumplir.
La revalidación del lado del cliente es la GARANTÍA. En cuanto tu aplicación incorpore nuestro SDK, el dispositivo de cada suscriptor reenviará su recibo vigente la próxima vez que la abra. El identificador procede de Apple o Google mismos: actual, correcto y completo. Funciona con todas las tiendas, no necesita la exportación de nadie y no puede arrastrar un identificador obsoleto. Todo suscriptor que abra tu aplicación queda migrado correctamente, por construcción.
La importación es el ACELERADOR. Existe para que tus suscriptores no queden bloqueados antes de volver a abrir la aplicación, y para que tus informes del primer día no aparezcan vacíos. Copia lo que sabía tu proveedor anterior.
Por qué ambos: los suscriptores abren las aplicaciones a ritmos muy distintos. Alguien que no abra tu aplicación durante seis semanas pasaría, solo con la revalidación, seis semanas apareciendo sin derechos. La importación le da continuidad desde el primer minuto; la revalidación la vuelve fidedigna.
🚩 Si solo puedes quedarte con uno, quédate con la revalidación. Una importación que no pueda aportar un identificador reconocible para alguna tienda (ver más abajo) no es una migración para esos suscriptores: es un registro que caduca en silencio.
Qué puede darte realmente tu proveedor anterior
La importación necesita originalTransactionId, y lo que entrega cada proveedor varía según la tienda. Comprueba el tuyo antes de planificar en torno a él:
| Proveedor | Apple | |
|---|---|---|
| RevenueCat | ✅ La exportación de datos incluye original_transaction_id. |
⚠️ La exportación da un Order ID, que no es el purchase token: un identificador distinto con el que una notificación de renovación no casará. Su API v2 es peor: solo expone el identificador de transacción más reciente, que cambia en cada renovación. |
| Adapty / Qonversion | Comprueba en la exportación el original transaction ID de Apple antes de planificar. | Comprueba si obtienes el purchase token o una referencia de pedido/transacción. Confírmalo, no lo des por hecho. |
| Superwall | ⚠️ Confírmalo antes de planificar. Operan su propia capa de derechos, compras y validación de recibos, así que el identificador existe en su lado; la duda es si la exportación te entrega el original_transaction_id de Apple o solo la referencia de suscripción propia de Superwall. |
⚠️ Misma pregunta, misma respuesta: establece por escrito si obtienes el purchase token o una referencia de pedido/transacción, antes de planificar en torno a ello. |
| Tu propio backend | Lo que guardaste en la compra: normalmente lo correcto. | Lo más probable es que ya tengas el purchase token, porque lo necesitabas para validar. |
| Stripe | n/a | n/a — leemos tu cuenta de Stripe directamente, sin exportación. Ver el §3b. |
🚩 Por tanto, una migración desde RevenueCat importa limpiamente para Apple y no puede importar suscriptores de Google con un identificador reconocible. No es algo que podamos resolver con código: el identificador que necesitamos no está presente en los datos que puedes obtener. Esos suscriptores llegan mediante la revalidación del lado del cliente: publica el SDK, redirige las notificaciones (§4) y cada uno quedará migrado correctamente la próxima vez que abra la aplicación. Importa el lado de Apple para dar continuidad y deja que Google se cure solo.
1. Qué necesitas
- Tu exportación de suscriptores, con una fila por cada derecho de acceso activo (ver el formato más abajo).
- Un mapa de productos. Cada ID de producto de tu exportación debe estar asociado primero a uno de tus niveles de acceso — Panel → tu aplicación → productos. 🚩 La importación no adivinará qué nivel conceder. Un producto sin asociar se rechaza y se informa, porque adivinar sería una forma de sortear la misma protección que impide que un cliente reclame un nivel que no ha comprado.
- Un acceso de administrador de la organización propietaria de la aplicación. Los lectores (viewers) no pueden importar: concede acceso de pago de forma masiva.
2. El formato
Un objeto JSON por cada derecho de acceso de suscriptor:
{
"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 | Obligatorio | Notas |
|---|---|---|
userId |
sí | Tu propio identificador de usuario, el mismo que tu aplicación pasa a nuestro SDK. |
store |
sí | apple · google · amazon · roku · stripe |
productId |
sí | Debe existir en tu mapa de productos o la fila se rechaza. |
originalTransactionId |
sí | 🚩 Ver más abajo. El original transaction ID de Apple, el purchase token de Google o el ID de suscripción del proveedor. |
status |
sí | active · trialing · grace_period · expired · lifetime · cancelled · refunded · billing_failed |
expiresAt |
no | ISO 8601. Omítelo para lifetime. |
amazonUserId |
para Amazon | 🚩 El identificador de usuario propio de Amazon, obtenido de su SDK, no el identificador de usuario de tu aplicación. Amazon busca un recibo por (ese identificador, receiptId), así que sin él el derecho de acceso nunca podría reverificarse. Las filas de Amazon que carezcan de él se rechazan. |
email, willRenew |
no |
🚩 originalTransactionId es el campo que decide si esto funciona
No es solo una clave para nosotros: es la forma en que una renovación encuentra al suscriptor meses después. Cuando Apple o Google envía una notificación de servidor por una renovación, ese identificador es con el que casa.
Una fila importada sin él produce el peor tipo de fallo: el primer día todo parece correcto y después el suscriptor pierde el acceso en silencio en su siguiente renovación, sin que se registre ningún error en ninguna parte.
Por eso rechazamos esas filas en lugar de aceptarlas. Importar sin él no es un atajo, es una caída diferida.
🚩 No todos los proveedores pueden darte este campo para todas las tiendas: ver la tabla anterior. Donde tu exportación realmente no pueda aportarlo (RevenueCat + Google es el caso conocido), no lo sortees inventando un valor: cualquier marcador de posición produce exactamente el fallo silencioso de renovación descrito aquí. Importa las tiendas para las que tengas identificadores reales y deja que la revalidación del lado del cliente se encargue del resto.
3. Ejecútalo — siempre una simulación primero
Esta sección describe la vía CSV / exportación, usada para Apple, Google, Amazon y Roku. Para Stripe, salta al §3b: leemos tu cuenta directamente y no hay nada que construir.
Lo más sencillo es hacerlo en el panel: Subscriber Migration (Insights → Customers) → From an export. Pega la exportación como CSV o JSON, o carga el archivo. La página comprueba las filas primero y no escribe nada hasta que se lo pidas; divide los archivos grandes en lotes por ti; y enumera cada fila rechazada por número de fila, para que puedas corregirlas en el archivo que realmente tienes. La API de más abajo hace exactamente lo mismo si prefieres automatizarlo.
🚩 Estas filas son una afirmación, no una confirmación. Una exportación es lo que te entregó tu proveedor anterior, así que las filas importadas llegan sin verificar y entran en la cola de reverificación (§6), donde después las contrastamos con la tienda real. Las filas leídas directamente de Stripe (§3b) son distintas: llegan ya confirmadas.
La simulación es el comportamiento por defecto. Tienes que pedir explícitamente que se escriba algo.
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": [ … hasta 1000 filas … ] }'
Recibes un informe:
{
"dryRun": true, "received": 1000, "accepted": 987, "newUsers": 964,
"missingTransactionIds": 6,
"unmappedProducts": ["legacy_annual_v1"],
"rejected": [ { "index": 12, "code": "unmapped_product", "reason": "…" } ]
}
Lee estos tres números antes de confirmar:
missingTransactionIds: filas que se renovarían hacia la nada. Corrige la exportación, no continúes.unmappedProducts: asócialos y vuelve a ejecutar.newUsers: cuántos suscriptores se van a crear. Si la cifra parece equivocada, probablemente tu campouserIdno sea el identificador correcto.
Cuando el informe esté limpio, añade "dryRun": false y envíalo de nuevo.
Lotes y reanudación
Envía como máximo 1000 filas por petición; una migración grande son muchos lotes. La importación es idempotente: reenviar el mismo lote actualiza en lugar de duplicar, así que un script que muera a medio camino puede sencillamente volver a ejecutarse desde el principio. Una fila incorrecta se informa y se omite; nunca descarta las filas buenas que la rodean.
Qué rechazará, y por qué
| Código | Significado |
|---|---|
missing_transaction_id |
Ver más arriba. El importante. |
unmapped_product |
Asocia primero el producto; no adivinaremos un nivel. |
transaction_owned_by_another_user |
Esa transacción de tienda ya pertenece a otra cuenta de esta aplicación. Una transacción, un propietario: normalmente una cuenta fusionada o datos erróneos en la exportación. |
invalid_store / invalid_status / invalid_expiry |
Valor mal formado. |
3b. Stripe — leemos tu cuenta directamente
Stripe es la única tienda capaz de responder a «¿quiénes son todos mis suscriptores?». Apple, Google, Amazon y Roku solo responden a «¿sigue siendo válido este recibo?», de uno en uno. Así que con Stripe no hay exportación que solicitar ni CSV que construir: Subscriber Migration en el panel (Insights → Customers) recorre tu cuenta y la importa.
Antes de empezar, deben estar en su sitio dos cosas, y la página te avisa si no lo están:
- Tu clave secreta de Stripe, guardada en Apps → store credentials. No hay alternativa a nivel de plataforma ni la habrá nunca: sin tu clave, la importación falla cerrándose en lugar de leer datos de otra persona.
- Tus productos asociados a niveles. Asocia el Price (
price_…) o el Product (prod_…); ambos valen. Los productos sin asociar se rechazan en lugar de adivinarse, lo cual es el comportamiento correcto y no un error.
Después indícanos dónde está el identificador de usuario de tu aplicación dentro de Stripe: metadatos de la suscripción, metadatos del cliente o el propio identificador de cliente de Stripe. 🚩 No hay valor por defecto y no lo adivinaremos. Stripe conoce al cliente como cus_…; solo tú sabes cuál de tus usuarios es, y equivocarse no falla de forma ruidosa: concede el acceso de pago de un suscriptor a la cuenta de otra persona.
Previsualiza primero. La vista previa no cambia nada y te muestra exactamente lo que ocurriría: cuántas suscripciones se leyeron, cuántas se importarían y cada una de las que se rechazaría, con el motivo en lenguaje claro. Lee los rechazos antes de importar: son la lista de cosas que corregir, no ruido.
Las cuentas grandes se recorren automáticamente en varias peticiones; la página se autorregula para no superar el límite de frecuencia y muestra el progreso a medida que avanza.
Qué rechaza, y por qué esas son las respuestas correctas
| Código | Significado |
|---|---|
never_paid |
La suscripción está incomplete: nunca completó un primer pago. Importarla concedería acceso de pago a alguien que jamás ha pagado. |
access_expired |
El estado concede acceso pero el periodo de facturación ya ha terminado. Aquí el acceso lo decide el estado, así que importarla concedería acceso indefinidamente. |
ambiguous_product |
Los elementos de una misma suscripción apuntan a dos niveles distintos. Rechazamos en lugar de elegir: quedarse con el más alto sería una subida de nivel colada por la vía de la migración, y quedarse con el primero sería arbitrario. |
no_user_id |
No hay identificador de usuario de la aplicación donde nos dijiste que mirásemos. Ver más arriba: este es el peligroso de adivinar. |
unmapped_product |
Asocia el producto y vuelve a ejecutar. |
🚩 Los productos vistos pero no asociados se informan aunque no hayan bloqueado nada. Hoy no bloquean nada, pero en cuanto asocies uno, cualquier suscripción que lo lleve junto a otro producto asociado se vuelve ambigua y empieza a rechazarse. Mejor saberlo ahora que en la segunda ejecución.
Por qué las filas de Stripe llegan ya verificadas
Una fila importada cuenta normalmente como una afirmación hasta que la confirmamos con la tienda (§6). Las filas de Stripe son distintas: se leyeron a través de la propia API de Stripe usando tus credenciales, así que llegan ya confirmadas por la tienda. Es una afirmación sobre la procedencia, no sobre Stripe: cualquier futuro procesador de pagos que leamos directamente se comportará igual, y cualquier fila aportada por una persona sigue siendo una afirmación, sea cual sea la tienda que nombre.
¿Prefieres la API? La página es un cliente de POST /apps/:appId/import/stripe; las mismas opciones son dryRun, userIdSource, userIdKey y startingAfter para la paginación.
4. 🚩 El cambio — planifícalo antes de importar
La importación copia un estado. No redirige el futuro. Mientras no reapuntes las notificaciones de servidor de tus tiendas hacia SubSovereign, las renovaciones, cancelaciones y reembolsos seguirán yendo a tu proveedor anterior.
La secuencia recomendada:
- Importa (esta guía). Tus derechos de acceso existen ya en ambos sistemas.
- Funciona en paralelo. Mantén activo el proveedor anterior y compara. Todavía no se ha movido nada.
- Publica una versión de la aplicación con nuestro SDK. Los derechos se resuelven desde nosotros y, como importaste los mismos identificadores de transacción, los suscriptores existentes conservan su acceso sin hacer nada.
- Reapunta las notificaciones de las tiendas a nuestros endpoints de webhook (Apple App Store Server Notifications, Google Real-Time Developer Notifications). Este es el cambio de verdad.
- Vuelve a ejecutar la importación para todo lo que haya cambiado durante la ventana. Es idempotente: para eso está este paso.
- Retira el proveedor anterior cuando haya pasado limpiamente un ciclo completo de renovación.
5. Qué es un derecho de acceso importado — y qué no
Un derecho de acceso importado es una concesión de acceso de pago basada en la palabra de tu proveedor anterior. Todavía no lo hemos comprobado nosotros mismos con Apple o Google.
Lo registramos con honestidad en lugar de ocultarlo: las filas importadas llevan imported_at e import_source, y su store_verified_at permanece vacío hasta que el derecho se haya confirmado con la tienda real. Así, «¿cuáles de estos concedimos por confianza?» es una pregunta con respuesta exacta, en cualquier momento.
🚩 Esto importa si alguna cifra parece equivocada. Los derechos importados y los verificados no son la misma clase de hecho, y una auditoría que los tratara como idénticos sería engañosa.
6. Reverificar una importación con la tienda
Una vez importado, puedes pedirnos que comprobemos cada derecho de acceso con la tienda 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 fila vuelve con uno de tres resultados:
| Resultado | Significado |
|---|---|
| verified | La tienda lo confirmó. Adoptamos la caducidad y el indicador de renovación de la tienda por encima de los importados: es la autoridad. |
| mismatch | La tienda contradijo la importación: no hay suscripción activa, o hay una activa para un producto distinto. |
| unverifiable | No pudimos obtener una respuesta utilizable: un límite de frecuencia, un problema de credenciales, un error transitorio. Esto no es una contradicción. |
🚩 Un mismatch nunca revoca el acceso. No cambia nada del derecho de acceso; solo se escriben las columnas de verificación y la discrepancia se expone para que la juzgue una persona. «La tienda no lo confirmó» y «este suscriptor no tiene derecho» son afirmaciones distintas, y la API de una tienda puede devolver la primera por motivos que nada tienen que ver con tu cliente. No estamos dispuestos a cancelar a un suscriptor que paga sobre esa base, y menos aún a uno que acaba de migrar con nosotros. Ver DECISIONS.md #70.
Ejecútalo por lotes (50 por defecto, 200 como máximo): las API de las tiendas tienen límite de frecuencia y estas llamadas usan tus credenciales de tienda. GET /apps/YOUR_APP_ID/import/status muestra la foto actual:
{ "imported": 4820, "verified": 4776, "awaiting": 44, "mismatch": 11, "unverifiable": 33 }
Todavía no hay una programación automática para esto, a propósito: preferimos no usar tus credenciales de tienda sin supervisión, en un temporizador que no has pedido. Ejecútalo cuando te convenga; es idempotente y reanudable.