Suscripciones a webhooks

La API de Google Health permite que tu aplicación reciba notificaciones en tiempo real cuando cambian los datos de salud de un usuario. En lugar de sondear los cambios, tu servidor recibe un webhook de solicitud POST de HTTPS en cuanto los datos están disponibles en la API de Google Health.

Tipos de datos admitidos

Las notificaciones de webhook son compatibles con los siguientes tipos de datos:

Tabla: Tipos de datos de la API de Google Health con compatibilidad con webhooks
Tipo de datos Operaciones de
disponibles
Alcance
Minutos de actividad
dataType: active-minutes
filter parameter: active_minutes
Tipo de registro: Intervalo
Resolución de almacenamiento: 1 minuto

Dispositivos compatibles

  • Fitbit Air
  • Fitbit Alta
  • Fitbit Alta HR
  • Fitbit Blaze
  • Fitbit Charge 2
  • Fitbit Charge 3
  • Fitbit Flex 2
  • Fitbit Inspire
  • Fitbit Inspire HR
  • Pixel Watch 4
list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
Minutos en zona activa
dataType: active-zone-minutes
filter parameter: active_zone_minutes
Tipo de registro: Intervalo
Resolución de almacenamiento: 1 minuto

Dispositivos compatibles

list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
Nivel de actividad
dataType: activity-level
filter parameter: activity_level
Tipo de registro: Intervalo
list, reconcile .activity_and_fitness.readonly
.activity_and_fitness.writeonly
Altitud
dataType: altitude
filter parameter: altitude
Tipo de registro: Intervalo
Resolución de almacenamiento: 1 minuto
list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
Glucemia
dataType: blood-glucose
filter parameter: blood_glucose
Tipo de registro: Muestra
list, get, reconcile, rollup, dailyRollup .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
Grasa corporal
dataType: body-fat
filter parameter: body_fat
Tipo de registro: Muestra

Dispositivos compatibles

list, get, reconcile, rollup, dailyRollup, create, update, batchDelete .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
Calorías en la zona de frecuencia cardíaca
dataType: calories-in-heart-rate-zone
filter parameter: calories_in_heart_rate_zone
Tipo de registro: Intervalo
Resolución de almacenamiento: 1 minuto
rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
Variabilidad de la frecuencia cardíaca diaria
dataType: daily-heart-rate-variability
filter parameter: daily_heart_rate_variability
Tipo de registro: Diario

Dispositivos compatibles

list, reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
Zonas de frecuencia cardíaca diarias
dataType: daily-heart-rate-zones
filter parameter: daily_heart_rate_zones
Tipo de registro: Diario
list, reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
Saturación de oxígeno diaria
dataType: daily-oxygen-saturation
filter parameter: daily_oxygen_saturation
Tipo de registro: Diario

Dispositivos compatibles

list, reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
Frecuencia respiratoria diaria
dataType: daily-respiratory-rate
filter parameter: daily_respiratory_rate
Tipo de registro: Diario

Dispositivos compatibles

list, reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
Frecuencia cardíaca en reposo diaria
dataType: daily-resting-heart-rate
filter parameter: daily_resting_heart_rate
Tipo de registro: Diario

Dispositivos compatibles

list, reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
Desviaciones diarias de la temperatura durante el sueño
dataType: daily-sleep-temperature-derivations
filter parameter: daily_sleep_temperature_derivations
Tipo de registro: Diario

Dispositivos compatibles

list, reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
Distancia
dataType: distance
filter parameter: distance
Tipo de registro: Intervalo
Resolución de almacenamiento: 1 minuto

Dispositivos compatibles

list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
Ejercicio
dataType: exercise
filter parameter: exercise
Tipo de registro: Sesión

Dispositivos compatibles

list, get, reconcile, create, update, batchDelete .activity_and_fitness.readonly
.activity_and_fitness.writeonly
Pisos
dataType: floors
filter parameter: floors
Tipo de registro: Intervalo
Resolución de almacenamiento: 1 minuto
reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
Frecuencia cardíaca
dataType: heart-rate
filter parameter: heart_rate
Tipo de registro: Muestra
Resolución de almacenamiento: 1 segundo

Dispositivos compatibles

list, reconcile, rollup, dailyRollup .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
Variabilidad de la frecuencia cardíaca
dataType: heart-rate-variability
filter parameter: heart_rate_variability
Tipo de registro: Muestra

Dispositivos compatibles

list, reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
Altura
dataType: height
filter parameter: height
Tipo de registro: Muestra
list, get, reconcile, create, update, batchDelete .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
Registro de hidratación
dataType: hydration-log
filter parameter: hydration_log
Tipo de registro: Sesión
list, get, reconcile, rollup, dailyRollup, create, update, batchDelete .nutrition.readonly
.nutrition.writeonly
Registro de nutrición
dataType: nutrition-log
filter parameter: nutrition_log
Tipo de registro: Sesión

Dispositivos compatibles

list, get, reconcile, rollup, dailyRollup, create, update, batchDelete .nutrition.readonly
.nutrition.writeonly
Resumen de sueño de la frecuencia respiratoria
dataType: respiratory-rate-sleep-summary
filter parameter: respiratory_rate_sleep_summary
Tipo de registro: Muestra

Dispositivos compatibles

list, reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
VO2 máx. en carreras
dataType: run-vo2-max
filter parameter: run_vo2_max
Tipo de registro: Muestra

Dispositivos compatibles

list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
Período sedentario
dataType: sedentary-period
filter parameter: sedentary_period
Tipo de registro: Intervalo

