The Partner Center Webhook APIs allow partners to register for resource change events. These events are delivered in the form of HTTP POSTs to the partner's registered URL. To receive an event from Partner Center, partners host a callback where Partner Center can POST the resource change event. The event is digitally signed so that the partner can verify that it was sent from Partner Center. Webhook notifications are only triggered to the environment that has the latest configuration for Co-sell.
Partner Center supports the following Webhook events.
This event is raised when the partner activates the Granular Delegated Admin Privileges access assignment once the Microsoft Entra roles are assigned to specific security groups.
Granular Admin Access Assignment Created Event ("granular-admin-access-assignment-created")
This event is raised when the partner creates the Granular Delegated Admin Privileges access assignment. Partners can assign customer-approved Microsoft Entra roles to specific security groups.
This event is raised when the subscription changes. These events are generated when there is an internal change in addition to when changes are made through the Partner Center API.
Note
There is a delay of up to 48 hours between the time a subscription changes and when the Subscription Updated event is triggered.
Test Event ("test-created")
This event allows you to self-onboard and test your registration by requesting a test event and then tracking its progress. You can see the failure messages that are being received from Microsoft while trying to deliver the event. This restriction only applies to "test-created" events. Data older than seven days is purged.
This event is raised when the amount of Microsoft Azure usage for any customer exceeds their usage spending budget (their threshold). For more information, see (Set an Azure spending budget for your customers/partner-center/set-an-azure-spending-budget-for-your-customers).
Future Webhook events will be added for resources that change in the system that the partner isn't in control of, and further updates will be made to get those events as close to "real time" as possible. Feedback from Partners on which events add value to their business is useful in determining what new events to add.
Credentials as described in Partner Center authentication. This scenario supports authentication with both standalone App and App+User credentials.
Receiving events from Partner Center
To receive events from Partner Center, you must expose a publicly accessible endpoint. Because this endpoint is exposed, you must validate that the communication is from Partner Center. All Webhook events that you receive are digitally signed with a certificate that chains to the Microsoft Root. A link to the certificate used to sign the event is also be provided. This allows the certificate to be renewed without you having to redeploy or reconfigure your service. Partner Center makes 10 attempts to deliver the event. If the event is still not delivered after 10 attempts, it is moved into an offline queue and no further attempts are made at delivery.
The following sample shows an event posted from Partner Center.
The Authorization header has a scheme of "Signature". This is a base64 encoded signature of the content.
How to authenticate the callback
To authenticate the callback event received from Partner Center, follow these steps:
Verify the required headers are present (Authorization, x-ms-certificate-url, x-ms-signature-algorithm).
Download the certificate used to sign the content (x-ms-certificate-url).
Verify the Certificate Chain.
Verify the "Organization" of the certificate.
Read the content with UTF8 encoding into a buffer.
Create an RSA Crypto Provider.
Verify the data matches what was signed with the specified hash algorithm (for example SHA256).
If the verification succeeds, process the message.
Note
By default, the signature token is sent in an Authorization header. If you set SignatureTokenToMsSignatureHeader to true in your registration, the signature token is sent in the x-ms-signature header instead.
Event model
The following table describes the properties of a Partner Center event.
Properties
Name
Description
EventName
The name of the event. In the form {resource}-{action}. For example, "test-created".
ResourceUri
The URI of the resource that changed.
ResourceName
The name of the resource that changed.
AuditUrl
Optional. The URI of the Audit record.
ResourceChangeUtcDate
The date and time, in UTC format, when the resource change occurred.
Sample
The following sample shows the structure of a Partner Center event.
All calls to the Webhook APIs are authenticated using the Bearer token in the Authorization Header. Acquire an access token to access https://api.partnercenter.microsoft.com. This token is the same token that is used to access the rest of the Partner Center APIs.
Get a list of events
Returns a list of the events currently supported by the Webhook APIs.
Generates a test event to validate the Webhooks registration. This test is intended to validate that you can receive events from Partner Center. Data for these events is deleted seven days after the initial event is created. You must be registered for the "test-created" event, using the registration API, before sending a validation event.
Note
There is a throttle limit of 2 requests per minute when posting a validation event.
Returns the current state of the validation event. This verification can be helpful for troubleshooting event delivery issues. The Response contains a result for each attempt that is made to deliver the event.
[AuthorizeSignature]
[Route("webhooks/callback")]
public IHttpActionResult Post(PartnerResourceChangeCallBack callback)
Signature Validation
The following example shows how to add an Authorization Attribute to the controller that is receiving callbacks from Webhook events.
C#
namespaceWebhooks.Security
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using Microsoft.Partner.Logging;
///<summary>/// Signature based Authorization///</summary>publicclassAuthorizeSignatureAttribute : AuthorizeAttribute
{
privateconststring MsSignatureHeader = "x-ms-signature";
privateconststring CertificateUrlHeader = "x-ms-certificate-url";
privateconststring SignatureAlgorithmHeader = "x-ms-signature-algorithm";
privateconststring MicrosoftCorporationIssuer = "O=Microsoft Corporation";
privateconststring SignatureScheme = "Signature";
///<inheritdoc/>publicoverrideasync Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
ValidateAuthorizationHeaders(actionContext.Request);
await VerifySignature(actionContext.Request);
}
privatestaticasync Task<string> GetContentAsync(HttpRequestMessage request)
{
// By default the stream can only be read once and we need to read it here so that we can hash the body to validate the signature from microsoft.// Load into a buffer, so that the stream can be accessed here and in the api when it binds the content to the expected model type.await request.Content.LoadIntoBufferAsync();
var s = await request.Content.ReadAsStreamAsync();
var reader = new StreamReader(s);
var body = await reader.ReadToEndAsync();
// set the stream position back to the beginningif (s.CanSeek)
{
s.Seek(0, SeekOrigin.Begin);
}
return body;
}
privatestaticvoidValidateAuthorizationHeaders(HttpRequestMessage request)
{
var authHeader = request.Headers.Authorization;
if (string.IsNullOrWhiteSpace(authHeader?.Parameter) && string.IsNullOrWhiteSpace(GetHeaderValue(request.Headers, MsSignatureHeader)))
{
thrownew HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Authorization header missing."));
}
var signatureHeaderValue = GetHeaderValue(request.Headers, MsSignatureHeader);
if (authHeader != null
&& !string.Equals(authHeader.Scheme, SignatureScheme, StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrWhiteSpace(signatureHeaderValue)
&& !signatureHeaderValue.StartsWith(SignatureScheme, StringComparison.OrdinalIgnoreCase))
{
thrownew HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, $"Authorization scheme needs to be '{SignatureScheme}'."));
}
if (string.IsNullOrWhiteSpace(GetHeaderValue(request.Headers, CertificateUrlHeader)))
{
thrownew HttpResponseException(request.CreateErrorResponse(HttpStatusCode.BadRequest, $"Request header {CertificateUrlHeader} missing."));
}
if (string.IsNullOrWhiteSpace(GetHeaderValue(request.Headers, SignatureAlgorithmHeader)))
{
thrownew HttpResponseException(request.CreateErrorResponse(HttpStatusCode.BadRequest, $"Request header {SignatureAlgorithmHeader} missing."));
}
}
privatestaticstringGetHeaderValue(HttpHeaders headers, string key)
{
headers.TryGetValues(key, outvar headerValues);
return headerValues?.FirstOrDefault();
}
privatestaticasync Task VerifySignature(HttpRequestMessage request)
{
// Get signature value from either authorization header or x-ms-signature header.var base64Signature = request.Headers.Authorization?.Parameter ?? GetHeaderValue(request.Headers, MsSignatureHeader).Split(' ')[1];
var signatureAlgorithm = GetHeaderValue(request.Headers, SignatureAlgorithmHeader);
var certificateUrl = GetHeaderValue(request.Headers, CertificateUrlHeader);
var certificate = await GetCertificate(certificateUrl);
var content = await GetContentAsync(request);
var alg = signatureAlgorithm.Split('-'); // for example RSA-SHA1var isValid = false;
var logger = GetLoggerIfAvailable(request);
// Validate the certificate
VerifyCertificate(certificate, request, logger);
if (alg.Length == 2 && alg[0].Equals("RSA", StringComparison.OrdinalIgnoreCase))
{
var signature = Convert.FromBase64String(base64Signature);
var csp = (RSACryptoServiceProvider)certificate.PublicKey.Key;
var encoding = new UTF8Encoding();
var data = encoding.GetBytes(content);
var hashAlgorithm = alg[1].ToUpper();
isValid = csp.VerifyData(data, CryptoConfig.MapNameToOID(hashAlgorithm), signature);
}
if (!isValid)
{
// log that we were not able to validate the signature
logger?.TrackTrace(
"Failed to validate signature for webhook callback",
new Dictionary<string, string> { { "base64Signature", base64Signature }, { "certificateUrl", certificateUrl }, { "signatureAlgorithm", signatureAlgorithm }, { "content", content } });
thrownew HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Signature verification failed"));
}
}
privatestatic ILogger GetLoggerIfAvailable(HttpRequestMessage request)
{
return request.GetDependencyScope().GetService(typeof(ILogger)) as ILogger;
}
privatestaticasync Task<X509Certificate2> GetCertificate(string certificateUrl)
{
byte[] certBytes;
using (var webClient = new WebClient())
{
certBytes = await webClient.DownloadDataTaskAsync(certificateUrl);
}
returnnew X509Certificate2(certBytes);
}
privatestaticvoidVerifyCertificate(X509Certificate2 certificate, HttpRequestMessage request, ILogger logger)
{
if (!certificate.Verify())
{
logger?.TrackTrace("Failed to verify certificate for webhook callback.", new Dictionary<string, string> { { "Subject", certificate.Subject }, { "Issuer", certificate.Issuer } });
thrownew HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Certificate verification failed."));
}
if (!certificate.Issuer.Contains(MicrosoftCorporationIssuer))
{
logger?.TrackTrace($"Certificate not issued by {MicrosoftCorporationIssuer}.", new Dictionary<string, string> { { "Issuer", certificate.Issuer } });
thrownew HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, $"Certificate not issued by {MicrosoftCorporationIssuer}."));
}
}
}
}