Cómo instalar una app iOS sin App Store ni TestFlight: distribución Ad Hoc paso a paso

Actualizado: 08/10/26 14:05

Creado: 08/10/26 14:05

Esta mañana Jaime se ha instalado en su iPhone una app que estoy desarrollando. No está en el App Store, no ha pasado por TestFlight y su iPhone no ha estado nunca conectado a mi Mac. Desde que abrió el primer enlace hasta que tuvo la app en la pantalla de inicio pasó una media hora, y casi toda fue mía, en el Mac.

Eso es una distribución Ad Hoc. En este artículo explico qué hace falta para montarla y cómo funciona por dentro, paso por paso, con el caso real del iPhone de Jaime.

Qué es una distribución Ad Hoc

Apple solo deja instalar apps firmadas, y la firma decide en qué dispositivos se puede instalar cada build. Hay varias formas de repartir una app sin publicarla:

VíaQuién puede instalarLo que pide al otroCaduca
Ad HocSolo los dispositivos cuyo UDID va dentro del perfil de la build (máx. 100 por tipo de dispositivo y año)Nada: un enlace en SafariEl perfil, al año
TestFlightTesters invitados (internos o con enlace público)Apple ID e instalar TestFlightCada build, a los 90 días
Enterprise (In-House)Cualquier dispositivoNadaAl año
App StoreTodo el mundoNadaNo caduca

Enterprise exige ser una organización de más de 100 empleados y pasar la verificación de Apple, así que para la mayoría no es opción. TestFlight es estupendo, pero obliga a pedir el Apple ID de cada persona o a pasar una revisión beta. Ad Hoc es la vía sin intermediarios: si el UDID del iPhone está en el perfil, se instala; si no, no.

Requisitos

  • Apple Developer Program de pago (99 € al año). Con un Apple ID gratuito no se puede exportar Ad Hoc.
  • Un Mac con Xcode para archivar y exportar el .ipa. Es el único paso que no se puede hacer desde otra máquina, porque la firma va con tu certificado.
  • El UDID de cada dispositivo, registrado en tu cuenta de desarrollador. El UDID no aparece en Ajustes y una app no lo puede leer: hay que sacarlo de una forma concreta (más abajo).
  • Un servidor HTTPS con certificado válido donde dejar el .ipa y un pequeño fichero manifest.plist. Con un certificado autofirmado o caducado, iOS falla en silencio.
  • Safari en el iPhone. Los navegadores integrados de otras apps (correo, mensajería, redes sociales) no saben abrir el enlace de instalación.
  • Modo de desarrollador activado en el iPhone, en Ajustes → Privacidad y seguridad. En nuestras pruebas iOS lo pidió la primera vez que se abrió la app; se activa una sola vez y requiere reiniciar.

Y dos límites que conviene conocer antes de empezar:

  • 100 dispositivos por tipo y año de membresía. Un dispositivo dado de baja sigue contando hasta que renuevas.
  • Cada dispositivo nuevo obliga a volver a exportar la build. El perfil viaja dentro del .ipa: si el UDID no estaba cuando firmaste, esa build no se instalará en ese iPhone por mucho que lo registres después.

Cómo funciona por dentro

Safari no instala un .ipa si lo descargas sin más. Lo que se le da es un enlace con un esquema especial:

itms-services://?action=download-manifest&url=https://tu-servidor/manifest.plist

Al pulsarlo, iOS (no Safari) descarga ese manifest.plist, que describe la app y dice dónde está el binario:

<plist version="1.0"><dict>
  <key>items</key><array><dict>
    <key>assets</key><array>
      <dict><key>kind</key><string>software-package</string>
            <key>url</key><string>https://tu-servidor/app.ipa</string></dict>
    </array>
    <key>metadata</key><dict>
      <key>bundle-identifier</key><string>com.ejemplo.app</string>
      <key>bundle-version</key><string>0.4.0</string>
      <key>kind</key><string>software</string>
      <key>title</key><string>Mi App</string>
    </dict>
  </dict></array>
</dict></plist>

Después pide confirmación, descarga el .ipa en segundo plano y comprueba la firma: que el certificado sea válido, que el perfil no haya caducado y que el UDID del dispositivo esté en la lista ProvisionedDevices del perfil. Si algo falla, el icono aparece un momento y desaparece con un escueto «No se ha podido instalar».

Un detalle que condiciona todo el diseño del servidor: esas dos descargas las hace el sistema, sin las cookies de Safari. No vale proteger el .ipa con un login; la protección tiene que ir en la propia URL, por ejemplo con un token firmado que caduque al cabo de una hora.


Paso a paso: el iPhone de Jaime

Para repartir mis builds uso appdist, un pequeño servicio propio tipo Diawi: un binario en Go con una web en Vue. Se le sube el .ipa por API y genera la página de instalación, el manifiesto y los enlaces firmados. Pero los pasos son los mismos con cualquier herramienta, o a mano.

El punto de partida: la app ya estaba instalada en mi iPhone y mi iPad, y la última build (0.4.0, build 23) estaba firmada con un perfil que incluía esos dos dispositivos. Jaime tiene un iPhone 13 con iOS 26 que yo no había registrado nunca.

1. Sacar el UDID desde Safari (32 segundos)

Le pasé a Jaime un enlace a la página /udid del servidor. Escribió su nombre y pulsó «Obtener UDID». Lo que pasa por debajo es un mecanismo documentado por Apple, el Profile Service:

  1. El servidor entrega un perfil de configuración .mobileconfig que pide los atributos del dispositivo (UDID, PRODUCT, VERSION…) y lleva una URL de retorno.
  2. Jaime lo instaló en Ajustes → General → VPN y gestión de dispositivos. iOS avisa de que el perfil «No está firmado»; se puede instalar igual.
  3. iOS envió al servidor un plist firmado con su UDID, modelo y versión de iOS.
  4. El servidor respondió con una redirección a una página que muestra el UDID con un botón de copiar. El perfil no queda instalado: se usa y desaparece.

