# DNSRadar

> DNS Monitoring for Developers | Real-time DNS record monitoring with instant notifications

DNSRadar is a purpose-built DNS monitoring service for developers and technical teams. Monitor your DNS records in real-time and get instant notifications when changes occur.

## Overview

DNSRadar provides comprehensive DNS monitoring through a developer-friendly API. Track A, AAAA, CNAME, MX, TXT, NS, and PTR records with customizable check frequencies and flexible matching options.

**Key Features:**
- Real-time DNS monitoring with our advanced check interval system
- Webhook notifications for DNS changes
- Bulk operations for managing multiple monitors
- Flexible matching for SPF, DMARC, MX, and A/AAAA records
- Integration with n8n, Zapier, and custom webhook endpoints
- Group-based monitor organization

**API Base URL:** https://api.dnsradar.dev

## Plans & Pricing

 - **Free Plan**: Free, 50 monitors, 60-minute check frequency, 7 days log retention
 - **Starter**: $9/month, 100 monitors, 5-minute frequency, 90-days log retention
 - **Professional**: $24/month, 1000 monitors, 5-minute frequency, 90-days log retention
 - **Business**: $49/month, 2500 monitors, 5-minute frequency, 90-days log retention
 - **Scale**: $99/month, 10000 monitors, 5-minute frequency, 90-days log retention


## Authentication

All API requests require an API key passed via the `X-API-Key` header:

```
X-API-Key: your_api_key_here
```

## Rate Limits

- Standard endpoints: 250 requests per minute
- Bulk operations: Recommended for managing multiple monitors

## Monitor States

- **UNSET:** Monitor created but not yet checked
- **VALID:** DNS record matches expected value
- **MISMATCH:** DNS record doesn't match expected value
- **NOT_FOUND:** DNS record doesn't exist
- **TIMEOUT:** DNS query timed out
- **INVALID:** DNS query returned invalid data

## Support

- **Website:** https://dnsradar.dev
- **Contact:** support@dnsradar.dev
- **Twitter:** @dnsradardev
- **Bluesky:** dnsradar.dev

## Additional Resources

- Full API Documentation: https://developers.dnsradar.dev
- Complete content with code examples: https://dnsradar.dev/llms-full.txt
- Markdown documentation index: https://dnsradar.dev/docs/index.json


## Documentation

All documentation is available in both HTML and clean markdown formats.

