Flexible SPF Record Matching
When monitoring SPF (Sender Policy Framework) records, strict exact matching can cause false alerts when customers add additional mail services to their SPF configuration. Flexible matching solves this by verifying only that your required SPF components are present.
The Challenge with Exact Matching
Imagine you require customers to include include:_spf.service.com in their SPF record. With exact matching enabled, you might set up monitoring like this:
curl -X POST https://api.dnsradar.dev/monitors \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com -all",
"is_exact_match": true
}'const response = await fetch('https://api.dnsradar.dev/monitors', {
method: 'POST',
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com -all",
"is_exact_match": true
})
});
const data = await response.json();import requests
response = requests.post(
'https://api.dnsradar.dev/monitors',
headers={
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
json={
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com -all",
"is_exact_match": true
}
)require 'net/http'
require 'json'
uri = URI('https://api.dnsradar.dev/monitors')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'YOUR_API_KEY'
request['Content-Type'] = 'application/json'
request.body = {
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com -all",
"is_exact_match": true
}.to_json
response = http.request(request)package main
import (
"bytes"
"encoding/json"
"net/http"
)
data := {
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com -all",
"is_exact_match": true
}
jsonData, _ := json.Marshal(data)
req, _ := http.NewRequest("POST", "https://api.dnsradar.dev/monitors", bytes.NewBuffer(jsonData))
req.Header.Set("X-API-Key", "YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
response, _ := client.Do(req)<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.dnsradar.dev/monitors');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
$headers = [
'X-API-Key: YOUR_API_KEY',
'Content-Type: application/json'
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$data = json_encode({
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com -all",
"is_exact_match": true
});
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
$response = curl_exec($ch);
curl_close($ch);import java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newHttpClient();
String json = """{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com -all",
"is_exact_match": true
}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.dnsradar.dev/monitors"))
.header("X-API-Key", "YOUR_API_KEY")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());using System.Net.Http;
using System.Text;
using System.Text.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "YOUR_API_KEY");
var data = new
{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com -all",
"is_exact_match": true
};
var json = JsonSerializer.Serialize(data);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync(
"https://api.dnsradar.dev/monitors",
content
);This works initially, but if the customer adds Google Workspace to their email setup, their SPF record becomes:
v=spf1 include:_spf.service.com include:_spf.google.com -all
With exact matching, your monitor would trigger a MISMATCH alert, even though your required include is still present.
Enabling Flexible Matching
Set is_exact_match to false and specify only the SPF components you require:
curl -X POST https://api.dnsradar.dev/monitors \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
}'const response = await fetch('https://api.dnsradar.dev/monitors', {
method: 'POST',
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
})
});
const data = await response.json();import requests
response = requests.post(
'https://api.dnsradar.dev/monitors',
headers={
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
json={
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
}
)require 'net/http'
require 'json'
uri = URI('https://api.dnsradar.dev/monitors')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'YOUR_API_KEY'
request['Content-Type'] = 'application/json'
request.body = {
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
}.to_json
response = http.request(request)package main
import (
"bytes"
"encoding/json"
"net/http"
)
data := {
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
}
jsonData, _ := json.Marshal(data)
req, _ := http.NewRequest("POST", "https://api.dnsradar.dev/monitors", bytes.NewBuffer(jsonData))
req.Header.Set("X-API-Key", "YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
response, _ := client.Do(req)<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.dnsradar.dev/monitors');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
$headers = [
'X-API-Key: YOUR_API_KEY',
'Content-Type: application/json'
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$data = json_encode({
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
});
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
$response = curl_exec($ch);
curl_close($ch);import java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newHttpClient();
String json = """{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.dnsradar.dev/monitors"))
.header("X-API-Key", "YOUR_API_KEY")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());using System.Net.Http;
using System.Text;
using System.Text.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "YOUR_API_KEY");
var data = new
{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
};
var json = JsonSerializer.Serialize(data);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync(
"https://api.dnsradar.dev/monitors",
content
);SPF Version Required: The v=spf1 prefix is required in your expected_value to indicate you're monitoring an SPF record. DNSRadar uses this to apply SPF-specific matching rules.
How Flexible Matching Works
With flexible matching enabled for SPF records, DNSRadar:
- Verifies the SPF record starts with
v=spf1 - Checks that all mechanisms and modifiers in your
expected_valueare present - Ignores additional mechanisms, modifiers, or qualifiers
Valid SPF Variations
With the flexible configuration above, these SPF records would all be considered VALID:
v=spf1 include:_spf.service.com -all
v=spf1 include:_spf.service.com ~all
v=spf1 include:_spf.service.com include:_spf.google.com -all
v=spf1 ip4:1.2.3.4 include:_spf.service.com include:_spf.microsoft.com -all
All these records contain your required include:_spf.service.com mechanism, so they pass validation regardless of additional components or modified qualifiers.
Monitoring Multiple SPF Components
You can require multiple specific SPF components:
curl -X POST https://api.dnsradar.dev/monitors \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com ip4:1.2.3.4",
"is_exact_match": false
}'const response = await fetch('https://api.dnsradar.dev/monitors', {
method: 'POST',
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com ip4:1.2.3.4",
"is_exact_match": false
})
});
const data = await response.json();import requests
response = requests.post(
'https://api.dnsradar.dev/monitors',
headers={
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
json={
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com ip4:1.2.3.4",
"is_exact_match": false
}
)require 'net/http'
require 'json'
uri = URI('https://api.dnsradar.dev/monitors')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'YOUR_API_KEY'
request['Content-Type'] = 'application/json'
request.body = {
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com ip4:1.2.3.4",
"is_exact_match": false
}.to_json
response = http.request(request)package main
import (
"bytes"
"encoding/json"
"net/http"
)
data := {
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com ip4:1.2.3.4",
"is_exact_match": false
}
jsonData, _ := json.Marshal(data)
req, _ := http.NewRequest("POST", "https://api.dnsradar.dev/monitors", bytes.NewBuffer(jsonData))
req.Header.Set("X-API-Key", "YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
response, _ := client.Do(req)<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.dnsradar.dev/monitors');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
$headers = [
'X-API-Key: YOUR_API_KEY',
'Content-Type: application/json'
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$data = json_encode({
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com ip4:1.2.3.4",
"is_exact_match": false
});
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
$response = curl_exec($ch);
curl_close($ch);import java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newHttpClient();
String json = """{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com ip4:1.2.3.4",
"is_exact_match": false
}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.dnsradar.dev/monitors"))
.header("X-API-Key", "YOUR_API_KEY")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());using System.Net.Http;
using System.Text;
using System.Text.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "YOUR_API_KEY");
var data = new
{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com ip4:1.2.3.4",
"is_exact_match": false
};
var json = JsonSerializer.Serialize(data);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync(
"https://api.dnsradar.dev/monitors",
content
);This configuration ensures both the include:_spf.service.com mechanism and the ip4:1.2.3.4 mechanism are present, while allowing additional SPF components.
When Monitors Trigger
A MISMATCH alert will be triggered if:
- Your required include is removed:
v=spf1 include:_spf.google.com -all - The SPF version is missing:
include:_spf.service.com -all - The SPF record is completely removed
- The SPF syntax is invalid
Qualifier Changes Allowed: Using flexible matching, if you omit the final qualifier (~all, -all, ?all), there won't be any MISMATCH triggered by DNSRadar when your customer changes it.
If you want to ensure that the qualifier is correctly set at a specific value, you can include in your list of parts to match on your SPF record, such as setting the expected_value to v=spf1 include:_spf.service.com -all will check that both include:_spf.service.com and -all are present.
Limitations
Mechanism Order: Flexible matching checks for presence, not order. If the order of SPF mechanisms matters for your use case, use exact matching instead.
Best Practices
- Specify Minimum Requirements: Only include the SPF components you absolutely need to verify
- Omit Final Qualifiers: Let customers choose
-all,~all, or?allbased on their needs - Test Configurations: Use the webhook testing endpoint to verify your monitoring setup
- Document Requirements: Clearly communicate to customers which SPF components you require
Combining with Other Record Types
Often, you'll want to monitor SPF alongside other DNS records:
curl -X POST https://api.dnsradar.dev/monitors/bulk \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"monitors": [
{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"subdomain": "_dmarc",
"record_type": "TXT",
"expected_value": "v=DMARC1",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"record_type": "MX",
"expected_value": [
"mx1.service.com",
"mx2.service.com"
],
"is_exact_match": false
}
],
"group": "email-infrastructure"
}'const response = await fetch('https://api.dnsradar.dev/monitors/bulk', {
method: 'POST',
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
"monitors": [
{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"subdomain": "_dmarc",
"record_type": "TXT",
"expected_value": "v=DMARC1",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"record_type": "MX",
"expected_value": [
"mx1.service.com",
"mx2.service.com"
],
"is_exact_match": false
}
],
"group": "email-infrastructure"
})
});
const data = await response.json();import requests
response = requests.post(
'https://api.dnsradar.dev/monitors/bulk',
headers={
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
json={
"monitors": [
{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"subdomain": "_dmarc",
"record_type": "TXT",
"expected_value": "v=DMARC1",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"record_type": "MX",
"expected_value": [
"mx1.service.com",
"mx2.service.com"
],
"is_exact_match": false
}
],
"group": "email-infrastructure"
}
)require 'net/http'
require 'json'
uri = URI('https://api.dnsradar.dev/monitors/bulk')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'YOUR_API_KEY'
request['Content-Type'] = 'application/json'
request.body = {
"monitors": [
{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"subdomain": "_dmarc",
"record_type": "TXT",
"expected_value": "v=DMARC1",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"record_type": "MX",
"expected_value": [
"mx1.service.com",
"mx2.service.com"
],
"is_exact_match": false
}
],
"group": "email-infrastructure"
}.to_json
response = http.request(request)package main
import (
"bytes"
"encoding/json"
"net/http"
)
data := {
"monitors": [
{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"subdomain": "_dmarc",
"record_type": "TXT",
"expected_value": "v=DMARC1",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"record_type": "MX",
"expected_value": [
"mx1.service.com",
"mx2.service.com"
],
"is_exact_match": false
}
],
"group": "email-infrastructure"
}
jsonData, _ := json.Marshal(data)
req, _ := http.NewRequest("POST", "https://api.dnsradar.dev/monitors/bulk", bytes.NewBuffer(jsonData))
req.Header.Set("X-API-Key", "YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
response, _ := client.Do(req)<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.dnsradar.dev/monitors/bulk');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
$headers = [
'X-API-Key: YOUR_API_KEY',
'Content-Type: application/json'
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$data = json_encode({
"monitors": [
{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"subdomain": "_dmarc",
"record_type": "TXT",
"expected_value": "v=DMARC1",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"record_type": "MX",
"expected_value": [
"mx1.service.com",
"mx2.service.com"
],
"is_exact_match": false
}
],
"group": "email-infrastructure"
});
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
$response = curl_exec($ch);
curl_close($ch);import java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newHttpClient();
String json = """{
"monitors": [
{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"subdomain": "_dmarc",
"record_type": "TXT",
"expected_value": "v=DMARC1",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"record_type": "MX",
"expected_value": [
"mx1.service.com",
"mx2.service.com"
],
"is_exact_match": false
}
],
"group": "email-infrastructure"
}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.dnsradar.dev/monitors/bulk"))
.header("X-API-Key", "YOUR_API_KEY")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());using System.Net.Http;
using System.Text;
using System.Text.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "YOUR_API_KEY");
var data = new
{
"monitors": [
{
"domain": "piedpiper.com",
"record_type": "TXT",
"expected_value": "v=spf1 include:_spf.service.com",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"subdomain": "_dmarc",
"record_type": "TXT",
"expected_value": "v=DMARC1",
"is_exact_match": false
},
{
"domain": "piedpiper.com",
"record_type": "MX",
"expected_value": [
"mx1.service.com",
"mx2.service.com"
],
"is_exact_match": false
}
],
"group": "email-infrastructure"
};
var json = JsonSerializer.Serialize(data);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync(
"https://api.dnsradar.dev/monitors/bulk",
content
);