---
title: "Sources · API v1"
description: "Who sends mail as your domains, and whether they should."
---
# Sources

Who sends mail as your domains, and whether they should.

## List sending sources

`GET /sources`

Sending IPs seen in reports, enriched with sender identification, reputation and DMARCguard's authorized-or-not verdict.

- Permission: `analytics:read`
- Key scope: `read`, `full`

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `domain` | `string` | Restrict to one monitored domain, matched exactly; a subdomain is a separate domain. Omit for the whole organization. |
| `search` | `string` | Substring match on IP, domain or sender name. Longer terms are truncated. |
| `reputation` | `string` | Keep only sources with this abuse status. An unrecognised value is ignored rather than rejected. One of "clean", "suspicious", "malicious", "unknown". |
| `has_failures` | `boolean` | true keeps only sources with SPF or DKIM failures. |
| `sort_by` | `string` | Sort column. Default "total_messages". One of "total_messages", "source_ip", "first_seen", "last_seen", "compliance", "auth_failures", "domains". |
| `sort_dir` | `string` | Sort direction. Default "desc". One of "asc", "desc". |
| `limit` | `integer` | Page size. Values above the maximum are clamped; zero, negative or non-numeric values are ignored and the default applies. Default 50. |
| `offset` | `integer` | Number of rows to skip; combine with limit to page. A negative or non-numeric value is treated as 0. Default 0. |

### Example request

```bash
curl "https://api.dmarcguard.io/api/v1/sources?domain=example.com" \
  -H "Authorization: Bearer dg_your_key"
```

### Response

| Field | Type | Notes |
| --- | --- | --- |
| `sources` | `object[]` | The requested page. Never null; an empty page is []. |
| `sources[].source_ip` | `string` | The sending IP, exactly as the reports carry it. |
| `sources[].total_messages` | `integer` | Messages this IP sent as your domains. |
| `sources[].pass_count` | `integer` | Of those, how many passed DMARC. |
| `sources[].fail_count` | `integer` | Of those, how many failed DMARC. |
| `sources[].spf_fail_count` | `integer` | Messages whose SPF did not produce an aligned pass. A source can pass DMARC on DKIM alone, so this can be non-zero while fail_count is 0. |
| `sources[].dkim_fail_count` | `integer` | Messages whose DKIM did not produce an aligned pass. A source can pass DMARC on SPF alone, so this can be non-zero while fail_count is 0. |
| `sources[].compliance_rate` | `number` | Percentage from 0 to 100 of this source's messages that passed DMARC. |
| `sources[].first_seen` | `string` (nullable) | RFC 3339 UTC instant with whole seconds, e.g. 2026-09-15T16:03:43Z; null when absent. |
| `sources[].last_seen` | `string` (nullable) | RFC 3339 UTC instant with whole seconds, e.g. 2026-09-15T16:03:43Z; null when absent. |
| `sources[].domains` | `string` | The domains this IP sent as, comma-separated. The single-source endpoint returns the same list as an array. |
| `sources[].service_name` | `string` (optional) | The sending service the IP was identified as, when it is recognised. |
| `sources[].service_icon` | `string` (optional) | Icon slug for the identified service. |
| `sources[].service_slug` | `string` (optional) | Stable identifier of the identified service. |
| `sources[].abuse_status` | `string` (optional) | Abuse standing of the IP: clean, suspicious or malicious. Absent when the IP has no reputation record at all, and unknown when it has one but was never scored. |
| `sources[].abuse_score` | `integer` (optional) | Abuse confidence for the IP, 0 to 100; higher is worse. Negative when the IP has a reputation record but was never scored. Omitted when it is zero, which is a clean IP. |
| `sources[].reputation_reason` | `string` (optional) | A ready-to-show sentence explaining abuse_status. |
| `sources[].blocklist_severity` | `string` | Blocklist standing: unchecked, clean, degraded when the evidence was too thin to call it clean, advisory for non-mail lists, and warning or critical for a live deliverability problem. One of "unchecked", "clean", "degraded", "advisory", "warning", "critical". |
| `sources[].blocklist_summary` | `string` (optional) | A ready-to-show sentence stating the blocklist standing. |
| `sources[].isp` | `string` (optional) | Network operator the IP belongs to, when known. |
| `sources[].country` | `string` (optional) | Two-letter country code for the IP, when known. |
| `sources[].hostname` | `string` (optional) | Reverse-DNS name of the IP, when it has one. |
| `sources[].classification` | `string` | Whether this source's mail should worry you: authenticated when SPF or DKIM passed, likely_forwarded when a failure looks like ordinary relaying, suspicious when a failing source also carries reputation red flags, and needs_review when the signals decide neither. |
| `total` | `integer` | Sources matching the filter, before paging. |
| `limit` | `integer` | The page size actually applied. |
| `offset` | `integer` | The offset actually applied. |
| `has_any` | `boolean` | True when the organization holds any report at all, ignoring filters; use it for empty states. |

