在 SaaS 服务上实现 Webhook

在合作伙伴中心创建可交易 SaaS 产品/服务时,合作伙伴提供 连接 Webhook URL,用作 HTTP 终结点。 此 Webhook 由 Microsoft 调用,方法是使用 POST HTTP 调用来通知发布者端Microsoft端发生的以下事件:

Webhook 事件 1. 收到时 2. 如果接受 3. 如果被拒绝
ChangePlan 使用 HTTP 200 做出响应 成功修补(此事件是可选的,并在 10 秒内自动接受) 具有失败的 PATCH 或响应 4xx (在 10 秒内)
ChangeQuantity 使用 HTTP 200 做出响应 成功修补(此事件是可选的,并在 10 秒内自动接受) 具有失败的 PATCH 或响应 4xx (在 10 秒内)
Renew 使用 HTTP 200 做出响应 不適用 不適用
Suspend 使用 HTTP 200 做出响应 不適用 不適用
Unsubscribe 使用 HTTP 200 做出响应 不適用 不適用
Reinstate 使用 HTTP 200 做出响应 不適用 不适用(如果无法接受恢复,则调用删除 API 以触发删除)

发布者必须在 SaaS 服务中实现 Webhook,以使 SaaS 订阅状态与Microsoft端保持一致。 在基于 Webhook 通知执行操作之前,需要 SaaS 服务来调用获取操作 API 来验证和授权 Webhook 调用和有效负载数据。 处理 Webhook 调用后,发布者应立即将 HTTP 200 返回到Microsoft。 此值确认发布者已成功收到 Webhook 调用。

重要

Webhook URL 服务必须启动并运行 24 x 7,并且随时可以接收来自Microsoft的新调用。 Microsoft Webhook 调用确实有重试策略(500 次重试时间超过 8 小时),但如果发布者不接受调用并返回响应,则 Webhook 通知的操作最终会在Microsoft端失败。

重要

ISV 应避免对 Webhook 架构进行严格的反序列化。 Microsoft保留将来扩展架构的权利。

重要

ISV 必须从请求标头验证其 Webhook 终结点上的Microsoft Entra 令牌(JWT 令牌)。 这是一个标准持有者令牌,将为 ISV 提供有关调用方是谁的详细信息。 详细了解如何验证本文中的令牌。 learn.microsoft.com/azure/active-directory/develop/access-tokens

ChangePlan 的 Webhook 有效负载示例:

{
    "id": "<guid>",
    "activityId": "<guid>",
    "publisherId": "XXX",
    "offerId": "YYY",
    "planId": "plan2",
    "quantity": 10,
    "subscriptionId": "<guid>",
    "timeStamp": "2023-02-10T18:48:58.4449937Z",
    "action": "ChangePlan",
    "status": "InProgress",
    "operationRequestSource": "Azure",
    "subscription":
    {
      "id": "<guid>",
      "name": "Test",
      "publisherId": "XXX",
      "offerId": "YYY",
      "planId": "plan1",
      "quantity": 10,
      "beneficiary":
        {
          "emailId": XX@outlook.com,
          "objectId": "<guid>",
          "tenantId": "<guid>",
          "puid": "1234567890",
        },
      "purchaser":
        {
          "emailId": XX@outlook.com,
          "objectId": "<guid>",
          "tenantId": "<guid>",
          "puid": "1234567890",
        },
      "allowedCustomerOperations": ["Delete", "Update", "Read"],
      "sessionMode": "None",
      "isFreeTrial": false,
      "isTest": false,
      "sandboxType": "None",
      "saasSubscriptionStatus": "Subscribed",
      "term":
        {
          "startDate": "2022-02-10T00:00:00Z",
          "endDate": "2022-03-12T00:00:00Z",
          "termUnit": "P1M",
          "chargeDuration": null,
        },
      "autoRenew": true,
      "created": "2022-01-10T23:15:03.365988Z",
      "lastModified": "2022-02-14T20:26:04.5632549Z",
    },
    "purchaseToken": null
}

ChangeQuantity 事件的 Webhook 有效负载示例:


{
    "id": "<guid>",
    "activityId": "<guid>",
    "publisherId": "XXX",
    "offerId": "YYY",
    "planId": "plan1",
    "quantity": 20,
    "subscriptionId": "<guid>",
    "timeStamp": "2023-02-10T18:54:00.6158973Z",
    "action": "ChangeQuantity",
    "status": "InProgress",
    "operationRequestSource": "Azure",
    "subscription": {
        "id": "<guid>",
        "name": "Test",
        "publisherId": "XXX",
        "offerId": "YYY",
        "planId": "plan1",
        "quantity": 10,
        "beneficiary":
            {
            "emailId": XX@outlook.com,
            "objectId": "<guid>",
            "tenantId": "<guid>",
            "puid": "1234567890",
            },
        "purchaser":
            {
            "emailId": XX@outlook.com,
            "objectId": "<guid>",
            "tenantId": "<guid>",
            "puid": "1234567890",
            },
        "allowedCustomerOperations": ["Delete", "Update", "Read"],
        "sessionMode": "None",
        "isFreeTrial": false,
        "isTest": false,
        "sandboxType": "None",
        "saasSubscriptionStatus": "Subscribed",
        "term":
            {
            "startDate": "2022-02-10T00:00:00Z",
            "endDate": "2022-03-12T00:00:00Z",
            "termUnit": "P1M",
            "chargeDuration": null,
            },
        "autoRenew": true,
        "created": "2022-01-10T23:15:03.365988Z",
        "lastModified": "2022-02-14T20:26:04.5632549Z",
    },
    "purchaseToken": null
}

