---
title: DNS Monitors
description: Create and manage domain monitors. Import a zone file, choose which records are watched, and read check history.
section:
  title: API Resources
  order: 7
order: 3
lastUpdated: 2026-10-24
---

A DNS monitor watches a **domain**, not a single record. One monitor covers a domain's apex — plus every name declared by a zone file you paste — and sweeps ten record types (`A`, `AAAA`, `CNAME`, `MX`, `TXT`, `NS`, `CAA`, `TLSA`, `DS`, `SVCB`) at each of them on every check. What it expects is the set of records it discovered, held one row per `(name, type, value)`, so a record appearing beside the ones you already have is reported as an addition rather than hidden inside a changed string.

## Breaking change

**These endpoints changed shape, and there is no deprecation window or `v2`.** Integrations written against the previous per-record-type monitor will break.

| Removed                               | Replacement                                                                                    |
| ------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `recordType` on create/update/read    | Every supported type is swept. Per-record control lives in the records sub-resource.           |
| `expectedValue` on create/update/read | The expectation is imported, not typed. Nothing to transcribe.                                 |
| `lastValue` on read                   | Per-record values, on the records sub-resource.                                                |
| `resolvedValue` on results            | Counters: `recordsChecked`, `recordsChanged`, `recordsMissing`, `recordsNew`, `queriesFailed`. |
| `intervalSeconds` as low as 60        | The minimum is now **900**, and the default is **86400** (once a day).                         |

The single-probe DNS check on `POST /api/v1/ping` is **unchanged**: it still takes `recordType` and `expectedValue`, because a stateless one-shot probe is the one place that shape is still the right question.

## Limits

- **We cannot list your DNS records.** DNS does not allow it. Without a zone file, a monitor covers your domain's apex and nothing else — a record at `staging.example.com` is invisible unless that name was in a zone file you pasted.
- **The zone file is a snapshot.** It is read once, parsed, and **never stored**. Names added to your zone afterwards are not tracked until you paste again.
- **One monitor may track at most 85 names.** A create carrying more is refused, because a check sweeps every tracked name in one go.
- **Seven record types.** `SOA`, `SRV`, `PTR`, `DS`, `DNSKEY`, `HTTPS` and `SVCB` are not checked.
- **Detection latency is floored by your records' TTL**, not by the check interval.
- **One check is one ping**, however many names and types it swept.

Every response on this resource carries the standard envelope — `data` alongside `meta`, which holds the `requestId` to quote in a support request and the `timestamp` the response was written.

## The DNS monitor object

```json
{
	"id": "dns_abc123",
	"name": "Production domain",
	"domain": "example.com",
	"zoneFileImportedAt": 1786400000000,
	"intervalSeconds": 86400,
	"isEnabled": true,
	"lastCheckedAt": 1786430000000,
	"lastStatus": "ok",
	"registrationStatus": "valid",
	"registrationExpiresAt": 1820000000000,
	"registrar": "Example Registrar, LLC",
	"registrationEppStatuses": ["clientTransferProhibited"],
	"registrationWarningDays": 30,
	"registrationCheckedAt": 1786420000000,
	"registrationError": null,
	"createdAt": 1786300000000,
	"updatedAt": 1786400000000
}
```

The `registration*` members and `registrar` describe the domain's registration, which the monitor looks up at the domain's registry over RDAP once a day:

| Field                     | Type            | Description                                                                                                                                                                                          |
| ------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `registrationStatus`      | string          | `unknown` until the first lookup, then `valid`, `expiring`, `expired`, `unavailable` (the registry has no record, or no RDAP service) or `error` (no lookup has succeeded within the warning window) |
| `registrationExpiresAt`   | integer \| null | When the registration expires, in milliseconds; `null` until looked up, or when the registry publishes no date                                                                                       |
| `registrar`               | string \| null  | The registrar's name                                                                                                                                                                                 |
| `registrationEppStatuses` | string[]        | The registry's EPP statuses from the last successful lookup, such as `clientTransferProhibited` or `serverHold`                                                                                      |
| `registrationWarningDays` | integer         | Days before expiry the registration counts as `expiring` and starts alerting                                                                                                                         |
| `registrationCheckedAt`   | integer \| null | When a lookup last succeeded                                                                                                                                                                         |
| `registrationError`       | string \| null  | Why the last lookup failed, such as `not-found`, `unsupported-tld` or `rate-limited`; `null` after a success                                                                                         |

## List All DNS Monitors

Retrieves your team's DNS monitors, newest first.

This endpoint is paginated. See [Pagination](/docs/api/pagination) for how to page through the full list.

```
GET /api/v1/dns-monitors
```

**Required scope:** `dns-monitors:read`

### Errors

| Status | Type | Description |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed |
| 401 | `unauthorized` | The API key is missing or invalid |
| 403 | `forbidden` | The API key lacks the required scope |

### Response Schema