Entre la petición del perfil y la llegada del UDID pasaron 32 segundos. La página además guarda el UDID en el navegador, lo que más tarde sirve para avisarle de si una build le vale o no.

Cuidado con las apps o webs que dicen darte «tu UDID» y te devuelven algo con formato XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX. Eso es un identificador de app (identifierForVendor), no el UDID, y Apple lo rechaza. Un UDID de un iPhone moderno tiene 25 caracteres con un solo guion: 00008030-001A2B3C4D5E6F7A.

2. Registrar el UDID en Apple

En developer.apple.com → Certificates, IDs & Profiles → Devices se da de alta el dispositivo con su UDID y un nombre. Si son varios, se puede subir un fichero de texto con una línea por dispositivo (appdist lo genera con el botón «Exportar para Apple»):

Device ID	Device Name	Device Platform
00008030-001A2B3C4D5E6F7A	iPhone Jaime	ios

También se puede automatizar con la App Store Connect API (POST /v1/devices). Pero registrar el dispositivo no cambia ninguna build que ya exista.

3. Regenerar el perfil y volver a exportar

Este es el paso que de verdad mete el UDID en «la imagen» de la app. En Xcode: Product → Archive → Distribute App → Release Testing (lo que antes se llamaba Ad Hoc). Desde la terminal:

xcodebuild archive -scheme MiApp -archivePath build/MiApp.xcarchive
xcodebuild -exportArchive -archivePath build/MiApp.xcarchive \
  -exportOptionsPlist ExportOptions.plist -exportPath build \
  -allowProvisioningUpdates

Con -allowProvisioningUpdates y firma automática, Xcode ve que hay un dispositivo nuevo en la cuenta y regenera el perfil Ad Hoc con los tres UDID. El ExportOptions.plist lleva method = release-testing (o ad-hoc en Xcode antiguos).

Para comprobar qué dispositivos lleva un perfil:

security cms -D -i embedded.mobileprovision | grep -A5 ProvisionedDevices

4. Subir la build

Subí el .ipa nuevo (misma versión 0.4.0, build 25) con la nota «Perfil con el iPhone de Jaime». appdist lee el .ipa al recibirlo: saca el nombre, la versión, el icono, el tipo de firma, la caducidad del perfil y la lista de UDID. Por eso el panel ya mostraba que esta build cubría tres dispositivos.

curl -H "Authorization: Bearer $TOKEN" \
  --data-binary @MiApp.ipa https://tu-servidor/api/builds

5. Instalar desde Safari (6 segundos)

Jaime volvió a abrir el enlace de la app, que siempre apunta a la última build. Como su navegador tenía guardado el UDID del paso 1, la página le dijo en verde que su iPhone estaba incluido en esta build. Pulsó «Instalar» y en los registros del servidor se ve exactamente lo que hace iOS:

11:05:00  GET  /i/a/…                    página de instalación
11:05:05  GET  /m/…/manifest.plist       iOS pide el manifiesto
11:05:06  HEAD /d/…/app.ipa              comprueba el binario
11:05:06  GET  /d/…/icon.png             icono para la pantalla de inicio
11:05:06  GET  /d/…/app.ipa              descarga el .ipa

Las tres últimas peticiones no las hace Safari: el user agent es com.apple.appstored, el mismo servicio que instala las apps del App Store. Seis segundos después de confirmar, la app estaba en la pantalla de inicio.

6. Primera apertura

Si es la primera app Ad Hoc del dispositivo, iOS puede pedir activar el Modo de desarrollador (Ajustes → Privacidad y seguridad → Modo de desarrollador, y reiniciar). Es una sola vez por dispositivo.


Por qué la build anterior no le habría funcionado

La build 23 era la misma app y la misma versión, pero su perfil solo tenía mi iPhone y mi iPad. Si Jaime la hubiera intentado instalar, iOS la habría rechazado aunque su UDID ya estuviera registrado en Apple. Es el error más habitual con Ad Hoc, y la razón por la que merece la pena que la página de instalación lo compruebe antes: «Tu iPhone no está en esta build: pide que lo añadan» es mucho más útil que un icono que desaparece.

Errores frecuentes

SíntomaCausa probable
El icono aparece y desaparece: «No se ha podido instalar»El UDID no está en el perfil de esa build, o el perfil ha caducado
El botón «Instalar» no hace nadaEl enlace se abrió dentro de otra app (correo, mensajería): abrirlo en Safari
Tampoco hace nada en SafariEl manifiesto no se sirve por HTTPS con un certificado válido
La app se instala pero no abreFalta activar el Modo de desarrollador, o el perfil caducó (la app deja de abrir al año)
Apple rechaza el UDID al registrarloEs un identifierForVendor (formato UUID), no un UDID

¿Cuándo no usar Ad Hoc?

  • Si los testers tienen Apple ID y no te importa invitarles, TestFlight es más cómodo: no hay UDID ni que volver a exportar por cada persona nueva.
  • Si vas a pasar de unas decenas de dispositivos, el cupo de 100 por año se acaba enseguida.
  • Si la app es para el público, no hay atajo: App Store.

Para lo que lo uso yo, enseñar una app en desarrollo a unas pocas personas sin pedirles nada más que abrir un enlace, Ad Hoc con un servidor propio funciona muy bien. El único peaje es volver a exportar en el Mac cada vez que llega un iPhone nuevo, y en el caso de Jaime eso fue casi todo el tiempo de la media hora.