# Webhooks

MoreLogin puede enviar por POST un evento JSON al endpoint HTTPS que tú controlas en cuanto una operación de cloud phone o cloud browser alcanza su resultado final, así que ya no tienes que hacer polling para conocerlo. Se almacena una sola URL de callback por equipo, y la Local API y la Open API escriben en el mismo registro.

## Configurar la URL de callback

Ambos entornos exponen la misma operación. Open API:

```bash
curl -X POST "https://api.morelogin.com/webhook/config" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "callbackUrl": "https://example.com/morelogin/webhook",
    "enabled": true
  }'
```

Local API:

```bash
curl -X POST "http://127.0.0.1:40000/api/webhook/config" \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://example.com/morelogin/webhook",
    "enabled": true
  }'
```

La ruta de la Local API la sirve el cliente de escritorio, así que necesita el cliente de MoreLogin v2.66.0 o superior. El endpoint de la Open API no depende del cliente.

`teamId` nunca se lee de la petición. Cada llamada configura el equipo del contexto del servidor, así que no puedes dirigir los eventos de otro equipo a tu endpoint.

La URL debe ser HTTPS, de 1 a 1024 caracteres, con host, sin información de usuario, sin fragmento y con un puerto válido. Todas las direcciones a las que resuelva tienen que ser enrutables públicamente: los rangos de localhost, privados, link-local, CGNAT, metadatos de nube, reservados por protocolo, de documentación y 6to4 se rechazan con [`41001`](/es/api-reference/getting-started/error-codes). La misma comprobación se repite cuando una entrega abre su conexión, así que un nombre de host que más tarde resuelva a un rango privado hace fallar esa entrega en lugar de llegar a tu red.

El guardado es un upsert por equipo, así que enviar el mismo cuerpo dos veces es inofensivo.

### Desactivar la entrega

`enabled: false` mantiene la URL guardada y detiene la creación de nuevos eventos. Los eventos ya en cola se cancelan uno a uno, en el momento en que a cada uno le tocaría autorizarse, y solo mientras la configuración siga desactivada: si la reactivas pronto, los eventos en cola continúan en lugar de descartarse.

## Cómo es una entrega

Cada entrega es un `POST` con estas cabeceras:

| Cabecera | Valor |
|  --- | --- |
| `Content-Type` | `application/json` |
| `User-Agent` | `MoreLogin-Webhook/1.0` |
| `X-Webhook-Event-Id` | Igual que `eventId` en el cuerpo |
| `X-Webhook-Event-Type` | Igual que `eventType` en el cuerpo |


Las dos cabeceras `X-Webhook-*` duplican campos del cuerpo a propósito, para que un router o una cola puedan repartir con ellas sin analizar el JSON.

```json
{
  "eventId": "c7f3a2b10d8e4f6a9c1b2d3e4f5a6b7c",
  "eventType": "cloud_phone.power_off",
  "eventTime": "1787241600000",
  "data": {
    "teamId": "10001",
    "cloudPhoneId": "20001",
    "reason": "MONEY_SAVING"
  },
  "result": {
    "success": true,
    "code": "0",
    "message": null
  }
}
```

| Campo | Descripción |
|  --- | --- |
| `eventId` | ID de evento único. Los reintentos y las repeticiones internas del mismo evento lo reutilizan, así que es la clave de idempotencia |
| `eventType` | Nombre de evento estable, de la tabla siguiente |
| `eventTime` | Cuándo ocurrió el resultado final, en milisegundos de época Unix. **Se envía como cadena** |
| `data.teamId` | Equipo propietario del recurso. **Se envía como cadena** |
| `data.cloudPhoneId` | Cloud phone al que se refiere el evento. **Se envía como cadena** |
| `data.cloudBrowserId` | Perfil de cloud browser al que se refiere el evento. **Se envía como cadena** |
| `data.runId` | Instancia de ejecución de cloud browser a la que se refiere el evento |
| `data.reason` | Solo en `cloud_phone.power_off` y `cloud_browser.stop` |
| `result.success` | Si la operación finalmente tuvo éxito |
| `result.code` | `0` en caso de éxito; si no, un código de error de negocio estable que no depende del idioma |
| `result.message` | `null` en caso de éxito; si no, una explicación en el idioma del equipo, con reserva a `en-US` |


Todos los identificadores numéricos y `eventTime` llegan como cadenas JSON, igual que en el resto de la API: consulta [Formato común de respuesta](/es/api-reference/getting-started/response-format). Interprétalos como cadenas y no como números, o perderás precisión en los ID de 64 bits.

## Eventos

| `eventType` | Resultado final que informa |
|  --- | --- |
| `cloud_phone.power_on` | Encendido correcto o fallido |
| `cloud_phone.power_off` | Apagado correcto. Un apagado fallido no produce evento |
| `cloud_phone.restart` | Reinicio correcto o fallido |
| `cloud_phone.reset` | Restablecimiento correcto o fallido |
| `cloud_phone.new_machine` | Nuevo dispositivo con un clic correcto o fallido |
| `cloud_browser.start` | Inicio del cloud browser correcto o fallido |
| `cloud_browser.stop` | Cada intento de cierre del cloud browser, con éxito o sin él |


