> ## Documentation Index
> Fetch the complete documentation index at: https://docs.adcontextprotocol.org/llms.txt
> Use this file to discover all available pages before exploring further.

# validate_property_delivery

> validate_property_delivery checks delivery records against a property list for compliance and supply path authorization in AdCP.

# validate\_property\_delivery

Validates delivery records against a property list to determine compliance. Answers two questions:

1. **Property compliance**: Did my impressions land on properties in my list?
2. **Supply path authorization**: Was the sales agent authorized to sell that inventory?

## Use Cases

* **Post-campaign validation**: Verify that impressions were delivered to compliant properties
* **Supply path verification**: Confirm sales agents were authorized by publishers
* **Real-time monitoring**: Check compliance rate during campaign execution
* **Audit trails**: Generate compliance reports for regulatory or brand safety reviews

## Request

```json theme={null}
{
  "$schema": "/schemas/3.2.0-rc.7/property/validate-property-delivery-request.json",
  "list_id": "pl_abc123",
  "records": [
    {
      "identifier": { "type": "domain", "value": "www.nytimes.com" },
      "impressions": 103
    },
    {
      "identifier": { "type": "domain", "value": "sketchy-site.example" },
      "impressions": 47
    },
    {
      "identifier": { "type": "android_package", "value": "com.unknown.app" },
      "impressions": 25
    }
  ],
  "include_compliant": false
}
```

### Parameters

| Parameter | Type | Required | Description |
| - | - | - | - |
| `list_id` | string | Yes | ID of the property list to validate against |
| `records` | array | Yes | Delivery records to validate (1-10,000 records) |
| `records[].identifier` | object | Yes | Property identifier (`type` and `value`) |
| `records[].impressions` | integer | Yes | Number of impressions delivered |
| `records[].record_id` | string | No | Client-provided ID for correlation |
| `records[].sales_agent_url` | string | No | Sales agent URL to validate authorization against adagents.json |
| `include_compliant` | boolean | No | Include compliant records in results (default: false) |

## Response

```json theme={null}
{
  "$schema": "/schemas/3.2.0-rc.7/property/validate-property-delivery-response.json",
  "status": "completed",
  "list_id": "pl_abc123",
  "summary": {
    "total_records": 4,
    "total_impressions": 200,
    "compliant_records": 1,
    "compliant_impressions": 103,
    "non_compliant_records": 1,
    "non_compliant_impressions": 47,
    "not_covered_records": 1,
    "not_covered_impressions": 25,
    "unidentified_records": 1,
    "unidentified_impressions": 25
  },
  "aggregate": {
    "score": 68.7,
    "grade": "C+",
    "label": "68.7% compliant",
    "methodology_url": "https://governance.example.com/methodology/compliance-scoring"
  },
  "results": [
    {
      "identifier": { "type": "domain", "value": "sketchy-site.example" },
      "status": "non_compliant",
      "impressions": 47,
      "features": [
        {
          "feature_id": "record:list_membership",
          "status": "failed",
          "explanation": "Identifier not found in resolved property list"
        }
      ]
    },
    {
      "identifier": { "type": "domain", "value": "new-site.example" },
      "status": "not_covered",
      "impressions": 25
    },
    {
      "identifier": { "type": "android_package", "value": "com.unknown.app" },
      "status": "unidentified",
      "impressions": 25
    }
  ],
  "validated_at": "2026-01-04T19:00:00Z",
  "list_resolved_at": "2026-01-04T12:00:00Z"
}
```

### Response Fields

| Field | Type | Description |
| - | - | - |
| `list_id` | string | ID of the property list validated against |
| `summary` | object | Raw counts for property compliance validation |
| `aggregate` | object | Optional computed metrics from the governance agent |
| `results` | array | Per-record validation results |
| `validated_at` | datetime | When validation was performed |
| `list_resolved_at` | datetime | Resolution timestamp of the property list used |

### Summary Fields

The summary provides raw counts - consumers calculate rates as needed:

| Field | Description |
| - | - |
| `total_records` | Total records validated |
| `total_impressions` | Total impressions across all records |
| `compliant_records` / `compliant_impressions` | Records/impressions in the property list |
| `non_compliant_records` / `non_compliant_impressions` | Records/impressions NOT in the property list |
| `not_covered_records` / `not_covered_impressions` | Identifier recognized but no data available |
| `unidentified_records` / `unidentified_impressions` | Identifier type not resolvable |

### Validation Statuses