### API Documentation Index
- [List all docs](https://dnsradar.dev/docs/index.json) - JSON index of all documentation


### Flexible A/AAAA Record Matching
- [Flexible A/AAAA Record Matching](/docs/a-aaaa-flexible-matching.md)

> Monitor A and AAAA records with flexible matching to allow additional IP addresses while ensuring your required infrastructure remains configured.

#### Flexible A/AAAA Record Matching

A records map domain names to IPv4 addresses, and AAAA records map to IPv6 addresses. Flexible matching allows you to verify specific IP addresses are configured while permitting customers to add additional addresses for load balancing, redundancy, or multi-CDN setups.

##### Why Flexible A/AAAA Matching

Modern applications often use multiple IP addresses for high availability, geographic distribution, and failover. Exact matching becomes problematic when customers need to add or adjust their infrastructure.

###### The Exact Matching Problem

```
POST https://api.dnsradar.dev/monitors

domain: api.piedpiper.com
  record_type: A
  expected_value: ["1.2.3.4", "5.6.7.8"]
  is_exact_match: true
```

If the customer adds a third server for load balancing (`9.10.11.12`), exact matching triggers a false `MISMATCH` alert.

##### Enabling Flexible Matching

Use `is_exact_match: false` to verify your required IPs exist while allowing additional addresses:

```
POST https://api.dnsradar.dev/monitors

domain: api.piedpiper.com
  record_type: A
  expected_value: ["1.2.3.4", "5.6.7.8"]
  is_exact_match: false
```

##### How It Works

With flexible matching for A/AAAA records, DNSRadar:

1. Verifies all IP addresses in your `expected_value` are present
2. Ignores additional IP addresses not in your list
3. Considers the monitor VALID as long as your required IPs exist
4. Triggers `MISMATCH` only if required IPs are removed


##### Valid Configurations

With the flexible configuration above, these A record sets would all be VALID:

```
#### Exactly your IPs
1.2.3.4
5.6.7.8

#### With additional IPs
1.2.3.4
5.6.7.8
9.10.11.12

#### Mixed with many others
1.2.3.4
5.6.7.8
10.20.30.40
50.60.70.80
100.110.120.130
```

All contain your required IP addresses `1.2.3.4` and `5.6.7.8`, so they pass validation.

##### IPv6 (AAAA) Monitoring

Flexible matching works identically for IPv6 addresses:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  record_type: AAAA
  expected_value: ["2001:db8::1", "2001:db8::2"]
  is_exact_match: false
```

##### When Monitors Trigger

A `MISMATCH` alert is triggered if:

- One or more of your required IP addresses are removed
- All A/AAAA records are removed
- DNS query returns an error


> ✅ **SUCCESS:** **IP Additions Safe**: Adding new IP addresses never triggers alerts with flexible matching, enabling customers to scale their infrastructure freely.

##### Common Use Cases

###### CDN and Origin Monitoring

Ensure your origin server remains configured while allowing CDN IPs:

```
POST https://api.dnsradar.dev/monitors

domain: cdn.piedpiper.com
  record_type: A
  expected_value: ["203.0.113.10"]
  is_exact_match: false
  group: cdn-configuration
```

###### Dual Stack Monitoring

Monitor both IPv4 and IPv6:

```
POST https://api.dnsradar.dev/monitors/bulk

monitors:
    - domain: piedpiper.com
      record_type: A
      expected_value: ["1.2.3.4"]
      is_exact_match: false
    - domain: piedpiper.com
      record_type: AAAA
      expected_value: ["2001:db8::1"]
      is_exact_match: false
  group: dual-stack-monitoring
```

###### Load Balancer Configuration

Ensure specific load balancer IPs remain configured:

```
POST https://api.dnsradar.dev/monitors

domain: app.piedpiper.com
  record_type: A
  expected_value: ["10.0.1.100", "10.0.2.100"]
  is_exact_match: false
```

###### Multi-Region Deployment

Monitor presence of regional endpoints:

```
POST https://api.dnsradar.dev/monitors

domain: api.piedpiper.com
  record_type: A
  expected_value: ["52.0.0.1", "13.0.0.1", "18.0.0.1"]
  is_exact_match: false
  frequency: 5
```

##### Best Practices

1. **Monitor Critical IPs Only**: Specify only the IP addresses essential to your service
2. **Use Flexible Matching by Default**: Enable it unless you need strict IP control
3. **Combine with Health Checks**: DNS monitoring verifies configuration; use separate health checks for service availability
4. **Monitor Both IPv4 and IPv6**: Create separate monitors for A and AAAA records
5. **Group Related Monitors**: Organize monitors by service or infrastructure component

##### Advanced Scenarios

###### Anycast IP Monitoring

Verify anycast IPs are advertised:

```
POST https://api.dnsradar.dev/monitors

domain: anycast.piedpiper.com
  record_type: A
  expected_value: ["192.0.2.1"]
  is_exact_match: false
  frequency: 5
```

###### Subnet Monitoring

Monitor multiple IPs from the same subnet:

```
POST https://api.dnsradar.dev/monitors

domain: cluster.piedpiper.com
  record_type: A
  expected_value: ["10.0.1.10", "10.0.1.11", "10.0.1.12"]
  is_exact_match: false
```

###### Failover Configuration

Ensure both primary and failover IPs exist:

```
POST https://api.dnsradar.dev/monitors

domain: service.piedpiper.com
  record_type: A
  expected_value: ["203.0.113.10", "203.0.113.20"]
  is_exact_match: false
  group: high-availability
```

##### Integration Examples

###### Infrastructure as Code

```yaml
#### Terraform/Pulumi example
monitors:
  - domain: api.example.com
    record_type: A
    expected_value:
      - 1.2.3.4  #### Primary load balancer
      - 5.6.7.8  #### Backup load balancer
    is_exact_match: false
    frequency: 5
    group: production-api
```

###### Automated Monitoring Setup

```javascript
// Automatically monitor all production services
const services = await getProductionServices()

const monitors = services.map(service => ({
  domain: service.domain,
  record_type: service.ipv6 ? 'AAAA' : 'A',
  expected_value: service.criticalIPs,
  is_exact_match: false,
  frequency: 5,
  group: 'production-infrastructure'
}))

await dnsradar.monitors.bulkCreate({ monitors })
```

##### Comparing Flexible vs Exact Matching

| Scenario | Exact Match | Flexible Match |
|----------|-------------|----------------|
| Customer adds IPs | ❌ MISMATCH | ✅ VALID |
| Customer removes required IP | ❌ MISMATCH | ❌ MISMATCH |
| Customer removes non-required IP | ❌ MISMATCH | ✅ VALID |
| IP order changes | ✅ VALID | ✅ VALID |
| All IPs removed | ❌ MISMATCH | ❌ MISMATCH |

##### Monitoring Complete Service Stack

```
POST https://api.dnsradar.dev/monitors/bulk

monitors:
    - domain: piedpiper.com
      record_type: A
      expected_value: ["1.2.3.4"]
      is_exact_match: false
    - domain: www.piedpiper.com
      record_type: CNAME
      expected_value: piedpiper.netlify.app
    - domain: api.piedpiper.com
      record_type: A
      expected_value: ["10.0.1.100"]
      is_exact_match: false
    - domain: api.piedpiper.com
      record_type: AAAA
      expected_value: ["2001:db8::1"]
      is_exact_match: false
  group: service-infrastructure
  frequency: 5
```

##### Next Steps

- [Managing MX Record Priorities](/docs/mx-priorities)
- [Bulk Update Monitors](/docs/update-monitors-bulk)
- [Configure Webhooks](/docs/configure-webhooks)



### Create Multiple Monitors at Once
- [Create Multiple Monitors at Once](/docs/bulk-create-monitors.md)

> Use the bulk creation API to efficiently add up to 1,000 DNS monitors in a single request for large-scale monitoring deployments.

#### Create Multiple Monitors at Once

The bulk monitor creation endpoint allows you to create up to 1,000 DNS monitors in a single API request. This is ideal for onboarding new customers, deploying monitoring across multiple domains, or managing large DNS infrastructures.

##### Bulk Creation Endpoint

Send a POST request to `/monitors/bulk` with an array of monitor configurations.

```
POST https://api.dnsradar.dev/monitors/bulk

monitors:
    - domain: piedpiper.com
      record_type: A
      expected_value: ["1.2.3.4", "5.6.7.8"]
    - domain: piedpiper.com
      subdomain: www
      record_type: CNAME
      expected_value: piedpiper.netlify.app
    - domain: piedpiper.com
      record_type: TXT
      expected_value: v=spf1 include:_spf.service.com -all
    - domain: piedpiper.com
      subdomain: _dmarc
      record_type: TXT
      expected_value: "v=DMARC1; p=reject; pct=100; adkim=s; aspf=s"
  group: production-dns
  frequency: 60
```

##### Request Structure

###### Required Fields

- `monitors` (array): Array of monitor objects to create (1-1000 items). Please refer to [Create a DNS Monitor](/docs/create-monitor) for its detailled structure.

Each monitor object requires:
- `domain`: The domain name to monitor
- `record_type`: DNS record type (A, AAAA, CNAME, MX, TXT, NS, PTR)
- `expected_value`: Expected DNS value(s)

###### Optional Global Parameters

You can specify default values that apply to all monitors in the batch:

- `group` (string): Default group slug for all monitors
- `frequency` (integer): Default check frequency in minutes (5, 10, 15, 30, 60, 120)

Individual monitors can override these defaults by including their own `group` or `frequency` fields.

> ℹ️ **INFO:** **Auto-Creation**: If a specified group provided by its slug doesn't exist, we will create one automatically during the bulk operation.

##### Per-Monitor Configuration

Each monitor in the array can have its own configuration:

```
POST https://api.dnsradar.dev/monitors/bulk

monitors:
    - domain: api.piedpiper.com
      record_type: A
      expected_value: ["1.2.3.4"]
      frequency: 5
      group: critical-services
    - domain: blog.piedpiper.com
      record_type: CNAME
      expected_value: piedpiper.netlify.app
      frequency: 60
      group: marketing-sites
    - domain: mail.piedpiper.com
      record_type: MX
      expected_value: ["10 mx1.provider.com", "20 mx2.provider.com"]
      is_exact_match: false
      group: email-infrastructure
```

##### API Response

The API returns an array of created monitor objects, maintaining the same order as your request:

```json
{
  "data": [
    {
      "uuid": "mon_abc123",
      "created": "2026-01-06T12:34:56Z",
      "domain": "piedpiper.com",
      "subdomain": null,
      "record_type": "A",
      "expected_value": ["1.2.3.4", "5.6.7.8"],
      "current_value": null,
      "is_exact_match": true,
      "state": "UNSET",
      "incidence_count": 0,
      "last_checked": null,
      "is_active": true,
      "frequency": 60
    },
    {
      "uuid": "mon_abc456",
      "created": "2026-01-06T12:34:56Z",
      "domain": "piedpiper.com",
      "subdomain": "www",
      "record_type": "CNAME",
      "expected_value": ["piedpiper.netlify.app"],
      "current_value": null,
      "is_exact_match": true,
      "state": "UNSET",
      "incidence_count": 0,
      "last_checked": null,
      "is_active": true,
      "frequency": 60
    }
  ]
}
```

##### Scalability and Performance

The bulk creation endpoint is optimized for high-throughput operations:

###### Rate Limiting

> ✅ **SUCCESS:** **Higher Limits**: The `/monitors/bulk` endpoint, like the `/monitors` endpoint, has a rate limit of 250 requests per minute.

In the `/monitors/bulk` endpoint, this allows you to create up to 250,000 monitors per minute (250 requests × 1,000 monitors).

###### Batch Size Recommendations

- **Small batches (1-100 monitors)**: Fast response times, ideal for real-time operations
- **Medium batches (100-500 monitors)**: Good balance of throughput and response time
- **Large batches (500-1000 monitors)**: Maximum efficiency for bulk imports

##### Error Handling

If any monitor in the batch fails validation, the entire request is rejected, and no monitors are created. This ensures data consistency.

> 🚨 **DANGER:** **Transaction Guarantee**: Bulk creation is atomic. Either all monitors are created successfully, or none are created. This prevents partial deployments.

Common validation errors include:
- Invalid domain format
- Unsupported record type
- Missing required fields
- Monitor limit exceeded


##### Plan Limitations

> ⚠️ **WARNING:** **Monitor Limits**: Free plan limited to 50 total monitors.

Premium plans support unlimited monitors. The bulk creation endpoint respects these limits.

##### Best Practices

1. **Group Related Monitors**: Use the `group` parameter to organize monitors logically
2. **Set Appropriate Frequencies**: Balance monitoring coverage with API usage
3. **Use Consistent Naming**: Establish naming conventions for your groups
4. **Implement Error Handling**: Always handle API errors gracefully in your application

##### Next Steps

- [Update multiple monitors at once](/docs/update-monitors-bulk)
- [Configure webhooks for notifications](/docs/configure-webhooks)
- [Monitor SPF records with flexible matching](/docs/spf-flexible-matching)



### Configure Webhooks for DNS Notifications
- [Configure Webhooks for DNS Notifications](/docs/configure-webhooks.md)

> Set up webhooks to receive real-time notifications when DNS changes are detected by your monitors.

#### Configure Webhooks for DNS Notifications

Webhooks allow DNSRadar to send HTTP requests to your endpoints when DNS changes are detected. This enables you to integrate DNS monitoring into your existing systems, trigger automated responses, or notify your team through custom channels.

##### Creating a Webhook

Send a POST request to `/webhooks` with your endpoint configuration:

```
POST https://api.dnsradar.dev/webhooks

url: https://api.service.com/webhooks/dnsradar
  method: POST
  secret: your-webhook-secret-key
  headers:
    X-Custom-Header: Value
```

###### Required Fields

- `url`: The endpoint URL to receive webhook notifications
- `method`: HTTP method (POST, PUT, PATCH, DELETE)

###### Optional Fields

- `secret`: Secret key for HMAC SHA256 signature verification (highly recommended)
- `headers`: Custom HTTP headers as key-value pairs
- `groups`: Array of group slugs to associate the webhook with

##### Webhook Security

DNSRadar provides built-in request signing to verify webhook authenticity. When you provide a `secret` parameter, DNSRadar signs each webhook request with HMAC SHA256.

```
POST https://api.dnsradar.dev/webhooks

url: https://api.service.com/webhooks/dnsradar
  method: POST
  secret: your-webhook-secret-key
  headers:
    Authorization: Bearer your-api-token
```

###### How Request Signing Works

When a webhook is triggered, DNSRadar:

1. Creates an HMAC SHA256 signature of the request body using your secret
2. Sends the signature in the `X-DNSRadar-Signature` header
3. Includes the event timestamp in the `X-Webhook-Timestamp` header (UTC)

Your endpoint should verify the signature to ensure the request came from DNSRadar and hasn't been tampered with.

> ⚠️ **WARNING:** **Security Best Practice**: Always provide a `secret` parameter and validate the signature on your endpoint. This prevents unauthorized requests and replay attacks.

##### Group Association

Webhooks are linked to groups, not individual monitors. This design allows efficient notification management:

###### Default Group

If you don't specify groups, the webhook is added to your default group:

```
POST https://api.dnsradar.dev/webhooks

url: https://api.service.com/webhooks/dnsradar
  method: POST
```

###### Specific Groups

Associate the webhook with one or more groups:

```
POST https://api.dnsradar.dev/webhooks

url: https://api.service.com/webhooks/dnsradar
  method: POST
  secret: your-webhook-secret-key
  groups: ["production-servers", "critical-dns"]
```

###### Auto-Create Groups

If specified groups don't exist, they're created automatically:

```
POST https://api.dnsradar.dev/webhooks

url: https://api.service.com/webhooks/dnsradar
  method: POST
  groups: ["customer-new-client", "monitoring-tier-premium"]
```

This creates both groups and attaches the webhook to them.

##### API Response

The API returns the created webhook object:

```json
{
  "uuid": "wh_abc123...",
  "created": "2026-01-08T10:30:00Z",
  "url": "https://api.service.com/webhooks/dnsradar",
  "method": "POST",
  "headers": {
    "Authorization": "Bearer token123",
    "X-Custom-Header": "value"
  },
  "is_active": true,
  "last_error": null,
  "last_executed": null
}
```

###### Response Fields

- `uuid`: Unique webhook identifier (prefixed with `wh_`)
- `created`: ISO 8601 timestamp of creation
- `is_active`: Whether webhook is enabled
- `last_error`: Most recent error message (if any)
- `last_executed`: ISO 8601 timestamp of last execution


##### Webhook Payload

When a DNS change is detected, DNSRadar sends a POST request with event details:

```json
{
  "uuid": "req_abc123...",
  "webhook_uuid": "wh_abc123...",
  "created": "2026-01-08T10:35:22Z",
  "event": {
    "event_uuid": "evt_xyz789",
    "monitor_uuid": "mon_abc123",
    "domain": "piedpiper.com",
    "subdomain": "www",
    "record_type": "A",
    "expected_value": ["1.2.3.4"],
    "previous_value": ["1.2.3.4"],
    "current_value": ["5.6.7.8"],
    "old_state": "VALID",
    "new_state": "MISMATCH",
    "occurred": "2026-01-08T10:35:22Z",
    "incidence_count": 1
  }
}
```

###### Payload Fields

The Webhook event payload contains information about the current request, and an object `event` containing the actual event.

- `event_uuid`: Unique identifier for this event
- `monitor_uuid`: UUID of the monitor that detected the change
- `domain/subdomain`: DNS record location
- `record_type`: Type of DNS record (A, AAAA, CNAME, MX, TXT, NS, PTR)
- `expected_value`: What the monitor expects to see
- `previous_value`: DNS value before the change
- `current_value`: New DNS value detected
- `old_state`: Previous monitor state
- `new_state`: Current monitor state
- `occurred`: ISO 8601 timestamp of the event
- `incidence_count`: Total number of incidents for this monitor


##### Multiple Webhooks Per Group

Groups can have multiple webhooks, enabling:

- Notifications to multiple channels (Slack + email)
- Different alert severities to different endpoints
- Redundant notification systems
- Multi-team notifications

```
POST https://api.dnsradar.dev/webhooks

url: https://hooks.slack.com/services/TEAM/WEBHOOK
  method: POST
  groups: ["production-dns"]
```

Then add another webhook to the same group:

```
POST https://api.dnsradar.dev/webhooks

url: https://api.pagerduty.com/incidents
  method: POST
  headers:
    Authorization: Token token=YOUR_KEY
  groups: ["production-dns"]
```

Now both webhooks receive notifications for monitors in the `production-dns` group.


##### Webhook Limits

> ℹ️ **INFO:** **Per-Group Limit**: Each group can have up to 5 webhooks (default).
This ensures efficient notification delivery while preventing excessive webhook calls.

If you need more than 5 webhooks for a group, feel free to reach out to us with your use case and we'll increase the limit accordingly.

##### Best Practices

1. **Use HTTPS**: Always use HTTPS endpoints for security
2. **Implement Authentication**: Validate webhook requests using headers
3. **Handle Retries**: Design endpoints to be idempotent
4. **Return Quickly**: Respond to webhooks quickly (< 5 seconds)
5. **Test Webhooks**: Use the [test endpoint](/docs/test-webhooks) before production
6. **Monitor Webhook Health**: Track webhook failures and errors


##### Security Considerations

###### Verify Webhook Signature

Validate the HMAC SHA256 signature to ensure requests come from DNSRadar:

```javascript
// Node.js example with Express
const crypto = require('crypto')

app.post('/webhooks/dnsradar', (req, res) => {
  const signature = req.headers['x-dnsradar-signature']
  const timestamp = req.headers['x-webhook-timestamp']
  const secret = process.env.DNSRADAR_WEBHOOK_SECRET

  // Create HMAC signature from request body
  const body = JSON.stringify(req.body)
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex')

  // Compare signatures
  if (signature !== expectedSignature) {
    return res.status(401).send('Invalid signature')
  }

  // Process webhook
  const event = req.body
  handleDNSEvent(event)

  res.status(200).send('OK')
})
```

```python
#### Python example with Flask
import hmac
import hashlib
from flask import Flask, request

@app.route('/webhooks/dnsradar', methods=['POST'])
def webhook():
    signature = request.headers.get('X-DNSRadar-Signature')
    timestamp = request.headers.get('X-Webhook-Timestamp')
    secret = os.environ['DNSRADAR_WEBHOOK_SECRET']

    #### Create HMAC signature from request body
    body = request.get_data()
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        body,
        hashlib.sha256
    ).hexdigest()

    #### Compare signatures
    if not hmac.compare_digest(signature, expected_signature):
        return 'Invalid signature', 401

    #### Process webhook
    event = request.get_json()
    handle_dns_event(event)

    return 'OK', 200
```

```php
// PHP example
<?php
$signature = $_SERVER['HTTP_X_DNSRADAR_SIGNATURE'];
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'];
$secret = getenv('DNSRADAR_WEBHOOK_SECRET');

// Create HMAC signature from request body
$body = file_get_contents('php://input');
$expectedSignature = hash_hmac('sha256', $body, $secret);

// Compare signatures
if (!hash_equals($signature, $expectedSignature)) {
    http_response_code(401);
    exit('Invalid signature');
}

// Process webhook
$event = json_decode($body, true);
handleDNSEvent($event);

http_response_code(200);
echo 'OK';
```

###### Prevent Replay Attacks

Use the `X-Webhook-Timestamp` header to reject old requests:

```javascript
const MAX_AGE = 5 * 60 * 1000 // 5 minutes

app.post('/webhooks/dnsradar', (req, res) => {
  const signature = req.headers['x-dnsradar-signature']
  const timestamp = req.headers['x-webhook-timestamp']
  const secret = process.env.DNSRADAR_WEBHOOK_SECRET

  // Verify timestamp is recent
  const eventTime = new Date(timestamp)
  const now = new Date()

  if (now - eventTime > MAX_AGE) {
    return res.status(400).send('Request too old')
  }

  // Verify signature
  const body = JSON.stringify(req.body)
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex')

  if (signature !== expectedSignature) {
    return res.status(401).send('Invalid signature')
  }

  // Process webhook
  const event = req.body
  handleDNSEvent(event)

  res.status(200).send('OK')
})
```

```python
#### Python replay attack prevention
from datetime import datetime, timedelta

@app.route('/webhooks/dnsradar', methods=['POST'])
def webhook():
    signature = request.headers.get('X-DNSRadar-Signature')
    timestamp = request.headers.get('X-Webhook-Timestamp')
    secret = os.environ['DNSRADAR_WEBHOOK_SECRET']

    #### Verify timestamp is recent (within 5 minutes)
    event_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
    now = datetime.now(timezone.utc)

    if (now - event_time).total_seconds() > 300:
        return 'Request too old', 400

    #### Verify signature
    body = request.get_data()
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected_signature):
        return 'Invalid signature', 401

    #### Process webhook
    event = request.get_json()
    handle_dns_event(event)

    return 'OK', 200
