מינויים ל-webhook

‫Google Health API מאפשר לאפליקציה לקבל התראות בזמן אמת כשנתוני הבריאות וכושר של המשתמש משתנים. במקום לבדוק אם יש שינויים, השרת מקבל webhook של בקשת HTTPS POST ברגע שהנתונים זמינים ב-Google Health API.

סוגי נתונים נתמכים

אפשר להשתמש בהתראות Webhook בסוגי הנתונים הבאים:

טבלה: סוגי נתונים של Google Health API עם תמיכה ב-webhook
סוג הנתונים פעולות
זמינות
היקף
דקות פעילות
dataType: active-minutes
filter parameter: active_minutes
סוג הרשומה: אינטרוולים
רזולוציית אחסון: דקה אחת

מכשירים תואמים

  • 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
filter parameter: active_zone_minutes
סוג הרשומה: אינטרוולים
רזולוציית אחסון: דקה אחת

מכשירים תואמים

list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
רמת הפעילות
dataType: activity-level
filter parameter: activity_level
סוג הרשומה: אינטרוולים
רשימה, התאמה .activity_and_fitness.readonly
.activity_and_fitness.writeonly
Altitude
dataType: altitude
filter parameter: altitude
סוג הרשומה: אינטרוולים
רזולוציית אחסון: דקה אחת
list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
רמת סוכר בדם
list, get, reconcile, rollup, dailyRollup .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
אחוז שומן בגוף
dataType: body-fat
filter parameter: 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
filter parameter: calories_in_heart_rate_zone
סוג הרשומה: אינטרוולים
רזולוציית אחסון: דקה אחת
rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
שונות קצב הלב היומית
dataType: daily-heart-rate-variability
filter parameter: daily_heart_rate_variability
סוג הרשומה: יומי

מכשירים תואמים

רשימה, התאמה .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
טווחי דופק יומיים
dataType: daily-heart-rate-zones
filter parameter: daily_heart_rate_zones
סוג הרשומה: יומי
רשימה, התאמה .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
רמת החמצן בדם מדי יום
dataType: daily-oxygen-saturation
filter parameter: daily_oxygen_saturation
סוג הרשומה: יומי

מכשירים תואמים

רשימה, התאמה .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
קצב הנשימה היומי
dataType: daily-respiratory-rate
filter parameter: daily_respiratory_rate
סוג הרשומה: יומי

מכשירים תואמים

רשימה, התאמה .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
הדופק היומי במנוחה
dataType: daily-resting-heart-rate
filter parameter: daily_resting_heart_rate
סוג הרשומה: יומי

מכשירים תואמים

רשימה, התאמה .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
סטיות יומיות בטמפרטורת השינה
dataType: daily-sleep-temperature-derivations
filter parameter: daily_sleep_temperature_derivations
סוג הרשומה: יומי

מכשירים תואמים

רשימה, התאמה .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
מרחק
dataType: distance
filter parameter: distance
סוג הרשומה: אינטרוולים
רזולוציית אחסון: דקה אחת

מכשירים תואמים

list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
פעילות גופנית
dataType: exercise
filter parameter: exercise
סוג הרשומה: סשן

מכשירים תואמים

‫list, ‏ get, ‏ reconcile, ‏ create, ‏ update, ‏ batchDelete .activity_and_fitness.readonly
.activity_and_fitness.writeonly
קומות
dataType: floors
filter parameter: floors
סוג הרשומה: אינטרוולים
רזולוציית אחסון: דקה אחת
reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
דופק
dataType: heart-rate
filter parameter: heart_rate
סוג הרשומה: דוגמה
רזולוציית אחסון: שנייה אחת

מכשירים תואמים

list, reconcile, rollup, dailyRollup .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
שונות קצב הלב
dataType: heart-rate-variability
filter parameter: heart_rate_variability
סוג הרשומה: דוגמה

מכשירים תואמים

רשימה, התאמה .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
גובה
dataType: height
filter parameter: height
סוג הרשומה: דוגמה
‫list, ‏ get, ‏ reconcile, ‏ create, ‏ update, ‏ batchDelete .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
יומן מאזן הנוזלים
dataType: hydration-log
filter parameter: hydration_log
סוג הרשומה: סשן
list, get, reconcile, rollup, dailyRollup, create, update, batchDelete .nutrition.readonly
.nutrition.writeonly
יומן תזונה
dataType: nutrition-log
filter parameter: nutrition_log
סוג הרשומה: סשן

