在 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 购买的门户)。 只能从管理中心触发更改数量和计划。
获取支持
有关发布者支持选项,请参阅合作伙伴中心 商业市场计划的 支持。
相关内容
- 有关商业市场中 SaaS 产品/服务的更多选项,请参阅 商业市场计量服务 API。
- 查看和使用 客户端的不同编程语言和示例。
- 观看以下 视频教程:
- SaaS Webhook 概述
- 在 .NET 中实现简单的 SaaS Webhook
- Microsoft Entra 应用程序注册