Dispositivos compatibles

list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
Sueño
dataType: sleep
filter parameter: sleep
Tipo de registro: Sesión

Dispositivos compatibles

list, get, reconcile, create, update, batchDelete .sleep.readonly
.sleep.writeonly
Pasos
dataType: steps
filter parameter: steps
Tipo de registro: Intervalo
Resolución de almacenamiento: 1 minuto

Dispositivos compatibles

list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
Tiempo en la zona de frecuencia cardíaca
dataType: time-in-heart-rate-zone
filter parameter: time_in_heart_rate_zone
Tipo de registro: Intervalo
Resolución de almacenamiento: 1 minuto
list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
Peso
dataType: weight
filter parameter: weight
Tipo de registro: Muestra

Dispositivos compatibles

list, get, reconcile, rollup, dailyRollup, create, update, batchDelete .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly

Permisos de notificaciones

Las notificaciones se envían para estos tipos de datos solo cuando un usuario otorgó el consentimiento para uno de los permisos correspondientes:

  • Actividad, que abarca los tipos de datos de minutos de actividad, pasos, altitud, distancia y pisos:
    • https://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonly
    • https://www.googleapis.com/auth/googlehealth.activity_and_fitness.writeonly
  • Métricas de salud, que abarca el tipo de datos de peso:
    • https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly
    • https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.writeonly
  • Sueño, que abarca el tipo de datos de sueño:
    • https://www.googleapis.com/auth/googlehealth.sleep.readonly
    • https://www.googleapis.com/auth/googlehealth.sleep.writeonly

Cuentas de servicio de IAM

Aunque no es obligatorio, te recomendamos que uses una cuenta de servicio de IAM cuando configures suscriptores para la API de Google Health. Las cuentas de servicio proporcionan una mejor seguridad para las cargas de trabajo de las aplicaciones en comparación con las cuentas de usuario estándar a través de las siguientes funciones:

  • Credenciales de corta duración automatizadas: Cuando se conectan a un entorno de ejecución de Google Cloud (como Compute Engine, Cloud Run o Google Kubernetes Engine), las cuentas de servicio obtienen y rotan automáticamente credenciales seguras de corta duración. Esto elimina los riesgos de administrar y almacenar claves estáticas persistentes.
  • Principio de privilegio mínimo: Las cuentas de servicio proporcionan identidades dedicadas para las cargas de trabajo. Puedes otorgarles solo los permisos específicos necesarios para administrar los extremos de los suscriptores, lo que evita un acceso más amplio a tus recursos de Google Cloud.
  • Independencia del ciclo de vida: Las cuentas de servicio operan de forma independiente de la cuenta de cualquier usuario individual, lo que garantiza que los cambios de personal no afecten la estabilidad de la autenticación a largo plazo.

Configurar una cuenta de servicio

Para configurar tu aplicación de suscriptor de modo que se autentique con una cuenta de servicio, sigue estos pasos:

  1. Crea una cuenta de servicio: En la consola de Google Cloud, navega a la página de IAM y administración de tu proyecto para crear una nueva cuenta de servicio administrada por el usuario.
  2. Otorga los roles de IAM necesarios: Asigna a la cuenta de servicio los roles adecuados necesarios para administrar los suscriptores en tu proyecto de Google Cloud.
  3. Adjunta la cuenta de servicio a tu carga de trabajo: Configura el entorno que aloja tu lógica de suscriptor para que se ejecute como la nueva cuenta de servicio. Esto permite que el código de tu aplicación (como las bibliotecas cliente de la API de Google) detecte y use automáticamente las credenciales de corta duración de la cuenta de servicio cuando llame a la API de REST de projects.subscribers.

Roles de CPE

Para administrar los suscriptores o las suscripciones de la API de Google Health, debes otorgar el rol adecuado a la cuenta de servicio que actúa en nombre de otra y que realiza las llamadas a la API. Según el nivel de acceso necesario, asigna uno de los siguientes roles:

  • Lectura de la API de Google Health
  • Editor de la API de Google Health
  • Administrador de la API de Google Health

Obtén más información sobre los roles y permisos de IAM de la API de Google Health.

Cómo administrar los suscriptores

Antes de recibir notificaciones, debes registrar un suscriptor, que representa el extremo de notificación de tu aplicación. Puedes administrar los suscriptores con la API de REST disponible en projects.subscribers.

El extremo del suscriptor debe usar HTTPS (TLSv1.2 o superior) y ser de acceso público. Durante la creación y las actualizaciones de los suscriptores, la API de Google Health realiza un desafío de verificación para garantizar que seas el propietario del URI del extremo. Si la verificación falla, las operaciones de creación y actualización de suscriptores fallan con un FailedPreconditionException.

Crea un suscriptor

Para registrar un suscriptor nuevo en tu proyecto, usa el extremo create. Debes proporcionar lo siguiente:

  • project-id: Es el número del proyecto en el que se creó la cuenta de servicio del webhook.
  • subscriberId: Es un identificador único que proporcionas para el suscriptor. Este ID debe tener entre 4 y 36 caracteres, y coincidir con la expresión regular ([a-z]([a-z0-9-]{2,34}[a-z0-9])).
  • endpointUri: Es la URL de destino para las notificaciones de webhook.
  • subscriberConfigs: Son los tipos de datos para los que deseas recibir notificaciones y la política de suscripción de cada uno.
  • endpointAuthorization: Es el mecanismo de autorización para tu extremo. Debe contener un secret que proporciones. El valor de secret se envía en el encabezado Authorization con cada mensaje de notificación. Puedes usar este token para verificar que las solicitudes entrantes provengan de la API de Google Health. Por ejemplo, puedes establecer secret en Bearer R4nd0m5tr1ng123 para la autenticación de portador o Basic dXNlcjpwYXNzd29yZA== para la autenticación básica.