מכשירים תואמים

list, get, reconcile, rollup, dailyRollup, create, update, batchDelete .nutrition.readonly
.nutrition.writeonly
סיכום השינה: קצב הנשימה
dataType: respiratory-rate-sleep-summary
filter parameter: respiratory_rate_sleep_summary
סוג הרשומה: דוגמה

מכשירים תואמים

רשימה, התאמה .health_metrics_and_measurements.readonly
.health_metrics_and_measurements.writeonly
ריצה – צריכת חמצן מרבית (VO2 max)
dataType: run-vo2-max
filter parameter: run_vo2_max
סוג הרשומה: דוגמה

מכשירים תואמים

list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
תקופת חוסר פעילות
dataType: sedentary-period
filter parameter: sedentary_period
סוג הרשומה: אינטרוולים

מכשירים תואמים

list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
שינה
dataType: sleep
filter parameter: sleep
סוג הרשומה: סשן

מכשירים תואמים

‫list, ‏ get, ‏ reconcile, ‏ create, ‏ update, ‏ batchDelete .sleep.readonly
.sleep.writeonly
השלבים
dataType: steps
filter parameter: steps
סוג הרשומה: אינטרוולים
רזולוציית אחסון: דקה אחת

מכשירים תואמים

list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
זמן בטווח הדופק
dataType: time-in-heart-rate-zone
filter parameter: time_in_heart_rate_zone
סוג הרשומה: אינטרוולים
רזולוציית אחסון: דקה אחת
list, reconcile, rollup, dailyRollup .activity_and_fitness.readonly
.activity_and_fitness.writeonly
משקל
dataType: weight
filter parameter: 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

לא חובה להשתמש בחשבון שירות של IAM כשמגדירים מנויים ל-Google Health API, אבל מומלץ לעשות את זה. חשבונות שירות מספקים אבטחה טובה יותר לעומסי עבודה של אפליקציות בהשוואה לחשבונות משתמשים רגילים, באמצעות התכונות הבאות:

  • פרטי כניסה אוטומטיים לטווח קצר: כשמצרפים חשבונות שירות לסביבת הפעלה של Google Cloud (כמו Compute Engine, ‏Cloud Run או Google Kubernetes Engine), הם מקבלים ומחליפים באופן אוטומטי פרטי כניסה מאובטחים לטווח קצר. כך לא צריך לנהל ולאחסן מפתחות סטטיים קבועים, ואין סיכונים שקשורים אליהם.
  • העיקרון של הרשאות מינימליות: חשבונות שירות מספקים זהויות ייעודיות לעומסי עבודה. אתם יכולים לתת להם רק את ההרשאות הספציפיות שנדרשות כדי לנהל את נקודות הקצה של המנויים, וכך למנוע גישה רחבה יותר למשאבים שלכם ב-Google Cloud.
  • מחזור חיים עצמאי: חשבונות שירות פועלים בנפרד מחשבונות של משתמשים ספציפיים, כך ששינויים בצוות לא משפיעים על היציבות של האימות לטווח הארוך.

הגדרה של חשבון שירות

כדי להגדיר את אפליקציית המנוי לאימות באמצעות חשבון שירות:

  1. יצירת חשבון שירות: במסוף Google Cloud, נכנסים לדף IAM & Admin של הפרויקט כדי ליצור חשבון שירות חדש בניהול המשתמש.
  2. הקצאת התפקידים הנדרשים ב-IAM: מקצים לחשבון השירות את התפקידים המתאימים שנדרשים כדי לנהל את האפליקציות הרשומות בפרויקט בענן ב-Google Cloud.
  3. מקשרים את חשבון השירות לעומס העבודה: מגדירים את הסביבה שמארחת את הלוגיקה של המנוי כך שתפעל כחשבון השירות החדש. כך קוד האפליקציה (כמו ספריות הלקוח של Google API) יכול לזהות באופן אוטומטי את פרטי הכניסה לטווח קצר של חשבון השירות, ולהשתמש בהם כשמתבצעת קריאה ל-API בארכיטקטורת REST של projects.subscribers.