```

###### IP Allowlisting

Consider restricting webhook requests to DNSRadar's IP ranges (contact support for current IP list).

##### Error Handling

Your webhook endpoint should always return a 2xx status code to be considered successful.

All other status code will trigger a retry.


##### Next Steps

- [Test Your Webhooks](/docs/test-webhooks)
- [Enable/Disable Webhooks](/docs/enable-disable-webhooks)
- [Understand Webhook Retries](/docs/webhook-retries)
- [View Webhook Request History](/docs/webhook-requests)



### Create a DNS Monitor
- [Create a DNS Monitor](/docs/create-monitor.md)

> Learn how to create DNS monitors programmatically using the DNSRadar API to track changes to your DNS records.

#### Create a DNS Monitor

Creating a DNS monitor with DNSRadar is straightforward. Monitors allow you to track specific DNS records and receive notifications when changes are detected.

This guide will walk you through creating monitors using the API.

##### Basic Monitor Creation

To create a monitor, send a POST request to the `/monitors` endpoint with the DNS record details you want to monitor.

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  record_type: A
  expected_value: ["1.2.3.4", "5.6.7.8"]
```

###### Request Parameters

- `domain` (required): The domain name to monitor
- `subdomain` (optional): The subdomain to monitor. Omit for apex domain monitoring
- `record_type` (required): The type of DNS record (A, AAAA, CNAME, MX, TXT, NS, PTR)
- `expected_value` (required): The expected DNS value(s). Can be a string or array of strings
- `frequency` (optional): Check frequency in minutes. Default: 60. Options: 5, 10, 15, 30, 60, 120
- `is_exact_match` (optional): Whether to require exact value matching. Default: true
- `group` (optional): Group slug to organize monitors. Uses default group if not specified

##### Monitoring with Subdomains

To monitor a specific subdomain, include the `subdomain` parameter in your request.

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  subdomain: www
  record_type: CNAME
  expected_value: piedpiper.netlify.app
  frequency: 5
```

> ℹ️ **INFO:** The `subdomain` parameter should contain only the subdomain portion. For example, to monitor `www.piedpiper.com`, use `subdomain: "www"` and `domain: "piedpiper.com"`.

##### Understanding Expected Values

The `expected_value` parameter accepts either a single string or an array of strings, depending on your monitoring needs.

###### Single Value

For DNS records that should have one specific value:

```json
{
  "expected_value": "piedpiper.netlify.app"
}
```

###### Multiple Values

For DNS records that may have multiple valid values (like A records with multiple IPs):

```json
{
  "expected_value": ["1.2.3.4", "5.6.7.8"]
}
```

##### Grouping Monitors

Groups allow you to organize monitors and manage webhook notifications collectively. By default, monitors are added to your default group, but you can choose another group when creating a Monitor, by passing a "slug".

If you pass a slug that doesn't exists at DNSRadar, we'll create the group for you on the fly.

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  record_type: A
  expected_value: ["1.2.3.4"]
  group: production-servers
```

> ℹ️ **INFO:** **Webhook Management**: Webhooks are linked to groups, not individual monitors. This design allows you to manage notifications for multiple monitors efficiently by updating the group's webhook configuration.

If the specified group doesn't exist, it will be created automatically.

##### API Response

When a monitor is successfully created, the API returns the monitor object with additional system-generated fields:

```json
{
  "uuid": "mon_abc123",
  "created": "2026-01-06T12:34:56Z",
  "domain": "piedpiper.com",
  "subdomain": null,
  "record_type": "A",
  "expected_value": ["1.2.3.4", "5.6.7.8"],
  "current_value": null,
  "is_exact_match": true,
  "state": "UNSET",
  "incidence_count": 0,
  "last_checked": null,
  "is_active": true,
  "frequency": 60
}
```

###### Response Fields

- **uuid**: Unique identifier for the monitor (prefixed with `mon_`)
- **created**: ISO 8601 timestamp of monitor creation
- **current_value**: Current DNS value (null until first check)
- **state**: Monitor state (`UNSET`, `VALID`, `INVALID`, `TIMEOUT`, `MISMATCH`, `NOT_FOUND`)
- **incidence_count**: Number of incidents/changes detected
- **last_checked**: ISO 8601 timestamp of last check
- **is_active**: Whether the monitor is actively checking

##### Monitor States

Understanding monitor states helps you interpret monitoring results:

- `UNSET`: Monitor created but not yet checked
- `VALID`: DNS record matches expected value
- `MISMATCH`: DNS record doesn't match expected value
- `NOT_FOUND`: DNS record doesn't exist
- `TIMEOUT`: DNS query timed out
- `INVALID`: DNS query returned invalid data

##### Flexible Matching with is_exact_match

The `is_exact_match` parameter controls how strictly DNSRadar validates DNS records. When set to `false`, additional behavior applies based on the record type:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  record_type: TXT
  expected_value: v=spf1 include:_spf.service.com
  is_exact_match: false
```

For detailed information about flexible matching:
- [Flexible SPF Record Matching](/docs/spf-flexible-matching)
- [Flexible DMARC Record Matching](/docs/dmarc-flexible-matching)
- [Flexible MX Record Matching](/docs/mx-flexible-matching)
- [Flexible A/AAAA Record Matching](/docs/a-aaaa-flexible-matching)

##### Rate Limits

> ⚠️ **WARNING:** The `/monitors` endpoint is rate-limited to 250 requests per minute. For bulk operations, consider using the [bulk creation endpoint](/docs/bulk-create-monitors).

##### Plan Limitations

> ℹ️ **INFO:** **Free Plan**: Limited to 50 monitors.

##### Next Steps

- [Create multiple monitors at once](/docs/bulk-create-monitors)
- [Configure webhooks for notifications](/docs/configure-webhooks)
- [Manage monitor groups](/docs/update-monitors-bulk)



### Flexible DMARC Record Matching
- [Flexible DMARC Record Matching](/docs/dmarc-flexible-matching.md)

> Monitor DMARC records with flexible matching to allow customers to adjust policies while ensuring core DMARC configuration remains intact.

#### Flexible DMARC Record Matching

DMARC (Domain-based Message Authentication, Reporting, and Conformance) records define email authentication policies. Flexible matching for DMARC records allows you to verify essential DMARC components are present while giving customers flexibility to adjust policy settings.

##### Understanding DMARC Monitoring

DMARC records are TXT records published at the `_dmarc` subdomain. They control how email servers handle authentication failures and where to send reports.

Example DMARC record:
```
v=DMARC1; p=reject; pct=100; rua=mailto:dmarc@example.com; adkim=s; aspf=s
```

##### The Flexibility Problem

With exact matching, any policy adjustment triggers an alert:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  subdomain: _dmarc
  record_type: TXT
  expected_value: "v=DMARC1; p=reject; pct=100; adkim=s; aspf=s"
  is_exact_match: true
```

If the customer changes `pct=100` to `pct=50` or adds a `rua` tag for reports, your monitor triggers a false alert.

##### Enabling Flexible DMARC Matching

Use flexible matching to verify only the DMARC components that matter to you by setting the `is_exact_match` parameter to `false`:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  subdomain: _dmarc
  record_type: TXT
  expected_value: v=DMARC1
  is_exact_match: false
```

> ℹ️ **INFO:** **DMARC Version Required**: Include `v=DMARC1` in your `expected_value` to indicate DMARC-specific matching rules should apply.

##### How Flexible Matching Works

With `is_exact_match: false` for DMARC records, DNSRadar:

1. Verifies the record starts with `v=DMARC1`
2. Checks that all tags in your `expected_value` are present
3. Ignores additional tags or modified tag values not in your expected value


##### Valid DMARC Variations

With the flexible configuration above, these DMARC records would all be VALID:

```
v=DMARC1; p=none
v=DMARC1; p=quarantine; pct=50
v=DMARC1; p=reject; pct=100; rua=mailto:dmarc@example.com
v=DMARC1; p=reject; rua=mailto:reports@example.com; adkim=s; aspf=s
```

All contain the required `v=DMARC1` version tag, so they pass validation.

##### Monitoring Specific DMARC Tags

Require specific DMARC policy tags while allowing others to vary:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  subdomain: _dmarc
  record_type: TXT
  expected_value: "v=DMARC1; p=reject"
  is_exact_match: false
```

This ensures the customer maintains a `p=reject` policy while allowing them to adjust reporting addresses, percentages, and alignment modes.

##### Enforcing Strict Alignment

Require strict DKIM and SPF alignment:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  subdomain: _dmarc
  record_type: TXT
  expected_value: "v=DMARC1; adkim=s; aspf=s"
  is_exact_match: false
```

This configuration ensures both `adkim=s` (strict DKIM alignment) and `aspf=s` (strict SPF alignment) are present, regardless of other DMARC settings.

##### When Monitors Trigger

A `MISMATCH` alert is triggered if:

- The DMARC record is removed
- The `v=DMARC1` version tag is missing
- Required tags from your `expected_value` are removed


> ✅ **SUCCESS:** **Policy Evolution**: Flexible matching allows customers to gradually strengthen their DMARC policy from `p=none` to `p=quarantine` to `p=reject` without triggering alerts if you're only monitoring for DMARC presence.


##### DMARC Tag Reference

Common DMARC tags you might want to monitor:

| Tag | Purpose | Example |
|-----|---------|---------|
| `v` | Version (required) | `v=DMARC1` |
| `p` | Policy for domain | `p=reject` |
| `sp` | Policy for subdomains | `sp=quarantine` |
| `pct` | Percentage of messages to filter | `pct=100` |
| `rua` | Aggregate report URI | `rua=mailto:dmarc@example.com` |
| `ruf` | Forensic report URI | `ruf=mailto:forensic@example.com` |
| `adkim` | DKIM alignment mode | `adkim=s` (strict) |
| `aspf` | SPF alignment mode | `aspf=r` (relaxed) |

##### Best Practices

1. **Start with Basic Monitoring**: Begin by monitoring for `v=DMARC1` presence only
2. **Gradually Add Requirements**: As customers mature their email authentication, add policy requirements
3. **Allow Reporting Flexibility**: Don't enforce specific `rua` or `ruf` values unless necessary
4. **Document Expectations**: Clearly communicate DMARC requirements to customers
5. **Combine with SPF/DKIM**: Monitor DMARC alongside SPF and DKIM for comprehensive email security

##### Monitoring Email Authentication Stack

Complete email authentication monitoring:

```
POST https://api.dnsradar.dev/monitors/bulk

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
      subdomain: default._domainkey
      record_type: TXT
      expected_value: "v=DKIM1; k=rsa"
      is_exact_match: false
    - domain: piedpiper.com
      record_type: MX
      expected_value: ["mx1.mail-provider.com", "mx2.mail-provider.com"]
      is_exact_match: true
  group: email-authentication
```

##### Next Steps

- [Flexible SPF Record Matching](/docs/spf-flexible-matching)
- [Flexible MX Record Matching](/docs/mx-flexible-matching)
- [Configure webhooks for email security alerts](/docs/configure-webhooks)



### Enable and Disable Monitors
- [Enable and Disable Monitors](/docs/enable-disable-monitors.md)

> Control individual monitor checking without deleting monitor configuration or history.

#### Enable and Disable Monitors

Individual monitors can be enabled or disabled to control DNS checking without losing configuration or event history.

##### Disabling a Monitor

Send a POST request to `/monitors/{uuid}/disable`:

```
POST https://api.dnsradar.dev/monitors/mon_abc123/disable
```

The monitor immediately stops checking DNS and won't trigger webhooks.

##### Enabling a Monitor

Send a POST request to `/monitors/{uuid}/enable`:

```
POST https://api.dnsradar.dev/monitors/mon_abc123/enable
```

The monitor resumes DNS checking according to its frequency setting.

##### API Response

Both endpoints return the updated monitor:

```json
{
  "uuid": "mon_abc123",
  "domain": "piedpiper.com",
  "record_type": "A",
  "is_active": false,
  "frequency": 15,
  ...
}
```


##### Bulk Operations

For bulk enable/disable, use group-level updates:

```
PATCH https://api.dnsradar.dev/groups/customer-123/monitors

