网络钩子订阅

借助 Google Health API,您的应用可以在用户的健康数据发生变化时收到实时通知。当 Google Health API 中有可用数据时,您的服务器会立即收到 HTTPS POST 请求 Webhook,而无需轮询更改。

支持的数据类型

以下数据类型支持 Webhook 通知:

表:支持 Webhook 的 Google Health API 数据类型
数据类型 可用的
操作
范围
活跃分钟数
dataType: active-minutes
过滤条件参数: active_minutes
记录类型: 间隔
存储分辨率 :1 分钟

兼容的设备

  • 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
活跃区间分钟数
dataType: active-zone-minutes
过滤条件参数: active_zone_minutes
记录类型: 间隔
存储分辨率 :1 分钟

兼容的设备

list、reconcile、rollup、dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
活动级别
dataType: activity-level
过滤条件参数: activity_level
记录类型: 间隔
list、reconcile .activity_and_fitness.readonly
.activity_and_fitness.writeonly
海拔高度
dataType: altitude
过滤条件参数: altitude
记录类型: 间隔
存储分辨率 :1 分钟
list、reconcile、rollup、dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
血糖
dataType: blood-glucose
过滤条件参数: blood_glucose
记录类型: 示例
list、get、reconcile、rollup、dailyRollup .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
体脂率
dataType: body-fat
过滤条件参数: body_fat
记录类型: 示例

兼容的设备

list、get、reconcile、rollup、dailyRollup、create、update、batchDelete .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
在心率区间内消耗的卡路里
dataType: calories-in-heart-rate-zone
过滤条件参数: calories_in_heart_rate_zone
记录类型: 间隔
存储分辨率 :1 分钟
rollup、dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
每日心率变异性
dataType: daily-heart-rate-variability
过滤条件参数: daily_heart_rate_variability
记录类型: 每日

兼容的设备

list、reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
每日心率区间
dataType: daily-heart-rate-zones
过滤条件参数: daily_heart_rate_zones
记录类型: 每日
list、reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
每日血氧饱和度
dataType: daily-oxygen-saturation
过滤条件参数: daily_oxygen_saturation
记录类型: 每日

兼容的设备

list、reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
每日呼吸频率
dataType: daily-respiratory-rate
过滤条件参数: daily_respiratory_rate
记录类型: 每日

兼容的设备

list、reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
每日静息心率
dataType: daily-resting-heart-rate
过滤条件参数: daily_resting_heart_rate
记录类型: 每日

兼容的设备

list、reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
每日睡眠温度偏差
dataType: daily-sleep-temperature-derivations
过滤条件参数: daily_sleep_temperature_derivations
记录类型: 每日

兼容的设备

list、reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
距离
dataType: distance
过滤条件参数: distance
记录类型: 间隔
存储分辨率 :1 分钟

兼容的设备

list、reconcile、rollup、dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
锻炼
dataType: exercise
过滤条件参数: exercise
记录类型: 练习

兼容的设备

list、get、reconcile、create、update、batchDelete .activity_and_fitness.readonly
.activity_and_fitness.writeonly
爬楼层数
dataType: floors
过滤条件参数: floors
记录类型: 间隔
存储分辨率 :1 分钟
reconcile、rollup、dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
心率
dataType: heart-rate
过滤条件参数: heart_rate
记录类型: 示例
存储分辨率 :1 秒

兼容的设备

list、reconcile、rollup、dailyRollup .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
心率变异性
dataType: heart-rate-variability
过滤条件参数: heart_rate_variability
记录类型: 示例

兼容的设备

list、reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
身高
dataType: height
过滤条件参数: height
记录类型: 示例
list、get、reconcile、create、update、batchDelete .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
饮水量记录
dataType: hydration-log
过滤条件参数: hydration_log
记录类型: 练习
list、get、reconcile、rollup、dailyRollup、create、update、batchDelete .nutrition.readonly
.nutrition.writeonly
营养记录
dataType: nutrition-log
过滤条件参数: nutrition_log
记录类型: 练习

兼容的设备

list、get、reconcile、rollup、dailyRollup、create、update、batchDelete .nutrition.readonly
.nutrition.writeonly
呼吸频率睡眠状况摘要
dataType: respiratory-rate-sleep-summary
过滤条件参数: respiratory_rate_sleep_summary
记录类型: 示例

兼容的设备

list、reconcile .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
跑步最大摄氧量
dataType: run-vo2-max
过滤条件参数: run_vo2_max
记录类型: 示例

兼容的设备

list、reconcile、rollup、dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
久坐时段
dataType: sedentary-period
过滤条件参数: sedentary_period
记录类型: 间隔

兼容的设备

list、reconcile、rollup、dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
睡眠
dataType: sleep
过滤条件参数: sleep
记录类型: 练习

兼容的设备

list、get、reconcile、create、update、batchDelete .sleep.readonly
.sleep.writeonly
步骤
dataType: steps
过滤条件参数: steps
记录类型: 间隔
存储分辨率 :1 分钟

兼容的设备

list、reconcile、rollup、dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
处于各心率区间的时长
dataType: time-in-heart-rate-zone
过滤条件参数: time_in_heart_rate_zone
记录类型: 间隔
存储分辨率 :1 分钟
list、reconcile、rollup、dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
重量
dataType: weight
过滤条件参数: weight
记录类型: 示例

兼容的设备

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

通知范围

仅当用户已针对其中一个相应范围授予同意时,系统才会针对这些数据类型发送通知:

  • 活动,涵盖活跃分钟数、步数、海拔高度、距离和楼层数据类型:
    • https://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonly
    • https://www.googleapis.com/auth/googlehealth.activity_and_fitness.writeonly
  • 健康指标,涵盖体重数据类型:
    • https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly
    • https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.writeonly
  • 睡眠,涵盖以下睡眠数据类型:
    • https://www.googleapis.com/auth/googlehealth.sleep.readonly
    • https://www.googleapis.com/auth/googlehealth.sleep.writeonly

IAM 服务账号

虽然并非必需,但我们建议您在为 Google Health API 配置订阅者时使用 IAM 服务账号。与标准用户账号相比,服务账号通过以下功能为应用工作负载提供更好的安全性:

  • 自动生成的短期凭证:服务账号附加到 Google Cloud 执行环境(例如 Compute Engine、Cloud Run 或 Google Kubernetes Engine)后,会自动获取并轮替安全的短期凭证。这样可以消除管理和存储永久性静态密钥的风险。
  • 最小权限原则:服务账号为工作负载提供专用身份。您可以仅向其授予管理订阅者端点所需的特定权限,从而避免其对您的 Google Cloud 资源拥有更广泛的访问权限。
  • 生命周期独立性:服务账号独立于任何个人用户账号运行,可确保人员变动不会影响长期身份验证稳定性。

设置服务账号

如需将订阅者应用配置为使用服务账号进行身份验证,请执行以下操作:

  1. 创建服务账号:在 Google Cloud 控制台中,前往项目的“IAM 和管理”页面,创建一个新的用户管理的服务账号。
  2. 授予必要的 IAM 角色: 为服务账号分配管理 Google Cloud 项目中的订阅者所需的适当角色。
  3. 将服务账号附加到工作负载: 将托管订阅者逻辑的环境配置为以新服务账号身份运行。这样,您的应用代码(例如 Google API 客户端库)在调用 projects.subscribers REST API 时,便可以自动检测并使用服务账号的短期有效凭证。

CPE 角色

如需管理 Google Health API 订阅者或订阅,您必须向进行 API 调用的模拟服务账号授予适当的角色。 根据所需的访问权限级别,分配以下角色之一:

  • Google Health API Read
  • Google Health API Editor
  • Google Health API Admin

详细了解 Google Health API IAM 角色和权限。

管理订阅者

您必须先注册一个订阅者(代表应用的通知端点),然后才能接收通知。您可以使用 projects.subscribers 提供的 REST API 管理订阅者。

