> For the complete documentation index, see [llms.txt](https://cookbook.geuer-pollmann.de/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://cookbook.geuer-pollmann.de/azure/marketplace-submit-manually-using-script.md).

# Manually submitting values to the Azure Metering API

## Requirements

In this script, we're using `curl` and `jq`. `curl` certainly is on your system, for `jq`, you might need to fetch it:

```shell
#!/bin/bash

curl --silent \
     --url https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 \
     --location \
     --output jq && \
chmod +x jq && \
sudo mv jq /usr/local/bin && \
sudo chown root.root /usr/local/bin/jq
```

## Credentials

I assume you have 2 service principal credentials in the publisher/ISV AAD tenant:

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](/files/Hvn5M03PSMhE5eSN7pZE)
   * 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. 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](/files/rIo8rWocTDlRzfYNE65R)

## The script

```shell
#!/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 environments
isv_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 plan
isv_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 hour
timestamp="2023-01-27T15:00:00Z" # Specific hour
echo "${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 API
management_access_token="$( curl \
    --silent \
    --request POST \
    --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 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 "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 --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 "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 \
   --request POST \
   --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
```

So the initial metering request looks like this:

```http
POST https://saasapi.azure.com/api/batchUsageEvent?api-version=2018-08-31 HTTP/1.1
Host: saasapi.azure.com
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyIsImtpZCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyJ9.eyJhdWQiOiIyMGU5NDBiMy00Yzc3LTRiMGItOWE1My05ZTE2YTFiMDEwYTciLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC81ZjllNzQ4ZC0zMDBiLTQ4ZjEtODVmNS0zYWE5NmQ2MjYwY2IvIiwiaWF0IjoxNjc0ODQ2MzAxLCJuYmYiOjE2NzQ4NDYzMDEsImV4cCI6MTY3NDg1MDIwMSwiYWlvIjoiRTJaZ1lJaXkvSmtwMWlreFFmaGhzNDdWMFU5Y0FBPT0iLCJhcHBpZCI6IjNlYjc4MTYwLWU0MzQtNDcxMy04Mzc0LWY0MDE3NGQ2NDM0OCIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzVmOWU3NDhkLTMwMGItNDhmMS04NWY1LTNhYTk2ZDYyNjBjYi8iLCJvaWQiOiIyZTgyMjA4ZS1lYWQ5LTQwMTYtOTY1OC03NTM4NTUxYTE0MWYiLCJyaCI6IjAuQVJFQWpYU2VYd3N3OFVpRjlUcXBiV0pneTdOQTZTQjNUQXRMbWxPZUZxR3dFS2NSQUFBLiIsInN1YiI6IjJlODIyMDhlLWVhZDktNDAxNi05NjU4LTc1Mzg1NTFhMTQxZiIsInRpZCI6IjVmOWU3NDhkLTMwMGItNDhmMS04NWY1LTNhYTk2ZDYyNjBjYiIsInV0aSI6Ik5XdVVOWE5uOWtPWUtRQkZUMkV1QUEiLCJ2ZXIiOiIxLjAifQ.ecjxnxfuckoffsignaturetOtG8kuXyMr0uEExxxx
Content-Type: application/json; charset=utf-8
Content-Length: 285

{
   "resourceUri":"/subscriptions/724467b5-bee4-484b-bf13-d6a5505d2b51/resourceGroups/managed-app-resourcegroup/providers/microsoft.solutions/applications/chgp20230118",
   "effectiveStartTime":"2023-01-27T18:00:00Z",
   "planId":"plan1",
   "dimension":"dimension-payg",
   "quantity":5.0
}

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

{
   "usageEventId":"5e1876e2-8b31-48fe-8810-864aeb67625b",
   "status":"Accepted",
   "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/microsoft.solutions/applications/chgp20230118",
   "quantity":5.0,
   "dimension":"dimension-payg",
   "effectiveStartTime":"2023-01-27T18:00:00Z",
   "planId":"plan1"
}
```

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:

```json
{
  "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/microsoft.solutions/applications/chgp20230118",
      "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.

## 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](https://cookbook.geuer-pollmann.de/azure/marketplace-metering-ids) or the [official](https://techcommunity.microsoft.com/t5/fasttrack-for-azure/azure-marketplace-metered-billing-picking-the-correct-id-when/ba-p/3542373) blog.
* <https://docs.microsoft.com/en-us/azure/marketplae/marketplace-metering-service-authentication>
* <https://docs.microsoft.com/en-us/azure/marketplace/partner-center-portal/pc-saas-registration#get-the-token-with-an-http-post>