Los eventos de cloud phone llevan `teamId` y `cloudPhoneId`. Los de cloud browser llevan `teamId`, `cloudBrowserId` y `runId`.

El encendido, el reinicio, el restablecimiento y el nuevo dispositivo con un clic informan `success: true` solo una vez que la devolución de llamada del proveedor tuvo éxito y la sincronización local se confirmó. Un fallo de facturación, o una comprobación de proxy asíncrona que falla y completa su limpieza, informa de un encendido fallido y **no** emite además un apagado. El trabajo diferido, como instalar aplicaciones preinstaladas o limpiar archivos caducados, no cambia el resultado.

Si un apagado interrumpe un arranque, ese intento no produce evento de encendido; el apagado correcto posterior sí produce su propio `cloud_phone.power_off`.

### Motivos de detención

`cloud_phone.power_off` añade `data.reason`:

| Valor | Significado |
|  --- | --- |
| `NORMAL` | Apagado ordinario |
| `MONEY_SAVING` | Modo de ahorro de dinero |
| `OTHER` | Impagos, caducidad, mantenimiento o cualquier otra causa |


`cloud_browser.stop` añade `data.reason`:

| Valor | Significado |
|  --- | --- |
| `NORMAL` | Cierre ordinario |
| `MONEY_SAVING` | Modo de ahorro de dinero |
| `ENERGY_SAVING` | Modo de ahorro de energía |
| `BROWSER_EXITED` | El navegador se cerró de forma inesperada |
| `OTHER` | Cualquier otra causa |


Un fallo interno de archivado solo cambia `result`; nunca sobrescribe el `reason` capturado cuando comenzó el cierre.

## Entrega, reintentos y orden

Cualquier `2xx` cuenta como entregado. Cualquier otro estado, más los tiempos de espera de conexión y de lectura y los errores de red, cuenta como fallo. El tiempo de espera de conexión es de 5 segundos y el de lectura, de 30 segundos.

Una entrega fallida se reintenta como máximo 6 veces, a los 10 segundos, 30 segundos, 60 segundos, 3 minutos, 10 minutos y 30 minutos. Son como máximo 7 peticiones para un evento. Cuando falla el sexto reintento, el evento deja de reintentarse automáticamente.

Los eventos de un mismo cloud phone, o de un mismo cloud browser, se entregan en orden de creación, de uno en uno. Los recursos distintos se entregan en paralelo, así que no asumas ningún orden entre ellos.

Los reintentos de un evento reutilizan siempre el mismo `eventId`, `eventTime` y cuerpo. En situaciones límite de red o de recuperación, el mismo evento puede entregarse más de una vez con esos valores sin cambios, así que **deduplica por `eventId`**.

En los cierres de cloud browser, un intento de cierre produce exactamente un evento. Un reintento manual posterior, una liberación forzada manual o una liberación forzada iniciada por el sistema son una operación nueva y reciben un `eventId` nuevo. Dos intentos nunca se fusionan en un solo evento.

Las operaciones de cloud phone se identifican por el ID de tarea del proveedor, `traceId` o una marca de tiempo de negocio estable. Cuando no existe ninguno, el servicio genera un UUID y trata la devolución de llamada final actual como una operación nueva, así que recibes un `eventId` nuevo en lugar de que dos operaciones distintas se colapsen en una.

## Escribir el receptor

Devuelve un `2xx` en cuanto hayas almacenado el evento, y haz el trabajo real de forma asíncrona. El cuerpo de la respuesta se ignora, y un manejador lento consume tus 30 segundos de lectura y empuja el evento al calendario de reintentos aunque lo hayas recibido.

Deduplica por `eventId`. Trata una repetición como ya gestionada en lugar de repetir los efectos secundarios.

No trates la carga como entrada de confianza solo porque llegó a tu endpoint. Esta versión no firma los callbacks, así que cualquiera que conozca tu URL puede publicar en ella. Usa una URL difícil de adivinar y, antes de hacer algo costoso, confirma el estado a través de la API: por ejemplo [`POST /cloudphone/info`](/es/api-reference/cloud-phone/open-api) para un cloud phone, o `POST /cloudbrowser/page` para una ejecución de cloud browser.

`result.code` usa los mismos códigos de negocio que la API sincrónica, así que un fallo se puede consultar en [Códigos de error](/es/api-reference/getting-started/error-codes). `result.message` es una cadena legible cuyo texto e idioma pueden cambiar; ramifica según `result.success` y `result.code`, nunca según `result.message`.

El contrato legible por máquina de cada evento, incluidos los esquemas y los ejemplos por evento, se publica con la especificación de recursos compartidos en [Shared Resources Open API](/es/api-reference/shared-resources/open-api).