is_active: false
```

This disables all monitors in the group at once. See [bulk updates documentation](/docs/update-monitors-bulk).

##### Monitoring State

You can check whether a monitor is currently active by retrieving its details.

###### Check Single Monitor Status

Get the monitor details to inspect the `is_active` field:

```
GET https://api.dnsradar.dev/monitors/mon_abc123
```

The response includes the `is_active` parameter:

```json
{
  "uuid": "mon_abc123",
  "domain": "piedpiper.com",
  "subdomain": "www",
  "record_type": "A",
  "expected_value": ["1.2.3.4"],
  "is_active": true,
  "frequency": 15,
  "last_checked": "2026-01-09T10:30:00Z",
  "created": "2026-01-01T08:00:00Z"
}
```

###### Monitor State Field

| Value | State | Description |
|-------|-------|-------------|
| `true` | Active | Monitor is checking DNS and triggering webhooks |
| `false` | Disabled | Monitor is paused and not performing checks |

###### List Monitors with State

Get all monitors to see their active status:

```
GET https://api.dnsradar.dev/monitors
```

Filter for only active monitors:

```
GET https://api.dnsradar.dev/monitors?is_active=true
```

Filter for only disabled monitors:

```
GET https://api.dnsradar.dev/monitors?is_active=false
```

> ℹ️ **INFO:** **Tip**: Use the `is_active` filter when listing monitors to quickly identify which monitors are currently running or paused.

##### Next Steps

- [Bulk Update Monitors](/docs/update-monitors-bulk)
- [View Monitor Events](/docs/monitor-events)
- [Enable/Disable Webhooks](/docs/enable-disable-webhooks)



### Enable and Disable Webhooks
- [Enable and Disable Webhooks](/docs/enable-disable-webhooks.md)

> Control webhook notification delivery by enabling or disabling webhooks without deleting their configuration.

#### Enable and Disable Webhooks

Webhooks can be temporarily disabled without deleting their configuration. This is useful for maintenance, debugging, or when you need to pause notifications temporarily.

##### Disabling a Webhook

Send a POST request to `/webhooks/{uuid}/disable`:

```
POST https://api.dnsradar.dev/webhooks/wh_abc123/disable
```

The webhook is immediately disabled and stops receiving notifications.

##### Enabling a Webhook

Send a POST request to `/webhooks/{uuid}/enable`:

```
POST https://api.dnsradar.dev/webhooks/wh_abc123/enable
```

The webhook is re-enabled and resumes receiving notifications. Any error states are cleared.

##### API Response

Both endpoints return the updated webhook object:

```json
{
  "uuid": "wh_abc123",
  "created": "2026-01-08T10:30:00Z",
  "url": "https://api.service.com/webhooks/dnsradar",
  "method": "POST",
  "headers": {
    "Authorization": "Bearer token123"
  },
  "is_active": false,
  "last_error": null,
  "last_executed": "2026-01-08T15:22:10Z"
}
```

Note the `is_active` field reflects the webhook's current state.


##### Automatic Disabling

DNSRadar automatically disables webhooks after repeated failures:

> ⚠️ **WARNING:** **Auto-Disable**: Webhooks are automatically disabled after 9 failed retry attempts over 24 hours. You'll be notified when this happens.

###### Re-enabling After Auto-Disable

When a webhook is auto-disabled:

1. Fix the underlying issue (endpoint availability, authentication, etc.)
2. Use the enable endpoint to reactivate
3. The `last_error` field is cleared upon re-enabling

```
POST https://api.dnsradar.dev/webhooks/wh_abc123/enable
```

##### Checking Webhook Status

###### Via API

Get webhook details to check status:
```
GET https://api.dnsradar.dev/webhooks/wh_abc123
```

And ensure that the response's body contains the appropriate `is_active` property, set to `true`.


##### Next Steps

- [Understand Webhook Retries](/docs/webhook-retries)
- [View Webhook Request History](/docs/webhook-requests)
- [Enable/Disable Monitors](/docs/enable-disable-monitors)



### View Monitor Event History
- [View Monitor Event History](/docs/monitor-events.md)

> Access the complete history of DNS changes detected by your monitors with the events API endpoint.

#### View Monitor Event History

Every DNS change detected by a monitor creates an event. The events API provides access to this history for analysis, debugging, and compliance.

##### Listing Events for a Monitor

Get all events for a specific monitor:

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events
```

###### Response

```json
{
  "limit": 20,
  "after": "evt_xyz789",
  "before": null,
  "has_more": true,
  "data": [
    {
      "uuid": "evt_def456",
      "monitor_uuid": "mon_abc123",
      "domain": "piedpiper.com",
      "subdomain": "www",
      "record_type": "A",
      "expected_value": ["1.2.3.4"],
      "occurred": "2026-01-08T14:22:10Z",
      "previous_value": ["1.2.3.4"],
      "current_value": ["5.6.7.8"],
      "old_state": "VALID",
      "new_state": "MISMATCH",
      "incidence_count": 3
    }
  ]
}
```

##### Pagination

Events use cursor-based pagination:

###### Next Page

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?after=evt_xyz789&limit=50
```

###### Previous Page

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?before=evt_abc123&limit=50
```

###### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `limit` | integer | Results per page (1-100, default: 20) |
| `after` | string | Cursor for next page |
| `before` | string | Cursor for previous page |
| `order_by` | string | Sort field (e.g., `occurred`) |
| `order_way` | string | Sort direction (`asc` or `desc`) |
| `occurred_after` | integer | Unix timestamp - events after this time |
| `occurred_before` | integer | Unix timestamp - events before this time |
| `old_state` | string | Filter by state (`VALID`, `MISMATCH`, `NOT_FOUND`, `TIMEOUT`, `INVALID`) |
| `new_state` | string | Filter by state (`VALID`, `MISMATCH`, `NOT_FOUND`, `TIMEOUT`, `INVALID`) |

##### Filtering Events

###### Filter by Time Range

Get events within a specific time period using Unix timestamps (seconds since epoch).

**Events after a specific time:**

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?occurred_after=1767793661
```

**Events before a specific time:**

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?occurred_before=1767880061
```

**Events within a time range:**

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?occurred_after=1767793661&occurred_before=1767880061
```

> ℹ️ **INFO:** **Unix Timestamps**: Convert dates to Unix timestamps using `date +%s` in Bash, `Date.now() / 1000` in JavaScript, or `int(time.time())` in Python.

###### Filter by State

Get only events with a specific state.

**DNS mismatches only:**

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?new_state=MISMATCH
```

**Missing DNS records:**

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?new_state=NOT_FOUND
```

**Valid states (back to normal):**

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?new_state=VALID
```

**Timeout errors:**

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?new_state=TIMEOUT
```

**Invalid DNS responses:**

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?new_state=INVALID
```

###### Sorting Options

Sort by occurred timestamp (descending):

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?order_by=occurred&order_way=desc
```

Sort by occurred timestamp (ascending):

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?order_by=occurred&order_way=asc
```

###### Combining Filters

You can combine multiple filters to create powerful queries.

**Recent MISMATCH events (last 24 hours):**

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?new_state=MISMATCH&occurred_after=1767793661&order_by=occurred&order_way=desc&limit=50
```

**All errors in a specific time range:**

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?new_state=TIMEOUT&occurred_after=1767793661&occurred_before=1767880061&order_by=occurred&order_way=asc
```

**Latest 100 events, sorted newest first:**

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?order_by=occurred&order_way=desc&limit=100
```

**Recovery events (state changed to VALID):**

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events?new_state=VALID&occurred_after=1767793661&limit=20
```

##### Get Single Event

Retrieve details for a specific event:

```
GET https://api.dnsradar.dev/monitors/mon_abc123/events/evt_def456
```

##### Event Fields

| Field | Description |
|-------|-------------|
| `uuid` | Unique event identifier (evt_) |
| `monitor_uuid` | Associated monitor ID |
| `domain` | Domain name |
| `subdomain` | Subdomain (if applicable) |
| `record_type` | DNS record type |
| `expected_value` | Expected DNS values |
| `occurred` | ISO 8601 timestamp |
| `previous_value` | DNS value before change |
| `current_value` | DNS value after change |
| `old_state` | Previous monitor state |
| `new_state` | Current monitor state |
| `incidence_count` | Total incidents for this monitor |

##### Monitor States

Events track state transitions:

| State | Meaning |
|-------|---------|
| `VALID` | DNS matches expected value |
| `MISMATCH` | DNS doesn't match |
| `NOT_FOUND` | DNS record missing |
| `TIMEOUT` | DNS query timeout |
| `INVALID` | Invalid DNS response |


##### Next Steps

- [View Webhook Request History](/docs/webhook-requests)
- [Configure Webhooks](/docs/configure-webhooks)
- [Enable/Disable Monitors](/docs/enable-disable-monitors)



### Flexible MX Record Matching
- [Flexible MX Record Matching](/docs/mx-flexible-matching.md)

> Monitor MX records with flexible matching to allow additional mail servers while ensuring your required email infrastructure remains configured.

#### Flexible MX Record Matching

MX (Mail Exchange) records direct email to mail servers. Flexible matching allows you to verify that specific mail servers are configured while permitting customers to add backup or additional mail providers.

##### The Need for Flexible MX Matching

When customers use your email service, they need your MX records in their DNS. But they might also want backup mail servers or use additional email services. Exact matching would trigger false alerts whenever they make these additions.

###### Exact Matching Limitations

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  record_type: MX
  expected_value: ["mx1.mail-provider.com", "mx2.mail-provider.com"]
  is_exact_match: true
```

If the customer adds a backup mail server, the DNS response becomes:

```
mx1.mail-provider.com
mx2.mail-provider.com
mx.backup-service.com
```

With exact matching, this triggers a `MISMATCH` alert even though your required servers are still present.

##### Enabling Flexible Matching

Set `is_exact_match` to `false` to verify only that your required MX records exist:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  record_type: MX
  expected_value: ["mx1.mail-provider.com", "mx2.mail-provider.com"]
  is_exact_match: false
```

##### How Flexible Matching Works

With flexible matching enabled for MX records, DNSRadar:

1. Verifies all MX records in your `expected_value` are present in the DNS response
2. Ignores additional MX records not in your expected list
3. Allows any priority values unless you explicitly specify them
4. Considers the monitor VALID as long as your required servers exist

##### Valid MX Configurations

With the flexible configuration above, these MX record sets would all be VALID:

```
#### Just your servers
mx1.mail-provider.com
mx2.mail-provider.com

#### With backup server
mx1.mail-provider.com
mx2.mail-provider.com
mx.backup-service.com

#### With multiple additional servers
mx1.mail-provider.com
mx2.mail-provider.com
mx.google.com
mx.microsoft.com
```

All contain your required MX records, so they pass validation.

##### Managing MX Priorities

MX records include priority values that determine mail server preference. You can monitor priorities or ignore them based on your needs.

###### Without Priorities (Default)

If you don't specify priorities, any priority value is accepted:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  record_type: MX
  expected_value: ["mx1.mail-provider.com", "mx2.mail-provider.com"]
  is_exact_match: false
```

These would all match:
```
10 mx1.mail-provider.com, 20 mx2.mail-provider.com
5 mx1.mail-provider.com, 10 mx2.mail-provider.com
100 mx1.mail-provider.com, 100 mx2.mail-provider.com
```

###### With Priorities

To enforce specific priorities, include them in your expected values:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  record_type: MX
  expected_value: ["10 mx1.mail-provider.com", "20 mx2.mail-provider.com"]
  is_exact_match: false
```

> ⚠️ **WARNING:** **Priority Format**: When specifying priorities, use the format `"<priority> <hostname>"`. The priority must be the first part, followed by a space, then the hostname.

With priorities specified, the monitor validates both the hostname AND the priority value. This configuration would only match:

```
10 mx1.mail-provider.com
20 mx2.mail-provider.com
```

But NOT:
```
5 mx1.mail-provider.com   ❌ Wrong priority
20 mx2.mail-provider.com
```

###### Priority Consistency

> 🚨 **DANGER:** **All or Nothing**: You must be consistent within each monitor. Either specify priorities for all MX records or for none. Mixing formatted records causes validation errors.

Invalid configuration (mixed formats):
```json
{
  "expected_value": ["10 mx1.mail-provider.com", "mx2.mail-provider.com"]
  // ❌ Inconsistent: one with priority, one without
}
```

Valid configurations:
```json
// All without priorities ✓
{
  "expected_value": ["mx1.mail-provider.com", "mx2.mail-provider.com"]
}

// All with priorities ✓
{
  "expected_value": ["10 mx1.mail-provider.com", "20 mx2.mail-provider.com"]
}
```

##### When Monitors Trigger

A `MISMATCH` alert is triggered if:

- One or more of your required MX records are removed
- Priority values don't match (when you've specified priorities)
- All MX records are removed
- DNS query returns an error

> ℹ️ **INFO:** **Priority Independence**: The `is_exact_match` parameter only controls whether additional MX records are allowed. It doesn't affect priority validation, which is controlled by whether you include priorities in `expected_value`.


