SaaS 서비스에서 webhook 구현

파트너 센터에서 거래 가능 SaaS 제품을 만들 때 파트너는 HTTP 엔드포인트로 사용할 연결 웹후크 URL을 제공합니다. 이 웹후크는 Microsoft 쪽에서 발생하는 다음 이벤트를 게시자 쪽에 알리기 위해 POST HTTP 호출을 사용하여 Microsoft에서 호출합니다.

웹후크 이벤트 1. 수신 시 2. 수락된 경우 3. 거부된 경우
ChangePlan HTTP 200으로 응답 PATCH 성공(이 이벤트는 선택 사항이며 10초 안에 자동 액세스됨) 오류가 있는 PATCH OR 응답(10초 이내) 4xx
ChangeQuantity HTTP 200으로 응답 PATCH 성공(이 이벤트는 선택 사항이며 10초 안에 자동 액세스됨) 오류가 있는 PATCH OR 응답(10초 이내) 4xx
Renew HTTP 200으로 응답 해당 없음 해당 없음
Suspend HTTP 200으로 응답 해당 없음 해당 없음
Unsubscribe HTTP 200으로 응답 해당 없음 해당 없음
Reinstate HTTP 200으로 응답 해당 없음 해당 없음(복구를 수락할 수 없는 경우 삭제를 트리거하기 위해 delete API 호출)

게시자는 SaaS 구독 상태를 Microsoft 쪽과 일치하도록 SaaS 서비스에서 웹후크를 구현해야 합니다. SaaS 서비스는 웹후크 알림에 따라 작업을 수행하기 전에 Get Operation API를 호출하여 웹후크 호출 및 페이로드 데이터의 유효성을 검사하고 권한을 부여해야 합니다. 게시자는 웹후크 호출이 처리되는 즉시 HTTP 200을 Microsoft에 반환해야 합니다. 이 값은 게시자가 웹후크 호출을 성공적으로 수신했음을 인정합니다.

Important

웹후크 URL 서비스는 24 x 7을 실행하고 항상 Microsoft로부터 새 전화를 받을 준비가 되어 있어야 합니다. Microsoft는 웹후크 호출에 대한 재시도 정책을 가지고 있지만(8시간 동안 500회 재시도) 게시자가 통화를 수락하고 응답을 반환하지 않으면 웹후크가 알려 주는 작업이 결국 Microsoft 쪽에서 실패합니다.

Important

ISV는 Webhook 스키마의 엄격한 역직렬화를 피해야 합니다. Microsoft는 나중에 스키마를 확장할 수 있는 권한을 보유합니다.

Important

ISV는 요청 헤더의 웹후크 엔드포인트에서 JWT 토큰(Microsoft Entra Token)의 유효성을 검사해야 합니다. 이 토큰은 표준 전달자 토큰이며 호출자가 누구인지에 대한 ISV 세부 정보를 제공합니다. 이 문서에서 토큰의 유효성을 검사하는 방법에 대해 자세히 알아봅니다. video2.skills-academy.com/azure/active-directory/develop/access-tokens

ChangePlan의 웹후크 페이로드 예제:

{
    "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 이벤트의 웹후크 페이로드 예제:


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

구독 복구 이벤트의 웹후크 페이로드 예제:

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

갱신 이벤트의 웹후크 페이로드 예제:

// 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,
}

일시 중단 이벤트의 웹후크 페이로드 예제:


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

구독 취소 이벤트의 웹후크 페이로드 예제:

알림 전용 이벤트입니다. 이 이벤트에 대한 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,
}

웹후크 보안

Microsoft 엔드포인트가 이러한 Webhook 호출을 수행하지 않도록 웹후크를 보호해야 합니다. 모든 기술을 사용하여 웹후크를 구현할 수 있지만 Webhook 구현은 다음 보안 지침을 따라야 합니다(자습서 참조).

  • Microsoft는 호출의 유효성을 검사하는 데 필요한 정보가 포함된 권한 부여 헤더를 사용하여 웹후크를 호출합니다. 권한 부여 헤더를 받을 수 있도록 웹후크를 사용하도록 설정해야 합니다. (권한 부여 세부 정보 또는 SAS 토큰과 같은 보안 토큰을 웹후크 URL에 직접 추가하지 마세요. 이러한 웹후크는 웹후크를 호출할 때 Microsoft에서 보내는 권한 부여 헤더를 검색하지 못할 수 있습니다.)

  • 권한 부여 헤더에 전달된 JWT 전달자 토큰은 엔드포인트를 보호하는 데 사용할 수 있는 다음 데이터를 페이로드에 포함합니다.

  • "aud": "Microsoft 파트너 센터에서 제품의 기술 구성에 추가하는 Microsoft Entra ID 애플리케이션 ID입니다."

  • "appid" 또는 "azp": SaaS 처리 API를 호출하는 게시자 권한 부여 토큰을 만들 때 사용하는 리소스 ID입니다. 애플리케이션 설정에 따라 "appid" 또는 "azp"에서 이 리소스 ID 값을 볼 수 있습니다. 토큰은 두 클레임 중 하나를 가지며 코드에 적절하게 반응해야 합니다.

  • "tid": "Microsoft 파트너 센터에서 제품의 기술 구성에 추가하는 Microsoft Entra 테넌트 ID입니다."

  • 위의 전달된 필드를 확인하여 Webhook 호출이 유효한지 확인할 수 있습니다.

Important

Microsoft는 ISV가 안전한 방식으로 웹후크를 만들고 권한 부여 헤더를 수락하도록 요구하기 시작합니다. 현재 웹후크 구현에서 권한 부여 헤더를 수락할 수 없는 경우 중단을 방지하려면 웹후크를 업데이트하고 이러한 엔드포인트를 보호해야 합니다(위의 지침 사용).

개발 및 테스트

개발 프로세스를 시작하려면 게시자 쪽에서 더미 API 응답을 만드는 것이 좋습니다. 이러한 응답은 이 문서에 제공된 샘플 응답을 기반으로 할 수 있습니다.

게시자가 종단 간 테스트를 수행할 준비가 되면 다음을 수행합니다.

  • 제한된 미리 보기 대상 그룹에 SaaS 제품을 게시하고 미리 보기 단계에 유지합니다.
  • 테스트하는 동안 실제 청구 비용이 트리거하지 않도록 계획 가격을 0으로 설정합니다. 또 다른 옵션은 0이 아닌 가격을 설정하고 24시간 이내에 모든 테스트 구매를 취소하는 것입니다.
  • 실제 고객 시나리오를 시뮬레이션하기 위해 모든 흐름이 종단 간 호출되는지 확인합니다.
  • 파트너가 전체 구매 및 청구 흐름을 테스트하려는 경우 가격이 $0 이상인 제품으로 테스트합니다. 구매 요금이 청구되고 청구서가 생성됩니다.

제품이 게시되는 위치에 따라 Azure Portal 또는 Microsoft AppSource 사이트에서 구매 흐름을 트리거할 수 있습니다.

플랜 변경, 수량 변경구독 취소 작업은 게시자 쪽에서 테스트합니다. Microsoft 쪽 에서는 Azure Portal 및 관리 센터(Microsoft AppSource 구매가 관리되는 포털)에서 구독 취소를 트리거할 수 있습니다. 변경 수량 및 계획은 관리 센터에서만 트리거할 수 있습니다.

지원 받기

게시자 지원 옵션은 파트너 센터의 상업용 Marketplace 프로그램 지원을 참조하세요.