En subscriberConfigs, debes establecer subscriptionCreatePolicy para cada tipo de datos. Establécelo en AUTOMATIC para usar suscripciones automáticas o en MANUAL si pretendes administrar las suscripciones de los usuarios por tu cuenta. Consulta Suscripciones automáticas y Suscripciones manuales para obtener más detalles sobre cada opción.

Solicitud

POST https://health.googleapis.com/v4/projects/project-id/subscribers?subscriberId=subscriber-id
{
  "endpointUri": "https://myapp.com/webhooks/health",
  "subscriberConfigs": [
    {
      "dataTypes": ["steps", "altitude", "distance", "floors", "weight"],
      "subscriptionCreatePolicy": "AUTOMATIC"
    },
    {
      "dataTypes": ["sleep"],
      "subscriptionCreatePolicy": "MANUAL"
    }
  ],
  "endpointAuthorization": {
    "secret": "Bearer example-secret-token"
  }
}

Respuesta

{
  "name": "projects/project-id/subscribers/subscriber-id",
  "endpointUri": "https://myapp.com/webhooks/health",
  "subscriberConfigs": [
    {
      "dataTypes": ["steps", "altitude", "distance", "floors", "weight"],
      "subscriptionCreatePolicy": "AUTOMATIC"
    },
    {
      "dataTypes": ["sleep"],
      "subscriptionCreatePolicy": "MANUAL"
    }
  ]
}

Enumera los suscriptores

Usa el extremo list para recuperar todos los suscriptores registrados en tu proyecto.

Solicitud

GET https://health.googleapis.com/v4/projects/project-id/subscribers

Respuesta

{
  "subscribers": [
    {
      "name": "projects/project-id/subscribers/subscriber-id",
      "endpointUri": "https://myapp.com/webhooks/health",
      "subscriberConfigs": [
        {
          "dataTypes": ["steps", "altitude", "distance", "floors", "weight"],
          "subscriptionCreatePolicy": "AUTOMATIC"
        },
        {
          "dataTypes": ["sleep"],
          "subscriptionCreatePolicy": "MANUAL"
        }
      ],
      "endpointAuthorization": {
        "authorizationTokenSet": true
      }
    }
  ],
  "totalSize": 1
}

Actualiza un suscriptor

Usa el extremo patch para actualizar un suscriptor en tu proyecto. Los campos que se pueden actualizar son endpointUri, subscriberConfigs y endpointAuthorization.

Para actualizar los campos, debes proporcionar un parámetro de consulta updateMask y un cuerpo de la solicitud. El updateMask debe contener una lista separada por comas de los nombres de los campos que deseas actualizar, con mayúsculas mediales para los nombres de los campos (por ejemplo, endpointUri). El cuerpo de la solicitud debe contener un objeto Subscriber parcial con los valores nuevos de los campos que deseas actualizar. Solo se actualizan los campos especificados en updateMask. Si proporcionas campos en el cuerpo de la solicitud que no están en updateMask, se ignoran.

Si actualizas endpointUri o endpointAuthorization, se realiza la verificación de extremos. Consulta Verificación de extremos para obtener más detalles.

Cuando actualices subscriberConfigs, ten en cuenta que es un reemplazo completo, no una combinación. Si se incluye subscriberConfigs en updateMask, todas las configuraciones almacenadas para ese suscriptor se reemplazan por la lista proporcionada en el cuerpo de la solicitud. Para agregar o quitar una configuración, debes proporcionar el conjunto completo de parámetros de configuración. Si actualizas otros campos y quieres mantener tus configuraciones actuales, omite subscriberConfigs de updateMask.

Solicitud

PATCH https://health.googleapis.com/v4/projects/project-id/subscribers/subscriber-id?updateMask=endpointUri
{
  "endpointUri": "https://myapp.com/new-webhooks/health"
}

Respuesta

{
  "name": "projects/project-id/subscribers/subscriber-id",
  "endpointUri": "https://myapp.com/new-webhooks/health",
  "subscriberConfigs": [
    {
      "dataTypes": ["steps", "altitude", "distance", "floors", "weight"],
      "subscriptionCreatePolicy": "AUTOMATIC"
    },
    {
      "dataTypes": ["sleep"],
      "subscriptionCreatePolicy": "MANUAL"
    }
  ]
}

Borra un suscriptor

Usa el extremo delete para quitar un suscriptor de tu proyecto. Una vez que se borre, el suscriptor ya no recibirá notificaciones.

Solicitud

DELETE https://health.googleapis.com/v4/projects/project-id/subscribers/subscriber-id

Respuesta

Si la eliminación se realiza correctamente, se devuelve un cuerpo de respuesta vacío con el estado HTTP `200 OK`.
{}

Verificación de extremos

