Skip to content

Instantly share code, notes, and snippets.

@PanosGreg
Last active November 27, 2025 17:24
Show Gist options
  • Select an option

  • Save PanosGreg/a16b5dc4f5471e477ca8604bbc762b39 to your computer and use it in GitHub Desktop.

Select an option

Save PanosGreg/a16b5dc4f5471e477ca8604bbc762b39 to your computer and use it in GitHub Desktop.
DNS Made Easy helper functions. Might expand and write a proper module for this at some stage.
<#
.SYNOPSIS
DNS Made Easy helper functions
Context: I needed to do some bulk operations in DNS Made Easy at work,
I didn't find anything adequate online and so I wrote this.
.DESCRIPTION
This file exposes 3 functions, an enum and a class.
The functions are:
- Get-DMEAuthHeader
- Get-DMEDomain
- New-DMERecord
The types are:
- DMERecordType [enum]
- DMERecord [type]
.EXAMPLE
# load the functions and the classes
. C:\temp\DNSME-API.ps1
# store the DNS ME api key and secret key into variables
$ApiKey = 'xxx...'
$SecKey = 'yyy...'
# generate a list of records that we'll create in DNS Made Easy
$List = @(
'aaa'
'bbb'
'ccc'
)
$data = $list | foreach {[DMERecord]::new("$_-stage",'stage','CNAME',600)}
# finally create the records in DNS ME
$records = New-DMERecord -ApiKey $ApiKey -SecKey $SecKey -DomainId 999999 -Data $data -Verbose
# check the results to see if the operation failed or not
$records
In this example we load the helper functions and then we create a number of CNAME records in DNS ME
Do note that each record takes a few seconds to create.
Note: the domain ID 999999 that is used here is just an example
.NOTES
Author: Panos Grigoriadis
Date: 27-Nov-2025
Other notes:
- Documentation: https://api-docs.dnsmadeeasy.com/
Also record management docs: https://api-docs.dnsmadeeasy.com/#99ec6f53-ecd1-4ca5-b0bf-c997014108ef
Note: you can change the language to PowerShell in the docs page at the top, to see examples in PS
- Rate Limiting:
150 requests per 5 minute scrolling window.
For example, 100 requests could be made in one minute, followed by a 5 minute wait, following by 150 requests.
- Record type Values: A, AAAA, CNAME, HTTPRED, MX, NS, PTR, SRV, TXT
- Input validation
The json data given to DNS ME is case-sensitive and also order matters.
So this thing for example:
{
"name": "aaa-stage",
"type": "CNAME",
"value": "stage",
"gtdLocation": "DEFAULT",
"ttl": 600
}
The property order matters, and the key names are case sensitive.
- HTTP Methods & their equivalent Actions
HTTP Methods and their corresponding actions based on the DNSME documentation
POST = create
GET = read
PUT = update
DELETE = delete
- Authentication with DNSME
Every time you send a request to DNSME, you have to get a fresh new $Auth variable
this is needed in order for the header to be close in time with the request.
Otherwise you get this error:
Invoke-RestMethod: {"error": ["Request sent with date header too far out of sync. Difference in times is 79125, header value is 1720619037000"]}
- Gtd = Global Traffic Director
- Timestamps
The "created" and "updated" timestamps returned from DNSME are the number of seconds since the domain was last created in Epoch time
#>
#Requires -Version 7
function Get-DMEAuthHeader {
<#
.SYNOPSIS
This function just prepares the web request header that we need to send to DNSME in order to authenticate.
But this function does not authenticate with DNSME.
The output of this function needs to be used in Invoke-RestMethod to authenticate with DNSME.
#>
[cmdletbinding()]
param (
[string]$ApiKey,
[string]$SecKey
)
$SecBytes = [Text.Encoding]::UTF8.GetBytes($SecKey)
$Hmac = [Security.Cryptography.HMACSHA1]::new($SecBytes,$true)
$ReqDate = [System.DateTimeOffset]::Now.ToString('r')
$DateBytes = [Text.Encoding]::UTF8.GetBytes($ReqDate)
$DateHash = [BitConverter]::ToString($Hmac.ComputeHash($DateBytes)).Replace('-','').ToLower()
$AuthHeader = @{
'x-dnsme-apiKey' = $ApiKey
'x-dnsme-requestDate' = $ReqDate
'x-dnsme-hmac' = $DateHash
}
$params = @{
Headers = $AuthHeader
ContentType = 'application/json'
}
Write-Output $params
} #function
function Get-DMEDomain {
[OutputType([psobject])] # <-- the object we receive from DNS Made-Easy
[cmdletbinding()]
param (
[Parameter(Mandatory)]
[string]$ApiKey,
[Parameter(Mandatory)]
[string]$SecKey,
[string]$BaseUrl = 'https://api.dnsmadeeasy.com/V2.0/dns/managed'
)
# get the domain list from DNSME
$Auth = Get-DMEAuthHeader -ApiKey $ApiKey -SecKey $SecKey
$list = (Invoke-RestMethod $BaseUrl @auth -Verbose:$false).data
# calculate the domain creation date and add it as a custom property
$Utc = [System.DateTimeKind]::Utc
$list | foreach {
$val = [DateTime]::new(1970, 1, 1, 0, 0, 0, 0, $Utc).AddSeconds($_.created/1000)
$_ | Add-Member -NotePropertyName createdAt -NotePropertyValue $val
}
# set the default view of the object
$Def = 'name','id','createdAt'
$Class = [Management.Automation.PSPropertySet]
$DDPS = $Class::new('DefaultDisplayPropertySet',[string[]]$Def)
$Std = [Management.Automation.PSMemberInfo[]]@($DDPS)
# finally return the results
$list | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $Std -PassThru
}
enum DMERecordType {
A
CNAME
}
## TODO: [maybe] re-write this in C# (with proper private method and constructor overloads)
class DMERecord {
[string] $Name
[DMERecordType] $Type = [DMERecordType]::A
[string] $Value
[string] $GtdLocation = 'DEFAULT'
[uint16] $Ttl = 600
## constructors
# constructor overload with a default TTL and default Type
DMERecord ([string]$Name,$Value) {
if (-not $this.HasCorrectValue($Value,$this.Type)) {throw 'Cannot create the record'}
$this.Name = $Name
$this.Value = $Value
}
# constructor overload with a default TTL
DMERecord ([string]$Name,$Value,[DMERecordType]$Type) {
if (-not $this.HasCorrectValue($Value,$Type)) {throw 'Cannot create the record'}
$this.Name = $Name
$this.Value = $Value
$this.Type = $Type
}
# constructor overload with a default type
DMERecord ([string]$Name,$Value,[uint16]$Ttl) {
if (-not $this.HasCorrectValue($Value,$this.Type)) {throw 'Cannot create the record'}
$this.Name = $Name
$this.Value = $Value
$this.Ttl = $Ttl
}
# constructor overload with a param to provide both the TTL and the type
DMERecord ([string]$Name,$Value,[DMERecordType]$Type,[uint16]$Ttl) {
if (-not $this.HasCorrectValue($Value,$Type)) {throw 'Cannot create the record'}
$this.Name = $Name
$this.Value = $Value
$this.Type = $Type
$this.Ttl = $Ttl
}
## methods
# "private" method (not really private since PS cant do that)
hidden [bool] HasCorrectValue ($Value,[DMERecordType]$Type) {
$ValueIsIP = [System.Net.IPAddress]::TryParse($Value,[ref]$null)
$result = $false
if ($Type.ToString() -eq 'A') { # <-- if A record, then value must be an IP
if (-not $ValueIsIP) {Write-Warning 'You must provide a valid IP Address for the value'}
$result = $ValueIsIP
}
elseif ($Type.ToString() -eq 'CNAME') { # <-- if CNAME then value must be a string
if ($ValueIsIP) {Write-Warning 'You must provide a name for the value, not an IP Address'}
$result = -not $ValueIsIP
}
return $result
} #method HasCorrectValue
[string] ToJson () { # Note: it maintains the property order and has all keys in lower-case
$json = [ordered] @{
name = $this.Name
type = $this.Type.ToString()
value = $this.Value
gtdLocation = $this.GtdLocation
ttl = $this.Ttl
} | ConvertTo-Json -Compress
return $json
} #method ToJson
[string] ToString () { # <-- this overwrites the default .ToString() method
$result = [string]::Empty
if ($this.Type.ToString() -eq 'A') {
$result = '{0} record "{1}" resolves to "{2}"' -f $this.Type,$this.Name,$this.Value
}
elseif ($this.Type.ToString() -eq 'CNAME') {
$result = '{0} record "{1}" points to "{2}"' -f $this.Type,$this.Name,$this.Value
}
return $result # <-- ex. CNAME record "aaa-stage" points to "stage"
}
} #class DMERecord
function New-DMERecord {
<#
.EXAMPLE
$data = [DMERecord]::new('aaa-stage','stage','CNAME',600)
New-DMERecord -ApiKey $ApiKey -SecKey $SecKey -DomainId 999999 -Data $data
.EXAMPLE
$List = @(
'aaa'
'bbb'
'ccc'
)
$data = $list | foreach {[DMERecord]::new("$_-stage",'stage','CNAME',600)}
$c = New-DMERecord -ApiKey $ApiKey -SecKey $SecKey -DomainId 999999 -Data $data -Verbose
Create multiple records in a DNS zone
Note: I explicitly did not use the DNSME REST API endpoint /records/createMulti due to issues.
.NOTES
If the value does not end in a dot, your domain name will be appended to the value.
#>
[OutputType([psobject])] # <-- the object we receive from DNSME
[cmdletbinding()]
param (
[Parameter(Mandatory)]
[string]$ApiKey,
[Parameter(Mandatory)]
[string]$SecKey,
[Parameter(Mandatory)]
[string]$DomainId,
[Parameter(Mandatory)]
[DMERecord[]]$Data,
[string]$BaseUrl = 'https://api.dnsmadeeasy.com/V2.0/dns/managed'
)
## TODO
# [maybe] add checks: if the domainid exists, if the record already exists.
# Although these are handled by DNSME and they'll just slow down the function
# (and will also add requests for the rate limit if you run this in parallel)
# Also [maybe] write a class for the output object
$Url = "$($BaseUrl.TrimEnd('/'))/$DomainId/records/"
# create the records in DNSME
$Results = foreach ($Record in $Data) {
Write-Verbose "Creating $($Record.Type) record for $($Record.Name)"
$Auth = Get-DMEAuthHeader -ApiKey $ApiKey -SecKey $SecKey # <-- need to do this on each request
$params = @{
Uri = $Url
Method = 'Post'
Body = $Record.ToJson()
AllowInsecureRedirect = $true
Verbose = $false
ErrorAction = 'Stop'
}
try {Invoke-RestMethod @Auth}
catch {
$ErrorMsg = ($_.ErrorDetails.Message | ConvertFrom-Json).error
$Response = $_.Exception.Response
$ErrorCode = 'Status Code: {0}, Reason: {1}' -f $Response.StatusCode.value__,$Response.ReasonPhrase
$Context = "Error while creating the $($Record.Type) record for $($Record.Name)"
Write-Error -Message "$Context`n$ErrorCode`n$ErrorMsg" # <-- non-terminating error
}
}
# set the default view of the object
$Def = 'name','type','id','failed'
$Class = [Management.Automation.PSPropertySet]
$DDPS = $Class::new('DefaultDisplayPropertySet',[string[]]$Def)
$Std = [Management.Automation.PSMemberInfo[]]@($DDPS)
# finally return the results
$Results | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $Std -PassThru
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment