Skip to main content

NCE Renewals Lock You In Whether You Noticed or Not. This Flow Makes Sure You Notice

Nick Ross5 min read

TL;DR

  • Microsoft NCE's commitment terms and cancellation windows mean a missed renewal date locks an MSP into the old seat count for another full term.
  • Coterminosity lets partners align NCE end dates to a calendar month charge cycle, but as of September 2022 many distributors have not updated early adopters' renewal dates.
  • A daily Power Automate flow can compare every Pax8 subscription end date against a target date computed with formatDateTime(addDays(utcNow(), N), 'yyyy-MM-dd').
  • The Pax8 subscriptions endpoint paginates and always returns a blank final page, so the flow decrements its total-pages variable by one before looping.
  • Wrapping the PSA ticket creation in two Scope actions gives the flow a try/catch pattern, so an API failure produces a Teams notice instead of a silent miss.

Microsoft New Commerce raised the stakes on a date most MSPs used to ignore. Commitment terms and cancellation policies mean that once a subscription renews, you are locked into that seat count, and the renewal dates themselves can be different for every customer if you did not migrate them all on the same day.

Microsoft's answer is what it calls coterminosity: the ability to align end dates across subscriptions, including to a calendar month charge cycle, which gets things closer to how legacy worked and how PSA billing actually runs. The catch, as of September 2022, is that if you adopted NCE early and work through a distributor, that distributor likely has not gotten around to updating your renewal dates to the calendar month. Add in that the discounted annual contracts most of us now use carry their own variable per-customer renewal dates, and the picture is messy.

The strict cancellation and decrement window is what makes the mess dangerous. You need to know a renewal is coming while there is still time to adjust seat counts, not after the term resets. That is a job for automation.

What does the finished flow do?

Using the Pax8 API, Power Automate, and a custom connector for the PSA, the flow automatically creates a ticket in Syncro (the example PSA here) when a subscription is approaching renewal:

Syncro ticket created automatically for an upcoming NCE renewal
Renewal ticket detail with subscription information

We want renewals to land as tickets so the team can track time and document customer communications, but the destination is your choice. Other options:

  • Generate a sales order so reps can review with the customer
  • Post a Teams message
  • Send seat approval notices to customers for upcoming renewals, via email or Teams

Example of the Teams variant:

Teams message announcing an upcoming subscription renewal

More advanced versions we have implemented, which take additional work:

  • Populating available counts versus consumed counts for the licenses into the ticket via the Microsoft Graph API (visible in the screenshot above)
  • Populating assigned users, also via the Graph API
  • Automatically adjusting licensing at renewal, using the Pax8 API and Graph API together to reduce seats by available minus consumed counts

The end goal is less liability and a process as close to touchless as possible.

What you need before building

Set the schedule and the target date

There are many steps in Power Automate here; the complete click-by-click list is in the video at the bottom of the page. The build summary:

  1. Set up a Recurrence trigger to run daily:
Daily recurrence trigger for the renewal flow
  1. Set the date you want to be notified about. A Compose action with two nested Power Automate expressions takes the current date and adds days, which is what lets the flow flag renewals N days out as it loops through Pax8 subscriptions. The formula:
text
formatDateTime(addDays(utcNow(),<Insert Number of Days to Add>), 'yyyy-MM-dd')
Compose action computing the renewal notice date

Authenticate to Pax8

  1. Get the client ID and secret for the Pax8 API from the Azure Key Vault connector (shown step by step in the Pax8 walkthrough):
Key Vault actions retrieving the Pax8 credentials
  1. Use the secrets in an HTTP request to get an access token, then parse the token response with Parse JSON, generating the schema from the Pax8 API docs:
HTTP request and Parse JSON for the Pax8 access token

Page through every subscription

  1. Call the subscriptions endpoint with the access token. This first call exists to learn the total number of pages, because the Pax8 API paginates. Generate the Parse JSON schema from the docs again:
Initial subscriptions call to read the total page count
  1. Use the Variable connector to create variables for Total Pages and Current Page; these drive the loop. Decrement the Total Pages variable by one, because the Pax8 API always returns a blank final page:
Initializing the Total Pages and Current Page variables
  1. Create two more variables for Customer Name and Product Name. Make both strings and leave the values blank; the flow defines them dynamically later:
Initializing the blank Customer Name and Product Name variables
  1. Add a Do Until control to loop through the pages:
Do Until control looping through subscription pages
  1. Inside the loop, make another HTTP request for subscriptions, this time setting the page query parameter to the Current Page variable. Parse the response with this schema:
Paged subscriptions request inside the Do Until loop
text
{
    "type": "object",
    "properties": {
        "content": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string"
                    },
                    "companyId": {
                        "type": "string"
                    },
                    "productId": {
                        "type": "string"
                    },
                    "quantity": {
                        "type": [
                            "integer",
                            "number"
                        ]
                    },
                    "startDate": {
                        "type": "string"
                    },
                    "createdDate": {
                        "type": "string"
                    },
                    "billingStart": {
                        "type": "string"
                    },
                    "status": {
                        "type": "string"
                    },
                    "price": {
                        "type": [
                            "integer",
                            "number"
                        ]
                    },
                    "billingTerm": {
                        "type": "string"
                    },
                    "commitment": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "term": {
                                "type": "string"
                            },
                            "endDate": {
                                "type": "string"
                            }
                        }
                    },
                    "endDate": {
                        "type": "string"
                    }
                },
                "required": [
                    "id",
                    "companyId",
                    "productId",
                    "quantity",
                    "startDate",
                    "createdDate",
                    "billingStart",
                    "status",
                    "price",
                    "billingTerm"
                ]
            }
        },
        "page": {
            "type": "object",
            "properties": {
                "size": {
                    "type": "integer"
                },
                "totalElements": {
                    "type": "integer"
                },
                "totalPages": {
                    "type": "integer"
                },
                "number": {
                    "type": "integer"
                }
            }
        }
    }
}