Para garantizar la seguridad y confiabilidad de la entrega de notificaciones, la API de Google Health realiza un protocolo de enlace de verificación en dos pasos obligatorio cada vez que creas un suscriptor o actualizas su configuración de extremo (endpointUri o endpointAuthorization). Este proceso se realiza de forma síncrona durante la llamada a la API. El servicio envía dos solicitudes POST automatizadas al URI de tu extremo, con el usuario-agente Google-Health-API-Webhooks y el cuerpo JSON {"type": "verification"}.

  • Protocolo de enlace autorizado: La primera solicitud se envía con el encabezado Authorization configurado. Tu servidor debe responder con un estado 200 OK o 201 Created.
  • Desafío no autorizado: La segunda solicitud se envía sin credenciales. Tu servidor debe responder con un estado 401 Unauthorized o 403 Forbidden.

Este protocolo de enlace confirma que tu extremo está activo y aplica la seguridad de forma correcta. Si alguno de los pasos falla, la solicitud a la API falla con un error FAILED_PRECONDITION. Solo después de que este protocolo de enlace se realice correctamente, se guardará y activará tu suscriptor para recibir notificaciones de datos de salud.

Rotación de claves

Si necesitas rotar las claves de endpointAuthorization, sigue estos pasos:

  1. Configura tu extremo para que acepte los valores de endpointAuthorization antiguos y nuevos.
  2. Actualiza la configuración del suscriptor con el nuevo valor de endpointAuthorization con una solicitud patch con ?updateMask=endpointAuthorization.
  3. Configura tu extremo para que acepte solo el valor endpointAuthorization nuevo después de confirmar que el paso 2 se realizó correctamente.

Suscripciones del usuario

La API de Google Health te ayuda a administrar las suscripciones de los usuarios de manera eficiente, lo que reduce la necesidad de realizar un registro manual durante la incorporación de los usuarios.

Suscripciones automáticas

Te recomendamos que uses las suscripciones automáticas. Para habilitar esta función, establece subscriptionCreatePolicy en AUTOMATIC en tu subscriberConfigs para los tipos de datos específicos. Los dataTypes que especificas con una política AUTOMATIC son los mismos tipos de datos para los que la API de Google Health envía notificaciones, siempre que también se otorgue el consentimiento del usuario para esos tipos de datos.

Cuando un usuario otorga el consentimiento de la aplicación para los permisos que corresponden a los tipos de datos con una política AUTOMATIC, la API de Google Health realiza un seguimiento automático y envía notificaciones para los tipos de datos que resultan de la intersección entre los tipos de datos obtenidos con consentimiento del usuario y los tipos de datos de configuración de suscriptor automático para ese usuario. Luego, se envían notificaciones a tu extremo cada vez que ese usuario genera datos nuevos para esos tipos. Esto funciona para los usuarios que otorgan el consentimiento antes o después de que crees el suscriptor. Las notificaciones no se reabastecen para los datos generados antes de que se creara el suscriptor.

Si un usuario revoca el consentimiento, se detendrán las notificaciones de los tipos de datos correspondientes. Google administra las suscripciones automáticas, que no se pueden enumerar ni borrar de forma individual. Solo se quitan cuando se borra el suscriptor superior.

Suscripciones manuales

Si tu suscriptor está configurado con un subscription_create_policy MANUAL para tipos de datos específicos, debes crear y administrar suscripciones de forma explícita para cada usuario. Una suscripción vincula a un usuario específico con tu extremo de suscriptor para un conjunto definido de tipos de datos. Los desarrolladores pueden usar APIs específicas para lo siguiente:

  • Crear suscripciones (manuales) por healthUserId: Crea una suscripción nueva para un usuario específico. Este método requiere que el suscriptor tenga un SubscriptionCreatePolicy establecido en MANUAL para los tipos de datos solicitados.
  • Actualizar suscripción (manual): Actualiza los tipos de datos de una suscripción de usuario existente.
  • Borrar suscripción (manual): Borra una suscripción de usuario específica. Una vez borrado, tu extremo de suscriptor ya no recibirá notificaciones de este usuario para los tipos de datos asociados.
  • Enumera las suscripciones (manuales): Enumera todas las suscripciones activas de un suscriptor determinado. Puedes filtrar los resultados por usuario o tipo de datos.

Notificaciones

Cuando cambian los datos de un usuario para un tipo de datos suscrito, la API de Google Health envía una solicitud POST de HTTPS a la URL del extremo del suscriptor.

Formato de las notificaciones

La carga útil de la notificación contiene detalles sobre el cambio de datos, incluidos el ID de usuario, el tipo de datos y los intervalos de tiempo, que puedes usar para consultar los datos actualizados. Cuando se producen varias actualizaciones o se agregan actualizaciones, las notificaciones se agrupan por lotes y se entregan como un array JSON de objetos de notificación.

Cuando se activa una notificación de webhook, la API de Google Health envía una solicitud POST HTTPS a tu extremo de suscriptor con el siguiente formato:

Actualizar o insertar (datos de series temporales)

POST https://your-endpoint-uri
Authorization: Bearer example-secret-token
GOOGLE-HEALTH-API-SIGNATURE: signature-value
Content-Type: application/json

{
  "data": {
    "version": "1",
    "clientProvidedSubscriptionName": "subscription-name",
    "healthUserId": "health-user-id",
    "operation": "UPSERT",
    "dataType": "steps",
    "intervals": [
      {
        "physicalTimeInterval": {
          "startTime": "2026-03-08T01:29:00Z",
          "endTime": "2026-03-08T01:34:00Z"
        },
        "civilDateTimeInterval": {
          "startDateTime": {
            "date": {
              "year": 2026,
              "month": 3,
              "day": 7
            },
            "time": {
              "hours": 17,
              "minutes": 29
            }
          },
          "endDateTime": {
            "date": {
              "year": 2026,
              "month": 3,
              "day": 7
            },
            "time": {
              "hours": 17,
              "minutes": 34
            }
          }
        },
        "civilIso8601TimeInterval": {
          "startTime": "2026-03-07T17:29:00",
          "endTime": "2026-03-07T17:34:00"
        }
      }
    ]
  }
}

