A Microsoft Dataverse managed solution that adds Custom APIs for validating and formatting IBANs, phone numbers, and email addresses directly inside Dataverse — from model-driven apps, Power Automate flows, plugins, or the Web API.
Dataverse doesn't ship with built-in validation for IBANs, international phone numbers, or email addresses. Teams often solve this by calling third-party validation services over the internet, which introduces several problems:
- 🔒 Data privacy / compliance risk — sensitive data (bank account details, phone numbers) leaves your Dataverse environment and is sent to an external service.
- 🌐 No offline / restricted-network support — external HTTP calls can fail or be blocked entirely in locked-down environments.
- 💸 Cost & rate limits — many 3rd party validation APIs are metered or rate-limited.
- 🐌 Latency & reliability — an extra network hop adds latency and another point of failure.
DigitallValidators solves this by validating entirely "offline", inside the Dataverse sandbox, without any outbound calls to external services. It is built on top of two well-established, actively maintained open source libraries:
- IbanNet — IBAN parsing, validation and formatting according to the IBAN registry.
- libphonenumber-csharp — a C# port of Google's libphonenumber, for parsing, validating and formatting phone numbers.
- MimeKit — RFC 5322 compliant email address parsing.
Email domain typo-detection (e.g. suggesting gmail.com for gmial.com) is a small, purpose-built helper included in this solution rather than a third-party library — see dgt_ValidateEmailAddress below.
All of the above ship with the solution and run as part of the plugin assembly, so no data ever leaves your Dataverse environment.
- A Microsoft Dataverse environment you have System Administrator/Customizer access to.
- The GitHub CLI (
gh) (optional, for downloading releases from the terminal) or a browser. - Power Platform CLI (
pac) (optional, for importing the solution from the command line).
Every push to main is released automatically via semantic-release and publishes a DigitallValidators.zip asset to GitHub Releases.
Using the GitHub CLI:
# Download the latest release asset into the current directory
gh release download --repo DIGITALLNature/DigitallValidators --pattern "DigitallValidators.zip"Or download the DigitallValidators.zip asset manually from the latest release.
Option A — Power Platform CLI:
pac auth create --url https://<your-org>.crm.dynamics.com
pac solution import --path .\DigitallValidators.zipOption B — Power Platform admin center / maker portal:
- Go to Solutions in the Power Apps maker portal.
- Select Import solution and choose the downloaded
DigitallValidators.zip. - Follow the import wizard and confirm.
Once imported, the three Custom APIs described below (dgt_ValidateIban, dgt_ValidatePhoneNumber, and dgt_ValidateEmailAddress) are available in your environment and can be called from plugins, Power Automate ("Perform an unbound action"), Power Fx, or the Dataverse Web API.
All Custom APIs are unbound, non-function actions (i.e. called via POST) and can be invoked at:
POST [Organization URI]/api/data/v9.2/<CustomApiName>
with header Content-Type: application/json.
The Message output parameter of all three APIs is localized. Pass LanguageCode (an LCID, matching how Dataverse represents languages elsewhere) to pick the language explicitly. If omitted, the calling user's configured Dataverse UI language (usersettings.uilanguageid) is used instead. If neither yields a supported language, messages default to English. Currently English (en) and German (de) are supported; unsupported languages fall back to English. ErrorCode values are always stable, untranslated identifiers suitable for programmatic handling — only Message changes with language.
Validates an IBAN and returns it in electronic and print format. Validation is based on the IBAN registry (structure, length, check digits, country support), with optional allow/reject lists per country.
| Name | Type | Required | Description |
|---|---|---|---|
Iban |
String |
Yes | The IBAN to validate. |
AllowedCountries |
String[] |
No | Only allow the specified countries (ISO 3166-1 alpha-2, e.g. ["DE", "AT"]). |
RejectedCountries |
String[] |
No | Reject the specified countries (ISO 3166-1 alpha-2, e.g. ["US"]). |
LanguageCode |
Whole Number |
No | The LCID to localize Message into (e.g. 1031 for German). If omitted, the calling user's configured Dataverse UI language is used; falls back to English if that can't be determined. |
| Name | Type | Description |
|---|---|---|
IsValid |
Boolean |
Whether the provided IBAN is valid. |
ElectronicFormat |
String |
IBAN in electronic / machine readable format (without spaces). Only set when valid. |
PrintFormat |
String |
IBAN in print / human readable format (with spaces). Only set when valid. |
ErrorCode |
String |
Error code identifying the validation failure (see below). Only set when invalid. |
Message |
String |
Human readable error message. Only set when invalid. |
ErrorCode corresponds to an IbanNet validation result type name, e.g.:
| ErrorCode | Meaning |
|---|---|
InvalidLengthResult |
The IBAN has an unexpected length for its country. |
InvalidStructureResult |
The IBAN does not match the expected structure/pattern. |
InvalidCheckDigitsResult |
The IBAN check digits do not validate (checksum failure). |
IllegalCharactersResult |
The IBAN contains characters that are not allowed. |
IllegalCountryCodeCharactersResult |
The country code portion contains invalid characters. |
UnknownCountryCodeResult |
The country code is not a known/supported IBAN country. |
CountryNotAcceptedResult |
The country was excluded via AllowedCountries/RejectedCountries. |
POST [Organization URI]/api/data/v9.2/dgt_ValidateIban
Content-Type: application/json
{
"Iban": "DE89370400440532013000"
}Sample response (200 OK):
{
"IsValid": true,
"ElectronicFormat": "DE89370400440532013000",
"PrintFormat": "DE89 3704 0044 0532 0130 00",
"ErrorCode": null,
"Message": null
}POST [Organization URI]/api/data/v9.2/dgt_ValidateIban
Content-Type: application/json
{
"Iban": "DE89370400440532013012"
}Sample response (200 OK):
{
"IsValid": false,
"ElectronicFormat": null,
"PrintFormat": null,
"ErrorCode": "InvalidCheckDigitsResult",
"Message": "The IBAN's check digits are invalid."
}POST [Organization URI]/api/data/v9.2/dgt_ValidateIban
Content-Type: application/json
{
"Iban": "DE75512108001245126199",
"AllowedCountries": ["FR"]
}Sample response (200 OK):
{
"IsValid": false,
"ElectronicFormat": null,
"PrintFormat": null,
"ErrorCode": "CountryNotAcceptedResult",
"Message": "The IBAN's country is not in the accepted list of countries."
}Validates a phone number and returns it formatted according to the requested format, based on libphonenumber-csharp.
| Name | Type | Required | Description |
|---|---|---|---|
PhoneNumber |
String |
Yes | The phone number to validate. Can be in national format (requires DefaultRegion) or international format (e.g. +491775628291). |
DefaultRegion |
String |
No | Two letter ISO 3166 country code (e.g. DE, US) used as a default/hint to parse numbers that are given in national format (no leading +). It is not enforced against the parsed result — a number with an explicit country code that differs from DefaultRegion is still considered valid. If omitted, PhoneNumber must be in international format (leading +). |
AllowedRegions |
String[] |
No | List of two letter ISO 3166 country codes. If provided, the number's detected region must be in this list, otherwise validation fails with REGION_NOT_ALLOWED. |
RejectedRegions |
String[] |
No | List of two letter ISO 3166 country codes. If provided and the number's detected region is in this list, validation fails with REGION_REJECTED. |
Format |
String |
No | Output format for FormattedPhoneNumber. One of E164 (default), INTERNATIONAL, NATIONAL, RFC3966. |
LanguageCode |
Whole Number |
No | The LCID to localize Message into (e.g. 1031 for German). If omitted, the calling user's configured Dataverse UI language is used; falls back to English if that can't be determined. |
| Name | Type | Description |
|---|---|---|
IsValid |
Boolean |
Whether the provided phone number is valid (and, if specified, allowed by AllowedRegions/RejectedRegions). |
FormattedPhoneNumber |
String |
The phone number formatted according to Format. Only set when valid. |
ErrorCode |
String |
Error code identifying the validation failure (see below). Only set when invalid. |
Message |
String |
Human readable message describing the validation error. Only set when invalid. |
ErrorCode values:
| ErrorCode | Meaning |
|---|---|
INVALID_COUNTRY_CODE |
The number's country calling code could not be determined (e.g. no DefaultRegion given and number not in international format). |
NOT_A_NUMBER |
The input string could not be parsed as a phone number at all. |
TOO_SHORT_AFTER_IDD |
The string is too short after an IDD (international dialing prefix). |
TOO_SHORT_NSN |
The national significant number is too short to be a valid number. |
TOO_LONG |
The number is too long to be a valid phone number. |
REGION_NOT_ALLOWED |
The number's detected region is not in the AllowedRegions list. |
REGION_REJECTED |
The number's detected region is in the RejectedRegions list. |
INVALID_PHONE_NUMBER |
The number is well-formed but not a valid, in-use phone number. |
An invalid DefaultRegion/AllowedRegions/RejectedRegions entry (not a valid ISO 3166 two-letter code) or invalid Format value results in an HTTP error response (InvalidPluginExecutionException) rather than an IsValid: false result, since these are caller/programming errors rather than validation outcomes.
POST [Organization URI]/api/data/v9.2/dgt_ValidatePhoneNumber
Content-Type: application/json
{
"PhoneNumber": "01775628291",
"DefaultRegion": "DE"
}Sample response (200 OK):
{
"IsValid": true,
"FormattedPhoneNumber": "+491775628291",
"ErrorCode": null,
"Message": null
}POST [Organization URI]/api/data/v9.2/dgt_ValidatePhoneNumber
Content-Type: application/json
{
"PhoneNumber": "01775628291",
"DefaultRegion": "DE",
"Format": "INTERNATIONAL"
}Sample response (200 OK):
{
"IsValid": true,
"FormattedPhoneNumber": "+49 177 5628291",
"ErrorCode": null,
"Message": null
}POST [Organization URI]/api/data/v9.2/dgt_ValidatePhoneNumber
Content-Type: application/json
{
"PhoneNumber": "+491778261829",
"AllowedRegions": ["US", "CA"]
}Sample response (200 OK):
{
"IsValid": false,
"FormattedPhoneNumber": null,
"ErrorCode": "REGION_NOT_ALLOWED",
"Message": "The phone number's region 'DE' is not in the allowed list (US, CA)."
}POST [Organization URI]/api/data/v9.2/dgt_ValidatePhoneNumber
Content-Type: application/json
{
"PhoneNumber": "017728371992"
}Sample response (200 OK):
{
"IsValid": false,
"FormattedPhoneNumber": null,
"ErrorCode": "INVALID_COUNTRY_CODE",
"Message": "The country calling code is invalid."
}Validates an email address's format (RFC 5322, via MimeKit), normalizes it (lowercased, IDN/punycode-encoded domain), and optionally restricts or rejects specific domains. It also suggests corrections for common domain typos (e.g. gmial.com → gmail.com) using a small built-in curated domain list, entirely offline — no DNS/MX lookups or outbound calls are made, consistent with this solution's design.
| Name | Type | Required | Description |
|---|---|---|---|
Email |
String |
Yes | The email address to validate. |
AllowedDomains |
String[] |
No | Only allow the specified domains (e.g. ["company.com"]). Also seeds the typo-suggestion corpus (see below), since the whole point of an allow-list is "these are the only correct domains". |
RejectedDomains |
String[] |
No | Reject the specified domains (e.g. ["mailinator.com"] to block known disposable-email domains). Also excluded from the typo-suggestion corpus, so a rejected domain is never suggested as a correction. |
AdditionalKnownDomains |
String[] |
No | Extra domains to recognize for typo-suggestion purposes only (e.g. partner/client domains), without affecting what's actually considered valid. Merged with the built-in curated domain list. |
LanguageCode |
Whole Number |
No | The LCID to localize Message into (e.g. 1031 for German). If omitted, the calling user's configured Dataverse UI language is used; falls back to English if that can't be determined. |
| Name | Type | Description |
|---|---|---|
IsValid |
Boolean |
Whether the provided email address is valid (well-formed and, if specified, allowed by AllowedDomains/RejectedDomains). |
NormalizedEmail |
String |
The email address with its domain lowercased and IDN/punycode-encoded. Only set when the address is well-formed (even if rejected by AllowedDomains/RejectedDomains). |
SuggestedEmails |
String[] |
Plausible typo-corrections for the domain (e.g. ["user@gmail.com"] for user@gmial.com), ordered by ascending edit distance. Independent of IsValid — a rejected/disallowed domain can still get a suggestion. Empty array if the domain already exactly matches a known domain or if there's no close match. |
ErrorCode |
String |
Error code identifying the validation failure (see below). Only set when invalid. |
Message |
String |
Human readable message describing the validation error. Only set when invalid. |
ErrorCode values:
| ErrorCode | Meaning |
|---|---|
INVALID_FORMAT |
The value could not be parsed as a valid, fully-qualified (local-part + domain) email address. |
DOMAIN_NOT_ALLOWED |
The email's domain is not in the AllowedDomains list. |
DOMAIN_REJECTED |
The email's domain is in the RejectedDomains list. |
POST [Organization URI]/api/data/v9.2/dgt_ValidateEmailAddress
Content-Type: application/json
{
"Email": "User@Example.com"
}Sample response (200 OK):
{
"IsValid": true,
"NormalizedEmail": "User@example.com",
"SuggestedEmails": [],
"ErrorCode": null,
"Message": null
}POST [Organization URI]/api/data/v9.2/dgt_ValidateEmailAddress
Content-Type: application/json
{
"Email": "user@gmial.com"
}Sample response (200 OK):
{
"IsValid": true,
"NormalizedEmail": "user@gmial.com",
"SuggestedEmails": ["user@gmail.com", "user@mail.com"],
"ErrorCode": null,
"Message": null
}POST [Organization URI]/api/data/v9.2/dgt_ValidateEmailAddress
Content-Type: application/json
{
"Email": "user@compnay.com",
"AllowedDomains": ["company.com"]
}Sample response (200 OK):
{
"IsValid": false,
"NormalizedEmail": "user@compnay.com",
"SuggestedEmails": ["user@company.com"],
"ErrorCode": "DOMAIN_NOT_ALLOWED",
"Message": "The email domain 'compnay.com' is not in the allowed list (company.com)."
}POST [Organization URI]/api/data/v9.2/dgt_ValidateEmailAddress
Content-Type: application/json
{
"Email": "not-an-email"
}Sample response (200 OK):
{
"IsValid": false,
"NormalizedEmail": null,
"SuggestedEmails": null,
"ErrorCode": "INVALID_FORMAT",
"Message": "The value could not be parsed as a valid email address."
}Licensed under the Microsoft Public License (Ms-PL).