##### Best Practices

1. **Omit Priorities for Flexibility**: Unless priority order is critical, omit priorities to avoid false alerts
2. **Monitor Primary Servers Only**: Focus on your critical mail servers, allowing customers to add backups
3. **Use Flexible Matching**: Enable `is_exact_match: false` for MX monitors unless you need strict control
4. **Document Requirements**: Clearly communicate to customers which MX records must remain configured


##### Combining with Email Authentication

Complete email infrastructure monitoring:

```
POST https://api.dnsradar.dev/monitors/bulk

monitors:
    - domain: piedpiper.com
      record_type: MX
      expected_value: ["mx1.service.com"]
      is_exact_match: false
    - 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
  group: email-infrastructure
```


##### Next Steps

- [Flexible A/AAAA Record Matching](/docs/a-aaaa-flexible-matching)
- [Flexible SPF Record Matching](/docs/spf-flexible-matching)
- [Configure webhook notifications](/docs/configure-webhooks)



### Managing MX Record Priorities
- [Managing MX Record Priorities](/docs/mx-priorities.md)

> Understand how to monitor MX record priorities to ensure proper email routing and maintain mail server preference order.

#### Managing MX Record Priorities

MX record priorities determine the order in which mail servers are contacted for email delivery. Understanding how to monitor priorities ensures your email infrastructure maintains proper routing configuration.

##### Understanding MX Priorities

MX records consist of two parts:
1. **Priority** (preference value): Lower numbers indicate higher priority
2. **Hostname**: The mail server address

Example MX records:
```
10 mx1.mail-provider.com    ← Primary (highest priority)
20 mx2.mail-provider.com    ← Secondary
30 mx3.mail-provider.com    ← Backup (lowest priority)
```

Email servers attempt delivery to the lowest priority number first, falling back to higher numbers if delivery fails.

##### Monitoring Without Priorities

By default, if you don't specify priorities, DNSRadar accepts any priority value:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  record_type: MX
  expected_value: ["mx1.mail-provider.com", "mx2.mail-provider.com"]
```

This configuration matches ANY of these:
```
10 mx1.mail-provider.com, 20 mx2.mail-provider.com  ✅
5 mx1.mail-provider.com, 10 mx2.mail-provider.com   ✅
100 mx1.mail-provider.com, 100 mx2.mail-provider.com ✅
50 mx1.mail-provider.com, 40 mx2.mail-provider.com  ✅
```

> ℹ️ **INFO:** **Priority Flexibility**: Omitting priorities is useful when you care only that specific mail servers are configured, not their preference order.

##### Monitoring With Priorities

To enforce specific priorities, include them in your expected values using the format `"<priority> <hostname>"`:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  record_type: MX
  expected_value: ["10 mx1.mail-provider.com", "20 mx2.mail-provider.com"]
```

This configuration ONLY matches:
```
10 mx1.mail-provider.com, 20 mx2.mail-provider.com  ✅
```

These would trigger `MISMATCH`:
```
5 mx1.mail-provider.com, 20 mx2.mail-provider.com   ❌ Wrong priority for mx1
10 mx1.mail-provider.com, 30 mx2.mail-provider.com  ❌ Wrong priority for mx2
20 mx1.mail-provider.com, 10 mx2.mail-provider.com  ❌ Priorities reversed
```

##### Priority Format Rules

> ⚠️ **WARNING:** **Format Requirements**:
- Priority must be the first element
- Separate priority and hostname with a single space
- Priority must be a positive integer (0-65535)
- Format: `"<priority> <hostname>"`

###### Valid Formats

```json
{
  "expected_value": [
    "10 mx1.service.com",
    "20 mx2.service.com",
    "30 mx3.service.com"
  ]
}
```

###### Invalid Formats

```json
{
  "expected_value": [
    "mx1.service.com 10",     // ❌ Priority must be first
    "10mx1.service.com",      // ❌ Missing space
    "10  mx1.service.com",    // ❌ Multiple spaces
    "10. mx1.service.com"     // ❌ Invalid priority format
  ]
}
```

##### Consistency Requirements

> 🚨 **DANGER:** **All or Nothing**: You must be consistent within each monitor. Either specify priorities for all MX records or for none. Mixing causes validation errors.

###### Invalid (Mixed)

```json
{
  "expected_value": [
    "10 mx1.service.com",  // With priority
    "mx2.service.com"      // Without priority
  ]
  // ❌ INVALID: Inconsistent format
}
```

###### Valid (Consistent)

```json
// All without priorities ✅
{
  "expected_value": [
    "mx1.service.com",
    "mx2.service.com"
  ]
}

// All with priorities ✅
{
  "expected_value": [
    "10 mx1.service.com",
    "20 mx2.service.com"
  ]
}
```

##### Priorities with Flexible Matching

You can combine priority enforcement with flexible matching to allow additional mail servers:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  record_type: MX
  expected_value: ["10 mx1.service.com", "20 mx2.service.com"]
  is_exact_match: false
```

This configuration:
- ✅ Enforces specific priorities for your mail servers
- ✅ Allows customers to add additional mail servers at any priority
- ❌ Triggers `MISMATCH` if your servers' priorities change

Example valid configurations:
```
10 mx1.service.com, 20 mx2.service.com                           ✅
10 mx1.service.com, 20 mx2.service.com, 30 mx.backup.com        ✅
10 mx1.service.com, 15 mx.other.com, 20 mx2.service.com         ✅
5 mx.customer.com, 10 mx1.service.com, 20 mx2.service.com       ✅
```


##### When Priorities Don't Matter

Skip priority monitoring when:

1. **Service Provider Changes**: Email providers may adjust priorities for optimization
2. **Customer Flexibility**: Customers should control their mail routing preferences
3. **Equal Priority Load Balancing**: When multiple servers share the same priority
4. **Backup Server Addition**: Customers adding their own backup servers


##### Next Steps

- [Bulk Update Monitors](/docs/update-monitors-bulk)
- [Flexible MX Record Matching](/docs/mx-flexible-matching)
- [Configure Webhooks](/docs/configure-webhooks)



### Integrate DNSRadar with N8N
- [Integrate DNSRadar with N8N](/docs/n8n-integration.md)

> Use N8N workflows to automate responses to DNS changes detected by DNSRadar monitors through webhook triggers.

#### Integrate DNSRadar with N8N

[n8n](https://n8n.io) is an open-source workflow automation tool that connects to hundreds of services. By integrating DNSRadar with n8n, you can automate responses to DNS changes with complex workflows.

##### Architecture Overview

The integration works through webhooks:

1. DNSRadar detects a DNS change
2. DNSRadar sends webhook to n8n
3. n8n workflow processes the event
4. n8n triggers downstream actions (notifications, tickets, API calls, etc.)

##### Setting Up the Integration

###### Step 1: Create an n8n Webhook

In your n8n workflow:

1. Add a **Webhook** node as the trigger
2. Set **HTTP Method** to `POST`
3. Set **Path** to something memorable like `dnsradar-events`
4. Set **Authentication** to `None` (DNSRadar uses HMAC signatures instead)
5. Copy the **Production URL** (e.g., `https://your-n8n.app/webhook/dnsradar-events`)

###### Step 2: Create DNSRadar Webhook

Create a webhook in DNSRadar pointing to your n8n workflow:

```
POST https://api.dnsradar.dev/webhooks

url: https://your-n8n.app/webhook/dnsradar-events
  method: POST
  secret: your-secure-webhook-secret
  groups: ["production-dns"]
```

DNSRadar will use the `secret` to sign each request with HMAC SHA256, sending the signature in the `X-DNSRadar-Signature` header along with a `X-Webhook-Timestamp` header to prevent replay attacks.

###### Step 3: Test the Integration

Test your webhook to verify n8n receives the payload:

```
POST https://api.dnsradar.dev/webhooks/wh_abc123/test
```

Check n8n to see if the webhook was received and the workflow triggered.

##### Understanding the Webhook Payload

DNSRadar sends this payload to n8n:

```json
{
  "uuid": "req_abc123...",
  "webhook_uuid": "wh_abc123...",
  "created": "2026-01-08T10:35:22Z",
  "event": {
    "event_uuid": "evt_xyz789",
    "monitor_uuid": "mon_abc123",
    "domain": "piedpiper.com",
    "subdomain": "www",
    "record_type": "A",
    "expected_value": ["1.2.3.4"],
    "previous_value": ["1.2.3.4"],
    "current_value": ["5.6.7.8"],
    "old_state": "VALID",
    "new_state": "MISMATCH",
    "occurred": "2026-01-08T10:35:22Z",
    "incidence_count": 1
  }
}
```

Access these values in n8n using expressions like `{{ $json.event.domain }}` or `{{ $json.event.new_state }}`.

##### Example Workflows

###### 1. Slack Notification

**Goal**: Send Slack message when DNS changes

**Workflow**:
1. **Webhook** (Trigger) - Receives DNSRadar event
2. **IF** node - Filter for `MISMATCH` state: `{{ $json.event.new_state === "MISMATCH" }}`
3. **Slack** node - Send message

**Slack message template**:
```
DNS Alert: {{ $json.event.domain }}{{ $json.event.subdomain ? '.' + $json.event.subdomain : '' }}

Record Type: {{ $json.event.record_type }}
Expected: {{ $json.event.expected_value.join(', ') }}
Current: {{ $json.event.current_value.join(', ') }}
State: {{ $json.event.new_state }}

Time: {{ $json.event.occurred }}
```

###### 2. Create PagerDuty Incident

**Goal**: Escalate critical DNS mismatches

**Workflow**:
1. **Webhook** (Trigger)
2. **IF** node - Check if critical: `{{ $json.event.new_state === "MISMATCH" && $json.event.domain.includes('api.') }}`
3. **PagerDuty** node - Create incident

**PagerDuty configuration**:
- **Action**: Create Incident
- **Title**: `DNS Mismatch: {{ $json.event.domain }}`
- **Service**: Your on-call service
- **Urgency**: High
- **Body**: Include all event details

###### 3. Create Jira Ticket

**Goal**: Track DNS changes as tickets

**Workflow**:
1. **Webhook** (Trigger)
2. **Jira** node - Create issue

**Jira issue**:
```
Summary: DNS Change - {{ $json.event.domain }}
Description:
Domain: {{ $json.event.domain }}{{ $json.event.subdomain ? '.' + $json.event.subdomain : '' }}
Record Type: {{ $json.event.record_type }}
Previous Value: {{ $json.event.previous_value.join(', ') }}
Current Value: {{ $json.event.current_value.join(', ') }}
State Change: {{ $json.event.old_state }} → {{ $json.event.new_state }}
Occurred: {{ $json.event.occurred }}

Event UUID: {{ $json.event_uuid }}
Monitor UUID: {{ $json.monitor_uuid }}

Issue Type: Task
Project: DNS-OPS
Priority: {{ $json.event.new_state === "MISMATCH" ? "High" : "Medium" }}
```

###### 4. Email Notification with Details

**Goal**: Send formatted email to team

**Workflow**:
1. **Webhook** (Trigger)
2. **Send Email** node

**Email template**:
```
Subject: DNS Change Alert - {{ $json.event.domain }}

Body:
A DNS change has been detected:

Domain: {{ $json.event.domain }}{{ $json.event.subdomain ? '.' + $json.event.subdomain : '' }}
Record Type: {{ $json.event.record_type }}

Expected Value: {{ $json.event.expected_value.join(', ') }}
Previous Value: {{ $json.event.previous_value.join(', ') }}
Current Value: {{ $json.event.current_value.join(', ') }}

State: {{ $json.event.old_state }} → {{ $json.event.new_state }}
Incident Count: {{ $json.event.incidence_count }}
Occurred: {{ $json.occurred }}

Event ID: {{ $json.event_uuid }}
Monitor ID: {{ $json.monitor_uuid }}
```

###### 5. Multi-Channel Alerting

**Goal**: Notify multiple channels based on severity

**Workflow**:
1. **Webhook** (Trigger)
2. **Switch** node - Route by state:
   - `MISMATCH` → Send to PagerDuty + Slack
   - `NOT_FOUND` → Send to Email + Jira
   - `TIMEOUT` → Send to Slack only
   - Default → Log to database


##### Advanced Patterns

###### Conditional Routing

Route events based on properties:

```javascript
// Switch node expression
{{ $json.event.domain.split('.')[0] }}

// Cases:
// "api" → Critical path (PagerDuty)
// "www" → Standard path (Slack)
// "dev" → Low priority (Email only)
```


##### Security Best Practices

###### Validate Webhook Signatures

Add a **Code** node after the webhook trigger to verify the HMAC signature:

```javascript
// JavaScript code in n8n Code node
const crypto = require('crypto');

const signature = $input.item.headers['x-dnsradar-signature'];
const timestamp = $input.item.headers['x-webhook-timestamp'];
const secret = 'your-secure-webhook-secret'; // Store in n8n environment variable

// Verify timestamp (prevent replay attacks)
const eventTime = new Date(timestamp);
const now = new Date();
const maxAge = 5 * 60 * 1000; // 5 minutes

if (now - eventTime > maxAge) {
  throw new Error('Webhook request too old');
}

// Verify signature
const body = JSON.stringify($input.item.json);
const expectedSignature = crypto
  .createHmac('sha256', secret)
  .update(body)
  .digest('hex');

if (signature !== expectedSignature) {
  throw new Error('Invalid webhook signature');
}

// Return the event data if validation passes
return $input.item.json;
```


##### Troubleshooting

###### Webhook Not Triggering

Check:
1. n8n webhook URL is correct in DNSRadar
2. Webhook is active in DNSRadar
3. n8n workflow is activated
4. Webhook secret is configured correctly
5. n8n is accessible from the internet via HTTPS
6. Signature validation (if implemented) is not rejecting requests

###### Test in n8n

Use n8n's **Listening** mode:
1. Open workflow
2. Click webhook node
3. Click "Listen for Test Event"
4. Send test from DNSRadar
5. Verify payload appears in n8n


###### Debug Payload

Add a **Function** node after webhook:

```javascript
// Log full payload
console.log('DNSRadar event:', $input.all())

// Transform if needed
return $input.all()
```

##### Integration Templates

###### Copy-Paste Ready Workflow

```json
{
  "nodes": [
    {
      "parameters": {
        "path": "dnsradar-events",
        "responseMode": "responseNode",
        "options": {}
      },
      "name": "DNSRadar Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [250, 300]
    },
    {
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{$json.event.new_state}}",
              "value2": "MISMATCH"
            }
          ]
        }
      },
      "name": "Filter Mismatches",
      "type": "n8n-nodes-base.if",
      "position": [450, 300]
    },
    {
      "parameters": {
        "message": "DNS Mismatch: {{$json.event.domain}}\nExpected: {{$json.event.expected_value}}\nCurrent: {{$json.event.current_value}}"
      },
      "name": "Send Slack Alert",
      "type": "n8n-nodes-base.slack",
      "position": [650, 300]
    }
  ]
}
```

##### Next Steps

- [Configure Webhooks](/docs/configure-webhooks)
- [Test Webhooks](/docs/test-webhooks)
- [View Webhook Request History](/docs/webhook-requests)

##### External Resources