Borra (datos de series temporales)

POST https://your-endpoint-uri
Authorization: Bearer example-secret-token
GOOGLE-HEALTH-API-SIGNATURE: signature-value
Content-Type: application/json

[
  {
    "data": {
      "version": "1",
      "clientProvidedSubscriptionName": "subscription-name",
      "healthUserId": "health-user-id",
      "operation": "DELETE",
      "dataType": "steps",
      "intervals": [
        {
          "physicalTimeInterval": {
            "startTime": "2026-08-12T12:00:00Z",
            "endTime": "2026-08-12T12:30:00Z"
          },
          "civilIso8601TimeInterval": {
            "startTime": "2026-08-12T12:00:00",
            "endTime": "2026-08-12T12:30:00"
          }
        }
      ]
    }
  }
]

Borrar (datos identificables)

POST https://your-endpoint-uri
Authorization: Bearer example-secret-token
GOOGLE-HEALTH-API-SIGNATURE: signature-value
Content-Type: application/json

{
  "data": {
    "version": "1",
    "clientProvidedSubscriptionName": "subscription-name",
    "healthUserId": "health-user-id",
    "operation": "DELETE",
    "dataType": "sleep",
    "intervals": [
      {
        "physicalTimeInterval": {
          "startTime": "2026-08-12T22:00:00Z",
          "endTime": "2026-08-13T06:00:00Z"
        },
        "civilIso8601TimeInterval": {
          "startTime": "2026-08-12T22:00:00",
          "endTime": "2026-08-13T06:00:00"
        }
      }
    ],
    "recordId": "9875412"
  }
}

Por lotes (varios usuarios y tipos de datos)

POST https://your-endpoint-uri
Authorization: Bearer example-secret-token
GOOGLE-HEALTH-API-SIGNATURE: signature-value
Content-Type: application/json

[
  {
    "data": {
      "version": "1",
      "clientProvidedSubscriptionName": "auto-project-number-subscriber-id-WEBHOOK_DATA_TYPE_STEPS",
      "healthUserId": "health-user-id-1",
      "operation": "UPSERT",
      "dataType": "steps",
      "intervals": [
        {
          "physicalTimeInterval": {
            "startTime": "2026-03-08T01:29:00Z",
            "endTime": "2026-03-08T01:34:00Z"
          },
          "civilDateTimeInterval": {
            "startDateTime": {
              "date": {
                "year": 2026,
                "month": 3,
                "day": 7
              },
              "time": {
                "hours": 17,
                "minutes": 29
              }
            },
            "endDateTime": {
              "date": {
                "year": 2026,
                "month": 3,
                "day": 7
              },
              "time": {
                "hours": 17,
                "minutes": 34
              }
            }
          },
          "civilIso8601TimeInterval": {
            "startTime": "2026-03-07T17:29:00",
            "endTime": "2026-03-07T17:34:00"
          }
        }
      ]
    }
  },
  {
    "data": {
      "version": "1",
      "clientProvidedSubscriptionName": "auto-project-number-subscriber-id-WEBHOOK_DATA_TYPE_WEIGHT",
      "healthUserId": "health-user-id-1",
      "operation": "UPSERT",
      "dataType": "weight",
      "intervals": [
        {
          "physicalTimeInterval": {
            "startTime": "2026-03-07T15:14:00Z",
            "endTime": "2026-03-07T15:16:00Z"
          },
          "civilDateTimeInterval": {
            "startDateTime": {
              "date": {
                "year": 2026,
                "month": 3,
                "day": 7
              },
              "time": {
                "hours": 7,
                "minutes": 14
              }
            },
            "endDateTime": {
              "date": {
                "year": 2026,
                "month": 3,
                "day": 7
              },
              "time": {
                "hours": 7,
                "minutes": 16
              }
            }
          },
          "civilIso8601TimeInterval": {
            "startTime": "2026-03-07T07:14:00",
            "endTime": "2026-03-07T07:16:00"
          }
        }
      ]
    }
  },
  {
    "data": {
      "version": "1",
      "clientProvidedSubscriptionName": "auto-project-number-subscriber-id-WEBHOOK_DATA_TYPE_STEPS",
      "healthUserId": "health-user-id-2",
      "operation": "UPSERT",
      "dataType": "steps",
      "intervals": [
        {
          "civilDateTimeInterval": {
            "startDateTime": {
              "date": {
                "year": 2026,
                "month": 8,
                "day": 20
              },
              "time": {
                "hours": 0,
                "minutes": 0
              }
            },
            "endDateTime": {
              "date": {
                "year": 2026,
                "month": 8,
                "day": 21
              },
              "time": {
                "hours": 0,
                "minutes": 0
              }
            }
          },
          "civilIso8601TimeInterval": {
            "startTime": "2026-08-20T00:00:00",
            "endTime": "2026-08-21T00:00:00"
          }
        }
      ]
    }
  },
  {
    "data": {
      "version": "1",
      "clientProvidedSubscriptionName": "subscription-name",
      "healthUserId": "health-user-id-2",
      "operation": "UPSERT",
      "dataType": "sleep",
      "intervals": [
        {
          "physicalTimeInterval": {
            "startTime": "2026-08-12T22:00:00Z",
            "endTime": "2026-08-13T06:00:00Z"
          },
          "civilDateTimeInterval": {
            "startDateTime": {
              "date": {
                "year": 2026,
                "month": 8,
                "day": 12
              },
              "time": {
                "hours": 22,
                "minutes": 0
              }
            },
            "endDateTime": {
              "date": {
                "year": 2026,
                "month": 8,
                "day": 13
              },
              "time": {
                "hours": 6,
                "minutes": 0
              }
            }
          },
          "civilIso8601TimeInterval": {
            "startTime": "2026-08-12T22:00:00",
            "endTime": "2026-08-13T06:00:00"
          }
        }
      ],
      "recordId": "9875412"
    }
  }
]