您的订阅者端点必须使用 HTTPS (TLSv1.2+) 且可公开访问。在创建和更新订阅者期间,Google Health API 会执行验证质询,以确保您拥有端点 URI。如果验证失败,则订阅者创建和更新操作将失败,并显示 FailedPreconditionException。

创建订阅者

如需为项目注册新订阅者,请使用 create 端点。您需要提供以下信息:

  • project-id:在其中创建了 webhook 服务账号的项目编号。
  • subscriberId:您为订阅者提供的唯一标识符。此 ID 必须介于 4 到 36 个字符之间,并且与正则表达式 ([a-z]([a-z0-9-]{2,34}[a-z0-9])) 匹配。
  • endpointUri:Webhook 通知的目标网址。
  • subscriberConfigs:您希望接收通知的数据类型,以及每种数据类型的订阅政策。
  • endpointAuthorization:端点的授权机制。此文件必须包含您提供的 secret。secret 的值会随每条通知消息在 Authorization 标头中发送。您可以使用此令牌验证传入请求是否来自 Google Health API。例如,您可以将 secret 设置为 Bearer R4nd0m5tr1ng123(对于 Bearer 身份验证)或 Basic dXNlcjpwYXNzd29yZA==(对于基本身份验证)。

在 subscriberConfigs 中,您必须为每种数据类型设置 subscriptionCreatePolicy。如果您要使用自动订阅,请将其设置为 AUTOMATIC;如果您打算自行管理用户订阅,请将其设置为 MANUAL。如需详细了解每个选项,请参阅自动订阅和手动订阅。

请求

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

响应

{
  "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"
    }
  ]
}

列出订阅者

使用 list 端点检索为您的项目注册的所有订阅者。

请求

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

响应

{
  "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
}

更新订阅者

使用 patch 端点更新项目中的订阅者。可以更新的字段包括 endpointUri、subscriberConfigs 和 endpointAuthorization。

您可以通过提供 updateMask 查询参数和请求正文来更新字段。updateMask 必须包含要更新的字段名称的英文逗号分隔列表,并使用驼峰命名法命名这些字段(例如 endpointUri)。请求正文必须包含一个部分 Subscriber 对象,其中包含要更新的字段的新值。只有 updateMask 中指定的字段会被更新。如果您在请求正文中提供的字段不在 updateMask 中,则系统会忽略这些字段。

如果您更新 endpointUri 或 endpointAuthorization,系统会执行端点验证。如需了解详情,请参阅端点验证。

更新 subscriberConfigs 时,请注意,这是完全替换,而不是合并。如果 subscriberConfigs 包含在 updateMask 中,则该订阅者的所有存储配置都会被请求正文中提供的列表覆盖。如需添加或移除配置,您必须提供完整的配置集。如果您要更新其他字段并希望保留当前配置,请从 updateMask 中省略 subscriberConfigs。

请求

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

响应

{
  "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"
    }
  ]
}

删除订阅者

使用 delete 端点从项目中移除订阅者。删除后,订阅者将不再收到通知。

请求

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

响应

如果删除成功,系统会返回一个 HTTP 状态为 `200 OK` 的空响应正文。
{}

端点验证

为确保通知传送的安全性和可靠性,每当您创建订阅者或更新其端点配置(endpointUri 或 endpointAuthorization)时,Google Health API 都会执行强制性的两步验证握手。此过程在 API 调用期间同步执行。该服务会使用 User-Agent Google-Health-API-Webhooks 向您的端点 URI 发送两个自动 POST 请求,其中包含 JSON 正文 {"type": "verification"}。

  • 授权握手:第一个请求会以您配置的 Authorization 标头发送。您的服务器必须以 200 OK 或 201 Created 状态进行响应。
  • 未经授权的质询:第二个请求在不提供凭据的情况下发送。您的服务器必须返回 401 Unauthorized 或 403 Forbidden 状态。

此握手可确认您的端点处于活跃状态,并且正确实施了安全措施。如果任一步骤失败,API 请求都会失败并显示 FAILED_PRECONDITION 错误。只有在此握手成功后,系统才会保存并激活您的订阅者,以便接收健康数据通知。