- [n8n Documentation](https://docs.n8n.io)
- [n8n Webhook Node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/)
- [n8n Community](https://community.n8n.io)



### Flexible SPF Record Matching
- [Flexible SPF Record Matching](/docs/spf-flexible-matching.md)

> Monitor SPF records with flexible matching to allow additional includes and mechanisms while ensuring your required SPF components are present.

#### 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:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  record_type: TXT
  expected_value: v=spf1 include:_spf.service.com -all
  is_exact_match: true
```

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:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  record_type: TXT
  expected_value: v=spf1 include:_spf.service.com
  is_exact_match: false
```

> ℹ️ **INFO:** **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:

1. Verifies the SPF record starts with `v=spf1`
2. Checks that all mechanisms and modifiers in your `expected_value` are present
3. 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:

```
POST https://api.dnsradar.dev/monitors

domain: piedpiper.com
  record_type: TXT
  expected_value: v=spf1 include:_spf.service.com ip4:1.2.3.4
  is_exact_match: false
```

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

> ✅ **SUCCESS:** **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

> ⚠️ **WARNING:** **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

1. **Specify Minimum Requirements**: Only include the SPF components you absolutely need to verify
2. **Omit Final Qualifiers**: Let customers choose `-all`, `~all`, or `?all` based on their needs
3. **Test Configurations**: Use the [webhook testing endpoint](/docs/test-webhooks) to verify your monitoring setup
4. **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:

```
POST https://api.dnsradar.dev/monitors/bulk

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
```

##### Next Steps

- [Flexible DMARC Record Matching](/docs/dmarc-flexible-matching)
- [Flexible MX Record Matching](/docs/mx-flexible-matching)
- [Configure webhook notifications](/docs/configure-webhooks)



### Test Webhook Endpoints
- [Test Webhook Endpoints](/docs/test-webhooks.md)

> Verify webhook configuration by sending test payloads to your endpoints before deploying to production.

#### Test Webhook Endpoints

Before relying on webhooks in production, test them to ensure proper configuration and endpoint functionality. DNSRadar provides a testing endpoint that sends a fake event to your webhook.

##### Testing a Webhook

Send a POST request to `/webhooks/{uuid}/test`:

```
POST https://api.dnsradar.dev/webhooks/wh_abc123/test
```

The API immediately returns HTTP 202 Accepted, and the test payload is sent to your webhook asynchronously.

##### Test Payload

The test endpoint sends a sample DNS change event:

```json
{
  "uuid": "req_test_abc123...",
  "webhook_uuid": "wh_test_abc123...",
  "created": "2026-01-08T10:35:22Z",
  "event": {
    "event_uuid": "evt_test_xyz789",
    "monitor_uuid": "mon_abc123",
    "domain": "piedpiper.com",
    "subdomain": "www",
    "record_type": "A",
    "expected_value": ["1.2.3.4"],
    "previous_value": ["1.2.3.4"],
    "current_value": ["5.6.7.8"],
    "old_state": "VALID",
    "new_state": "MISMATCH",
    "occurred": "2026-01-08T10:35:22Z",
    "incidence_count": 1
  }
}
```

> ℹ️ **INFO:** **Test Identifiers**: Test events use UUIDs prefixed with `_test_` to help you distinguish them from real events.

##### Rate Limits

To prevent abuse, the test endpoint has strict rate limits:

- **5 requests per minute** per webhook
- **20 requests per hour** per webhook

> ⚠️ **WARNING:** **Rate Limiting**: Exceeding these limits returns HTTP 429 Too Many Requests. Space out your tests appropriately.

##### Response Codes

| Status | Meaning |
|--------|---------|
| 202 Accepted | Test payload queued for delivery |
| 401 Unauthorized | Invalid API key |
| 404 Not Found | Webhook doesn't exist |
| 429 Too Many Requests | Rate limit exceeded |


##### Testing Checklist

Before deploying webhooks to production:

- [ ] Webhook endpoint is accessible from the internet
- [ ] HTTPS is configured properly (valid SSL certificate)
- [ ] Authentication headers are validated
- [ ] Endpoint returns 2xx status codes for successful processing
- [ ] Endpoint responds within 30 seconds
- [ ] Error handling is implemented
- [ ] Rate limiting won't cause issues
- [ ] Payload validation is working
- [ ] Test event was received successfully


##### Common Testing Issues

###### Endpoint Not Receiving Test Events

**Possible causes:**
1. Firewall blocking requests
2. Incorrect URL
3. Server not running
4. HTTPS certificate issues

**Solution:**
```bash
#### Test endpoint accessibility
curl -X POST https://your-endpoint.com/webhooks/dnsradar \
  -H "Content-Type: application/json" \
  -d '{"test": "data"}'
```

###### Endpoint Returns Errors

**Check:**
1. Authentication header validation
2. Payload structure expectations
3. Server logs for errors

**Debug:**

```javascript
app.post('/webhooks/dnsradar', (req, res) => {
  // Log everything during debugging
  console.log('Headers:', req.headers)
  console.log('Body:', req.body)
  console.log('Method:', req.method)

  // Return detailed error info
  try {
    processWebhook(req.body)
    res.status(200).send('OK')
  } catch (error) {
    console.error('Error:', error)
    res.status(500).json({
      error: error.message,
      stack: error.stack
    })
  }
})
```

##### Testing Tools

###### cURL

```bash
#### Test your endpoint directly
curl -X POST https://your-app.com/webhooks/dnsradar \
  -H "X-Webhook-Secret: your-secret" \
  -H "Content-Type: application/json" \
  -d '{
    "event_uuid": "evt_test_manual",
    "monitor_uuid": "mon_test_manual",
    "domain": "test.com",
    "record_type": "A",
    "new_state": "MISMATCH",
    "occurred": "2026-01-08T12:00:00Z"
  }'
```

###### Webhook.site

Use [webhook.site](https://webhook.site) to inspect webhook payloads:

```
POST https://api.dnsradar.dev/webhooks

url: https://webhook.site/YOUR-UNIQUE-URL
  method: POST
```

Then test it:

```
POST https://api.dnsradar.dev/webhooks/wh_abc123/test
```

Visit [webhook.site](https://webhook.site) to see the payload.

###### Ngrok

Test local endpoints:

```bash
#### Expose local server
ngrok http 3000

#### Use ngrok URL in webhook
curl -X POST https://api.dnsradar.dev/webhooks \
  -H "X-API-Key: $API_KEY" \
  -d '{"url": "https://abc123.ngrok.io/webhooks/dnsradar", "method": "POST"}'
```

##### Next Steps

- [View Webhook Request History](/docs/webhook-requests)
- [Enable/Disable Webhooks](/docs/enable-disable-webhooks)
- [Understand Webhook Retries](/docs/webhook-retries)



### Update Multiple Monitors at Once
- [Update Multiple Monitors at Once](/docs/update-monitors-bulk.md)

> Efficiently update check frequency and active status for all monitors in a group with a single API request.

#### Update Multiple Monitors at Once

Instead of updating monitors individually, you can modify all monitors in a group simultaneously. This is useful for managing monitoring behavior across related DNS records or customer domains.

##### Bulk Update Endpoint

Use the `PATCH /groups/{slug}/monitors` endpoint to update all monitors in a group:

```
PATCH https://api.dnsradar.dev/groups/production-servers/monitors

frequency: 15
```

This updates the check frequency for ALL monitors in the `production-servers` group to 15 minutes.

##### Updatable Properties

You can bulk update these monitor properties:

###### Check Frequency

Change how often monitors are checked (in minutes):

```
PATCH https://api.dnsradar.dev/groups/critical-infrastructure/monitors

frequency: 5
```

**Available frequencies**: 5, 10, 15, 30, 60, 120 (minutes)

> ⚠️ **WARNING:** **Plan Limits**: Free plan limited to 60-minute frequency.

###### Active Status

Enable or disable all monitors in a group:

```
PATCH https://api.dnsradar.dev/groups/customer-123/monitors

is_active: false
```

Use cases for disabling monitors:
- Temporary maintenance windows
- Customer account suspension
- Testing or debugging
- Service migration

##### Update Multiple Properties

Combine frequency and status changes:

```
PATCH https://api.dnsradar.dev/groups/maintenance-mode/monitors

frequency: 120
  is_active: false
```

##### API Response

The API returns HTTP 204 No Content on success, indicating all monitors were updated without returning data.

```
HTTP/1.1 204 No Content
```


##### Next Steps

- [Enable/Disable Individual Monitors](/docs/enable-disable-monitors)
- [Configure Webhooks](/docs/configure-webhooks)
- [Monitor Events and History](/docs/monitor-events)



### View Webhook Request History
- [View Webhook Request History](/docs/webhook-requests.md)

> Track webhook delivery attempts with detailed request history including status codes, response times, and error messages.

#### View Webhook Request History

The webhook requests API provides visibility into every delivery attempt, helping you debug issues, monitor performance, and verify webhook behavior.

##### List Webhook Requests

Get request history for a webhook:

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests
```

###### Response

```json
{
  "limit": 20,
  "after": "req_xyz789",
  "before": null,
  "has_more": true,
  "data": [
    {
      "uuid": "req_def456",
      "webhook_uuid": "wh_abc123",
      "event_uuid": "evt_ghi789",
      "created": "2026-01-08T14:25:30Z",
      "status_code": 200,
      "response_body": "OK",
      "error_message": null
    },
    {
      "uuid": "req_jkl012",
      "webhook_uuid": "wh_abc123",
      "event_uuid": "evt_mno345",
      "created": "2026-01-08T13:10:15Z",
      "status_code": 500,
      "response_body": "Internal Server Error",
      "error_message": "Connection timeout"
    }
  ]
}
```

##### Pagination

Webhook requests use cursor-based pagination:

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?after=req_xyz789&limit=50
```

###### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `limit` | integer | Results per page (1-100, default: 20) |
| `after` | string | Cursor for next page |
| `before` | string | Cursor for previous page |
| `order_by` | string | Sort field (e.g., `created`) |
| `order_way` | string | Sort direction (`asc` or `desc`) |
| `created_after` | integer | UTC timestamp - requests created after this time |
| `created_before` | integer | UTC timestamp - requests created before this time |
| `delivered_after` | integer | UTC timestamp - requests delivered after this time |
| `delivered_before` | integer | UTC timestamp - requests delivered before this time |
| `last_attempt_after` | integer | UTC timestamp - last delivery attempt after this time |
| `last_attempt_before` | integer | UTC timestamp - last delivery attempt before this time |
| `status_code` | integer | Filter by HTTP status code (e.g., `200`, `500`) |

##### Filtering Requests

###### Filter by Creation Time

Get requests based on when they were created, using UTC timestamps (seconds since epoch).

**Requests created after a specific time:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?created_after=1767793661
```

**Requests created before a specific time:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?created_before=1767880061
```

**Requests created within a time range:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?created_after=1767793661&created_before=1767880061
```

###### Filter by Delivery Time

Get requests based on when they were successfully delivered.

**Requests delivered after a specific time:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?delivered_after=1767793661
```

**Requests delivered before a specific time:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?delivered_before=1767880061
```

**Requests delivered within a time range:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?delivered_after=1767793661&delivered_before=1767880061
```

###### Filter by Last Attempt Time

Get requests based on when the last delivery attempt was made.

**Requests with last attempt after a specific time:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?last_attempt_after=1767793661
```

**Requests with last attempt before a specific time:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?last_attempt_before=1767880061
```

**Requests with last attempt within a time range:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?last_attempt_after=1767793661&last_attempt_before=1767880061
```

> ℹ️ **INFO:** **UTC Timestamps**: All timestamp parameters use UTC time in seconds since epoch.

###### Filter by Status Code

Get requests by HTTP response status code.

**Successful requests only (200-299):**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?status_code=200
```

**Server errors (500-599):**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?status_code=500
```

**Unauthorized requests (401):**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?status_code=401
```

**Not found errors (404):**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?status_code=404
```

**Rate limit errors (429):**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?status_code=429
```

###### Sorting Options

Sort by creation timestamp.

**Sort by creation time (descending, newest first):**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?order_by=created&order_way=desc
```

**Sort by creation time (ascending, oldest first):**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?order_by=created&order_way=asc
```

###### Combining Filters

Combine multiple filters to create powerful queries.

**Recent failures (last 24 hours):**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?status_code=500&created_after=1767793661&order_by=created&order_way=desc&limit=50
```

**Successful requests in a time range:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?status_code=200&created_after=1767793661&created_before=1767880061&order_by=created&order_way=asc
```

**Latest 100 requests, sorted newest first:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?order_by=created&order_way=desc&limit=100
```

**Recent authorization failures:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?status_code=401&created_after=1767793661&limit=20
```

**Successfully delivered requests in last hour:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?status_code=200&delivered_after=1767880061&order_by=created&order_way=desc
```

**Failed delivery attempts in specific period:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?status_code=500&last_attempt_after=1767793661&last_attempt_before=1767880061&order_by=created&order_way=desc
```

**Recent retry attempts (last attempt in last 30 minutes):**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?last_attempt_after=1767878261&order_by=created&order_way=desc&limit=50
```

**Requests created but not delivered yet:**

```
GET https://api.dnsradar.dev/webhooks/wh_abc123/requests?created_after=1767793661&delivered_before=1767793661&order_by=created&order_way=desc
```

##### Request Fields

| Field | Description |
|-------|-------------|
| uuid | Unique request identifier (req_) |
| webhook_uuid | Associated webhook ID |
| event_uuid | Associated event ID |
| created | ISO 8601 timestamp |
| status_code | HTTP status code from endpoint |
| response_body | Response body (truncated to 4000 chars) |
| error_message | Error description (if failed) |


##### Identifying Issues

###### Common Error Messages

| Error | Cause | Solution |
|-------|-------|----------|
| Connection timeout | Endpoint slow/unresponsive | Optimize response time |
| DNS resolution failed | Invalid hostname | Verify webhook URL |
| SSL certificate error | Invalid/expired cert | Update SSL certificate |
| Connection refused | Endpoint not listening | Start endpoint server |


##### Next Steps

- [Understand Webhook Retries](/docs/webhook-retries)
- [Test Webhooks](/docs/test-webhooks)
- [Enable/Disable Webhooks](/docs/enable-disable-webhooks)



### Understanding Webhook Retries
- [Understanding Webhook Retries](/docs/webhook-retries.md)

> Learn how DNSRadar handles webhook delivery failures with intelligent retry mechanisms and exponential backoff.

#### Understanding Webhook Retries

When your webhook endpoint returns a non 2XX response or is unavailable, DNSRadar automatically retries delivery with increasing delays.

Understanding this retry behavior helps you build resilient integrations.


##### Retry Schedule

DNSRadar uses an exponential backoff strategy with 9 retry attempts over 24 hours:

| Attempt | Delay After Failure |
|---------|---------------------|
| 1st retry | 5 minutes |
| 2nd retry | 10 minutes |
| 3rd retry | 15 minutes |
| 4th retry | 30 minutes |
| 5th retry | 1 hour |
| 6th retry | 2 hours |
| 7th retry | 6 hours |
| 8th retry | 12 hours |
| 9th retry | 24 hours |


After the 9th failed attempt, the webhook is automatically disabled.


##### What Triggers Retries

Retries are triggered when:

- HTTP status codes 300-599 (server errors)
- Network timeouts (> 5 seconds)
- Connection failures
- DNS resolution failures
- SSL/TLS errors


##### Retry Behavior

###### Pending Events Queue

When a webhook fails, subsequent events are queued:

```
Event 1: Fails → Queued for retry in 5 minutes
Event 2: Detected → Queued (waits for Event 1)
Event 3: Detected → Queued (waits for Event 1)
```

Once Event 1 successfully delivers, Events 2 and 3 are sent immediately.

This is to ensure that if a webhook is down, we won't try at every new Events to deliver you the payload. All the subsequent Events payload will be queued until one request succeed (either by automatic retry, or via a call to our API).


###### Successful Retry

When a retry succeeds:
1. The failed event is delivered
2. All queued events are delivered
3. Retry counter resets
4. Webhook remains active


###### Failed Retry

After all retries fail:
1. Webhook is automatically disabled
2. You receive a notification email
3. Queued events are not delivered
4. You must manually re-enable the webhook


##### Automatic Webhook Disabling

> 🚨 **DANGER:** **Auto-Disable After 24 Hours**: Webhooks that fail all 9 retry attempts are automatically disabled. Fix the issue and manually [re-enable](/docs/enable-disable-webhooks) the webhook.


##### Best Practices

* Return a correct status code, in the range 2xx
* Respond quickly. If your endpoint is doing a heavy work, run it after responding or schedule the work via a task queue


##### Retry vs. Delivery Guarantees

> ℹ️ **INFO:** **At-Least-Once Delivery**: DNSRadar guarantees at-least-once delivery for successful webhooks. Events may be delivered multiple times if retries succeed after temporary failures.

DNSRadar provides:
- ✅ At-least-once delivery (events delivered one or more times)
- ✅ Ordered delivery (events delivered in occurrence order)
- ❌ Exactly-once delivery (duplicates possible during retries)

Design your endpoints to be idempotent to handle this guarantee.


##### Troubleshooting

###### Webhook Keeps Getting Disabled

Check:

1. Endpoint accessibility from internet
2. SSL certificate validity
3. Response time (< 30 seconds)
4. Error logs in your application
5. Rate limiting on your endpoint


###### Events Not Arriving

Check:

1. Webhook is enabled (`is_active: true`)
2. No errors in [webhook request history](/docs/webhook-requests)
3. Endpoint logs show no requests
4. Firewall rules allow DNSRadar IPs


##### Next Steps

- [View Webhook Request History](/docs/webhook-requests)
- [Enable/Disable Webhooks](/docs/enable-disable-webhooks)
- [Test Webhooks](/docs/test-webhooks)



### Integrate DNSRadar with Zapier
- [Integrate DNSRadar with Zapier](/docs/zapier-integration.md)

> Use Zapier workflows to automate responses to DNS changes detected by DNSRadar monitors through webhook triggers.

#### Integrate DNSRadar with Zapier

[Zapier](https://zapier.com) is a workflow automation platform that connects to thousands of apps. By integrating DNSRadar with Zapier, you can automate responses to DNS changes with powerful multi-step workflows called "Zaps".

##### Architecture Overview

The integration works through webhooks:

1. DNSRadar detects a DNS change
2. DNSRadar sends webhook to Zapier
3. Zapier workflow processes the event
4. Zapier triggers downstream actions (notifications, tickets, API calls, etc.)

##### Setting Up the Integration

###### Step 1: Create a Zapier Webhook

In your Zapier account:

1. Create a new Zap
2. Search for and select **Webhooks by Zapier** as the trigger app
3. Choose **Catch Hook** as the trigger event
4. Click Continue
5. Copy the **Custom Webhook URL** (e.g., `https://hooks.zapier.com/hooks/catch/123456/abcdef/`)
6. Leave the Zapier editor open to test the webhook

###### Step 2: Create DNSRadar Webhook

Create a webhook in DNSRadar pointing to your Zapier webhook:

```
POST https://api.dnsradar.dev/webhooks

url: https://hooks.zapier.com/hooks/catch/123456/abcdef/
  method: POST
  secret: your-secure-webhook-secret
  groups: ["production-dns"]
```

DNSRadar will use the `secret` to sign each request with HMAC SHA256, sending the signature in the `X-DNSRadar-Signature` header along with a `X-Webhook-Timestamp` header to prevent replay attacks.

###### Step 3: Test the Integration

Test your webhook to send a sample payload to Zapier:

```
POST https://api.dnsradar.dev/webhooks/wh_abc123/test
```

Return to Zapier and click **Test trigger** to see if the webhook was received. You should see the sample data appear in Zapier.

##### Understanding the Webhook Payload

DNSRadar sends this payload to Zapier:

```json
{
  "uuid": "req_abc123...",
  "webhook_uuid": "wh_abc123...",
  "created": "2026-01-08T10:35:22Z",
  "event": {
    "event_uuid": "evt_xyz789",
    "monitor_uuid": "mon_abc123",
    "domain": "piedpiper.com",
    "subdomain": "www",
    "record_type": "A",
    "expected_value": ["1.2.3.4"],
    "previous_value": ["1.2.3.4"],
    "current_value": ["5.6.7.8"],
    "old_state": "VALID",
    "new_state": "MISMATCH",
    "occurred": "2026-01-08T10:35:22Z",
    "incidence_count": 1
  }
}
```

###### Payload Structure

| Field | Description |
|-------|-------------|
| `uuid` | Unique webhook request identifier |
| `webhook_uuid` | The webhook that sent this request |
| `created` | When the webhook request was created (ISO 8601) |
| `event` | The DNS change event details (nested object) |

###### Event Object Fields

All DNS change data is nested within the `event` object:

| Field | Description |
|-------|-------------|
| `event_uuid` | Unique event identifier |
| `monitor_uuid` | Monitor that detected the change |
| `domain` | Domain name (e.g., "piedpiper.com") |
| `subdomain` | Subdomain (e.g., "www") |
| `record_type` | DNS record type (A, AAAA, CNAME, MX, TXT, SPF, DMARC) |
| `expected_value` | Expected DNS values (array) |
| `previous_value` | Previous DNS values (array) |
| `current_value` | Current DNS values (array) |
| `old_state` | Previous monitor state |
| `new_state` | Current monitor state |
| `occurred` | When the DNS change occurred (ISO 8601) |
| `incidence_count` | Total incident count for this monitor |

Access these values in Zapier using the field picker. For nested event fields, use `Event` prefix in Zapier (e.g., `Event Domain`, `Event New State`).

##### Example Zaps

###### 1. Slack Notification

**Goal**: Send Slack message when DNS changes

**Zap Setup**:
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Action**: Slack - Send Channel Message

**Slack message configuration**:
- **Channel**: #dns-alerts
- **Message Text**:
```
DNS Alert: {{event__domain}}.{{event__subdomain}}

Record Type: {{event__record_type}}
Expected: {{event__expected_value}}
Current: {{event__current_value}}
State: {{event__new_state}}

Time: {{event__occurred}}
Event ID: {{event__event_uuid}}
Request ID: {{uuid}}
```

**Optional**: Add a **Filter** step between trigger and action:
- **Field**: Event New State
- **Condition**: Exactly matches
- **Value**: `MISMATCH`

###### 2. Gmail Email Notification

**Goal**: Send email alerts for DNS changes

**Zap Setup**:
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Action**: Gmail - Send Email

**Email configuration**:
- **To**: ops-team@company.com
- **Subject**: `DNS Change Alert - {{event__domain}}`
- **Body**:
```
A DNS change has been detected:

Domain: {{event__domain}}.{{event__subdomain}}
Record Type: {{event__record_type}}

Expected Value: {{event__expected_value}}
Previous Value: {{event__previous_value}}
Current Value: {{event__current_value}}

State: {{event__old_state}} → {{event__new_state}}
Incident Count: {{event__incidence_count}}
Occurred: {{event__occurred}}

Event ID: {{event__event_uuid}}
Monitor ID: {{event__monitor_uuid}}
Request ID: {{uuid}}
```

###### 3. Google Sheets Logging

**Goal**: Track all DNS changes in a spreadsheet

**Zap Setup**:
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Action**: Google Sheets - Create Spreadsheet Row

**Spreadsheet columns**:
- Request UUID: `{{uuid}}`
- Webhook UUID: `{{webhook_uuid}}`
- Created: `{{created}}`
- Event UUID: `{{event__event_uuid}}`
- Monitor UUID: `{{event__monitor_uuid}}`
- Domain: `{{event__domain}}`
- Subdomain: `{{event__subdomain}}`
- Record Type: `{{event__record_type}}`
- Expected Value: `{{event__expected_value}}`
- Current Value: `{{event__current_value}}`
- Old State: `{{event__old_state}}`
- New State: `{{event__new_state}}`
- Occurred: `{{event__occurred}}`
- Incident Count: `{{event__incidence_count}}`

###### 4. Jira Ticket Creation

**Goal**: Create Jira tickets for DNS changes

**Zap Setup**:
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Filter**: Only continue if Event New State is `MISMATCH`
3. **Action**: Jira - Create Issue

**Jira issue configuration**:
- **Project**: DNS-OPS
- **Issue Type**: Task
- **Summary**: `DNS Change - {{event__domain}}.{{event__subdomain}}`
- **Description**:
```
Domain: {{event__domain}}.{{event__subdomain}}
Record Type: {{event__record_type}}

Previous Value: {{event__previous_value}}
Current Value: {{event__current_value}}

State Change: {{event__old_state}} → {{event__new_state}}
Occurred: {{event__occurred}}

Event UUID: {{event__event_uuid}}
Monitor UUID: {{event__monitor_uuid}}
Request ID: {{uuid}}
```
- **Priority**: High (for `MISMATCH`)

###### 5. PagerDuty Incident

**Goal**: Create PagerDuty incidents for critical DNS issues

**Zap Setup**:
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Filter**: Only continue if Event New State is `MISMATCH`
3. **Action**: PagerDuty - Create Incident

**PagerDuty configuration**:
- **Service**: Your DNS on-call service
- **Title**: `DNS Mismatch: {{event__domain}}.{{event__subdomain}}`
- **Urgency**: High
- **Body**:
```
Domain: {{event__domain}}.{{event__subdomain}}
Record Type: {{event__record_type}}
Expected: {{event__expected_value}}
Current: {{event__current_value}}
State: {{event__new_state}}
Event: {{event__event_uuid}}
Request: {{uuid}}
```

###### 6. Microsoft Teams Notification

**Goal**: Alert team in Microsoft Teams

**Zap Setup**:
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Action**: Microsoft Teams - Send Channel Message

**Teams message**:
- **Team**: Operations
- **Channel**: DNS Monitoring
- **Message**:
```
**DNS Alert Detected**

**Domain**: {{event__domain}}.{{event__subdomain}}
**Record Type**: {{event__record_type}}
**State**: {{event__old_state}} → {{event__new_state}}

**Expected**: {{event__expected_value}}
**Current**: {{event__current_value}}

**Time**: {{event__occurred}}
**Event ID**: {{event__event_uuid}}
**Request ID**: {{uuid}}
```

###### 7. Trello Card Creation

**Goal**: Track DNS changes as Trello cards

**Zap Setup**:
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Action**: Trello - Create Card

**Trello card configuration**:
- **Board**: DNS Operations
- **List**: New Alerts
- **Card Name**: `DNS Change - {{event__domain}}.{{event__subdomain}}`
- **Description**:
```
Record Type: {{event__record_type}}
State: {{event__old_state}} → {{event__new_state}}

Expected: {{event__expected_value}}
Current: {{event__current_value}}

Occurred: {{event__occurred}}
Event: {{event__event_uuid}}
Request: {{uuid}}
```
- **Labels**: Based on Event New State (use Formatter to map states to label names)

###### 8. Discord Notification

**Goal**: Send alerts to Discord server

**Zap Setup**:
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Action**: Discord - Send Channel Message

**Discord message**:
- **Channel**: dns-alerts
- **Message Text**:
```
:warning: **DNS Change Detected**

**Domain**: {{event__domain}}.{{event__subdomain}}
**Type**: {{event__record_type}}
**Status**: {{event__new_state}}

**Expected**: `{{event__expected_value}}`
**Current**: `{{event__current_value}}`

**Time**: {{event__occurred}}
**Event ID**: {{event__event_uuid}}
```

##### Advanced Patterns

###### Multi-Path Workflows

Route events based on severity using Paths:

**Zap Setup**:
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Paths by Zapier**:
   - **Path A** (Critical): Event New State = `MISMATCH`
     - Send to PagerDuty
     - Send to Slack
   - **Path B** (Warning): Event New State = `NOT_FOUND`
     - Send to Email
     - Create Jira ticket
   - **Path C** (Info): Event New State = `TIMEOUT`
     - Log to Google Sheets

###### Adding Context with Formatter

Transform data before sending:

**Example**: Format domain display name
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Formatter by Zapier**: Text - Replace
   - **Input**: `{{event__domain}}.{{event__subdomain}}`
   - **Find**: Empty subdomain text
   - **Replace**: Clean format
3. **Action**: Send notification with formatted domain

**Example**: Create custom message based on state
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Formatter by Zapier**: Utilities - Lookup Table
   - **Lookup Value**: `{{event__new_state}}`
   - **Lookup Table**:
     - `MISMATCH` → Critical DNS mismatch detected
     - `NOT_FOUND` → DNS record not found
     - `TIMEOUT` → DNS query timeout
     - `VALID` → DNS record restored to valid state
3. **Action**: Use formatted message

###### Combining Multiple Actions

Send to multiple channels for critical events:

**Zap Setup**:
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Filter**: Event New State = `MISMATCH`
3. **Action 1**: Slack - Send message
4. **Action 2**: PagerDuty - Create incident
5. **Action 3**: Gmail - Send email
6. **Action 4**: Google Sheets - Log event

###### Delay and Digest Pattern

Batch alerts to prevent notification fatigue:

**Zap Setup**:
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Append Entry and Schedule Digest by Zapier**:
   - **Digest Key**: dns-alerts-hourly
   - **Schedule**: Every 1 hour
3. When digest fires, send summary to Slack with all events

###### Using Storage for Deduplication

Track recent alerts to avoid duplicates:

**Zap Setup**:
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Storage by Zapier**: Get Value
   - **Key**: `alert-{{event__monitor_uuid}}`
3. **Filter**: Only continue if storage value is empty or old
4. **Action**: Send alert
5. **Storage by Zapier**: Set Value
   - **Key**: `alert-{{event__monitor_uuid}}`
   - **Value**: `{{event__occurred}}`

##### Webhook Security and Validation

###### Understanding DNSRadar Webhook Signatures

DNSRadar signs webhook requests using HMAC SHA256 for security:

```
POST https://api.dnsradar.dev/webhooks

url: https://hooks.zapier.com/hooks/catch/123456/abcdef/
  method: POST
  secret: your-secure-webhook-secret
  groups: ["production-dns"]
```

DNSRadar includes these security headers:
- `X-DNSRadar-Signature`: HMAC SHA256 signature of the request body
- `X-Webhook-Timestamp`: UTC timestamp to prevent replay attacks

###### Validating Signatures in Zapier

Use **Code by Zapier** to verify the webhook signature:

**Zap Setup**:
1. **Trigger**: Webhooks by Zapier - Catch Hook
2. **Code by Zapier**: Run Python

```python
import hmac
import hashlib
import json
from datetime import datetime, timezone

#### Get headers and body
signature = input_data.get('headers', {}).get('X-Dnsradar-Signature', '')
timestamp = input_data.get('headers', {}).get('X-Webhook-Timestamp', '')
secret = 'your-secure-webhook-secret'

#### Verify timestamp (prevent replay attacks - within 5 minutes)
try:
    event_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
    now = datetime.now(timezone.utc)

    if (now - event_time).total_seconds() > 300:
        raise Exception('Webhook request too old')
except:
    raise Exception('Invalid timestamp')

#### Verify signature
body = json.dumps(input_data).encode('utf-8')
expected_signature = hmac.new(
    secret.encode('utf-8'),
    body,
    hashlib.sha256
).hexdigest()

if not hmac.compare_digest(signature, expected_signature):
    raise Exception('Invalid webhook signature')

#### Return data if validation passes
return input_data
```

3. Continue with actions

###### Optional: Custom Headers

You can still add custom headers for additional authentication layers:

```
POST https://api.dnsradar.dev/webhooks

url: https://hooks.zapier.com/hooks/catch/123456/abcdef/
  method: POST
  secret: your-secure-webhook-secret
  headers:
    X-DNSRadar-Source: production
  groups: ["production-dns"]
```

##### Working with Arrays

DNSRadar sends `expected_value`, `previous_value`, and `current_value` as arrays within the event object. Handle them in Zapier:

###### Display Array Values

Use Formatter to join arrays:

1. **Formatter by Zapier**: Text - Join
   - **Input**: `{{event__expected_value}}`
   - **Separator**: `, `
   - **Output**: Use in subsequent steps

###### Compare Array Values

Use Code by Zapier for complex comparisons:

```python
event = input_data.get('event', {})
expected = event.get('expected_value', [])
current = event.get('current_value', [])

if set(expected) == set(current):
    result = "Match"
else:
    result = "Mismatch"

return {'comparison': result, 'details': f"Expected: {expected}, Got: {current}"}
```

##### Troubleshooting

###### Webhook Not Triggering

Check these items:
1. Zapier webhook URL is correct in DNSRadar
2. Webhook is enabled in DNSRadar
3. Zap is turned ON (not paused)
4. Check Zapier's Zap History for errors

###### Test Your Webhook

From DNSRadar, send a test event:

```
POST https://api.dnsradar.dev/webhooks/wh_abc123/test
```

Check Zapier's Zap History to see if it was received.

###### Missing Data in Zapier

If fields aren't appearing:
1. Send a test webhook from DNSRadar
2. In Zapier, click "Retest trigger" to refresh sample data
3. Verify the field names match the webhook payload
4. Check if the field contains data in the sample

###### Zap Not Continuing

Common causes:
1. **Filter blocking execution**: Check filter conditions
2. **Required field empty**: Ensure all required action fields have data
3. **Authentication failed**: Reconnect the app account
4. **Rate limits**: Check if you've hit API rate limits

###### View Zap History

Check execution logs:
1. Open your Zap
2. Click "Zap History" tab
3. Review recent runs for errors
4. Click individual runs to see detailed logs

###### Debug with Formatter

Add a Formatter step to inspect data:

1. **Formatter by Zapier**: Utilities - Line Item to Text
   - **Input**: All webhook data
2. **Email by Zapier**: Send yourself the formatted output
3. Review what data is actually being received

##### Best Practices

###### Security

Implement webhook signature validation:
1. Always provide a `secret` when creating webhooks
2. Add signature validation using Code by Zapier
3. Verify timestamps to prevent replay attacks
4. Store webhook secrets in environment variables or secure storage

###### Error Handling

Set up error notifications:
1. In Zap settings, enable "Send Zap error emails"
2. Create a separate error-handling Zap
3. Use try-catch in Code steps

###### Testing

Before going live:
1. Test with DNSRadar webhook test endpoint
2. Verify all actions complete successfully
3. Check that notifications arrive correctly
4. Validate formatting and data accuracy

###### Performance

Optimize your Zaps:
1. Use Filters early to avoid unnecessary actions
2. Consider digest patterns for high-volume events
3. Use multi-step Zaps instead of multiple single-step Zaps
4. Monitor task usage to stay within plan limits


##### Next Steps

- [Configure Webhooks](/docs/configure-webhooks)
- [Test Webhooks](/docs/test-webhooks)
- [View Webhook Request History](/docs/webhook-requests)
- [N8n Integration](/docs/n8n-integration) (alternative automation platform)

##### External Resources

- [Zapier Documentation](https://zapier.com/help)
- [Webhooks by Zapier](https://zapier.com/apps/webhook/integrations)
- [Zapier Community](https://community.zapier.com)
- [Zapier Templates](https://zapier.com/apps/webhooks/integrations)