Webhook Actions
This Fulfillment Webhook is called by Marketplace Elements when actions need to be performed on a subscription.
Request Body
All requests to your webhook will be sent with a JWT payload in the body of the request. The JWT payload needs to be validated and decrypted using the Offer Public Key from the offer details page in Marketplace Elements, to get the action Json object.
Body example
{
"payload": "<jwt>"
}
JWT Payload
The JWT payload is a JSON object with an action
property to indicate the action to be performed. The JWT is signed with a Private Key specific for your offer and requires validation with the Public Key on the Offer details page.
Validating the JWT
Refer to the JWT Validation page.
Actions
CreateAccount
The CreateAccount action is the first action sent when a new subscription is activated by the subscriber. This notifies your SaaS product that a new account needs to be created and contains all the subscriber’s default details, as well as any custom fields you included in Marketplace Elements for the offer.
After your SaaS products confirms receipt of this action, the subscriber’s billing period is initiated. For this reason, it is expected that the subscriber can start accessing the SaaS product soon after this action.
Note that the CreateAccount action does not include the current billing terms as these terms are not generated until after the CreateAccount action has been completed. Current billing terms will be sent in the TermsUpdate action.
Action Json example:
{
"action": "CreateAccount",
"email": "example@example.com",
"firstName": "John",
"lastName": "Doe",
"subscriptionId": "ee437b7e-e388-4f63-a604-45b44d6e684b",
"planIdentifier": "plan01",
"aadUserId": "31d3bf12-eea1-43b7-a87f-be119837d932",
"tenantId": "9c714ef2-3ce6-4dce-8a5c-ac6e9126d2f3",
"notificationEmail": "notification@example.com", // Email to use when notifying the subscriber of changes to their subscription. This defaults to the subscribing users email if an alternative is not provided
"autorenew": true,
"quantity": 1,
"isFreeTrial": false,
"customFields": {
// Contains any additional fields configured in the Marketplace Elements Landing Page
"name": "John Doe",
"organization": "Joes Pizzas"
},
"marketplaceInformation": {
"publisherIdentifier": "my-publisher-name" // The name of your publisher account in the Microsoft Partner Centre
}
}
Response Return
Code: 200 - The code 200 should be returned for both a successful response and a response with validation errors.
Successful example:
{
"success": true
}
With a validation error return, you can return validation errors for any fields and those errors will be displayed to the user. Processing of the subscription will stop until a new request is sent and returns a success.
Validation Failure Example:
Status: 200
Content-Type: application/json
{
"success": false,
"message": "Required fields are not complete",
"fieldErrors": {
"userName": "User name is required",
"publisherName": "Organization name is required.",
},
}
Code: 500 Internal server error, 404 Not found, 403 Forbidden. Subscription will not be processed, and user will be notified to contact the publishers support using the support email defined for the publisher or offer.
TermsUpdate
The TermsUpdate action is sent once the current terms for the subscription is received from Microsoft Partner Centre. The current terms are not generated and available until after activation has been completed. Generally, the termsUpdate action will only be sent once in the lifecycle of a subscription and will be sent shortly after activation.
Action Json example:
{
"action": "TermsUpdate",
"subscriptionId": "ee437b7e-e388-4f63-a604-45b44d6e684b",
"startDate": "2022-03-04T00:00:00Z",
"endDate": "2022-04-03T00:00:00Z",
"termUnit": "P1M",
"quantity": 1,
"isFreeTrial": false
}
Response Return
Code: 200 - Send once you have confirmed receipt of the action. Response does not require a body.
Code: 500 Internal server error, 404 Not found, 403 Forbidden. Action will be resent in increasing intervals until a success is received and 10 attempts have been made.
UpdateAccount
The update account action is sent when a user updates any of the fields on the manage subscription page, within Marketplace Elements.
Action Json example:
{
"action": "UpdateAccount",
"subscriptionId": "ee437b7e-e388-4f63-a604-45b44d6e684b", //The Elements subscription ID
"email": "example@example.com",
"notificationEmail": "notification@example.com", // Email to use when notifying the subscriber of changes to their subscription. This defaults to the subscribing users email if an alternative is not provided
"customFields": {
// Contains any additional fields configured in the Marketplace Elements Landing Page
"name": "John Doe",
"organization": "Joes Pizzas"
}
}
Response Return
Code: 200 - Send once you have confirmed receipt of the action. Response does not require a body.
Code: 500 Internal server error, 404 Not found, 403 Forbidden. Action will be resent in increasing intervals until a success is received and 10 attempts have been made.
ChangePlan
The ChangePlan Action is sent when the subscriber changes their subscription from one plan to another of the same offer.
Action Json example:
{
"action": "ChangePlan",
"subscriptionId": "ee437b7e-e388-4f63-a604-45b44d6e684b", //The Elements subscription ID
"planIdentifier": "plan02" //The Elements plan identifier
}
Response Return
Code: 200 - Send once you have confirmed receipt of the action. Response does not require a body.
Code: 500 Internal server error, 404 Not found, 403 Forbidden. Action will be resent in increasing intervals until a success is received and 10 attempts have been made.
ChangeQuantity
The ChangeQuantity action is sent when the subscriber increases or decreases the quantity of plans they have subscribed for. This is generally only used for per user plans.
Action Json example:
{
"action": "ChangeQuantity",
"subscriptionId": "ee437b7e-e388-4f63-a604-45b44d6e684b", //The Elements subscription ID
"quantity": 10
}
Response Return
Code: 200 - Send once you have confirmed receipt of the action. Response does not require a body.
Code: 500 Internal server error, 404 Not found, 403 Forbidden. Action will be resent in increasing intervals until a success is received and 10 attempts have been made.
Suspend
The Suspend action is sent when the subscription has been suspended but Azure Marketplace or App Source. A suspension will occure if payment for the subscription has not be made before the due date.
Once suspended, the subscriber has 30days to make payment. If payment is made then a reinstate action will be sent. If payment is not made then an unsubscribe action will be sent.
Action Json example:
{
"action": "Suspend",
"subscriptionId": "ee437b7e-e388-4f63-a604-45b44d6e684b" //The Elements subscription ID
}
Response Return
Code: 200 - Send once you have confirmed receipt of the action. Response does not require a body.
Code: 500 Internal server error, 404 Not found, 403 Forbidden. Action will be resent in increasing intervals until a success is received and 10 attempts have been made.
Reinstate
The Reinstate action will be sent if the subscription is to change from a suspended state, to a subscribed state.
Action Json example:
{
"action": "Reinstate",
"subscriptionId": "ee437b7e-e388-4f63-a604-45b44d6e684b", //The Elements subscription ID
"termUnit": "P1M",
"startDate": "2022-03-04T00:00:00Z",
"endDate": "2022-04-03T00:00:00Z"
}
Response Return
Code: 200 - Send once you have confirmed receipt of the action. Response does not require a body.
Code: 500 Internal server error, 404 Not found, 403 Forbidden. Action will be resent in increasing intervals until a success is received and 10 attempts have been made.
Renew
The Renew action is a periodic action that is sent and the end of each term, to initiate the next term if the subscriber has enabled auto renew. If auto renew is disabled, then no action will be sent and the subscription should end.
Action Json example:
{
"action": "Renew",
"subscriptionId": "ee437b7e-e388-4f63-a604-45b44d6e684b", //The Elements subscription ID
"termUnit": "P1M",
"startDate": "2022-03-04T00:00:00Z",
"endDate": "2022-04-03T00:00:00Z",
"isFreeTrial": false
}
Response Return
Code: 200 - Send once you have confirmed receipt of the action. Response does not require a body.
Code: 500 Internal server error, 404 Not found, 403 Forbidden. Action will be resent in increasing intervals until a success is received and 10 attempts have been made.
Unsubscribe
The unsubscribe action is sent in response to an explicit customer or CSP action by the cancellation of a subscription from the publisher site, the Azure portal, Microsoft 365 Admin Centre or Marketplace Elements. A subscription can also be cancelled implicitly, as a result of nonpayment of dues, after being in the Suspended state for 30 days.
When you receive an unsubscribe action you should retain customer data for recovery on request for at least seven days. Only then can customer data be deleted.
A SaaS subscription can be cancelled at any point in its life cycle. After a subscription is cancelled, it can’t be reactivated.
Action Json example:
{
"action": "Unsubscribe",
"subscriptionId": "ee437b7e-e388-4f63-a604-45b44d6e684b",
"email": "example@example.com",
"firstName": "John",
"lastName": "Doe",
"tenantId": "9c714ef2-3ce6-4dce-8a5c-ac6e9126d2f3"
}
Response Return
Code: 200 - Send once you have confirmed receipt of the action. Response does not require a body.
Code: 500 Internal server error, 404 Not found, 403 Forbidden. Action will be resent in increasing intervals until a success is received and 10 attempts have been made.
Examples
Below is examples of how to implement a webhook into your SaaS product using different technology stacks.
TypeScript - Zod validation
Zod is a TypeScript validation library and can be used to validate the request body and give type safety.
export const createAccountActionType = z.literal('CreateAccount')
export const termsUpdateActionType = z.literal('TermsUpdate')
export const updateAccountActionType = z.literal('UpdateAccount')
export const changePlanActionType = z.literal('ChangePlan')
export const changeQuantityActionType = z.literal('ChangeQuantity')
export const renewActionType = z.literal('Renew')
export const suspendActionType = z.literal('Suspend')
export const unsubscribeActionType = z.literal('Unsubscribe')
export const reinstateActionType = z.literal('Reinstate')
export const actionTypeSchema = z.union([
createAccountActionType,
termsUpdateActionType,
updateAccountActionType,
changePlanActionType,
changeQuantityActionType,
renewActionType,
suspendActionType,
unsubscribeActionType,
reinstateActionType,
])
export type ActionType = z.infer<typeof actionTypeSchema>
export const createAccountSchema = z.object({
action: createAccountActionType,
aadUserId: z.string().uuid(),
email: z.string(),
firstName: z.string(),
lastName: z.string(),
tenantId: z.string(),
subscriptionId: z.string(),
planIdentifier: z.string(),
notificationEmail: z.string().email(),
autoRenew: z.boolean(),
quantity: z.number(),
isFreeTrial: z.boolean(),
customFields: z.record(z.string().optional()),
marketplaceInformation: z.object({
publisherIdentifier: z.string(),
}),
})
export const termsUpdateSchema = z.object({
action: termsUpdateActionType,
subscriptionId: z.string(),
startDate: z.string(),
endDate: z.string(),
termUnit: z.string(),
quantity: z.number(),
isFreeTrial: z.boolean(),
})
export const updateAccountSchema = z.object({
action: updateAccountActionType,
subscriptionId: z.string(),
email: z.string().email(),
notificationEmail: z.string().email().optional(),
})
export const changePlanSchema = z.object({
action: changePlanActionType,
planIdentifier: z.string(),
subscriptionId: z.string(),
})
export const changeQuantitySchema = z.object({
action: changeQuantityActionType,
quantity: z.number(),
subscriptionId: z.string(),
})
export const reinstateSchema = z.object({
action: reinstateActionType,
subscriptionId: z.string(),
termUnit: z.string(),
startDate: z.string(),
endDate: z.string(),
})
export const renewSchema = z.object({
action: renewActionType,
subscriptionId: z.string(),
isFreeTrial: z.boolean(),
termUnit: z.string(),
startDate: z.string(),
endDate: z.string(),
})
export const suspendSchema = z.object({
action: suspendActionType,
subscriptionId: z.string(),
})
export const unsubscribeSchema = z.object({
action: unsubscribeActionType,
subscriptionId: z.string().uuid(),
firstname: z.string().optional(),
lastname: z.string().optional(),
email: z.string(),
tenantId: z.string(),
})
export const actionsSchema = z.union([
createAccountSchema,
termsUpdateSchema,
updateAccountSchema,
changePlanSchema,
changeQuantitySchema,
reinstateSchema,
renewSchema,
suspendSchema,
unsubscribeSchema,
])
function requestHandler(request: Request) {
const requestBodySchema = z.object({
payload: z.string(),
})
const requestBodyValidation = requestBodySchema.safeParse(requestBody)
if (!requestBodyValidation.success)
throw new Response(JSON.stringify({ error: requestBodyValidation.error }), { status: 400 })
const publicKey = Buffer.from(process.env.MARKETPLACE_ELEMENTS_OFFER_PUBLIC_KEY, 'base64').toString()
const token = requestBodyValidation.data.payload
const decodedToken = jwt.verify(token, publicKey, { algorithms: ['RS256'] })
const decodedTokenValidation = tokenSchema.safeParse(decodedToken)
if (!decodedTokenValidation.success) {
throw new Response(JSON.stringify({ error: decodedTokenValidation.error }), { status: 400 })
}
const event = decodedTokenValidation.data
if (event.action === 'CreateAccount') {
// Create account
} else if ()
}