תפקידי CPE

כדי לנהל את המנויים או המינויים של Google Health API, צריך להקצות את התפקיד המתאים לחשבון השירות שמתחזה למשתמש ומבצע את הקריאות ל-API. בהתאם לרמת הגישה הנדרשת, מקצים את אחד מהתפקידים הבאים:

  • קריאה של Google Health API
  • עריכה באמצעות Google Health API
  • אדמין של Google Health API

מידע נוסף על תפקידים והרשאות ב-Google Health API IAM

ניהול המנויים

כדי לקבל התראות, צריך לרשום מנוי שמייצג את נקודת הקצה של ההתראות באפליקציה. אפשר לנהל את המנויים באמצעות ה-API בארכיטקטורת REST שזמין בכתובת projects.subscribers.

נקודת הקצה של המנוי צריכה להשתמש ב-HTTPS ‏ (TLSv1.2 ואילך) ולהיות נגישה לכולם. במהלך היצירה והעדכון של המנויים, Google Health API מבצע אתגר אימות כדי לוודא שאתם הבעלים של ה-URI של נקודת הקצה. אם האימות נכשל, פעולות היצירה והעדכון של המנוי נכשלות עם השגיאה FailedPreconditionException.

יצירת מנוי

כדי לרשום מנוי חדש לפרויקט, משתמשים בנקודת הקצה create. תצטרכו לספק:

  • ‫project-id: מספר הפרויקט שבו נוצר חשבון השירות של ה-webhook.
  • ‫subscriberId: מזהה ייחודי שאתם מספקים למנוי. המזהה צריך להיות באורך שבין 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, כל ההגדרות האישיות השמורות של המנוי הזה יוחלפו ברשימה שסופקה בגוף הבקשה. כדי להוסיף או להסיר הגדרה, צריך לספק את קבוצת ההגדרות המלאה. אם אתם מעדכנים שדות אחרים ורוצים לשמור על ההגדרות הקיימות, השמיטו את subscriberConfigs מ-updateMask.