```json
{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"type": "object",
	"properties": {
		"data": {
			"type": "object",
			"properties": {
				"dnsMonitors": {
					"type": "array",
					"items": {
						"$ref": "#/$defs/DnsMonitor"
					}
				}
			},
			"required": [
				"dnsMonitors"
			]
		},
		"meta": {
			"type": "object",
			"properties": {
				"requestId": {
					"type": "string",
					"format": "uuid"
				},
				"timestamp": {
					"type": "string",
					"format": "date-time"
				},
				"pagination": {
					"$ref": "#/$defs/Pagination"
				}
			},
			"required": [
				"requestId",
				"timestamp",
				"pagination"
			]
		}
	},
	"required": [
		"data",
		"meta"
	],
	"$defs": {
		"DnsMonitor": {
			"type": "object",
			"properties": {
				"id": {
					"type": "string",
					"pattern": "^dns_[0-7][0-9a-hjkmnp-tv-z]{25}$"
				},
				"name": {
					"type": "string"
				},
				"domain": {
					"type": "string"
				},
				"zoneFileImportedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When a zone file was last imported; null if only the apex is covered"
				},
				"intervalSeconds": {
					"type": "integer"
				},
				"isEnabled": {
					"type": "boolean"
				},
				"lastCheckedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "Epoch milliseconds"
				},
				"lastStatus": {
					"type": [
						"string",
						"null"
					],
					"enum": [
						"ok",
						"changed",
						"error",
						null
					]
				},
				"registrationStatus": {
					"type": "string",
					"enum": [
						"unknown",
						"valid",
						"expiring",
						"expired",
						"unavailable",
						"error"
					],
					"description": "The domain registration's state from the last RDAP lookup; `unavailable` when the registry has no record or no RDAP service, `error` when no lookup has succeeded within the warning window"
				},
				"registrationExpiresAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When the registration expires; null until looked up or when the registry publishes none"
				},
				"registrar": {
					"type": [
						"string",
						"null"
					]
				},
				"registrationEppStatuses": {
					"type": "array",
					"items": {
						"type": "string"
					},
					"description": "EPP status codes from the last successful lookup, such as `clientTransferProhibited`"
				},
				"registrationWarningDays": {
					"type": "integer"
				},
				"registrationCheckedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When a registration lookup last succeeded"
				},
				"registrationError": {
					"type": [
						"string",
						"null"
					],
					"description": "Why the last lookup failed, such as `not-found` or `rate-limited`; null after a success"
				},
				"createdAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				},
				"updatedAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				}
			},
			"required": [
				"id",
				"name",
				"domain",
				"zoneFileImportedAt",
				"intervalSeconds",
				"isEnabled",
				"lastCheckedAt",
				"lastStatus",
				"registrationStatus",
				"registrationExpiresAt",
				"registrar",
				"registrationEppStatuses",
				"registrationWarningDays",
				"registrationCheckedAt",
				"registrationError",
				"createdAt",
				"updatedAt"
			]
		},
		"Pagination": {
			"type": "object",
			"properties": {
				"next": {
					"type": [
						"string",
						"null"
					],
					"description": "Cursor of the following page"
				},
				"prev": {
					"type": [
						"string",
						"null"
					],
					"description": "Cursor of the preceding page"
				},
				"perPage": {
					"type": "integer"
				},
				"total": {
					"type": "integer"
				}
			},
			"required": [
				"next",
				"prev",
				"perPage"
			]
		}
	}
}
```

### Query Parameters

| Parameter | Type    | Required | Description                               |
| --------- | ------- | -------- | ----------------------------------------- |
| `perPage` | integer | No       | Results per page, 1-200 (default: 50)     |
| `cursor`  | string  | No       | Page to fetch, taken from a `Link` header |

### cURL

```bash
curl "https://uptime.sergiodxa.com/api/v1/dns-monitors?perPage=25" \
  -H "Authorization: Bearer uptime_your_api_key"
```

### Response

```json
{
	"data": {
		"dnsMonitors": [
			{
				"id": "dns_abc123",
				"name": "Production domain",
				"domain": "example.com",
				"zoneFileImportedAt": 1786400000000,
				"intervalSeconds": 86400,
				"isEnabled": true,
				"lastCheckedAt": 1786430000000,
				"lastStatus": "ok",
				"createdAt": 1786300000000,
				"updatedAt": 1786400000000
			}
		]
	},
	"meta": {
		"requestId": "6b1f9d5e-4a2c-4f7e-9a10-2c8d5f3b7e41",
		"timestamp": "2026-08-11T10:30:00.000Z",
		"pagination": {
			"next": "eyJkIjoiYWZ0ZXIi",
			"prev": null,
			"perPage": 25,
			"total": 31
		}
	}
}
```

## Create a DNS Monitor

Creates a domain monitor and runs discovery immediately: every supported record type is queried at the domain, and at every name a pasted zone file declares.

**Everything the resolver answered with is imported and watched.** There is no review step on an API call — the dashboard's exists because a human is standing there — so a script that wants something left alone turns it off through the records sub-resource afterwards. The one exception is a record the zone file declares that the resolver does not answer for: it is imported unwatched, for the reason given below.

A team may hold **20 DNS monitors**. Past that the create is refused with a `400` `limit-exceeded` problem, before the monitor row is written and before a single query is sent — one domain monitor sweeps every supported type at every tracked name, so the ceiling is what keeps a team's checks inside the platform's per-invocation budget. Delete a monitor to make room.

The response is `201 Created`.

```
POST /api/v1/dns-monitors
```

**Required scope:** `dns-monitors:write`

### Errors

| Status | Type | Description |
| --- | --- | --- |
| 400 | `validation-error` | The request failed validation |
| 400 | `limit-exceeded` | The team has reached its limit for this resource |
| 400 | `idempotency-key-invalid` | The Idempotency-Key header is not valid |
| 401 | `unauthorized` | The API key is missing or invalid |
| 403 | `forbidden` | The API key lacks the required scope |
| 409 | `idempotency-key-in-use` | A request with this idempotency key is still being processed |
| 422 | `idempotency-key-reused` | This idempotency key was already used for a different request |

### Request Body Schema

```json
{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"type": "object",
	"properties": {
		"name": {
			"type": "string",
			"minLength": 1,
			"maxLength": 255
		},
		"domain": {
			"type": "string",
			"minLength": 1,
			"maxLength": 255
		},
		"intervalSeconds": {
			"type": "number",
			"minimum": 900,
			"maximum": 86400,
			"default": 86400
		},
		"isEnabled": {
			"type": "boolean",
			"default": true
		},
		"registrationWarningDays": {
			"type": "integer",
			"minimum": 1,
			"maximum": 365,
			"description": "Days before the domain registration expires to start alerting",
			"default": 30
		},
		"zoneFile": {
			"type": "string",
			"description": "A BIND zone file, parsed once; only the records it declares persist"
		}
	},
	"required": [
		"name",
		"domain"
	]
}
```

### Response Schema