El campo operation indica el tipo de cambio que activó la notificación:

  • UPSERT: Se envía para cualquier adición o modificación de datos.
  • DELETE: Se envía cuando un usuario borra datos de forma explícita (como borrar dataPoints con la app de Fitbit o realizar una eliminación por lotes con la API). No se envían notificaciones de webhook para eventos de eliminación a nivel del sistema, como cuando un usuario revoca los permisos de la aplicación o borra su cuenta de Fitbit.

Te recomendamos que hagas que tu lógica de control de notificaciones sea idempotente, en especial para las operaciones UPSERT, ya que los reintentos pueden provocar que se envíen notificaciones duplicadas.

El campo clientProvidedSubscriptionName es un identificador único:

  • En el caso de las suscripciones con una política MANUAL, este campo contiene el nombre de suscripción persistente proporcionado por el desarrollador que se especifica cuando se crea la suscripción. Esto proporciona un ID estable para administrar las suscripciones manuales.
  • Para las suscripciones creadas con una política AUTOMATIC, la API de Google Health genera y asigna automáticamente un identificador con el siguiente formato: auto-<project-number>-<subscriber-id>-<WEBHOOK_DATA_TYPE_TOKEN>, donde:
    • <project-number> es el número de proyecto de Google Cloud del desarrollador (por ejemplo, 362978535920).
    • <subscriber-id> es el ID de registro o el nombre del suscriptor (por ejemplo, google-health-sync).
    • <WEBHOOK_DATA_TYPE_TOKEN> es el token que representa el tipo de datos específico. Consulta Vocabulario de tokens de tipo de datos de webhook para conocer todos los tokens posibles.

Incluir clientProvidedSubscriptionName para las políticas manuales y automáticas garantiza un formato de carga útil de notificación coherente en todos los tipos de suscripción.

Asignación de ID de usuario

El healthUserId es un identificador de la API de Google Health para el usuario cuyos datos cambiaron. Si tu aplicación admite varios usuarios, puedes recibir notificaciones de cualquier usuario que haya otorgado su consentimiento a la aplicación. Cuando recibas una notificación, usa healthUserId para identificar qué datos del usuario cambiaron, de modo que puedas usar sus credenciales de OAuth para consultar sus datos.

Para asignar las credenciales de OAuth de un usuario a su healthUserId, usa el extremo getIdentity. Llama a este extremo con las credenciales de un usuario durante su incorporación para recuperar su healthUserId y almacenar esta asignación. Esta asignación no cambia con el tiempo, por lo que se puede almacenar en caché de forma indefinida. Para ver un ejemplo, consulta Obtén el ID de usuario. Esto te permite seleccionar las credenciales de usuario correctas cuando consultas datos según el healthUserId en una notificación.

Intervalos

El array intervals contiene uno o más objetos que representan los intervalos de tiempo de los datos modificados:

Campo Tipo Obligatorio Descripción
physicalTimeInterval object No Es el intervalo de tiempo físico en UTC de los datos modificados (formato YYYY-MM-DDTHH:MM:SSZ). Se completa para las métricas intradía y se omite para los resúmenes diarios y las actividades de todo el día.
civilDateTimeInterval object Sí Es el intervalo de fecha y hora del calendario civil (local) de los datos modificados, sin compensaciones de UTC ni de zona horaria.
civilIso8601TimeInterval object Sí Es el intervalo de tiempo civil (local) representado como una cadena ISO 8601 (formato YYYY-MM-DDTHH:MM:SS) sin compensaciones de UTC ni de zona horaria.

En el caso de las actividades de todo el día y los resúmenes diarios, el campo physicalTimeInterval se omite de forma intencional de la carga útil de la notificación. Esto se debe a que los eventos de día completo se agrupan por su fecha civil nominal, y las duraciones de tiempo físico pueden ser imprecisas cuando se cruzan zonas horarias. Cuando proceses eventos de todo el día, debes confiar en los campos civilDateTimeInterval o civilIso8601TimeInterval como práctica recomendada. Para obtener detalles sobre cómo consultar estos datos con filtros de hora civil, consulta Filtra datos por una hora de inicio civil de intervalo.

A continuación, se muestra un ejemplo de carga útil de notificación para una actividad de todo el día en la que se omite el campo physicalTimeInterval:

{
  "data": {
    "version": "1",
    "clientProvidedSubscriptionName": "auto-503461148219-lindasubscriber-WEBHOOK_DATA_TYPE_STEPS",
    "healthUserId": "343864966078399341",
    "operation": "UPSERT",
    "dataType": "steps",
    "intervals": [
      {
        "civilDateTimeInterval": {
          "startDateTime": {
            "date": {
              "year": 2026,
              "month": 7,
              "day": 14
            },
            "time": {}
          },
          "endDateTime": {
            "date": {
              "year": 2026,
              "month": 7,
              "day": 15
            },
            "time": {}
          }
        },
        "civilIso8601TimeInterval": {
          "startTime": "2026-07-14T00:00:00",
          "endTime": "2026-07-15T00:00:00"
        }
      }
    ]
  }
}

