Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ In Security Store, open **Private solutions** and select your approved subscript
its available offers. See [sample offers](docs/ONBOARDING.md#before-purchasing-or-deploying) in the
detailed guide; a listing does not guarantee support in this setup script.

Use the [SMS cost estimation guide](docs/COST-CALCULATOR.md) to create a planning estimate,
then confirm destination rates, fees, and final costs with your provider.

Have these ready:

- **Provider account:** complete the provider's account and sender registration for your chosen
Expand Down
226 changes: 226 additions & 0 deletions docs/COST-CALCULATOR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# Estimate SMS provider costs

Use this guide to create a planning estimate for SMS traffic before onboarding an
External Phone Provider (EPP). The report finds SMS and voice authentication steps in
recent sign-in activity and groups them by the phone number's international calling
code, such as `+1` or `+91`. Apply your provider's destination rates to the report to
estimate a budget.

**This is an estimate, not a usage or billing report.** A sign-in does not prove that
the provider accepted or delivered an SMS or voice call. Authentication details can
contain multiple SMS or voice steps for one sign-in, so this guide counts each matching
step as one estimated request. Actual provider billing can differ because of retries,
fallback, failures, message segmentation, and provider-specific charging rules. Confirm
the assumptions, supported destinations, taxes, fees, and final pricing with your provider.

The `authenticationDetails` data used by this guide is currently available through the
[Microsoft Graph beta API](https://learn.microsoft.com/en-us/graph/api/resources/authenticationdetail?view=graph-rest-beta).
Beta APIs are subject to change and are not supported for production applications. Review
and test this reporting script before each planning exercise. Because sign-in details mask
phone numbers, the script matches their visible trailing digits to the user's registered
phone authentication methods and derives the calling code from the matched registration.

## Prerequisites

Run the script in PowerShell 7 from a secured administrator workstation. The signed-in
account must be allowed to consent to or use these Microsoft Graph delegated permissions:

- `AuditLog.Read.All`
- `UserAuthenticationMethod.Read.All`

Access to sign-in logs also depends on your Microsoft Entra licensing, directory role,
and log-retention period. Review the requested permissions before consenting. The script
processes sign-in authentication details that can include phone numbers. It exports only
aggregated calling codes, methods, and counts, not full phone numbers. Store, share, and
delete the output according to your organization's privacy and retention requirements.

Create `C:\temp` before running the script, or change the `Export-Csv` path to an
approved folder.

## Generate the activity report

The following sample installs the required Microsoft Graph modules for the current user,
connects to Microsoft Graph, identifies SMS and voice authentication steps, and matches
each masked phone detail to a registered phone method. It caches phone-method lookups so
each user is queried only once. Replace `$start` and `$end` with the period you want to
analyze.

```powershell
# 1. Install required modules if they are missing
"Microsoft.Graph.Beta.Reports", "Microsoft.Graph.Identity.SignIns" |
Where-Object { -not (Get-Module -ListAvailable $_) } |
ForEach-Object {
Install-Module $_ -Scope CurrentUser -Force -AllowClobber | Out-Null
}

# 2. Connect to Graph API
Connect-MgGraph `
-Scopes "AuditLog.Read.All", "UserAuthenticationMethod.Read.All" |
Out-Null

# 3. Define Start and End Time Range (ISO 8601 UTC)
$start = "2026-10-01T00:00:00Z"
$end = "2026-10-30T23:59:59Z"

# 4. Cache registered phone methods and resolve masked sign-in details
$phoneMethodsByUser = @{}

$activity = Get-MgBetaAuditLogSignIn `
-Filter "createdDateTime ge $start and createdDateTime le $end" `
-All `
-ErrorAction Stop |
ForEach-Object {
$signIn = $_
$phoneDetails = @(
$signIn.AuthenticationDetails |
Where-Object {
$_.AuthenticationMethod -in "SMS", "Text message", "Voice"
}
)

if ($phoneDetails.Count -gt 0) {
if ([string]::IsNullOrWhiteSpace($signIn.UserId)) {
$registeredMethods = @()
}
else {
if (-not $phoneMethodsByUser.ContainsKey($signIn.UserId)) {
$phoneMethodsByUser[$signIn.UserId] = @(
Get-MgUserAuthenticationPhoneMethod `
-UserId $signIn.UserId `
-ErrorAction Stop
)
}

$registeredMethods = $phoneMethodsByUser[$signIn.UserId]
}

$phoneDetails |
ForEach-Object {
$authenticationDetail = $_
$authenticationMethod = if (
$authenticationDetail.AuthenticationMethod -in "SMS", "Text message"
) {
"SMS"
}
else {
"Voice"
}

$visibleDigits = $authenticationDetail.AuthenticationMethodDetail -replace '\D', ''

$matchingMethods = @(
$registeredMethods |
Where-Object {
$registeredDigits = $_.PhoneNumber -replace '\D', ''
$supportsMethod = (
$authenticationMethod -eq "Voice" -or
$_.PhoneType -in "mobile", "alternateMobile"
)

$supportsMethod -and
$visibleDigits.Length -ge 2 -and
$registeredDigits.EndsWith($visibleDigits)
}
)

$callingCode = if (
$matchingMethods.Count -eq 1 -and
$matchingMethods[0].PhoneNumber -match '^\s*(\+\d{1,3})(?:\s|$)'
) {
$Matches[1]
}
else {
"Unknown"
}

[PSCustomObject]@{
CountryCallingCode = $callingCode
AuthenticationMethod = $authenticationMethod
}
}
}
}

# 5. Group and count by calling code and authentication method
$report = $activity |
Group-Object CountryCallingCode, AuthenticationMethod |
ForEach-Object {
[PSCustomObject]@{
CountryCallingCode = $_.Group[0].CountryCallingCode
AuthenticationMethod = $_.Group[0].AuthenticationMethod
EstimatedRequests = $_.Count
}
} |
Sort-Object CountryCallingCode, AuthenticationMethod

# 6. Display and export the aggregated report
$report | Format-Table -AutoSize
$report | Export-Csv -Path "C:\temp\SMS_Voice_SignIns_By_Calling_Code.csv" -NoTypeInformation
```

Microsoft documents `SMS` and `Voice` as authentication method values and states that
`authenticationMethodDetail` can contain the phone number for those methods. Some tenant
sign-in results report SMS as `Text message`; the script accepts both values and normalizes
them to `SMS` in the exported report. It does not use the cmdlet's `-Property` parameter,
because `authenticationDetails` is returned on the beta sign-in objects without that
selection and the parameter has not behaved consistently across module versions.

The phone detail can be masked, for example `+x XXXXXXXX90`. The script removes non-digits
from that detail and matches the visible suffix (`90`) against the user's registered
methods. It then extracts the calling code from the uniquely matched registered number,
whose Graph format includes a space after the code, such as `+91 1234567890`.

For SMS, only `mobile` and `alternateMobile` registrations are considered; voice can use
any registered phone type. A row is grouped as `Unknown` when the masked detail exposes
fewer than two digits, no registered number matches, more than one registered number has
the same visible suffix, or the matched number has an unexpected format. This avoids
guessing a destination. Review the `Unknown` total before relying on the estimate.

The script does not use sign-in geography. `-All` requests all available pages, but large
tenants should still account for Microsoft Graph throttling and execution time. The script
uses `-ErrorAction Stop` rather than silently producing a partial report when either
sign-in or phone-method retrieval fails.

The Microsoft Graph
[list signIns API documentation](https://learn.microsoft.com/en-us/graph/api/signin-list?view=graph-rest-1.0&tabs=http)
states: **"The maximum and default page size is 1,000 objects and by default, the most
recent sign-ins are returned first. Only sign-in events that occurred within the
Microsoft Entra ID default retention period are available."** Selecting an earlier
`$start` date does not make events outside the available retention period accessible.

## Convert activity into a cost estimate

The exported report contains one row per calling code and authentication method. It counts
each SMS or voice authentication step as one estimated request:

```text
Estimated SMS/voice requests = matching authentication-step count
Estimated cost = estimated requests x provider price per request
Projected cost = estimated cost x projected days / observed days
```

For example, if the report covers 30 days and contains 10,000 matching authentication
steps:

```text
Estimated SMS/voice requests = 10,000
```

Apply the provider's destination-specific rates to those estimated requests. Use separate
rows when rates differ by destination, sender type, route, or message category. Include a
contingency for growth, retries, fallback behavior, and seasonal peaks.

## Confirm the final estimate with your provider

Work with your provider to validate:

- How each international calling code maps to the provider's destination pricing.
- Country and carrier coverage, sender registration, and route-specific rates.
- SMS segment rules, including Unicode and messages that exceed one segment.
- Minimum commitments, volume tiers, taxes, regulatory fees, and other surcharges.
- Charges for failed, rejected, retried, or duplicate submissions.
- The expected ratio of SMS sends to sign-ins for your authentication policies and users.

After deployment, compare the estimate with provider billing and approved operational
telemetry. Do not treat this report as proof of message submission, acceptance, delivery,
or the amount that the provider will invoice.
Loading