```json
{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"type": "object",
	"properties": {
		"data": {
			"type": "object",
			"properties": {
				"dnsMonitor": {
					"$ref": "#/$defs/DnsMonitor"
				},
				"discovery": {
					"$ref": "#/$defs/DnsDiscovery"
				}
			},
			"required": [
				"dnsMonitor",
				"discovery"
			]
		},
		"meta": {
			"type": "object",
			"properties": {
				"requestId": {
					"type": "string",
					"format": "uuid"
				},
				"timestamp": {
					"type": "string",
					"format": "date-time"
				}
			},
			"required": [
				"requestId",
				"timestamp"
			]
		}
	},
	"required": [
		"data",
		"meta"
	],
	"$defs": {
		"DnsMonitor": {
			"type": "object",
			"properties": {
				"id": {
					"type": "string",
					"pattern": "^dns_[0-7][0-9a-hjkmnp-tv-z]{25}$"
				},
				"name": {
					"type": "string"
				},
				"domain": {
					"type": "string"
				},
				"zoneFileImportedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When a zone file was last imported; null if only the apex is covered"
				},
				"intervalSeconds": {
					"type": "integer"
				},
				"isEnabled": {
					"type": "boolean"
				},
				"lastCheckedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "Epoch milliseconds"
				},
				"lastStatus": {
					"type": [
						"string",
						"null"
					],
					"enum": [
						"ok",
						"changed",
						"error",
						null
					]
				},
				"registrationStatus": {
					"type": "string",
					"enum": [
						"unknown",
						"valid",
						"expiring",
						"expired",
						"unavailable",
						"error"
					],
					"description": "The domain registration's state from the last RDAP lookup; `unavailable` when the registry has no record or no RDAP service, `error` when no lookup has succeeded within the warning window"
				},
				"registrationExpiresAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When the registration expires; null until looked up or when the registry publishes none"
				},
				"registrar": {
					"type": [
						"string",
						"null"
					]
				},
				"registrationEppStatuses": {
					"type": "array",
					"items": {
						"type": "string"
					},
					"description": "EPP status codes from the last successful lookup, such as `clientTransferProhibited`"
				},
				"registrationWarningDays": {
					"type": "integer"
				},
				"registrationCheckedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When a registration lookup last succeeded"
				},
				"registrationError": {
					"type": [
						"string",
						"null"
					],
					"description": "Why the last lookup failed, such as `not-found` or `rate-limited`; null after a success"
				},
				"createdAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				},
				"updatedAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				}
			},
			"required": [
				"id",
				"name",
				"domain",
				"zoneFileImportedAt",
				"intervalSeconds",
				"isEnabled",
				"lastCheckedAt",
				"lastStatus",
				"registrationStatus",
				"registrationExpiresAt",
				"registrar",
				"registrationEppStatuses",
				"registrationWarningDays",
				"registrationCheckedAt",
				"registrationError",
				"createdAt",
				"updatedAt"
			]
		},
		"DnsDiscovery": {
			"type": "object",
			"properties": {
				"names": {
					"type": "integer",
					"description": "Names swept: the apex plus every zone-file name"
				},
				"recordsImported": {
					"type": "integer"
				},
				"queriesFailed": {
					"type": "integer",
					"description": "Queries that did not answer"
				},
				"rejectedLines": {
					"type": "array",
					"items": {
						"type": "object",
						"properties": {
							"line": {
								"type": "integer"
							},
							"reason": {
								"type": "string",
								"enum": [
									"includeDirective",
									"generateDirective",
									"unsupportedDirective",
									"nonInternetClass",
									"unsupportedType",
									"outOfZone",
									"malformed"
								]
							}
						},
						"required": [
							"line",
							"reason"
						]
					}
				},
				"duplicateLines": {
					"type": "array",
					"items": {
						"type": "integer"
					},
					"description": "Lines repeating a record an earlier line declared"
				}
			},
			"required": [
				"names",
				"recordsImported",
				"queriesFailed",
				"rejectedLines",
				"duplicateLines"
			]
		}
	}
}
```

### Request Body

| Field                     | Type    | Required | Description                                                                        |
| ------------------------- | ------- | -------- | ---------------------------------------------------------------------------------- |
| `name`                    | string  | Yes      | Monitor name (1-255 characters)                                                    |
| `domain`                  | string  | Yes      | The domain to cover (1-255 characters)                                             |
| `zoneFile`                | string  | No       | A BIND zone file, up to 262144 bytes. Read once, parsed, and never stored.         |
| `intervalSeconds`         | integer | No       | Check interval in seconds (900-86400, default: 86400)                              |
| `isEnabled`               | boolean | No       | Whether the monitor is checked on its interval (default: true)                     |
| `registrationWarningDays` | integer | No       | Days before the domain registration expires to start alerting (1-365, default: 30) |

The parser reads standard RFC 1035 zone-file syntax: `<owner> [<ttl>] [IN] <TYPE> <rdata>` records, `;` comments, absolute and relative names, `@` for the apex, `$ORIGIN` and `$TTL`, parenthesised multi-line records, owner-inheriting continuation lines, and quoted TXT character-strings. Anything it cannot use — `$INCLUDE`, `$GENERATE`, non-`IN` classes, untracked types, names outside the domain — is **reported, never silently dropped**, in `discovery.rejectedLines`, with the line the entry starts on.

A record the zone file declares but the resolver does not answer is imported **unwatched**, with status `missing`. That is a real finding at import — a stale delegation, a change that never published — and a poor standing alert, since the file is a snapshot that only gets older. Enable it through the records sub-resource if you want it watched. On a proxied zone this is the common case rather than the exceptional one: a proxied record is not in public DNS at all.

### cURL

```bash
curl https://uptime.sergiodxa.com/api/v1/dns-monitors \
  -H "Authorization: Bearer uptime_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production domain",
    "domain": "example.com",
    "zoneFile": "www\t1\tIN\tA\t192.0.2.1\n_dmarc\t1\tIN\tTXT\t\"v=DMARC1; p=none\"",
    "intervalSeconds": 86400
  }'
```

### Response

```json
{
	"data": {
		"dnsMonitor": {
			"id": "dns_abc123",
			"name": "Production domain",
			"domain": "example.com",
			"zoneFileImportedAt": 1786400000000,
			"intervalSeconds": 86400,
			"isEnabled": true,
			"lastCheckedAt": null,
			"lastStatus": null,
			"createdAt": 1786400000000,
			"updatedAt": 1786400000000
		},
		"discovery": {
			"names": 3,
			"recordsImported": 11,
			"queriesFailed": 0,
			"rejectedLines": [{ "line": 1, "reason": "includeDirective" }],
			"duplicateLines": [14]
		}
	}
}
```

`discovery.queriesFailed` counts queries that did not answer. Those names are not covered by this import and no record is inferred from them; the monitor's next scheduled check tries again.

## Get a DNS Monitor

Retrieves a single DNS monitor by ID.

```
GET /api/v1/dns-monitors/:dnsMonitorId
```

**Required scope:** `dns-monitors:read`

### Errors

| Status | Type | Description |
| --- | --- | --- |
| 400 | `validation-error` | The request failed validation |
| 401 | `unauthorized` | The API key is missing or invalid |
| 403 | `forbidden` | The API key lacks the required scope |
| 404 | `not-found` | The resource does not exist |

### Response Schema