| Status | Meaning |
| - | - |
| `compliant` | Identifier is in the resolved property list |
| `non_compliant` | Identifier is NOT in the resolved property list |
| `not_covered` | Identifier recognized but governance agent has no data for it |
| `unidentified` | Identifier type not resolvable by this agent |

## Understanding not\_covered vs unidentified

These two statuses distinguish different types of "unknown" scenarios:

**`not_covered`** - The governance agent recognized the identifier (e.g., it's a valid domain) but doesn't have data for that specific property. This happens when:

* A property is too new to be in the agent's database
* The property exists but hasn't been evaluated yet
* The agent's coverage doesn't include that property category

**`unidentified`** - The governance agent couldn't recognize the identifier at all. This happens when:

* Client-side detection failed to capture the property
* The identifier type isn't supported (e.g., agent handles domains but received an app ID)
* The identifier value is malformed or invalid

Both statuses should be excluded from compliance rate calculations - you cannot penalize for detection or coverage gaps.

## Optional Aggregate Metrics

Governance agents can optionally return computed metrics in the `aggregate` field:

```json theme={null}
"aggregate": {
  "score": 68.7,
  "grade": "C+",
  "label": "68.7% compliant",
  "methodology_url": "https://governance.example.com/methodology"
}
```

| Field | Description |
| - | - |
| `score` | Numeric score (scale is agent-defined, typically 0-100) |
| `grade` | Letter grade or category (e.g., "A+", "B-", "Gold") |
| `label` | Human-readable summary (e.g., "85% compliant") |
| `methodology_url` | URL explaining how the aggregate was calculated |

The `aggregate` field is optional and agent-specific. Consumers should not assume a particular format - always check `methodology_url` for interpretation.

## Calculating Your Own Rates

The response always includes raw counts. Calculate rates as needed:

```python theme={null}
# Compliance rate (exclude unverifiable from denominator)
unverifiable = summary.not_covered_impressions + summary.unidentified_impressions
verifiable = summary.total_impressions - unverifiable
compliance_rate = summary.compliant_impressions / verifiable if verifiable > 0 else None
```

In the example above:

* Compliant impressions: 103
* Non-compliant impressions: 47
* not\_covered + unidentified impressions: 50 (excluded)
* Compliance rate: 103 / (200 - 50) = 103 / 150 = 68.7%

## Feature Results

Every entry in `features[]` carries a `feature_id` and a `status`. Data features come from the governance agent's feature catalog (discovered via [`get_adcp_capabilities`](/dist/docs/3.2.0-rc.7/protocol/get_adcp_capabilities)). Record-level structural checks use reserved namespaces so they sit in the same identity space as data features.

### Reserved feature\_id prefixes

| Prefix | Scope | Canonical feature\_ids |
| - | - | - |
| `record:` | Record-level structural checks | `record:list_membership`, `record:excluded`, `record:country_mismatch`, `record:channel_mismatch` |
| `delivery:` | Delivery-path checks | `delivery:seller_authorization`, `delivery:click_url_presence` |

Governance agents MAY add new checks within these namespaces and publish them in their feature catalog.

### Feature-Level Failures

When a property fails a specific feature requirement, the entry references the feature by id. The response carries the verdict and a directional explanation. When the caller authored the requirement (e.g., `feature_requirements` on a property list), the evaluator MAY echo the `requirement` back to enable fix-and-retry loops without re-reading the list definition.

```json theme={null}
{
  "identifier": { "type": "domain", "value": "low-quality-site.example" },
  "status": "non_compliant",
  "impressions": 150,
  "features": [
    {
      "feature_id": "mfa_score",
      "status": "failed",
      "explanation": "Property below MFA score requirement",
      "requirement": { "min_value": 85 }
    }
  ]
}
```

Oracle pattern: the response tells you **what** failed (feature\_id) and **where to find the rule** (policy\_id, optional). Evaluator internals (confidence thresholds, inference logic) stay hidden.

## Supply Path Authorization

When `sales_agent_url` is provided in delivery records, the governance agent validates that the sales agent is authorized to sell the property by checking the publisher's `adagents.json`.

### Request with Authorization

```json theme={null}
{
  "list_id": "pl_abc123",
  "records": [
    {
      "identifier": { "type": "domain", "value": "www.nytimes.com" },
      "impressions": 103,
      "sales_agent_url": "https://legitimate-ssp.example.com"
    },
    {
      "identifier": { "type": "domain", "value": "www.nytimes.com" },
      "impressions": 50,
      "sales_agent_url": "https://unauthorized-reseller.example.com"
    }
  ]
}
```

### Response with Authorization

When authorization is validated, each result includes an `authorization` field:

```json theme={null}
{
  "list_id": "pl_abc123",
  "summary": {
    "total_records": 2,
    "total_impressions": 153,
    "compliant_records": 2,
    "compliant_impressions": 153,
    "non_compliant_records": 0,
    "non_compliant_impressions": 0,
    "unknown_records": 0,
    "unknown_impressions": 0
  },
  "authorization_summary": {
    "records_checked": 2,
    "impressions_checked": 153,
    "authorized_records": 1,
    "authorized_impressions": 103,
    "unauthorized_records": 1,
    "unauthorized_impressions": 50,
    "unknown_records": 0,
    "unknown_impressions": 0
  },
  "results": [
    {
      "identifier": { "type": "domain", "value": "www.nytimes.com" },
      "status": "compliant",
      "impressions": 50,
      "authorization": {
        "status": "unauthorized",
        "publisher_domain": "nytimes.com",
        "sales_agent_url": "https://unauthorized-reseller.example.com",
        "violation": {
          "code": "agent_not_authorized",
          "message": "Sales agent not listed in nytimes.com/.well-known/adagents.json"
        }
      }
    }
  ],
  "validated_at": "2026-01-04T19:00:00Z"
}
```

### Authorization Statuses

| Status | Meaning | Included in authorization\_rate? |
| - | - | - |
| `authorized` | Sales agent is listed in publisher's adagents.json | Yes (numerator) |
| `unauthorized` | Sales agent is NOT listed in publisher's adagents.json | Yes (denominator only) |
| `unknown` | Could not fetch or parse adagents.json | No (excluded) |

### Authorization Violation Codes

| Code | Description |
| - | - |
| `agent_not_authorized` | Sales agent URL not found in publisher's authorized\_agents list |
| `adagents_not_found` | Publisher's adagents.json could not be fetched (404, timeout, etc.) |
| `adagents_invalid` | Publisher's adagents.json exists but is malformed |
| `property_not_declared` | Property identifier not declared in publisher's adagents.json |

### Two Independent Checks

Property compliance and authorization are **independent checks**. A record can be:

| Property Status | Authorization Status | Meaning |
| - | - | - |
| compliant | authorized | Fully valid - property in list, sold by authorized agent |
| compliant | unauthorized | Property is approved but sold by unauthorized reseller |
| non\_compliant | authorized | Authorized agent sold property outside your list |
| non\_compliant | unauthorized | Neither property nor agent validated |

Both checks use the same "unknown excludes from rate" pattern - you cannot penalize for detection gaps.

## Best Practices

### Batch Validation

For large-scale validation, batch records up to the 10,000 limit:

```python theme={null}
def validate_delivery_batch(records, list_id, governance_agent):
    """Validate delivery records in batches."""
    batch_size = 10000
    all_results = []

    for i in range(0, len(records), batch_size):
        batch = records[i:i + batch_size]
        response = governance_agent.validate_property_delivery(
            list_id=list_id,
            records=batch
        )
        all_results.extend(response.results)

    return all_results
```

### Sampling Strategy

For real-time monitoring during campaign execution, validate a statistical sample rather than all records:

```python theme={null}
import random

def sample_and_validate(records, sample_size=1000):
    """Validate a random sample for real-time monitoring."""
    sample = random.sample(records, min(sample_size, len(records)))
    return governance_agent.validate_property_delivery(
        list_id=list_id,
        records=sample
    )
```

### Handling Unknown Records

Track unknown rates separately to identify detection gaps:

```python theme={null}
def analyze_validation(response):
    """Analyze validation results with unknown handling."""
    summary = response.summary

    # Core compliance metric
    compliance_rate = summary.compliance_rate

    # Detection quality metric
    unknown_rate = summary.unknown_impressions / summary.total_impressions

    if unknown_rate > 0.1:
        print(f"Warning: {unknown_rate:.1%} of impressions unresolvable")

    return {
        "compliance_rate": compliance_rate,
        "unknown_rate": unknown_rate,
        "non_compliant_impressions": summary.non_compliant_impressions
    }
```

## Related Tasks

* [create\_property\_list](./property_lists#create_property_list) - Create the list to validate against
* [get\_property\_list](./property_lists#get_property_list) - Retrieve current list membership
* [get\_adcp\_capabilities](/dist/docs/3.2.0-rc.7/protocol/get_adcp_capabilities) - Discover available filter features