Vocabulario de tokens de tipo de datos de webhook

Cuando usas una política AUTOMATIC, el clientProvidedSubscriptionName generado contiene uno de los siguientes tokens de sufijo WEBHOOK_DATA_TYPE_* en mayúsculas, según el tipo de datos:

Token de tipo de datos de webhook Tipo de datos correspondiente
WEBHOOK_DATA_TYPE_ACTIVE_MINUTES active-minutes
WEBHOOK_DATA_TYPE_ACTIVE_ZONE_MINUTES active-zone-minutes
WEBHOOK_DATA_TYPE_ACTIVITY_LEVEL activity-level
WEBHOOK_DATA_TYPE_ALTITUDE altitude
WEBHOOK_DATA_TYPE_BLOOD_GLUCOSE blood-glucose
WEBHOOK_DATA_TYPE_BODY_FAT body-fat
WEBHOOK_DATA_TYPE_CALORIES_IN_HEART_RATE_ZONE calories-in-heart-rate-zone
WEBHOOK_DATA_TYPE_DAILY_HEART_RATE_VARIABILITY daily-heart-rate-variability
WEBHOOK_DATA_TYPE_DAILY_HEART_RATE_ZONES daily-heart-rate-zones
WEBHOOK_DATA_TYPE_DAILY_OXYGEN_SATURATION daily-oxygen-saturation
WEBHOOK_DATA_TYPE_DAILY_RESPIRATORY_RATE daily-respiratory-rate
WEBHOOK_DATA_TYPE_DAILY_RESTING_HEART_RATE daily-resting-heart-rate
WEBHOOK_DATA_TYPE_DAILY_SLEEP_TEMPERATURE_DERIVATIONS daily-sleep-temperature-derivations
WEBHOOK_DATA_TYPE_DISTANCE distance
WEBHOOK_DATA_TYPE_EXERCISE exercise
WEBHOOK_DATA_TYPE_FLOORS floors
WEBHOOK_DATA_TYPE_HEART_RATE heart-rate
WEBHOOK_DATA_TYPE_HEART_RATE_VARIABILITY heart-rate-variability
WEBHOOK_DATA_TYPE_HEIGHT height
WEBHOOK_DATA_TYPE_HYDRATION_LOG hydration-log
WEBHOOK_DATA_TYPE_NUTRITION_LOG nutrition-log
WEBHOOK_DATA_TYPE_RESPIRATORY_RATE_SLEEP_SUMMARY respiratory-rate-sleep-summary
WEBHOOK_DATA_TYPE_RUN_VO2_MAX run-vo2-max
WEBHOOK_DATA_TYPE_SEDENTARY_PERIOD sedentary-period
WEBHOOK_DATA_TYPE_SLEEP sleep
WEBHOOK_DATA_TYPE_STEPS steps
WEBHOOK_DATA_TYPE_TIME_IN_HEART_RATE_ZONE time-in-heart-rate-zone
WEBHOOK_DATA_TYPE_WEIGHT weight
WEBHOOK_DATA_TYPE_USER_DELETED user-deleted (evento del sistema)
WEBHOOK_DATA_TYPE_USER_REVOKED_ACCESS user-revoked-access (evento del sistema)

Agregación de actualizaciones

Para minimizar el tráfico de red, la API de Google Health agrupa los mensajes. El sistema está configurado para enviar un máximo de 99 mensajes por lote, que se envían automáticamente a medida que están disponibles.

Cuando los mensajes se envían por lotes, la carga útil se entrega como un array JSON que contiene varios objetos de notificación. Un solo lote puede incluir actualizaciones para varios usuarios en varios tipos de datos suscritos. Para ver un ejemplo de carga útil, consulta el ejemplo de Lote (varios usuarios y tipos de datos) en la sección Formato de notificación.

Responde a una notificación

Tu servidor debe validar el encabezado Authorization, controlar el protocolo de enlace síncrono de la verificación de extremos y responder a las notificaciones de cambio de datos con un código de estado HTTP 204 No Content de inmediato. Para evitar tiempos de espera, procesa la carga útil de la notificación de forma asíncrona después de enviar la respuesta. Si la API de Google Health recibe cualquier otro código de estado o se agota el tiempo de espera de la solicitud, volverá a intentar enviar la notificación más tarde.

Ejemplo de Node.js (Express):

app.post('/webhook-receiver', (req, res) => {
    if (req.headers.authorization !== EXPECTED_SECRET) {
        return res.status(401).send();
    }
    if (req.body && !Array.isArray(req.body) && req.body.type === 'verification') {
        return res.status(200).send();
    }

    // 1. Immediately acknowledge the notification batch
    res.status(204).send();

    // 2. Normalize single-object or batched array notifications and reconcile asynchronously
    const notifications = Array.isArray(req.body) ? req.body : [req.body];
    setImmediate(async () => {
        for (const notification of notifications) {
            const { healthUserId, dataType, intervals = [] } = notification.data || {};
            console.log(`Update for user ${healthUserId} of type ${dataType}`);
            // Query /v4/users/${healthUserId}/dataTypes/${dataType}/dataPoints:reconcile?filter=...
            // for each interval in `intervals` (using >= and < comparators; for sleep, filter on
            // sleep.interval.end_time or sleep.interval.civil_end_time).
        }
    });
});