```json
{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"type": "object",
	"properties": {
		"data": {
			"type": "object",
			"properties": {
				"dnsMonitor": {
					"$ref": "#/$defs/DnsMonitor"
				}
			},
			"required": [
				"dnsMonitor"
			]
		},
		"meta": {
			"type": "object",
			"properties": {
				"requestId": {
					"type": "string",
					"format": "uuid"
				},
				"timestamp": {
					"type": "string",
					"format": "date-time"
				}
			},
			"required": [
				"requestId",
				"timestamp"
			]
		}
	},
	"required": [
		"data",
		"meta"
	],
	"$defs": {
		"DnsMonitor": {
			"type": "object",
			"properties": {
				"id": {
					"type": "string",
					"pattern": "^dns_[0-7][0-9a-hjkmnp-tv-z]{25}$"
				},
				"name": {
					"type": "string"
				},
				"domain": {
					"type": "string"
				},
				"zoneFileImportedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When a zone file was last imported; null if only the apex is covered"
				},
				"intervalSeconds": {
					"type": "integer"
				},
				"isEnabled": {
					"type": "boolean"
				},
				"lastCheckedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "Epoch milliseconds"
				},
				"lastStatus": {
					"type": [
						"string",
						"null"
					],
					"enum": [
						"ok",
						"changed",
						"error",
						null
					]
				},
				"registrationStatus": {
					"type": "string",
					"enum": [
						"unknown",
						"valid",
						"expiring",
						"expired",
						"unavailable",
						"error"
					],
					"description": "The domain registration's state from the last RDAP lookup; `unavailable` when the registry has no record or no RDAP service, `error` when no lookup has succeeded within the warning window"
				},
				"registrationExpiresAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When the registration expires; null until looked up or when the registry publishes none"
				},
				"registrar": {
					"type": [
						"string",
						"null"
					]
				},
				"registrationEppStatuses": {
					"type": "array",
					"items": {
						"type": "string"
					},
					"description": "EPP status codes from the last successful lookup, such as `clientTransferProhibited`"
				},
				"registrationWarningDays": {
					"type": "integer"
				},
				"registrationCheckedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When a registration lookup last succeeded"
				},
				"registrationError": {
					"type": [
						"string",
						"null"
					],
					"description": "Why the last lookup failed, such as `not-found` or `rate-limited`; null after a success"
				},
				"createdAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				},
				"updatedAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				}
			},
			"required": [
				"id",
				"name",
				"domain",
				"zoneFileImportedAt",
				"intervalSeconds",
				"isEnabled",
				"lastCheckedAt",
				"lastStatus",
				"registrationStatus",
				"registrationExpiresAt",
				"registrar",
				"registrationEppStatuses",
				"registrationWarningDays",
				"registrationCheckedAt",
				"registrationError",
				"createdAt",
				"updatedAt"
			]
		}
	}
}
```

### cURL

```bash
curl https://uptime.sergiodxa.com/api/v1/dns-monitors/dns_abc123 \
  -H "Authorization: Bearer uptime_your_api_key"
```

### Response

```json
{
	"data": {
		"dnsMonitor": {
			"id": "dns_abc123",
			"name": "Production domain",
			"domain": "example.com",
			"zoneFileImportedAt": 1786400000000,
			"intervalSeconds": 86400,
			"isEnabled": true,
			"lastCheckedAt": 1786430000000,
			"lastStatus": "ok",
			"createdAt": 1786300000000,
			"updatedAt": 1786400000000
		}
	}
}
```

## Update a DNS Monitor

