I assume you have 2 service principal credentials in the publisher/ISV AAD tenant:
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.
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.
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:
The script
#!/bin/bash# This is the ISV/publisher app which is also in the partner center# All of these 3 below work (for me)isv_tenant_id="5f9e748d-300b-48f1-85f5-3aa96d6260cb"isv_tenant_id="geuerpollmannde.onmicrosoft.com"isv_tenant_id="geuer-pollmann.de"# This service principal on the ISV AAD tenant has permissions to manage "managed apps" in customer environmentsisv_management_client_id="8eff18b7-aeeb-4a88-b19c-bf60813a587c"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 planisv_metering_client_id="3eb78160-e434-4713-8374-f40174d64348"isv_metering_client_secret="$( cat "/mnt/c/Users/chgeuer/.secrets/3eb78160-e434-4713-8374-f40174d64348-metering.txt" )"isv_metering_client_id="${AZURE_METERING_MARKETPLACE_CLIENT_ID}"isv_metering_client_secret="=${AZURE_METERING_MARKETPLACE_CLIENT_SECRET}"## 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 hourtimestamp="2023-01-27T15:00:00Z"# Specific hourecho"${timestamp}"planName="plan1"dimensionName="dimension-payg"quantity=5# 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# - https://cookbook.geuer-pollmann.de/azure/marketplace-metering-ids# - https://techcommunity.microsoft.com/t5/fasttrack-for-azure/azure-marketplace-metered-billing-picking-the-correct-id-when/ba-p/3542373
## 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"
#customer_subscription_id="724467b5-bee4-484b-bf13-d6a5505d2b51"customer_managed_resource_group_that_contains_the_resources="mrg-chgp20230118"# As the ISV, grab an access token for the ARM APImanagement_access_token="$( curl \--silent \--requestPOST \--url "https://login.microsoftonline.com/${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=https://management.azure.com/.default" \|jq-r ".access_token" )"# Look at the token contents, if you likeecho"${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 propertymanagedBy="$( curl--silent--get \ --url "https://management.azure.com/subscriptions/${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--argx "${managedBy}" '.resourceUri=$x' | \jq--argx "${planName}" '.planId=$x' | \jq--argx "${dimensionName}" '.dimension=$x' | \jq--argx "${quantity}" '.quantity=($x | fromjson)' | \jq--argx "${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 \--requestPOST \--url "https://login.microsoftonline.com/${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 \--requestPOST \--url "https://marketplaceapi.microsoft.com/api/usageEvent?api-version=2018-08-31" \--header "Authorization: Bearer ${isv_metering_access_token}" \--header "Content-Type: application/json" \--data "${meteringPayloadJson}" )"echo"${marketplace_response}"|jq.>marketplace_response.json
Marketplace here tells us that the submission is Conflict / Duplicate of a previously submitted event.
Links
"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.