Match renewal dates and enrich the record

  1. Add an Apply to Each control using the dynamic content from the parse you just did. Inside it, add a Condition control comparing endDate against the outputs of the Get Date compose from earlier in the flow. If you see two dynamic endDate options, choose the first one. The condition asks: does this subscription's renewal date (the endDate) equal the target date? Leave the If no branch blank, since nothing should happen when there is no match:
Condition comparing subscription endDate to the target date
  1. In the If yes branch, make two more HTTP requests, one for the company record and one for the product record, dynamically populating the record IDs from the subscription. Parse JSON on both to get the content:
HTTP requests fetching the company and product records
  1. Use Set Variable actions to fill the Product Name and Company Name variables initialized earlier:
Set Variable actions populating product and company names

Find the customer in the PSA and cut the ticket

  1. Use the Syncro custom connector's search endpoint, populated with the Customer Name variable, then parse the result with the schema below:
Syncro search request using the customer name
text
{
    "type": "object",
    "properties": {
        "quick_result": {},
        "results": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "table": {
                        "type": "object",
                        "properties": {
                            "_id": {
                                "type": "integer"
                            },
                            "_type": {
                                "type": "string"
                            },
                            "_index": {
                                "type": "string"
                            },
                            "_source": {
                                "type": "object",
                                "properties": {
                                    "table": {
                                        "type": "object",
                                        "properties": {
                                            "firstname": {
                                                "type": "string"
                                            },
                                            "lastname": {
                                                "type": "string"
                                            },
                                            "email": {
                                                "type": "string"
                                            },
                                            "business_name": {
                                                "type": "string"
                                            },
                                            "phones": {
                                                "type": "array"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "required": [
                    "table"
                ]
            }
        },
        "error": {}
    }
}
  1. Add another Condition to determine whether the search returned a value, using the native empty() expression populated with the dynamic search results value:
Condition using empty() to test the search results
  1. If the value is empty, post about it in Teams using the message connector so a human can investigate. If a value was found (the If no branch), use two Scope actions to build a try/catch block:
Try and catch Scope actions wrapping the ticket creation
  1. In the Try block, start with a Compose action defining this expression:
text
outputs('Parse_Search')['body']['results'][0]['table']['_id']

The expression traverses the array returned by the Syncro search endpoint to extract the customer ID, which the ticket creation needs:

Compose action extracting the Syncro customer ID

Here is the array being traversed, for reference:

The Syncro search response array structure
  1. Create the ticket through the custom connector. The customer ID comes from the previous step's output, the startAt field uses the utcNow() expression, and the rest is dynamically populated from the variables and JSON outputs above. The fields available here are whatever you defined when you built the custom connector:
Create ticket action populated with dynamic renewal data
  1. Decide what goes in the second Scope, the Catch block. Think of it as error handling for a failed API request; another Teams notice is a reasonable choice. Make sure its run-after settings are configured as follows:
Configure run after settings on the Catch scope
Run after conditions set to failure states

The complete build on video

Every click of this flow is shown end to end in the full video tutorial (opens in new tab). Post any questions about the build there.

Frequently asked questions

Does this only work with Syncro as the PSA?

No. Syncro is the example because the custom connector build for it is already documented. Any PSA with an API can fill the same role; the Pax8 side of the flow does not change.

Why create a ticket instead of just sending a Teams message?

A ticket drives action you can track: time entries, customer communications, and an audit trail of the renewal decision. Teams messages work as a notification layer, and the flow shown here does both.

Can the flow adjust licenses automatically instead of notifying?

Yes, as a more advanced build. Combining the Pax8 API with the Microsoft Graph API lets you reduce seats to consumed counts automatically at renewal, making the process close to touchless.

Renewal dates are one deadline. Drift has no deadline at all

A missed renewal costs a term of licensing; a missed misconfiguration costs more. CloudCapsule watches 250+ security controls across every tenant you manage and shows you what changed.

Run a free scan
Nick Ross

Written by

Nick Ross

CEO · Microsoft MVP · Founder, T-Minus 365

Nick is not just a CEO, he's a respected thought leader and influencer in the MSP space. Tens of thousands of MSPs learn through his YouTube channel, T-Minus365. Nick has been honored as a three-time Microsoft MVP for his educational content; his expertise and influence are the backbone of our mission, ensuring that you are in the best hands when it comes to security.

Nick joined Pax8 in 2017, where he would ultimately oversee product management for PSA and Microsoft integrations. Following his tenure at Pax8, Nick has continued to demonstrate his leadership prowess as an executive at various MSPs, culminating in his most recent role at Sourcepass.

Nick holds a Bachelor's Degree in Business Management from Florida State University, as well as a Minor Degree in Entrepreneurship. In his free time, Nick is an avid hiker, reader, and fitness-junkie.

Keep reading