Updates a DNS monitor with a [JSON merge patch](/docs/api/overview#updating-resources): send only the fields you want to change.

There is no `zoneFile` here, and one sent is ignored rather than refused. The pasted text is never stored, so re-importing is a deliberate act rather than something carried along by a rename — import again from the dashboard, or create a new monitor.

```
PATCH /api/v1/dns-monitors/:dnsMonitorId
```

**Required scope:** `dns-monitors:write`

### Errors

| Status | Type | Description |
| --- | --- | --- |
| 400 | `validation-error` | The request failed validation |
| 401 | `unauthorized` | The API key is missing or invalid |
| 403 | `forbidden` | The API key lacks the required scope |
| 404 | `not-found` | The resource does not exist |
| 415 | `unsupported-media-type` | The request body's media type is not accepted |

### Response Schema

```json
{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"type": "object",
	"properties": {
		"data": {
			"type": "object",
			"properties": {
				"dnsMonitor": {
					"$ref": "#/$defs/DnsMonitor"
				}
			},
			"required": [
				"dnsMonitor"
			]
		},
		"meta": {
			"type": "object",
			"properties": {
				"requestId": {
					"type": "string",
					"format": "uuid"
				},
				"timestamp": {
					"type": "string",
					"format": "date-time"
				}
			},
			"required": [
				"requestId",
				"timestamp"
			]
		}
	},
	"required": [
		"data",
		"meta"
	],
	"$defs": {
		"DnsMonitor": {
			"type": "object",
			"properties": {
				"id": {
					"type": "string",
					"pattern": "^dns_[0-7][0-9a-hjkmnp-tv-z]{25}$"
				},
				"name": {
					"type": "string"
				},
				"domain": {
					"type": "string"
				},
				"zoneFileImportedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When a zone file was last imported; null if only the apex is covered"
				},
				"intervalSeconds": {
					"type": "integer"
				},
				"isEnabled": {
					"type": "boolean"
				},
				"lastCheckedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "Epoch milliseconds"
				},
				"lastStatus": {
					"type": [
						"string",
						"null"
					],
					"enum": [
						"ok",
						"changed",
						"error",
						null
					]
				},
				"registrationStatus": {
					"type": "string",
					"enum": [
						"unknown",
						"valid",
						"expiring",
						"expired",
						"unavailable",
						"error"
					],
					"description": "The domain registration's state from the last RDAP lookup; `unavailable` when the registry has no record or no RDAP service, `error` when no lookup has succeeded within the warning window"
				},
				"registrationExpiresAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When the registration expires; null until looked up or when the registry publishes none"
				},
				"registrar": {
					"type": [
						"string",
						"null"
					]
				},
				"registrationEppStatuses": {
					"type": "array",
					"items": {
						"type": "string"
					},
					"description": "EPP status codes from the last successful lookup, such as `clientTransferProhibited`"
				},
				"registrationWarningDays": {
					"type": "integer"
				},
				"registrationCheckedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When a registration lookup last succeeded"
				},
				"registrationError": {
					"type": [
						"string",
						"null"
					],
					"description": "Why the last lookup failed, such as `not-found` or `rate-limited`; null after a success"
				},
				"createdAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				},
				"updatedAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				}
			},
			"required": [
				"id",
				"name",
				"domain",
				"zoneFileImportedAt",
				"intervalSeconds",
				"isEnabled",
				"lastCheckedAt",
				"lastStatus",
				"registrationStatus",
				"registrationExpiresAt",
				"registrar",
				"registrationEppStatuses",
				"registrationWarningDays",
				"registrationCheckedAt",
				"registrationError",
				"createdAt",
				"updatedAt"
			]
		}
	}
}
```

### Request Body

| Field                     | Type    | Required | Description                                                           |
| ------------------------- | ------- | -------- | --------------------------------------------------------------------- |
| `name`                    | string  | No       | Monitor name (1-255 characters)                                       |
| `domain`                  | string  | No       | The domain this monitor covers (1-255 characters)                     |
| `intervalSeconds`         | integer | No       | Check interval in seconds (900-86400)                                 |
| `isEnabled`               | boolean | No       | Whether the monitor is checked on its interval                        |
| `registrationWarningDays` | integer | No       | Days before the domain registration expires to start alerting (1-365) |

The limits are the ones [Create a DNS Monitor](#create-a-dns-monitor) applies. `null` resets `intervalSeconds` to `86400`, `isEnabled` to `true` and `registrationWarningDays` to `30`; `name` and `domain` cannot be removed, so `null` on either is a `400` `validation-error`.

A new `registrationWarningDays` makes the registration due for a lookup on the next hourly sweep, so `registrationStatus` reflects the new window within the hour.

Changing `domain` does not re-run discovery: the records already tracked stay as they are, so point a monitor at a different domain only if you also mean to review its records.

### cURL

```bash
curl https://uptime.sergiodxa.com/api/v1/dns-monitors/dns_abc123 \
  -X PATCH \
  -H "Authorization: Bearer uptime_your_api_key" \
  -H "Content-Type: application/merge-patch+json" \
  -d '{ "intervalSeconds": 3600 }'
```

### Response

```json
{
	"data": {
		"dnsMonitor": {
			"id": "dns_abc123",
			"name": "Production domain",
			"domain": "example.com",
			"zoneFileImportedAt": 1786400000000,
			"intervalSeconds": 3600,
			"isEnabled": true,
			"lastCheckedAt": 1786430000000,
			"lastStatus": "ok",
			"createdAt": 1786300000000,
			"updatedAt": 1786440000000
		}
	}
}
```

### Update With PUT

`PUT /api/v1/dns-monitors/:dnsMonitorId` takes the same fields, for integrations written before `PATCH` existed. A field you leave out keeps its value, `null` is refused, and sending `isEnabled` schedules the next check again on every request, even when the value is unchanged.

**Required scope:** `dns-monitors:write`

### Errors

| Status | Type | Description |
| --- | --- | --- |
| 400 | `validation-error` | The request failed validation |
| 401 | `unauthorized` | The API key is missing or invalid |
| 403 | `forbidden` | The API key lacks the required scope |
| 404 | `not-found` | The resource does not exist |

### Request Body Schema

```json
{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"type": "object",
	"properties": {
		"name": {
			"type": "string",
			"minLength": 1,
			"maxLength": 255
		},
		"domain": {
			"type": "string",
			"minLength": 1,
			"maxLength": 255
		},
		"intervalSeconds": {
			"type": "number",
			"minimum": 900,
			"maximum": 86400
		},
		"isEnabled": {
			"type": "boolean"
		},
		"registrationWarningDays": {
			"type": "integer",
			"minimum": 1,
			"maximum": 365,
			"description": "Days before the domain registration expires to start alerting"
		}
	}
}
```

### Response Schema

```json
{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"type": "object",
	"properties": {
		"data": {
			"type": "object",
			"properties": {
				"dnsMonitor": {
					"$ref": "#/$defs/DnsMonitor"
				}
			},
			"required": [
				"dnsMonitor"
			]
		},
		"meta": {
			"type": "object",
			"properties": {
				"requestId": {
					"type": "string",
					"format": "uuid"
				},
				"timestamp": {
					"type": "string",
					"format": "date-time"
				}
			},
			"required": [
				"requestId",
				"timestamp"
			]
		}
	},
	"required": [
		"data",
		"meta"
	],
	"$defs": {
		"DnsMonitor": {
			"type": "object",
			"properties": {
				"id": {
					"type": "string",
					"pattern": "^dns_[0-7][0-9a-hjkmnp-tv-z]{25}$"
				},
				"name": {
					"type": "string"
				},
				"domain": {
					"type": "string"
				},
				"zoneFileImportedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When a zone file was last imported; null if only the apex is covered"
				},
				"intervalSeconds": {
					"type": "integer"
				},
				"isEnabled": {
					"type": "boolean"
				},
				"lastCheckedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "Epoch milliseconds"
				},
				"lastStatus": {
					"type": [
						"string",
						"null"
					],
					"enum": [
						"ok",
						"changed",
						"error",
						null
					]
				},
				"registrationStatus": {
					"type": "string",
					"enum": [
						"unknown",
						"valid",
						"expiring",
						"expired",
						"unavailable",
						"error"
					],
					"description": "The domain registration's state from the last RDAP lookup; `unavailable` when the registry has no record or no RDAP service, `error` when no lookup has succeeded within the warning window"
				},
				"registrationExpiresAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When the registration expires; null until looked up or when the registry publishes none"
				},
				"registrar": {
					"type": [
						"string",
						"null"
					]
				},
				"registrationEppStatuses": {
					"type": "array",
					"items": {
						"type": "string"
					},
					"description": "EPP status codes from the last successful lookup, such as `clientTransferProhibited`"
				},
				"registrationWarningDays": {
					"type": "integer"
				},
				"registrationCheckedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "When a registration lookup last succeeded"
				},
				"registrationError": {
					"type": [
						"string",
						"null"
					],
					"description": "Why the last lookup failed, such as `not-found` or `rate-limited`; null after a success"
				},
				"createdAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				},
				"updatedAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				}
			},
			"required": [
				"id",
				"name",
				"domain",
				"zoneFileImportedAt",
				"intervalSeconds",
				"isEnabled",
				"lastCheckedAt",
				"lastStatus",
				"registrationStatus",
				"registrationExpiresAt",
				"registrar",
				"registrationEppStatuses",
				"registrationWarningDays",
				"registrationCheckedAt",
				"registrationError",
				"createdAt",
				"updatedAt"
			]
		}
	}
}
```

## Delete a DNS Monitor

Permanently deletes a DNS monitor, every record it tracks, and its check history.

```
DELETE /api/v1/dns-monitors/:dnsMonitorId
```

**Required scope:** `dns-monitors:delete`

### Errors

| Status | Type | Description |
| --- | --- | --- |
| 400 | `validation-error` | The request failed validation |
| 401 | `unauthorized` | The API key is missing or invalid |
| 403 | `forbidden` | The API key lacks the required scope |
| 404 | `not-found` | The resource does not exist |

### Response Schema

```json
{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"type": "object",
	"properties": {
		"data": {
			"type": "object",
			"properties": {
				"deleted": {
					"type": "boolean",
					"const": true
				}
			},
			"required": [
				"deleted"
			]
		},
		"meta": {
			"type": "object",
			"properties": {
				"requestId": {
					"type": "string",
					"format": "uuid"
				},
				"timestamp": {
					"type": "string",
					"format": "date-time"
				}
			},
			"required": [
				"requestId",
				"timestamp"
			]
		}
	},
	"required": [
		"data",
		"meta"
	]
}
```

### cURL

```bash
curl https://uptime.sergiodxa.com/api/v1/dns-monitors/dns_abc123 \
  -X DELETE \
  -H "Authorization: Bearer uptime_your_api_key"
```

### Response

```json
{ "data": { "deleted": true } }
```

## Get DNS Monitor Results

Retrieves the check history for a DNS monitor: **one row per check of the monitor**, whatever the sweep cost in queries.

Results arrive newest first, a page at a time. See [Pagination](/docs/api/pagination) for how to walk the whole history.

```
GET /api/v1/dns-monitors/:dnsMonitorId/results
```

**Required scope:** `dns-monitors:read`

### Errors

| Status | Type | Description |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed |
| 400 | `validation-error` | The request failed validation |
| 401 | `unauthorized` | The API key is missing or invalid |
| 403 | `forbidden` | The API key lacks the required scope |
| 404 | `not-found` | The resource does not exist |

### Response Schema

```json
{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"type": "object",
	"properties": {
		"data": {
			"type": "object",
			"properties": {
				"results": {
					"type": "array",
					"items": {
						"$ref": "#/$defs/DnsMonitorResult"
					}
				}
			},
			"required": [
				"results"
			]
		},
		"meta": {
			"type": "object",
			"properties": {
				"requestId": {
					"type": "string",
					"format": "uuid"
				},
				"timestamp": {
					"type": "string",
					"format": "date-time"
				},
				"pagination": {
					"$ref": "#/$defs/Pagination"
				}
			},
			"required": [
				"requestId",
				"timestamp",
				"pagination"
			]
		}
	},
	"required": [
		"data",
		"meta"
	],
	"$defs": {
		"DnsMonitorResult": {
			"type": "object",
			"properties": {
				"id": {
					"type": "string",
					"pattern": "^res_[0-7][0-9a-hjkmnp-tv-z]{25}$"
				},
				"status": {
					"type": "string",
					"enum": [
						"ok",
						"changed",
						"error"
					]
				},
				"recordsChecked": {
					"type": "integer"
				},
				"recordsChanged": {
					"type": "integer"
				},
				"recordsMissing": {
					"type": "integer"
				},
				"recordsNew": {
					"type": "integer"
				},
				"queriesFailed": {
					"type": "integer"
				},
				"responseTimeMs": {
					"type": [
						"integer",
						"null"
					],
					"description": "The slowest single query"
				},
				"errorMessage": {
					"type": [
						"string",
						"null"
					]
				},
				"checkedAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				}
			},
			"required": [
				"id",
				"status",
				"recordsChecked",
				"recordsChanged",
				"recordsMissing",
				"recordsNew",
				"queriesFailed",
				"responseTimeMs",
				"errorMessage",
				"checkedAt"
			]
		},
		"Pagination": {
			"type": "object",
			"properties": {
				"next": {
					"type": [
						"string",
						"null"
					],
					"description": "Cursor of the following page"
				},
				"prev": {
					"type": [
						"string",
						"null"
					],
					"description": "Cursor of the preceding page"
				},
				"perPage": {
					"type": "integer"
				},
				"total": {
					"type": "integer"
				}
			},
			"required": [
				"next",
				"prev",
				"perPage"
			]
		}
	}
}
```

### Query Parameters

| Parameter | Type    | Required | Description                               |
| --------- | ------- | -------- | ----------------------------------------- |
| `perPage` | integer | No       | Results per page, 1-200 (default: 50)     |
| `cursor`  | string  | No       | Page to fetch, taken from a `Link` header |

### cURL

```bash
curl -i "https://uptime.sergiodxa.com/api/v1/dns-monitors/dns_abc123/results?perPage=10" \
  -H "Authorization: Bearer uptime_your_api_key"
```

### Response

```json
{
	"data": {
		"results": [
			{
				"id": "res_xyz789",
				"status": "changed",
				"recordsChecked": 18,
				"recordsChanged": 0,
				"recordsMissing": 1,
				"recordsNew": 1,
				"queriesFailed": 0,
				"responseTimeMs": 42,
				"errorMessage": null,
				"checkedAt": 1786430000000
			}
		]
	},
	"meta": {
		"requestId": "6b1f9d5e-4a2c-4f7e-9a10-2c8d5f3b7e41",
		"timestamp": "2026-08-11T10:30:00.000Z",
		"pagination": {
			"next": "eyJkIjoiYWZ0ZXIi",
			"prev": null,
			"perPage": 10
		}
	}
}
```

`responseTimeMs` is the **slowest single query** in the sweep, not the sum: it answers "how long did DNS take to answer", which is a latency figure rather than a cost one.

A value edited inside a record set holding several values reads as one missing record plus one new one. That is truthful rather than a bug: DNS gives an individual record no identity of its own, so editing one value is indistinguishable, on the wire, from removing one and adding another. `recordsChanged` counts only the case a diff can attribute without guessing — a name and type holding exactly one watched record and answering with exactly one differing value.

## Records

Which records a monitor watches is edited through its records sub-resource, on the same `dns-monitors:read`/`dns-monitors:write` scopes — a key that may reconfigure a domain monitor may decide which of its records are watched, since the two authorities are the same authority.

```
GET   /api/v1/dns-monitors/:dnsMonitorId/records
PATCH /api/v1/dns-monitors/:dnsMonitorId/records/:recordId
```

A record carries its `name`, `recordType`, `value`, how it was discovered (`source`), whether a deviation from it alerts (`isEnabled`), and its `status` (`ok`, `changed`, `missing`, `new`, `error`). `PATCH` takes `{ "isEnabled": boolean }`.

Two things about that table are worth knowing before you script against it:

- **It holds everything we have ever seen for the domain**, including records you turned off. Turning one off says only that deviations from it do not alert; deleting the row instead would make the next check rediscover it as new, forever.
- **A record discovered by a check is stored unwatched**, with status `new`. Accepting something that appeared without you putting it there should be an act, not the thing that happens by ignoring an email.

## List DNS Monitor Records

Retrieves every record the monitor tracks, including the ones you have declined to watch.
The table is the complete set of everything ever seen for the domain: `isEnabled` says only
whether a deviation from that record alerts, so a declined record stays listed rather than
being rediscovered as new on the next check.

```
GET /api/v1/dns-monitors/:dnsMonitorId/records
```

**Required scope:** `dns-monitors:read`

### Errors

| Status | Type | Description |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed |
| 400 | `validation-error` | The request failed validation |
| 401 | `unauthorized` | The API key is missing or invalid |
| 403 | `forbidden` | The API key lacks the required scope |
| 404 | `not-found` | The resource does not exist |

### Response Schema

```json
{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"type": "object",
	"properties": {
		"data": {
			"type": "object",
			"properties": {
				"records": {
					"type": "array",
					"items": {
						"$ref": "#/$defs/DnsMonitorRecord"
					}
				}
			},
			"required": [
				"records"
			]
		},
		"meta": {
			"type": "object",
			"properties": {
				"requestId": {
					"type": "string",
					"format": "uuid"
				},
				"timestamp": {
					"type": "string",
					"format": "date-time"
				},
				"pagination": {
					"$ref": "#/$defs/Pagination"
				}
			},
			"required": [
				"requestId",
				"timestamp",
				"pagination"
			]
		}
	},
	"required": [
		"data",
		"meta"
	],
	"$defs": {
		"DnsMonitorRecord": {
			"type": "object",
			"properties": {
				"id": {
					"type": "string",
					"pattern": "^dnsrec_[0-7][0-9a-hjkmnp-tv-z]{25}$"
				},
				"dnsMonitorId": {
					"type": "string",
					"pattern": "^dns_[0-7][0-9a-hjkmnp-tv-z]{25}$"
				},
				"name": {
					"type": "string"
				},
				"recordType": {
					"type": "string",
					"enum": [
						"A",
						"AAAA",
						"CNAME",
						"MX",
						"TXT",
						"NS",
						"CAA",
						"TLSA",
						"DS",
						"SVCB"
					]
				},
				"value": {
					"type": "string"
				},
				"source": {
					"type": "string",
					"enum": [
						"resolver",
						"zone_file"
					]
				},
				"isEnabled": {
					"type": "boolean",
					"description": "Whether a deviation from this record alerts"
				},
				"status": {
					"type": "string",
					"enum": [
						"ok",
						"changed",
						"missing",
						"new",
						"error"
					]
				},
				"firstSeenAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				},
				"lastSeenAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "Last check that resolved this record; null for zone-file-only records"
				},
				"lastCheckedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "Epoch milliseconds"
				},
				"createdAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				},
				"updatedAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				}
			},
			"required": [
				"id",
				"dnsMonitorId",
				"name",
				"recordType",
				"value",
				"source",
				"isEnabled",
				"status",
				"firstSeenAt",
				"lastSeenAt",
				"lastCheckedAt",
				"createdAt",
				"updatedAt"
			]
		},
		"Pagination": {
			"type": "object",
			"properties": {
				"next": {
					"type": [
						"string",
						"null"
					],
					"description": "Cursor of the following page"
				},
				"prev": {
					"type": [
						"string",
						"null"
					],
					"description": "Cursor of the preceding page"
				},
				"perPage": {
					"type": "integer"
				},
				"total": {
					"type": "integer"
				}
			},
			"required": [
				"next",
				"prev",
				"perPage"
			]
		}
	}
}
```

Records read alphabetically by `name`, then by `recordType`, then by `value` — the order you
review a zone in — and arrive a page at a time. See [Pagination](/docs/api/pagination) for how
to walk the whole table.

### Query Parameters

| Parameter | Type    | Required | Description                               |
| --------- | ------- | -------- | ----------------------------------------- |
| `perPage` | integer | No       | Results per page, 1-200 (default: 50)     |
| `cursor`  | string  | No       | Page to fetch, taken from a `Link` header |

### cURL

```bash
curl -i "https://uptime.sergiodxa.com/api/v1/dns-monitors/dns_abc123/records?perPage=50" \
  -H "Authorization: Bearer uptime_your_api_key"
```

### Response

```json
{
	"data": {
		"records": [
			{
				"id": "dnsrec_abc123",
				"dnsMonitorId": "dns_abc123",
				"name": "example.com",
				"recordType": "A",
				"value": "192.0.2.1",
				"source": "resolver",
				"isEnabled": true,
				"status": "ok",
				"firstSeenAt": 1770000000000,
				"lastSeenAt": 1770086400000,
				"lastCheckedAt": 1770086400000,
				"createdAt": 1770000000000,
				"updatedAt": 1770086400000
			},
			{
				"id": "dnsrec_abc124",
				"dnsMonitorId": "dns_abc123",
				"name": "mail.example.com",
				"recordType": "MX",
				"value": "10 mx.example.com",
				"source": "zone_file",
				"isEnabled": false,
				"status": "missing",
				"firstSeenAt": 1770086400000,
				"lastSeenAt": null,
				"lastCheckedAt": 1770086400000,
				"createdAt": 1770086400000,
				"updatedAt": 1770086400000
			}
		]
	},
	"meta": {
		"requestId": "6b1f9d5e-4a2c-4f7e-9a10-2c8d5f3b7e41",
		"timestamp": "2026-08-11T10:30:00.000Z",
		"pagination": {
			"next": "eyJkIjoiYWZ0ZXIi",
			"prev": null,
			"perPage": 50,
			"total": 86
		}
	}
}
```

### Field Meanings

| Field           | Type                                                        | Description                                                                                                           |
| --------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `name`          | string                                                      | Absolute owner name, lowercased, no trailing dot. The apex is the monitor's `domain`.                                 |
| `recordType`    | `A` `AAAA` `CNAME` `MX` `TXT` `NS` `CAA` `TLSA` `DS` `SVCB` | The ten types checked.                                                                                                |
| `value`         | string                                                      | Normalized RDATA. Together with `name` and `recordType` it is the record's identity, and it is never client-writable. |
| `source`        | `resolver` `zone_file`                                      | How the record first entered the table.                                                                               |
| `isEnabled`     | boolean                                                     | Whether a deviation from this record alerts. The only writable field.                                                 |
| `status`        | `ok` `changed` `missing` `new` `error`                      | What the last check found. `new` and `missing` are states of the record, not of a check.                              |
| `lastSeenAt`    | integer \| null                                             | Last check at which this exact record resolved. `null` for a zone-file record that has never resolved.                |
| `lastCheckedAt` | integer \| null                                             | Last check that had an answer about this record. `null` until the first check.                                        |

Timestamps are epoch milliseconds.

### Ownership

A monitor belonging to another team returns `404`, not `403`: a `403` would confirm the id
names a real monitor somebody else owns.

## Update a DNS Monitor Record

Enables or declines one record. This is how a script decides what a monitor watches: an
API-created monitor imports and enables everything discovery found, because there is no
reviewer standing at the other end of an API call.

```
PATCH /api/v1/dns-monitors/:dnsMonitorId/records/:recordId
```

**Required scope:** `dns-monitors:write`

### Errors

| Status | Type | Description |
| --- | --- | --- |
| 400 | `validation-error` | The request failed validation |
| 401 | `unauthorized` | The API key is missing or invalid |
| 403 | `forbidden` | The API key lacks the required scope |
| 404 | `not-found` | The resource does not exist |

### Response Schema

```json
{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"type": "object",
	"properties": {
		"data": {
			"type": "object",
			"properties": {
				"record": {
					"$ref": "#/$defs/DnsMonitorRecord"
				}
			},
			"required": [
				"record"
			]
		},
		"meta": {
			"type": "object",
			"properties": {
				"requestId": {
					"type": "string",
					"format": "uuid"
				},
				"timestamp": {
					"type": "string",
					"format": "date-time"
				}
			},
			"required": [
				"requestId",
				"timestamp"
			]
		}
	},
	"required": [
		"data",
		"meta"
	],
	"$defs": {
		"DnsMonitorRecord": {
			"type": "object",
			"properties": {
				"id": {
					"type": "string",
					"pattern": "^dnsrec_[0-7][0-9a-hjkmnp-tv-z]{25}$"
				},
				"dnsMonitorId": {
					"type": "string",
					"pattern": "^dns_[0-7][0-9a-hjkmnp-tv-z]{25}$"
				},
				"name": {
					"type": "string"
				},
				"recordType": {
					"type": "string",
					"enum": [
						"A",
						"AAAA",
						"CNAME",
						"MX",
						"TXT",
						"NS",
						"CAA",
						"TLSA",
						"DS",
						"SVCB"
					]
				},
				"value": {
					"type": "string"
				},
				"source": {
					"type": "string",
					"enum": [
						"resolver",
						"zone_file"
					]
				},
				"isEnabled": {
					"type": "boolean",
					"description": "Whether a deviation from this record alerts"
				},
				"status": {
					"type": "string",
					"enum": [
						"ok",
						"changed",
						"missing",
						"new",
						"error"
					]
				},
				"firstSeenAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				},
				"lastSeenAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "Last check that resolved this record; null for zone-file-only records"
				},
				"lastCheckedAt": {
					"type": [
						"integer",
						"null"
					],
					"description": "Epoch milliseconds"
				},
				"createdAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				},
				"updatedAt": {
					"type": "integer",
					"description": "Epoch milliseconds"
				}
			},
			"required": [
				"id",
				"dnsMonitorId",
				"name",
				"recordType",
				"value",
				"source",
				"isEnabled",
				"status",
				"firstSeenAt",
				"lastSeenAt",
				"lastCheckedAt",
				"createdAt",
				"updatedAt"
			]
		}
	}
}
```

### Request Body

Send the body as `application/merge-patch+json` or `application/json`; both read the same.

```json
{
	"isEnabled": false
}
```

### Accepted Fields

| Field       | Type    | Required | Description                                       |
| ----------- | ------- | -------- | ------------------------------------------------- |
| `isEnabled` | boolean | Yes      | Whether a deviation from this record should alert |

**`isEnabled` is the only accepted field, and the request is rejected rather than filtered.**
Sending `name`, `recordType`, `value`, `status`, or any other key returns
a `400` `validation-error` problem with a message naming the key — for example `value: Unknown key`.

A record's identity is `(name, recordType, value)`, and it comes from DNS or from the zone
file you imported, never from a client. It is also the key the check diffs on, so accepting
an edit to it would silently retarget the expectation instead of changing it — and a request
that was quietly ignored is worse than one that was refused, because the caller goes on
believing the edit landed. Change your DNS, or re-import your zone file.

Omitting `isEnabled`, or sending it as `null`, is also a `400`: a request with nothing
writable in it expresses no decision, and answering `200` would claim one was made.

### cURL

```bash
curl -X PATCH "https://uptime.sergiodxa.com/api/v1/dns-monitors/dns_abc123/records/dnsrec_abc124" \
  -H "Authorization: Bearer uptime_your_api_key" \
  -H "Content-Type: application/merge-patch+json" \
  -d '{"isEnabled": true}'
```

### Response

```json
{
	"data": {
		"record": {
			"id": "dnsrec_abc124",
			"dnsMonitorId": "dns_abc123",
			"name": "mail.example.com",
			"recordType": "MX",
			"value": "10 mx.example.com",
			"source": "zone_file",
			"isEnabled": true,
			"status": "ok",
			"firstSeenAt": 1770086400000,
			"lastSeenAt": 1770086400000,
			"lastCheckedAt": 1770086400000,
			"createdAt": 1770086400000,
			"updatedAt": 1770090000000
		}
	},
	"meta": {
		"requestId": "6b1f9d5e-4a2c-4f7e-9a10-2c8d5f3b7e41",
		"timestamp": "2026-08-11T10:30:00.000Z"
	}
}
```

Enabling a record whose `status` is `new` also settles it to `ok`: it resolved when we found
it, and you have now said you want it watched, so leaving it `new` would keep it on the
"needs your attention" list forever. Every other status survives — enabling a zone-file
record that has never resolved leaves it `missing`, which is true and is presumably why you
enabled it. Declining a record never changes its `status`.

### Ownership

A record id belonging to a different monitor — including one on your own team — returns
`404`. Records are addressable only through the monitor that tracks them.