订阅恢复事件的 Webhook 有效负载示例:

// end user's payment instrument became valid again, after being suspended, and the SaaS subscription is being reinstated


{
    "id": "<guid>",
    "activityId": "<guid>",
    "publisherId": "XXX",
    "offerId": "YYY",
    "planId": "plan1",
    "quantity": 100,
    "subscriptionId": "<guid>",
    "timeStamp": "2023-02-11T11:38:10.3508619Z",
    "action": "Reinstate",
    "status": "InProgress",
    "operationRequestSource": "Azure",
    "subscription":
    {
      "id": "<guid>",
      "name": "Test",
      "publisherId": "XXX",
      "offerId": "YYY",
      "planId": "plan1",
      "quantity": 100,
      "beneficiary":
        {
          "emailId": XX@outlook.com,
          "objectId": "<guid>",
          "tenantId": "<guid>",
          "puid": "1234567890",
        },
      "purchaser":
        {
          "emailId": XX@outlook.com,
          "objectId": "<guid>",
          "tenantId": "<guid>",
          "puid": "1234567890",
        },
      "allowedCustomerOperations": ["Delete", "Update", "Read"],
      "sessionMode": "None",
      "isFreeTrial": false,
      "isTest": false,
      "sandboxType": "None",
      "saasSubscriptionStatus": "Suspended",
      "term":
        {
          "startDate": "2022-02-10T00:00:00Z",
          "endDate": "2022-03-12T00:00:00Z",
          "termUnit": "P1M",
          "chargeDuration": null,
        },
      "autoRenew": true,
      "created": "2022-01-10T23:15:03.365988Z",
      "lastModified": "2022-02-14T20:26:04.5632549Z",
    },
    "purchaseToken": null
}
 

续订事件的 Webhook 有效负载示例:

// end user's subscription renewal
 
{
    "id": "<guid>",
    "activityId": "<guid>",
    "publisherId": "XXX",
    "offerId": "YYY",
    "planId": "plan1",
    "quantity": 100,
    "subscriptionId": "<guid>",
    "timeStamp": "2023-02-10T08:49:01.8613208Z",
    "action": "Renew",
    "status": "Succeeded",
    "operationRequestSource": "Azure",
    "subscription":
    {
      "id": "<guid>",
      "name": "Test",
      "publisherId": "XXX",
      "offerId": "YYY",
      "planId": "plan1",
      "quantity": 100,
      "beneficiary":
        {
          "emailId": XX@outlook.com,
          "objectId": "<guid>",
          "tenantId": "<guid>",
          "puid": "1234567890",
        },
      "purchaser":
        {
          "emailId": XX@outlook.com,
          "objectId": "<guid>",
          "tenantId": "<guid>",
          "puid": "1234567890",
        },
      "allowedCustomerOperations": ["Delete", "Update", "Read"],
      "sessionMode": "None",
      "isFreeTrial": false,
      "isTest": false,
      "sandboxType": "None",
      "saasSubscriptionStatus": "Subscribed",
      "term":
        {
          "startDate": "2022-02-10T00:00:00Z",
          "endDate": "2022-03-12T00:00:00Z",
          "termUnit": "P1M",
          "chargeDuration": null,
        },
      "autoRenew": true,
      "created": "2022-01-10T23:15:03.365988Z",
      "lastModified": "2022-02-14T20:26:04.5632549Z",
    },
  "purchaseToken": null,
}

暂停事件的 Webhook 有效负载示例:


{
    "id": "<guid>",
    "activityId": "<guid>",
    "publisherId": "XXX",
    "offerId": "YYY",
    "planId": "plan1",
    "quantity": 100,
    "subscriptionId": "<guid>",
    "timeStamp": "2023-02-10T08:49:01.8613208Z",
    "action": "Suspend",
    "status": "Succeeded",
    "operationRequestSource": "Azure",
    "subscription":
    {
      "id": "<guid>",
      "name": "Test",
      "publisherId": "XXX",
      "offerId": "YYY",
      "planId": "plan1",
      "quantity": 100,
      "beneficiary":
        {
          "emailId": XX@outlook.com,
          "objectId": "<guid>",
          "tenantId": "<guid>",
          "puid": "1234567890",
        },
      "purchaser":
        {
          "emailId": XX@outlook.com,
          "objectId": "<guid>",
          "tenantId": "<guid>",
          "puid": "1234567890",
        },
      "allowedCustomerOperations": ["Delete", "Update", "Read"],
      "sessionMode": "None",
      "isFreeTrial": false,
      "isTest": false,
      "sandboxType": "None",
      "saasSubscriptionStatus": "Suspended",
      "term":
        {
          "startDate": "2022-02-10T00:00:00Z",
          "endDate": "2022-03-12T00:00:00Z",
          "termUnit": "P1M",
          "chargeDuration": null,
        },
      "autoRenew": true,
      "created": "2022-01-10T23:15:03.365988Z",
      "lastModified": "2022-02-14T20:26:04.5632549Z",
    },
  "purchaseToken": null,
}