密钥轮替

如果您需要轮替 endpointAuthorization 的密钥,请按以下步骤操作:

  1. 将端点配置为同时接受新旧 endpointAuthorization 值。
  2. 使用带有 ?updateMask=endpointAuthorization 的 patch 请求,通过新的 endpointAuthorization 值更新订阅者配置。
  3. 确认第 2 步成功后,将端点配置为仅接受新的 endpointAuthorization 值。

用户订阅

Google Health API 可帮助您高效管理用户订阅,从而减少用户新手入门期间的手动注册需求。

自动订阅

我们建议您使用自动订阅。如需启用此功能,请在 subscriberConfigs 中将特定数据类型的 subscriptionCreatePolicy 设置为 AUTOMATIC。您使用 AUTOMATIC 政策指定的 dataTypes 与 Google Health API 发送通知的数据类型相同,前提是用户也同意这些数据类型。

当用户针对与具有 AUTOMATIC 政策的数据类型对应的范围授予应用权限时,Google Health API 会自动跟踪用户同意的数据类型与该用户的自动订阅者配置数据类型的交集所产生的数据类型,并发送通知。然后,每当该用户为这些类型生成新数据时,系统都会向您的端点发送通知。这适用于在您创建订阅者之前或之后授予同意的用户。对于在订阅者创建之前生成的数据,系统不会回填通知。

如果用户撤消同意,系统将停止发送相应数据类型的通知。自动订阅由 Google 管理,无法单独列出或删除;只有在删除父级订阅者时,系统才会移除自动订阅。

手动订阅

如果您的订阅者针对特定数据类型配置了 MANUAL subscription_create_policy,您必须为每个用户明确创建和管理订阅。订阅关联会将特定用户与您的订阅者端点关联起来,以便接收一组指定的数据类型。开发者可以使用特定 API 执行以下操作:

  • 创建(手动)订阅(按 healthUserId)- 为特定用户创建新订阅。此方法要求订阅者针对请求的数据类型将 SubscriptionCreatePolicy 设置为 MANUAL。
  • 更新(手动)订阅 - 更新现有用户订阅的数据类型。
  • 删除(手动)订阅 - 删除特定用户订阅。删除后,您的订阅者端点将不再收到此用户针对关联数据类型的通知。
  • 列出(手动)订阅 - 列出指定订阅者的所有有效订阅。您可以按用户或数据类型过滤结果。

通知

当用户的数据针对订阅的数据类型发生变化时,Google Health API 会向订阅者端点网址发送 HTTPS POST 请求。

通知格式

通知载荷包含有关数据更改的详细信息,包括用户 ID、数据类型和时间间隔,您可以使用这些信息查询更新后的数据。当发生多项更新或更新被汇总时,通知会批量发送,并以通知对象的 JSON 数组形式传送。

触发网络钩子通知后,Google Health API 会向您的订阅者端点发送 HTTPS POST 请求,格式如下:

Upsert(时序数据)

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"
        }
      }
    ]
  }
}

删除(时间序列数据)

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"
          }
        }
      ]
    }
  }
]

删除(可识别的数据)

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

批量(多用户和数据类型)

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"
    }
  }
]

operation 字段指示触发通知的更改类型:

  • UPSERT:针对任何数据添加或修改发送。
  • DELETE:在用户明确删除数据(例如使用 Fitbit 应用删除dataPoints或使用 API 执行批量删除)时发送。系统级删除事件(例如用户撤消应用权限或删除其 Fitbit 账号)不会发送 Webhook 通知。

我们建议您将通知处理逻辑设置为幂等,尤其是对于 UPSERT 操作,因为重试可能会导致发送重复的通知。