### Errors

| Status | When |
| --- | --- |
| `400` | The domain query parameter is not a valid domain name. |
| `401` | Missing, malformed, revoked, expired or not-entitled API key. |
| `403` | The key's scope or its owner's role does not allow this operation. |
| `429` | Hourly quota or an abuse limit exceeded; the body's code says which. Honour Retry-After. |
| `500` | Unexpected server error; retry with backoff. |
| `503` | A transient failure resolving the organization or reading storage; retry after a short pause. |

Plus the errors every request can return: https://dmarcguard.io/docs/api/v1/#errors

## Get one sending source

`GET /sources/{ip}`

Everything known about one sending IP: per-domain records, timeline, geo, blocklist verdict and classification.

- Permission: `analytics:read`
- Key scope: `read`, `full`

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `ip` (required) | `string` | IPv4 or IPv6 address exactly as reported. |

### Example request

```bash
curl "https://api.dmarcguard.io/api/v1/sources/203.0.113.10" \
  -H "Authorization: Bearer dg_your_key"
```

### Response

| Field | Type | Notes |
| --- | --- | --- |
| `source_ip` | `string` | The sending IP, exactly as the reports carry it. |
| `total_messages` | `integer` | Messages this IP sent as your domains. |
| `pass_count` | `integer` | Of those, how many passed DMARC. |
| `fail_count` | `integer` | Of those, how many failed DMARC. |
| `spf_fail_count` | `integer` | Messages whose SPF did not produce an aligned pass. A source can pass DMARC on DKIM alone, so this can be non-zero while fail_count is 0. |
| `dkim_fail_count` | `integer` | Messages whose DKIM did not produce an aligned pass. A source can pass DMARC on SPF alone, so this can be non-zero while fail_count is 0. |
| `compliance_rate` | `number` | Percentage from 0 to 100 of this source's messages that passed DMARC. |
| `first_seen` | `string` (nullable) | RFC 3339 UTC instant with whole seconds, e.g. 2026-09-15T16:03:43Z; null when absent. |
| `last_seen` | `string` (nullable) | RFC 3339 UTC instant with whole seconds, e.g. 2026-09-15T16:03:43Z; null when absent. |
| `domains` | `string[]` | The domains this IP sent as. The list endpoint returns the same names as one comma-separated string. |
| `records` | `object[]` | The most recent report rows this IP appears in, one per reporting period and domain. |
| `records[].report_id` | `string` | The report this row came from; fetch it to see the row in context. |
| `records[].domain` | `string` | The domain the mail claimed to be from. |
| `records[].count` | `integer` | Messages this row accounts for. |
| `records[].disposition` | `string` | What the receiver did with them: none, quarantine or reject. |
| `records[].dkim_result` | `string` | Whether DKIM produced an aligned pass for DMARC: pass or fail. |
| `records[].spf_result` | `string` | Whether SPF produced an aligned pass for DMARC: pass or fail. |
| `records[].header_from` | `string` | The From domain the recipient saw. |
| `records[].org_name` | `string` | The organization that sent the report this row came from. |
| `records[].date_begin` | `string` (nullable) | RFC 3339 UTC instant with whole seconds, e.g. 2026-09-15T16:03:43Z; null when absent. |
| `records[].date_end` | `string` (nullable) | RFC 3339 UTC instant with whole seconds, e.g. 2026-09-15T16:03:43Z; null when absent. |
| `records_total` | `integer` | All-time report rows for this IP; larger than the records array, which carries only the recent ones. |
| `timeline` | `object[]` | One bucket per calendar day, oldest first. |
| `timeline[].total_messages` | `integer` | Messages reported for the day. |
| `timeline[].compliant_messages` | `integer` | Of those, how many passed DMARC. |
| `timeline[].compliance_rate` | `number` | Percentage from 0 to 100 of the day's messages that passed DMARC. |
| `timeline[].failed_messages` | `integer` | Of the day's messages, how many failed DMARC. |
| `timeline[].reports` | `integer` | Reports covering the day. |
| `timeline[].date` | `string` (nullable) | Calendar day, YYYY-MM-DD (UTC); null when absent. |
| `service_name` | `string` (optional) | The sending service the IP was identified as, when it is recognised. |
| `service_icon` | `string` (optional) | Icon slug for the identified service. |
| `service_slug` | `string` (optional) | Stable identifier of the identified service. |
| `hostname` | `string` (optional) | Reverse-DNS name of the IP, when it has one. |
| `geo` | `object` (nullable, optional) | Where the IP geolocates to; absent when it cannot be placed. |
| `geo.country` | `string` | Two-letter country code for the IP. |
| `geo.city` | `string` | City the IP geolocates to. |
| `geo.latitude` | `number` | Latitude of the geolocated IP. |
| `geo.longitude` | `number` | Longitude of the geolocated IP. |
| `enrichment` | `object` (nullable, optional) | Everything the reputation lookup knows about the IP; absent until it has been looked up. |
| `enrichment.ip` | `string` | The IP this record is about. |
| `enrichment.country` | `string` (optional) | Two-letter country code the IP geolocates to. |
| `enrichment.city` | `string` (optional) | City the IP geolocates to. |
| `enrichment.latitude` | `number` (optional) | Latitude of the geolocated IP. |
| `enrichment.longitude` | `number` (optional) | Longitude of the geolocated IP. |
| `enrichment.isp` | `string` (optional) | Network operator the IP belongs to. |
| `enrichment.usage_type` | `string` (optional) | What the address range is used for, such as a data centre or a residential network. |
| `enrichment.country_code` | `string` (optional) | Two-letter country code as the reputation lookup reported it. |
| `enrichment.abuse_score` | `integer` | Abuse confidence for the IP, 0 to 100; higher is worse. Negative when the IP was never scored. |
| `enrichment.abuse_reports` | `integer` | How many abuse reports the IP has attracted. |
| `enrichment.abuse_categories` | `string[]` (nullable, optional) | What the IP was reported for, such as port scanning or spam. |
| `enrichment.abuse_status` | `string` | Abuse standing of the IP: clean, suspicious or malicious, and unknown when this record exists but the IP was never scored. The enrichment block itself is absent for an IP with no record at all. |
| `enrichment.sender_name` | `string` (optional) | The sending service the IP was identified as, when it is recognised. |
| `enrichment.sender_slug` | `string` (optional) | Stable identifier of the identified service. |
| `enrichment.sender_icon` | `string` (optional) | Icon slug for the identified service. |
| `enrichment.hostname` | `string` (optional) | Reverse-DNS name of the IP, when it has one. |
| `enrichment.blocklist_listed` | `integer` | Lists holding the IP. Read blocklist_verdict instead; that is where the standing is interpreted. |
| `enrichment.blocklist_total` | `integer` | Lists consulted. Read blocklist_verdict instead; it separates the lists that answered from those that did not. |
| `enrichment.blocklist_results` | `object[]` (nullable, optional) | One result per blocklist consulted, each with a delisting link when there is one. |
| `enrichment.blocklist_results[].name` | `string` | The blocklist this result came from. |
| `enrichment.blocklist_results[].listed` | `boolean` | Whether the list holds the IP. Prefer status, which also distinguishes a list that could not be reached. |
| `enrichment.blocklist_results[].status` | `string` (optional) | What this list said: listed, clean, or error when it could not be reached. |
| `enrichment.blocklist_results[].reason` | `string` (optional) | Why the list holds the IP, in its own words. |
| `enrichment.blocklist_results[].category` | `string` (optional) | What kind of abuse the list tracks, such as spam or malware. |
| `enrichment.blocklist_results[].details` | `string` (optional) | The evidence behind the listing, such as the covering address range or the list's response code. |
| `enrichment.blocklist_results[].tier` | `integer` (optional) | How much this list counts, as a rank: 0 unrecognised, 1 advisory and not about mail, 2 an ordinary mail blocklist, 3 one mailbox providers widely enforce, 4 hijacked address space or malware infrastructure. |
| `enrichment.blocklist_results[].delist_url` | `string` (optional) | The list operator's self-service delisting page, present only when the list is holding the IP and publishes one. |
| `enrichment.abuse_last_reported` | `string` (nullable, optional) | RFC 3339 UTC instant with whole seconds, e.g. 2026-09-15T16:03:43Z; null when absent. |
| `enrichment.enriched_at` | `string` (nullable) | RFC 3339 UTC instant with whole seconds, e.g. 2026-09-15T16:03:43Z; null when absent. |
| `enrichment.updated_at` | `string` (nullable) | RFC 3339 UTC instant with whole seconds, e.g. 2026-09-15T16:03:43Z; null when absent. |
| `blocklist_verdict` | `object` | The blocklist standing of this IP, already interpreted: severity, weighted score and a sentence you can show. |
| `blocklist_verdict.severity` | `string` | Blocklist standing, listed best to worst: unchecked and clean need nothing, degraded means the evidence was too thin to call it clean, advisory covers non-mail lists, and warning or critical is a live deliverability problem. One of "unchecked", "clean", "degraded", "advisory", "warning", "critical". |
| `blocklist_verdict.listed` | `integer` | Blocklists that answered and reported the IP as listed. |
| `blocklist_verdict.clean` | `integer` | Blocklists that answered and did not list the IP. |
| `blocklist_verdict.errored` | `integer` | Blocklists that refused or failed to answer, so they prove nothing either way. |
| `blocklist_verdict.checked` | `integer` | Blocklists that actually answered, listed plus clean. This is the denominator to quote, not total. |
| `blocklist_verdict.total` | `integer` | Blocklists consulted, including any that did not answer. |
| `blocklist_verdict.score` | `integer` | Weighted seriousness of the listings, 0 to 100. A list mailbox providers enforce counts for far more than an advisory one. |
| `blocklist_verdict.names` | `string[]` (nullable, optional) | The lists the IP is on, most consequential first. |
| `blocklist_verdict.top_list` | `string` (optional) | The most consequential list the IP is on. |
| `blocklist_verdict.top_reason` | `string` (optional) | Why that list holds the IP. |
| `blocklist_verdict.top_tier` | `integer` (optional) | How much that list counts, as a rank: 0 unrecognised, 1 advisory and not about mail, 2 an ordinary mail blocklist, 3 one mailbox providers widely enforce, 4 hijacked address space or malware infrastructure. |
| `blocklist_verdict.top_errored` | `string` (optional) | The most consequential list that did not answer. It names which evidence is missing when the standing is degraded. |
| `blocklist_verdict.has_malware_c2` | `boolean` | True when a listing is for malware infrastructure rather than spam, which points at an active compromise. |
| `blocklist_verdict.urgency` | `string` | How soon to act: none, monitor, soon or now. |
| `blocklist_verdict.summary` | `string` | A ready-to-show sentence stating the blocklist standing. |
| `blocklist_verdict.scope` | `string` (optional) | Set to ingest when this verdict consulted the smaller set of lists checked as reports arrive, so its counts cover fewer lists. Absent for a full check. |
| `spam_signals` | `object[]` (nullable, optional) | Spam-filter signals observed for this IP, when any were recorded. |
| `spam_signals[].name` | `string` | Identifier of the spam-filter rule that fired for this IP. |
| `spam_signals[].score` | `number` | How much the rule moved the spam score; a positive value counts against the sender. |
| `spam_signals[].description` | `string` (optional) | What the rule checks, in words. |
| `classification` | `string` | Whether this source's mail should worry you: authenticated when SPF or DKIM passed, likely_forwarded when a failure looks like ordinary relaying, suspicious when a failing source also carries reputation red flags, and needs_review when the signals decide neither. |
| `classification_description` | `string` (optional) | A ready-to-show sentence explaining the classification. Absent for an authenticated source, which needs none. |

### Errors

| Status | When |
| --- | --- |
| `400` | The path segment is not a valid IP address. |
| `401` | Missing, malformed, revoked, expired or not-entitled API key. |
| `403` | The key's scope or its owner's role does not allow this operation. |
| `404` | No report in this organization mentions that IP. |
| `429` | Hourly quota or an abuse limit exceeded; the body's code says which. Honour Retry-After. |
| `500` | Unexpected server error; retry with backoff. |
| `503` | A transient failure resolving the organization or reading storage; retry after a short pause. |

Plus the errors every request can return: https://dmarcguard.io/docs/api/v1/#errors