取消订阅事件的 Webhook 有效负载示例:

这是仅限通知的事件。 此事件没有发送到 ACK。


{
    "id": "<guid>",
    "activityId": "<guid>",
    "publisherId": "XXX",
    "offerId": "YYY",
    "planId": "plan1",
    "quantity": 100,
    "subscriptionId": "<guid>",
    "timeStamp": "2023-02-10T08:49:01.8613208Z",
    "action": "Unsubscribe",
    "status": "Succeeded",
    "operationRequestSource": "Azure",
    "subscription":
    {
      "id": "<guid>",
      "name": "Test",
      "publisherId": "XXX",
      "offerId": "YYY",
      "planId": "plan1",
      "quantity": 100,
      "beneficiary":
        {
          "emailId": XX@outlook.com,
          "objectId": "<guid>",
          "tenantId": "<guid>",
          "puid": "1234567890",
        },
      "purchaser":
        {
          "emailId": XX@outlook.com,
          "objectId": "<guid>",
          "tenantId": "<guid>",
          "puid": "1234567890",
        },
      "allowedCustomerOperations": ["Delete", "Update", "Read"],
      "sessionMode": "None",
      "isFreeTrial": false,
      "isTest": false,
      "sandboxType": "None",
      "saasSubscriptionStatus": "Unsubscribed",
      "term":
        {
          "startDate": "2022-02-10T00:00:00Z",
          "endDate": "2022-03-12T00:00:00Z",
          "termUnit": "P1M",
          "chargeDuration": null,
        },
      "autoRenew": true,
      "created": "2022-01-10T23:15:03.365988Z",
      "lastModified": "2022-02-14T20:26:04.5632549Z",
    },
  "purchaseToken": null,
}

保护 Webhook

必须保护 Webhook,因此除了Microsoft终结点之外,没有人进行此类 Webhook 调用。 可以使用任何技术来实现 Webhook,但 Webhook 实现必须遵循以下安全准则(请参阅教程)。

  • Microsoft使用授权标头调用 Webhook,其中包含验证调用所需的信息。 必须启用 Webhook 才能接收授权标头。 (请勿在 Webhook URL 中添加授权详细信息或安全令牌,如 SAS 令牌)。此类 Webhook 可能无法检索在调用 Webhook 时Microsoft发送的授权标头)。

  • 在 Authorization 标头中传递的 JWT 持有者令牌包含可用于保护终结点的有效负载中的以下数据。

  • “aud”:“这是在Microsoft合作伙伴中心添加到产品/服务技术配置的Microsoft Entra Identity 应用程序 ID”

  • “appid”或“azp”:创建发布者授权令牌以调用 SaaS 履行 API 时使用的资源 ID。 根据应用程序设置,你可能会在“appid”或“azp”中看到此资源 ID 值。 令牌有两个声明之一,必须在代码中做出相应的反应。

  • “tid”:“这是在Microsoft合作伙伴中心添加到产品/服务技术配置的Microsoft Entra 租户 ID”

  • 可以针对上述传递的字段进行检查,以确保 Webhook 调用有效。

重要

Microsoft将开始要求 ISV 以安全方式创建其 Webhook 并接受授权标头。 如果当前的 Webhook 实现不接受授权标头,则必须更新 Webhook 并保护此类终结点(使用上述准则),以避免任何中断。

开发和测试

若要开始开发过程,建议在发布者端创建虚拟 API 响应。 这些响应可以基于本文中提供的示例响应。

当发布者准备好进行端到端测试时:

  • 将 SaaS 产品/服务发布到有限的预览版受众,并将其保留在预览阶段。
  • 将计划价格设置为零,以避免在测试时触发实际计费费用。 另一种选择是设置非零价格,并在 24 小时内取消所有测试购买。
  • 确保所有流都以端到端方式调用,以模拟实际的客户方案。
  • 如果合作伙伴想要测试完整的购买和计费流,请使用价格高于 $0 的产品/服务执行此操作。 购买将计费,并生成发票。

可以从 Azure 门户或Microsoft AppSource 站点触发购买流,具体取决于产品/服务发布位置。

更改计划更改数量,并从发布者端测试 取消订阅 操作。 从Microsoft端,可以从 Azure 门户和管理中心触发 取消订阅(托管Microsoft AppSource 购买的门户)。 只能从管理中心触发更改数量和计划

获取支持

有关发布者支持选项,请参阅合作伙伴中心 商业市场计划的 支持。