clientProvidedSubscriptionName 字段是唯一标识符:

  • 对于具有 MANUAL 政策的订阅,此字段包含在创建订阅时指定的由开发者提供的永久性订阅名称。这为管理手动订阅提供了稳定的 ID。
  • 对于使用 AUTOMATIC 政策创建的订阅,Google Health API 会自动生成并分配一个格式为 auto-<project-number>-<subscriber-id>-<WEBHOOK_DATA_TYPE_TOKEN> 的标识符,其中:
    • <project-number> 是开发者的 Google Cloud 项目编号(例如 362978535920)。
    • <subscriber-id> 是订阅者的注册 ID / 订阅者名称(例如 google-health-sync)。
    • <WEBHOOK_DATA_TYPE_TOKEN> 是表示特定数据类型的令牌。如需了解所有可能的令牌,请参阅 Webhook 数据类型令牌的词汇表。

为手动和自动政策添加 clientProvidedSubscriptionName 可确保所有订阅类型的通知载荷格式一致。

用户 ID 映射

healthUserId 是 Google Health API 标识符,用于标识数据发生更改的用户。如果您的应用支持多用户,您可以接收任何已向您的应用授予同意的用户发出的通知。收到通知后,请使用 healthUserId 确定哪些用户的数据发生了更改,以便您可以使用其 OAuth 凭据查询其数据。

如需将用户的 OAuth 凭据映射到其 healthUserId,请使用 getIdentity 端点。在用户初始配置期间,使用用户的凭据调用此端点,以检索其 healthUserId,并存储此映射。此映射不会随时间而变化,因此可以无限期缓存。如需查看示例,请参阅获取用户 ID。这样,您就可以在根据通知中的 healthUserId 查询数据时选择正确的用户凭据。

时间间隔

intervals 数组包含一个或多个对象,这些对象表示修改后的数据的时间间隔:

字段 类型 是否必需 说明
physicalTimeInterval object 否 修改后的数据的 UTC 物理时间间隔(YYYY-MM-DDTHH:MM:SSZ 格式)。针对当日指标填充;针对每日汇总和全天活动省略。
civilDateTimeInterval object 是 修改后的数据的民用(本地)日历日期和时间间隔,不含世界协调时间 (UTC) 或时区偏移。
civilIso8601TimeInterval object 是 以 ISO 8601 字符串(YYYY-MM-DDTHH:MM:SS 格式)表示的民用(本地)时间间隔,不含世界协调时间 (UTC) 或时区偏移量。

对于全天活动和每日汇总,通知载荷中会特意省略 physicalTimeInterval 字段。这是因为全天活动是按其名义上的民用日期进行分桶的,并且在跨时区时,实际时长可能不准确。处理全天活动时,最佳做法是依赖 civilDateTimeInterval 或 civilIso8601TimeInterval 字段。如需详细了解如何使用民用时间过滤条件查询这些数据点,请参阅按间隔民用开始时间过滤数据。

以下是省略了 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"
        }
      }
    ]
  }
}

Webhook 数据类型令牌的词汇表

使用 AUTOMATIC 政策时,生成的 clientProvidedSubscriptionName 包含以下大写 WEBHOOK_DATA_TYPE_* 后缀词元之一,具体取决于数据类型:

Webhook 数据类型令牌 对应的数据类型
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 (系统事件)
WEBHOOK_DATA_TYPE_USER_REVOKED_ACCESS user-revoked-access (系统事件)

更新汇总

为了最大限度地减少网络流量,Google Health API 会将消息批量处理。系统配置为每批最多发送 99 条消息,这些消息会在可用时自动推送。

批量发送消息时,载荷会以包含多个通知对象的 JSON 数组形式传送。一个批次可以包含多个用户在多种订阅数据类型中的更新。如需查看载荷示例,请参阅“通知格式”部分中的批量(多用户和数据类型)示例。

回应通知

您的服务器必须验证 Authorization 标头,处理同步端点验证握手,并立即使用 HTTP 204 No Content 状态代码响应数据更改通知。为避免超时,请在发送响应后异步处理通知载荷。如果 Google Health API 收到任何其他状态代码或请求超时,则会稍后重试发送通知。

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).
        }
    });
});

如需提取每个通知时间间隔的更新数据,请使用时间间隔的时间范围过滤条件调用 reconcile 端点。这会自动对已连接的设备中的重叠时间间隔进行去重。如需了解详情和示例,请参阅获取间隔数据的对账视图。