בקשה

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`.
{}

אימות של נקודות קצה

כדי להבטיח את האבטחה והאמינות של שליחת ההתראות, Google Health API מבצע לחיצת יד חובה של אימות דו-שלבי בכל פעם שיוצרים מנוי או מעדכנים את הגדרת נקודת הקצה שלו (endpointUri או endpointAuthorization). התהליך הזה מתבצע באופן סינכרוני במהלך הקריאה ל-API. השירות שולח שתי בקשות POST אוטומטיות ל-URI של נקודת הקצה, באמצעות סוכן המשתמש Google-Health-API-Webhooks, עם גוף ה-JSON‏ {"type": "verification"}.

  • לחיצת יד מורשית: הבקשה הראשונה נשלחת עם הכותרת Authorization שהגדרתם. השרת צריך להחזיר סטטוס 200 OK או 201 Created.
  • אימות לא מורשה: הבקשה השנייה נשלחת בלי פרטי כניסה. השרת צריך להחזיר סטטוס 401 Unauthorized או 403 Forbidden.

הלחיצת היד הזו מאשרת שנקודת הקצה פעילה ואוכפת את האבטחה בצורה נכונה. אם אחד מהשלבים נכשל, בקשת ה-API נכשלת ומוצגת השגיאה FAILED_PRECONDITION. רק אחרי שהלחיצת היד הזו מצליחה, המנוי נשמר ומופעל כדי לקבל התראות על נתוני בריאות וכושר.

רוטציית מפתחות

כדי להחליף מפתחות ב-endpointAuthorization:

  1. מגדירים את נקודת הקצה כך שתקבל גם את הערכים הישנים וגם את הערכים החדשים של endpointAuthorization.
  2. מעדכנים את הגדרות המנוי עם ערך endpointAuthorization חדש באמצעות בקשת patch עם ?updateMask=endpointAuthorization.
  3. מגדירים את נקודת הקצה כך שתקבל רק את הערך החדש של endpointAuthorization אחרי שמוודאים ששלב 2 בוצע בהצלחה.

מינויים של משתמשים

‫Google Health API עוזר לכם לנהל את המינויים של המשתמשים ביעילות, ומפחית את הצורך ברישום ידני במהלך ההצטרפות של המשתמשים.

מינויים אוטומטיים

מומלץ להשתמש במינויים אוטומטיים. כדי להפעיל את התכונה הזו, צריך להגדיר את subscriptionCreatePolicy ל-AUTOMATIC ב-subscriberConfigs לסוגי הנתונים הספציפיים. סוגי הנתונים (dataTypes) שאתם מציינים במדיניות AUTOMATIC הם אותם סוגי נתונים שעליהם Google Health API שולח התראות, בתנאי שניתנה הסכמה מהמשתמש גם לסוגי הנתונים האלה.

כשמשתמש נותן לאפליקציה הרשאה להיקפים שתואמים לסוגי נתונים עם מדיניות AUTOMATIC, Google Health API עוקב באופן אוטומטי אחרי סוגי הנתונים שנובעים מהחיתוך בין סוגי הנתונים שהמשתמש הסכים לשתף לבין סוגי הנתונים של הגדרת המנוי האוטומטי של אותו משתמש, ושולח התראות לגביהם. לאחר מכן, ההתראות נשלחות לנקודת הקצה בכל פעם שהמשתמש יוצר נתונים חדשים מהסוגים האלה. השיטה הזו מתאימה למשתמשים שנותנים הסכמה לפני או אחרי שאתם יוצרים את המנוי. ההתראות לא מתמלאות באופן רטרואקטיבי בנתונים שנוצרו לפני שהמנוי נוצר.

אם משתמש מבטל את ההסכמה, ההתראות לגבי סוגי הנתונים הרלוונטיים יפסיקו. מינויים אוטומטיים מנוהלים על ידי Google ואי אפשר להציג או למחוק אותם בנפרד. הם מוסרים רק כשמוחקים את המינוי הראשי.

מינויים ידניים

אם המנוי מוגדר עם subscription_create_policy ידני לסוגי נתונים ספציפיים, צריך ליצור ולנהל מינויים באופן מפורש לכל משתמש. מינוי מקשר משתמש ספציפי לנקודת הקצה של המנוי שלכם לקבוצה מוגדרת של סוגי נתונים. מפתחים יכולים להשתמש בממשקי API ספציפיים כדי:

  • יצירת מינויים (ידניים) לכל healthUserId – יצירת מינוי חדש למשתמש ספציפי. כדי להשתמש בשיטה הזו, המנוי צריך להגדיר את SubscriptionCreatePolicy לערך MANUAL לסוגי הנתונים המבוקשים.
  • עדכון מינוי (ידני) – עדכון סוגי הנתונים של מינוי משתמש קיים.
  • מחיקת מינוי (ידנית) – מחיקת מינוי של משתמש ספציפי. אחרי המחיקה, נקודת הקצה של המנוי לא תקבל יותר התראות לגבי המשתמש הזה עבור סוגי הנתונים המשויכים.
  • רשימת מינויים (ידנית) – רשימה של כל המינויים הפעילים של מנוי מסוים. אפשר לסנן את התוצאות לפי משתמש או סוג נתונים.

התראות

כשנתונים של משתמש משתנים בסוג נתונים שהמנוי נרשם אליו, Google Health API שולח בקשת HTTPS POST לכתובת ה-URL של נקודת הקצה של המנוי.

פורמט ההתראות

המטען הייעודי (payload) של ההתראה מכיל פרטים על השינוי בנתונים, כולל מזהה המשתמש, סוג הנתונים ומרווחי הזמן, שבהם אפשר להשתמש כדי לשלוח שאילתה על הנתונים המעודכנים. כשמתרחשים כמה עדכונים או כשהעדכונים נצברים, ההתראות נשלחות יחד כמערך JSON של אובייקטים של התראות.

כשמופעלת התראה של webhook, ה-Google Health API שולח בקשת HTTPS POST לנקודת הקצה של המנוי בפורמט הבא:

עדכון או הוספה (נתונים של פעולות על ציר הזמן)

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: נשלח כשמשתמש מוחק נתונים באופן מפורש (למשל, מחיקת dataPoints באמצעות אפליקציית Fitbit או מחיקה בכמות גדולה באמצעות ה-API). התראות Webhook לא נשלחות לאירועי מחיקה ברמת המערכת, כמו משתמש שמבטל את הרשאות האפליקציה או מוחק את חשבון Fitbit שלו.

מומלץ להגדיר את הלוגיקה של הטיפול בהתראות כאידמפוטנטית, במיוחד לפעולות UPSERT, כי ניסיונות חוזרים עלולים לגרום לשליחת התראות כפולות.

השדה clientProvidedSubscriptionName הוא מזהה ייחודי:

  • במינויים עם מדיניות MANUAL, השדה הזה מכיל את שם המינוי הקבוע שסופק על ידי המפתח וצוין כשהמינוי נוצר. כך מקבלים מזהה יציב לניהול מינויים ידניים.
  • במינויים שנוצרו באמצעות מדיניות AUTOMATIC, ה-Google Health API יוצר ומקצה באופן אוטומטי מזהה בפורמט auto-<project-number>-<subscriber-id>-<WEBHOOK_DATA_TYPE_TOKEN>, כאשר:
    • ‫<project-number> הוא מספר הפרויקט של המפתח ב-Google Cloud (לדוגמה, 362978535920).
    • ‫<subscriber-id> הוא מזהה הרישום של המנוי או שם המנוי (לדוגמה, google-health-sync).
    • ‫<WEBHOOK_DATA_TYPE_TOKEN> הוא האסימון שמייצג את סוג הנתונים הספציפי. בקטע אוצר מילים של אסימונים מסוג נתונים של webhook מפורטים כל האסימונים האפשריים.

הכללת clientProvidedSubscriptionName במדיניות ידנית ואוטומטית מבטיחה פורמט עקבי של מטען ייעודי (payload) של התראות בכל סוגי המינויים.

מיפוי מזהי משתמשים

השדה healthUserId הוא מזהה של Google Health API למשתמש שהנתונים שלו השתנו. אם האפליקציה שלכם תומכת במספר משתמשים, אתם יכולים לקבל התראות לגבי כל משתמש שנתן לאפליקציה הסכמה. כשמקבלים התראה, משתמשים ב-healthUserId כדי לזהות את המשתמש שהנתונים שלו השתנו, ואז משתמשים בפרטי הכניסה שלו ב-OAuth כדי לשלוח שאילתה על הנתונים שלו.

כדי למפות את פרטי הכניסה של משתמש ב-OAuth ל-healthUserId שלו, משתמשים בנקודת הקצה getIdentity. כדי לאחזר את ה-healthUserId של המשתמש, צריך לשלוח קריאה לנקודת הקצה הזו עם פרטי הכניסה של המשתמש במהלך תהליך ההצטרפות, ולשמור את המיפוי הזה. המיפוי הזה לא משתנה עם הזמן, ולכן אפשר לשמור אותו במטמון ללא הגבלת זמן. לדוגמה, ראו קבלת מזהה משתמש. כך תוכלו לבחור את פרטי הכניסה הנכונים של המשתמש כשאתם שולחים שאילתה על נתונים על סמך ה-healthUserId בהתראה.

מרווחי זמן

המערך intervals מכיל אובייקט אחד או יותר שמייצג את מרווחי הזמן של הנתונים שהשתנו:

שדה סוג נדרש תיאור
physicalTimeInterval object לא מרווח הזמן הפיזי ב-UTC של הנתונים ששונו (פורמט YYYY-MM-DDTHH:MM:SSZ). השדה הזה מאוכלס במדדים שמתקבלים במהלך היום, ומושמט בסיכומים יומיים ובפעילויות של יום שלם.
civilDateTimeInterval object כן התאריך והשעה של הנתונים ששונו לפי לוח השנה האזרחי (המקומי), בלי קיזוזים של UTC או של אזור הזמן.
civilIso8601TimeInterval object כן מרווח הזמן האזרחי (המקומי) מיוצג כמחרוזת ISO 8601 (פורמט YYYY-MM-DDTHH:MM:SS) ללא הפרשי שעות מ-UTC או מאזור הזמן.

בפעילויות של יום שלם ובסיכומים יומיים, השדה physicalTimeInterval מושמט בכוונה מהמטען הייעודי (payload) של ההתראה. הסיבה לכך היא שאירועים שנמשכים יום שלם מקובצים לפי התאריך האזרחי הנומינלי שלהם, ומשך הזמן הפיזי יכול להיות לא מדויק כשחוצים אזורי זמן. כשמעבדים אירועים שנמשכים יום שלם, מומלץ להסתמך על השדות civilDateTimeInterval או civilIso8601TimeInterval. במאמר סינון נתונים לפי מרווח זמן של שעת התחלה אזרחית מוסבר איך לשלוח שאילתות לגבי נקודות על הגרף האלה באמצעות מסנני זמן אזרחי.

הדוגמה הבאה היא מטען ייעודי (payload) של התראה על פעילות של יום שלם, שבה השדה 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 שמכיל כמה אובייקטים של התראות. אצווה אחת יכולה לכלול עדכונים לכמה משתמשים בכמה סוגי נתונים שהם נרשמו אליהם. דוגמה ל-payload מופיעה בקטע Batch (ריבוי משתמשים וסוגי נתונים) במאמר Notification format.

איך מגיבים להתראה

השרת צריך לאמת את הכותרת Authorization, לטפל בלחיצת היד הסינכרונית של אימות נקודת הקצה (endpoint) ולהגיב מיד להתראות על שינויים בנתונים באמצעות קוד סטטוס HTTP‏ 204 No Content. כדי למנוע זמן קצוב לתפוגה, צריך לעבד את המטען הייעודי (payload) של ההתראה באופן אסינכרוני אחרי שליחת התגובה. אם 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).
        }
    });
});

כדי לאחזר את הנתונים המעודכנים של כל מרווח זמן שקיבלתם עליו התראה, צריך לשלוח קריאה לנקודת הקצה (endpoint) reconcile עם מסנן לטווח התאריכים של המרווח. כך, המערכת מסירה באופן אוטומטי כפילויות של פרקי זמן חופפים ממכשירים מחוברים. לפרטים נוספים ולעיון בדוגמאות, ראו תצוגה מותאמת של נתוני מרווחים.

יש שלוש אסטרטגיות נפוצות לעדכון מסד הנתונים אחרי קבלת התראה על שינוי בנתונים:

  • הוספה לתור (מומלץ): הוספת ההתראות הנכנסות לתור של הודעות ועיבוד שלהן מחוץ לתור על סמך משאבי המערכת.
  • אחזור מיידי: אתם יכולים להנחות את ה-backend לשלוח קריאה מיידית ל-Google Health API כדי לאחזר את הנתונים המעודכנים ולשמור אותם במסד הנתונים.
  • עדכונים עצלים: מסמנים את הרשומות במסד הנתונים המקומי של המשתמש כלא מעודכנות, ומאחזרים את הנתונים המעודכנים מ-Google Health API רק כשהמשתמש נכנס לאפליקציה בפעם הבאה.

אסטרטגיית התור מאפשרת לעבד נתונים כמעט בזמן אמת, תוך שמירה על עמידות בפני הצפה של התראות (לדוגמה, אם הפסקת שירות גורמת להצטברות של התראות שצריך לשלוח בבת אחת). כדי לחסוך במשאבים, אתם יכולים לבחור לעבד את התורים האלה לפי לוח זמנים של אצווה במרווחים ארוכים (לדוגמה, כל 3 שעות), אבל החיסרון הוא שבדרך כלל המשתמשים מצפים שהנתונים שלהם יתעדכנו כמעט בזמן אמת.

שיטות מומלצות

כדי לוודא שעדכוני הנתונים מטופלים בצורה מהימנה ויעילה, כדאי לפעול לפי השיטות המומלצות הבאות:

  • שימוש ב-Subscription API: משתמשים ב-Subscription API כדי לקבל התראה כשלמשתמש יש נתונים חדשים להורדה, במקום לדגום נקודות קצה.
  • הוספה לתור ועיבוד של התראות בנפרד: כשמקבלים התראות על מינויים, כדאי להוסיף אותן לתור ולעבד אותן בנפרד בהתאם למשאבים של המערכת. כדי שנקודת הקצה של ה-webhook תהיה קלה, צריך לאשר את הבקשה באופן מיידי.

אימות חתימה

כדי לוודא את האותנטיות של ההתראות של ה-Webhooks, המטען הייעודי (payload) הגולמי של JSON של כל התראת webhook יוצאת נחתם באמצעות מפתח פרטי באמצעות PublicKeySign של Tink, ומספק את החתימה בקידוד Base64 בכותרת ה-HTTP של GOOGLE-HEALTH-API-SIGNATURE בבקשה. מפתחות החתימה האלה עוברים רוטציה אוטומטית כל 30 יום, וקבוצת המפתחות הציבוריים הרשמיים התואמים מופצת כקובץ JSON בכתובת ה-URL הקבועה https://www.gstatic.com/googlehealthapi/webhooks/webhooks_public_keyset.json.

איך מאמתים את החתימה

שימוש ב-Tink (מומלץ): מפתחים יכולים לאמת את החתימה באמצעות הפרימיטיב PublicKeyVerify של Tink. אחזר את קבוצת המפתחות הציבוריים מכתובת ה-URL הקבועה, צור מופע של הפרימיטיב PublicKeyVerify עם קבוצת המפתחות, ואמת את הכותרת GOOGLE-HEALTH-API-SIGNATURE המפוענחת מול המטען הייעודי (payload) הגולמי של ה-webhook בפורמט JSON.

אימות ידני (ללא Tink): אם המפתחים בוחרים לא להשתמש ב-Tink, הם יכולים לאמת את החתימה באופן ידני לפי השלבים הבאים:

  1. מפענח את הכותרת GOOGLE-HEALTH-API-SIGNATURE בקידוד Base64 כדי להפריד בין הקידומת של Tink בגודל 5 בייטים (שמכילה קידומת גרסה בגודל בייט אחד ו-keyId בגודל 4 בייטים) לבין החתימה בפועל בקידוד DER.
  2. אחזור של קבוצת המפתחות בפורמט JSON מהכתובת https://www.gstatic.com/googlehealthapi/webhooks/webhooks_public_keyset.json.
  3. מאתרים את המפתח שתואם ל-keyId שנותח, ומפענחים את שדה הערך שלו בקידוד Base64, שמכיל מאגר אחסון לפרוטוקולים מסוג EcdsaPublicKey שעבר סריאליזציה.
  4. מחלצים את הקואורדינטות x ו-y בפורמט big-endian (תגי Protobuf‏ 3 ו-4) ממטען הייעודי (payload) הבינארי הזה.
  5. יוצרים מופע של מפתח ציבורי ECDSA P-256 סטנדרטי בספריית קריפטוגרפיה מובנית, באמצעות הקואורדינטות x ו-y שחולצו.
  6. מאמתים את המטען הייעודי (payload) הגולמי של ה-webhook בפורמט JSON מול החתימה שחולצה בפורמט DER באמצעות אלגוריתם SHA-256.

סטטוס המנוי ושחזור

אם נקודת הקצה של המנוי לא זמינה או מחזירה קוד סטטוס של שגיאה (כל קוד שהוא לא 204), ההתראות בהמתנה נשמרות ב-Google Health API למשך עד 7 ימים, והמערכת מנסה לשלוח אותן שוב עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff).

כשהנקודת הקצה חוזרת לפעול ומגיבה עם 204, ה-API מעביר אוטומטית את צבר ההודעות המאוחסנות. התראות שנוצרו לפני יותר מ-7 ימים נמחקות ואי אפשר לשחזר אותן.

שגיאות נפוצות

קוד שגיאה שליחת הודעה תיאור המלצה
‫400 בקשה לא תקינה מספר הפרויקט בשם המשאב לא תקין כשמוחקים או מעדכנים מנוי באמצעות מזהה הפרויקט ב-Google Cloud בכתובת ה-URL של הבקשה, במקום מספר הפרויקט. ההגדרה הזו חלה על מינויים ל-webhook שמשתמשים בנקודת הקצה projects.subscribers. צריך להשתמש במספר הפרויקט ב-Google Cloud בכתובת ה-URL של הבקשה, ולא במזהה הפרויקט.
‫403 אסור לגורם המזמן אין הרשאה כשיוצרים או מציגים רשימה של מנויים באמצעות מזהה הפרויקט ב-Google Cloud בכתובת ה-URL של הבקשה, במקום מספר הפרויקט. ההגדרה הזו חלה על מינויים ל-webhook שמשתמשים בנקודת הקצה projects.subscribers. צריך להשתמש במספר הפרויקט ב-Google Cloud בכתובת ה-URL של הבקשה, ולא במזהה הפרויקט.