在 SaaS 服务上实现 Webhook

在合作伙伴中心创建可交易的 SaaS 产品/服务时,合作伙伴需提供要用作 HTTP 终结点的连接 Webhook URL。 Microsoft 使用 POST HTTP 调用来调用此 Webhook,以通知发布者以下事件将在 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 调用后,发布者应立即向 Microsoft 返回 HTTP 200。 此值确认发布者已成功收到 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 购买项的门户)触发取消订阅操作。 只能从管理中心触发更改数量和计划操作

获取支持

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