收到数据更改通知后,有三种常见的数据库更新策略:

  • 排队(推荐):将传入的通知放入消息队列中,并根据系统资源从队列中处理这些通知。
  • 立即检索:指示后端立即调用 Google Health API,以提取更新后的数据并将其提交到数据库。
  • 延迟更新:将用户的本地数据库记录标记为脏数据,并且仅在用户下次访问您的应用时,才从 Google Health API 提取更新后的数据。

借助排队策略,您可以近乎实时地处理数据,同时保持对大量通知的弹性(例如,如果服务中断导致积压的通知一次性全部传送)。虽然您可以选择按长间隔批处理时间表(例如每 3 小时)处理这些队列以节省资源,但缺点是用户通常希望看到他们的数据近乎实时地更新。

最佳做法

请遵循以下最佳实践,确保可靠、高效地处理数据更新:

  • 使用 Subscription API:使用 Subscription API,在用户有新数据可供下载时收到通知,而不是轮询端点。
  • 单独将通知加入队列并进行处理:收到订阅通知后,将其放入队列中,并根据系统资源单独进行处理。通过立即确认请求,使 webhook 端点保持轻量。

签名验证

为确保 Webhook 通知的真实性,每个传出 Webhook 通知的原始 JSON 载荷都会使用 Tink 的 PublicKeySign 通过私钥进行签名,并在请求的 GOOGLE-HEALTH-API-SIGNATURE HTTP 标头中提供 Base64 编码的签名。这些签名密钥每 30 天自动轮替一次,相应的官方公钥集会以 JSON 文件的形式分发到永久网址 https://www.gstatic.com/googlehealthapi/webhooks/webhooks_public_keyset.json。

如何验证签名

使用 Tink(推荐):开发者可以使用 Tink 的 PublicKeyVerify 基元来验证签名。从永久网址提取公钥集,使用该密钥集实例化 PublicKeyVerify 原语,然后根据原始 webhook JSON 载荷验证解码后的 GOOGLE-HEALTH-API-SIGNATURE 标头。

手动验证(不使用 Tink):如果开发者选择不使用 Tink,可以按照以下步骤手动验证签名:

  1. 对 GOOGLE-HEALTH-API-SIGNATURE 标头进行 Base64 解码,以将 5 字节的 Tink 前缀(包含 1 字节的版本前缀和 4 字节的 keyId)与实际的 DER 编码签名分开。
  2. 从 https://www.gstatic.com/googlehealthapi/webhooks/webhooks_public_keyset.json 提取 JSON 密钥集。
  3. 找到与解析的 keyId 匹配的密钥,并对其值字段进行 Base64 解码,该字段包含序列化的 EcdsaPublicKey 协议缓冲区。
  4. 从此二进制载荷中提取大端序 x 和 y 坐标(Protobuf 标记 3 和 4)。
  5. 使用提取的 x 和 y 坐标在内置加密库中实例化标准 ECDSA P-256 公钥。
  6. 使用 SHA-256 算法,根据提取的 DER 签名验证原始 webhook JSON 载荷。

订阅者状态和恢复

如果您的订阅者端点不可用或返回错误状态代码(204 以外的任何代码),Google Health API 会将待处理通知最多存储 7 天,并使用指数退避算法重试传送。

当您的端点恢复在线状态并返回 204 时,API 会自动传送积压的已存储消息。超过 7 天的通知会被舍弃,且无法恢复。

常见错误

错误代码 消息 说明 建议
400 无效请求 资源名称中的项目编号无效 在请求网址中使用 Google Cloud 项目 ID(而非项目编号)删除或更新订阅者时。这适用于使用 projects.subscribers 端点的 webhook 订阅。 在请求网址中使用 Google Cloud 项目编号,而不是项目 ID。
403 禁止访问 调用方无权限 在请求网址中使用 Google Cloud 项目 ID(而非项目编号)创建或列出订阅者时。这适用于使用 projects.subscribers 端点的 webhook 订阅。 在请求网址中使用 Google Cloud 项目编号,而不是项目 ID。