Manually submitting values to the Azure Metering API


In this script, we're using curl and jq. curl certainly is on your system, for jq, you might need to fetch it:
curl --silent \
--url \
--location \
--output jq && \
chmod +x jq && \
sudo mv jq /usr/local/bin && \
sudo chown root.root /usr/local/bin/jq


I assume you have 2 service principal credentials in the publisher/ISV AAD tenant:
  1. 1.
    We need a management credential which is authorized to manage customer deployments, using with the ARM API.
    • I created an AAD group, called managed-app-admins, and made the service principal a member of that group.
    • In Partner Center -> Offer -> Plan -> Technical Configuration -> Authorizations, I granted the AAD group Owner of managed apps.
    • As a result, the isv_management_cliend_id below can interact with managed apps.
    • image-20230127212906222
    • So 8cdea44d-38fe-4e3c-bf2a-6b96809d1d27 above is the object ID of the group, and the service principal with app id 8eff18b7-aeeb-4a88-b19c-bf60813a587c is member of that group.
  2. 2.
    We need a metering credential, which is authorized to submit usage to the metering API.
    • We do not use an identity in the managed app, i.e. we do not SSH/RDP into a VM in the managed resource group to do our work there. Having a VM just for that is too complicated (and expensive).
    • This isv_metering_client_id (app id) service principal is configured under Partner Center -> Offer -> Technical Configuration:
    • image-20230127212946875

The script

# This is the ISV/publisher app which is also in the partner center
# All of these 3 below work (for me)
# This service principal on the ISV AAD tenant has permissions to manage "managed apps" in customer environments
isv_management_client_secret="$( cat "/mnt/c/Users/chgeuer/.secrets/8eff18b7-aeeb-4a88-b19c-bf60813a587c-admin.txt" )"
# This service principal is the one that is configured in partner center, under the offer's technical plan
isv_metering_client_secret="$( cat "/mnt/c/Users/chgeuer/.secrets/3eb78160-e434-4713-8374-f40174d64348-metering.txt" )"
# You must now what you want to submit:
# Something like "2023-01-27T15:00:00Z". I'm always submitting for the top of the hour
# timestamp="$( date --utc --date='-20 hour' '+%Y-%m-%dT%H:00:00Z' )"
timestamp="$( date --utc '+%Y-%m-%dT%H:00:00Z' )" # Current hour
timestamp="2023-01-27T15:00:00Z" # Specific hour
echo "${timestamp}"
# We now need to determine the resourceUri of the of the managed app (in the customer subscription). When submitting to the metering API, your JSON body must either contain
# a resourceUri, or a resourceId, or both. In case of a managed app, resourceUri is the way to go.
# Please check my blog articles, one of those
# -
# -
# The resourceUri we want to determine is the `managedBy` property of the managed resource group. In this sample, we go the long way, i.e. talk to ARM
# on customer side. We query the **managed resource group**, using our management credential.
# The value we want to retrieve looks like this:
# "/subscriptions/724467b5-bee4-484b-bf13-d6a5505d2b51/resourceGroups/managed-app-resourcegroup/providers/Microsoft.Solutions/applications/chgp20230118"
# As the ISV, grab an access token for the ARM API
management_access_token="$( curl \
--silent \
--request POST \
--url "${isv_tenant_id}/oauth2/v2.0/token" \
--data-urlencode "response_type=token" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=${isv_management_client_id}" \
--data-urlencode "client_secret=${isv_management_client_secret}" \
--data-urlencode "scope=" \
| jq -r ".access_token" )"
# Look at the token contents, if you like
echo "${management_access_token}" | jq -R 'split(".") | .[1] | @base64d | fromjson'
# managedBy="/subscriptions/724467b5-bee4-484b-bf13-d6a5505d2b51/resourceGroups/managed-app-resourcegroup/providers/Microsoft.Solutions/applications/chgp20230118"
# Now query the properties of the managed resource group, and grab the managedBy property
managedBy="$( curl --silent --get \
--url "${customer_subscription_id}/resourcegroups/${customer_managed_resource_group_that_contains_the_resources}" \
--data-urlencode "api-version=2019-07-01" \
--header "Authorization: Bearer ${management_access_token}" \
| jq -r '.managedBy' )"
echo "managedBy: ${managedBy}"
# Now we can compose the JSON body for the marketplace call.
# As you can see, the managedBy value goes as resourceUri.
meteringPayloadJson="$( echo "{}" | \
jq --arg x "${managedBy}" '.resourceUri=$x' | \
jq --arg x "${planName}" '.planId=$x' | \
jq --arg x "${dimensionName}" '.dimension=$x' | \
jq --arg x "${quantity}" '.quantity=($x | fromjson)' | \
jq --arg x "${timestamp}" '.effectiveStartTime=$x' \
echo "${meteringPayloadJson}" > meteringPayload.json
# Now use the ISV's metering credential (the one in partner center), to fetch a token to talk to the metering API.
# The ID 20e940b3-4c77-4b0b-9a53-9e16a1b010a7 is effectively the metering API.
isv_metering_access_token="$( curl \
--silent \
--request POST \
--url "${isv_tenant_id}/oauth2/token" \
--data-urlencode "response_type=token" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=${isv_metering_client_id}" \
--data-urlencode "client_secret=${isv_metering_client_secret}" \
--data-urlencode "resource=20e940b3-4c77-4b0b-9a53-9e16a1b010a7" \
| jq -r ".access_token" )"
# Look at the token's content.
echo "${isv_metering_access_token}" | jq -R 'split(".") | .[1] | @base64d | fromjson' > isv_metering_access_token.json
# Now finally send the data to Azure Marketplace...
marketplace_response="$( curl \
--silent \
--request POST \
--url "" \
--header "Authorization: Bearer ${isv_metering_access_token}" \
--header "Content-Type: application/json" \
--data "${meteringPayloadJson}" )"
echo "${marketplace_response}" | jq . > marketplace_response.json
So the initial metering request looks like this:
Content-Type: application/json; charset=utf-8
Content-Length: 285
HTTP/1.1 200 OK
Content-Length: 464
Content-Type: application/json; charset=utf-8
Date: Fri, 27 Jan 2023 19:10:03 GMT
mise-correlation-id: 505bdb68-7806-4ab7-9b1c-12bee2a3b5b8
x-ms-requestid: fa209de8-f599-4b49-9a25-fc48c3920fd9
x-ms-correlationid: fa209de8-f599-4b49-9a25-fc48c3920fd9
You can see the "status":"Accepted", and a Microsoft-issued "usageEventId".
If you try to emit the same usage a 2nd time, you'll get a different response:
"message": "This usage event already exist.",
"code": "Conflict",
"additionalInfo": {
"acceptedMessage": {
"usageEventId": "5e1876e2-8b31-48fe-8810-864aeb67625b",
"status": "Duplicate",
"messageTime": "2023-01-27T19:10:02.527833Z",
"resourceId": "ffb5220f-0876-492c-b63b-26b73a1ad74f",
"resourceUri": "/subscriptions/724467b5-bee4-484b-bf13-d6a5505d2b51/resourceGroups/managed-app-resourcegroup/providers/",
"quantity": 5.0,
"dimension": "dimension-payg",
"effectiveStartTime": "2023-01-27T18:00:00Z",
"planId": "plan1"
Marketplace here tells us that the submission is Conflict / Duplicate of a previously submitted event.
  • "Azure Marketplace Metered Billing- Picking the correct ID when submitting usage events": To understand the difference between the resourceUri and resourceId in Azure Marketplace, please read my blog article , either in my private or the official blog.