Para recuperar los datos actualizados de cada intervalo notificado, llama al extremo reconcile con un filtro para el período del intervalo. Esto anula automáticamente los intervalos superpuestos de los dispositivos conectados. Para obtener detalles y ejemplos, consulta Obtén una vista conciliada de los datos de intervalos.

Existen tres estrategias comunes para actualizar tu base de datos después de recibir una notificación de cambio de datos:

  • Colas (recomendado): Coloca las notificaciones entrantes en una cola de mensajes y procésalas fuera de la cola según los recursos del sistema.
  • Recuperación inmediata: Dirige tu backend para que llame de inmediato a la API de Google Health para recuperar los datos actualizados y confirmarlos en tu base de datos.
  • Actualizaciones diferidas: Marca los registros de la base de datos local del usuario como modificados y solo recupera los datos actualizados de la API de Google Health cuando el usuario vuelva a visitar tu aplicación.

La estrategia de cola te permite procesar datos casi en tiempo real y, al mismo tiempo, mantener la resiliencia ante una gran cantidad de notificaciones (por ejemplo, si una interrupción del servicio provoca que se entregue una gran cantidad de notificaciones a la vez). Si bien puedes elegir procesar estas colas en un programa por lotes de intervalos largos (por ejemplo, cada 3 horas) para ahorrar recursos, la desventaja es que los usuarios suelen esperar ver sus datos actualizados casi en tiempo real.

Prácticas recomendadas

Sigue estas prácticas recomendadas para garantizar un manejo confiable y eficiente de las actualizaciones de datos:

  • Usa la API de Subscription: Usa la API de Subscription para recibir notificaciones cuando un usuario tenga datos nuevos para descargar, en lugar de sondear extremos.
  • Pon en cola y procesa las notificaciones por separado: Cuando recibas notificaciones de suscripción, colócalas en una cola y procésalas por separado según los recursos de tu sistema. Mantén el extremo del webhook ligero confirmando la solicitud de inmediato.

Verificación de firmas

Para garantizar la autenticidad de las notificaciones de webhooks, la carga útil de JSON sin procesar de cada notificación de webhook saliente se firma con una clave privada usando PublicKeySign de Tink, lo que proporciona la firma codificada en Base64 en el encabezado HTTP GOOGLE-HEALTH-API-SIGNATURE de la solicitud. Estas claves de firma se rotan automáticamente cada 30 días, y el conjunto de claves públicas oficial correspondiente se distribuye como un archivo JSON en la URL permanente https://www.gstatic.com/googlehealthapi/webhooks/webhooks_public_keyset.json.

Cómo verificar la firma

Con Tink (recomendado): Los desarrolladores pueden verificar la firma con la primitiva PublicKeyVerify de Tink. Recupera el conjunto de claves públicas de la URL permanente, crea una instancia de la primitiva PublicKeyVerify con el conjunto de claves y verifica el encabezado GOOGLE-HEALTH-API-SIGNATURE decodificado con la carga útil de JSON del webhook sin procesar.

Verificación manual (sin Tink): Si los desarrolladores deciden no usar Tink, pueden verificar la firma de forma manual siguiendo estos pasos:

  1. Decodifica en Base64 el encabezado GOOGLE-HEALTH-API-SIGNATURE para separar el prefijo de Tink de 5 bytes (que contiene un prefijo de versión de 1 byte y un keyId de 4 bytes) de la firma real codificada en DER.
  2. Recupera el conjunto de claves JSON de https://www.gstatic.com/googlehealthapi/webhooks/webhooks_public_keyset.json.
  3. Ubica la clave que coincida con el keyId analizado y decodifica en Base64 su campo de valor, que contiene un búfer de protocolo EcdsaPublicKey serializado.
  4. Extrae las coordenadas x e y big-endian (etiquetas de Protobuf 3 y 4) de esta carga útil binaria.
  5. Crea una instancia de una clave pública ECDSA P-256 estándar en una biblioteca de criptografía integrada con las coordenadas x e y extraídas.
  6. Verifica la carga útil de JSON del webhook sin procesar con la firma DER extraída con el algoritmo SHA-256.

Estado y recuperación de los suscriptores

Si tu extremo de suscriptor deja de estar disponible o devuelve un código de estado de error (cualquier valor que no sea 204), la API de Google Health almacena las notificaciones pendientes durante un máximo de 7 días y vuelve a intentar la entrega con una retirada exponencial.

Una vez que tu extremo vuelva a estar en línea y responda con 204, la API entregará automáticamente la lista de mensajes almacenados. Las notificaciones con más de 7 días de antigüedad se descartan y no se pueden recuperar.

Errores comunes

Código de error Mensaje Descripción Recomendación
400 Bad Request Número de proyecto no válido en el nombre del recurso Cuando borras o actualizas un suscriptor con el ID del proyecto de Google Cloud en la URL de la solicitud en lugar del número del proyecto. Esto se aplica a las suscripciones de webhook que usan el extremo projects.subscribers. Usa el número de tu proyecto de Google Cloud en la URL de la solicitud, no el ID del proyecto.
403 Forbidden El emisor no tiene permiso Cuando creas o enumeras suscriptores con el ID del proyecto de Google Cloud en la URL de la solicitud en lugar del número del proyecto. Esto se aplica a las suscripciones de webhook que usan el extremo projects.subscribers. Usa el número de tu proyecto de Google Cloud en la URL de la solicitud, no el ID del proyecto.