# Connection Diagnostics API Reference

Every report made under your brand is available through the Netavo public API: list them, fetch one as JSON or as your branded PDF, and erase one. Webhooks tell your systems when a report arrives and when it finds a problem.

## Quick Start

The API is at `https://partners.netavo.com/api/v1` and returns JSON. Create an API token in Beacon under **API Tokens** with the `diagnostics.reports:read` scope, then list the latest reports:

```
export NETAVO_API_TOKEN="your token"
curl "https://partners.netavo.com/api/v1/diagnostics/reports?limit=5" \
  -H "Authorization: Bearer $NETAVO_API_TOKEN"
```

The API covers the reports of the Partner Branded options. The free Netavo-branded check has no API: its reports are opened by reference at check.netavo.com.

## Authentication

Send the token as a bearer token on every request. A token belongs to your organisation, not to the person who created it, and carries only the scopes chosen for it.

| Scope | Allows |
| --- | --- |
| `diagnostics.reports:read` | List reports, and fetch one as JSON or PDF |
| `diagnostics.reports:write` | Erase a report |
| `platform.webhooks:manage` | Create and change webhooks |
| `platform.events:read` | Read the event log, to catch up on missed webhooks |

A missing, expired or revoked token gets `401`. A valid token without the scope an endpoint needs gets `403` with the code `insufficient-scope`. Tokens are secrets: keep them on your servers.

## Endpoints

A report is identified by its reference, such as `EF-4M8T-2HVQ`, in any case and with or without the dashes. Partners see the reports of their customers’ brands too.

| Request | Returns | Scope |
| --- | --- | --- |
| `GET /diagnostics/reports` | A page of report summaries, newest first | `diagnostics.reports:read` |
| `GET /diagnostics/reports/{reference}` | The full report as JSON | `diagnostics.reports:read` |
| `GET /diagnostics/reports/{reference}/pdf` | The report as your branded PDF | `diagnostics.reports:read` |
| `DELETE /diagnostics/reports/{reference}` | `204`; the report is erased | `diagnostics.reports:write` |

## List Reports

`GET /diagnostics/reports` returns every report still within your retention period, newest first. The filters combine, and each only narrows the list.

| Query parameter | Meaning |
| --- | --- |
| `limit` | Reports per page, 1 to 100; 25 if left out |
| `startingAfter` | The previous page’s `nextCursor`, for the next page |
| `receivedFrom` | Only reports received at or after this time (UTC) |
| `receivedTo` | Only reports received before this time (UTC) |
| `severity` | Only reports whose worst finding is `ok`, `info`, `warn` or `problem` |
| `collector` | Only reports from `web`, `windows`, `macos` or `other` |
| `ip` | Only reports whose public IPv4 or IPv6 address contains this text |
| `search` | Only reports whose reference, or the name, company, ticket or description entered, contains this text |

```
curl "https://partners.netavo.com/api/v1/diagnostics/reports?severity=problem&receivedFrom=2026-11-01T00:00:00Z" \
  -H "Authorization: Bearer $NETAVO_API_TOKEN"
```

```
{
  "data": [
    {
      "reference": "EF-9C2D-7KLP",
      "tenant": "examplefibre",
      "createdAt": "2026-11-06T10:15:30Z",
      "receivedAt": "2026-11-06T10:15:32Z",
      "expiresAt": "2027-02-04T10:15:32Z",
      "collector": "windows",
      "collectorVersion": "1.1.0",
      "publicIpv4": "198.51.100.23",
      "publicIpv6": "2001:db8:77:4::10",
      "asn": 64501,
      "network": "AS64501 EXAMPLEFIBRE - Example Fibre Ltd",
      "context": {
        "name": "Sam Example",
        "company": "Example Dental Ltd",
        "providerTicket": "CASE-1042",
        "description": "Calls drop after 30 seconds"
      },
      "worstSeverity": "problem",
      "worstFinding": "Your router's SIP ALG is rewriting SIP messages",
      "problemCount": 1,
      "warningCount": 0,
      "infoCount": 2,
      "demo": false,
      "schemaVersion": 2,
      "attachedToTicket": false,
      "link": "https://partners.netavo.com/api/v1/diagnostics/reports/EF-9C2D-7KLP"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

The `context` fields are what the person who ran the check typed in, so treat them as untrusted text. When `hasMore` is `true`, pass `nextCursor` back as `startingAfter` for the next page.

## Get a Report

`GET /diagnostics/reports/{reference}` returns the report exactly as it was made, in the Connection Check report format: snake_case fields, with a `schema_version`. Everything in `client` and `context` was reported by the device that ran the check; `server` and `findings` are the test servers’ own observations. Ignore fields you do not recognise: new versions only add them.

```
curl "https://partners.netavo.com/api/v1/diagnostics/reports/EF-4M8T-2HVQ" \
  -H "Authorization: Bearer $NETAVO_API_TOKEN"
```

Trimmed here to a few fields of each section:

```
{
  "schema_version": 2,
  "reference": "EF-4M8T-2HVQ",
  "created_utc": "2026-11-06T09:41:27Z",
  "expires_utc": "2026-11-13T09:41:27Z",
  "tenant": { "id": "examplefibre", "name": "Example Fibre", "demo": false, "visibility": "staff" },
  "collector": { "kind": "macos", "version": "1.1.0", "os": "macOS 15.6" },
  "context": { "name": "Alex Sample", "company": "Sample Accountants LLP", "provider_ticket": "EF-CASE-2207" },
  "server": {
    "addresses": [
      { "address": "198.51.100.77", "ip_version": 4, "asn": 64501, "holder": "EXAMPLEFIBRE - Example Fibre Ltd" }
    ],
    "dns": { "resolvers": [ { "address": "198.51.100.2", "ip_version": 4, "asn": 64501 } ] }
  },
  "client": {
    "ipv4_reachable": true,
    "ipv6_reachable": true,
    "dnssec_validating": true,
    "latency": [ { "target": "check.netavo.com", "rtt_avg_ms": 7.0, "jitter_ms": 0.8, "sent": 250, "received": 250 } ],
    "wifi": { "connected": true, "band": "5 GHz", "rssi_dbm": -51 }
  },
  "findings": [
    {
      "code": "sip-ok",
      "severity": "ok",
      "area": "lan",
      "title": "SIP passes through unaltered",
      "detail": null,
      "observed_by": "server",
      "values": {}
    }
  ]
}
```

Each finding has a stable `code` to branch on, a `severity` (`ok`, `info`, `warn` or `problem`), and a `title` and `detail` written for people, in your own wording where you have set it.

## Get a Report as PDF

`GET /diagnostics/reports/{reference}/pdf` returns the report as your branded PDF (`application/pdf`), the same document your staff download in Beacon.

```
curl "https://partners.netavo.com/api/v1/diagnostics/reports/EF-4M8T-2HVQ/pdf" \
  -H "Authorization: Bearer $NETAVO_API_TOKEN" \
  -o EF-4M8T-2HVQ.pdf
```

See the [example partner-branded PDF](https://netavo.com/downloads/connection-diagnostics/example-report-example-fibre.pdf).

## Erase a Report

`DELETE /diagnostics/reports/{reference}` erases the report from your account now, ahead of your retention period, and answers `204`. Copies already attached to support tickets belong to those tickets and are not removed. It needs `diagnostics.reports:write`.

```
curl -X DELETE "https://partners.netavo.com/api/v1/diagnostics/reports/EF-4M8T-2HVQ" \
  -H "Authorization: Bearer $NETAVO_API_TOKEN"
```

## Errors

Errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem documents, served as `application/problem+json`. Branch on `code`, which never changes; `title` and `detail` are written for people. A report outside your organisation is `404`, exactly like one that does not exist.

```
{
  "type": "https://partners.netavo.com/api/problems/not-found",
  "title": "Not found",
  "status": 404,
  "detail": "No report with that reference.",
  "instance": "/api/v1/diagnostics/reports/EF-0000-0000",
  "code": "not-found",
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
```

| Status | Code | When |
| --- | --- | --- |
| 400 | `validation-failed` | A filter or paging value is invalid; `errors` names the field |
| 401 | `unauthorized` | The token is missing, expired or revoked |
| 403 | `insufficient-scope` | The token lacks the endpoint’s scope |
| 404 | `not-found` | No report with that reference in your organisation |
| 429 | `rate-limited` | Too many requests; wait for the `Retry-After` header’s seconds |

## Webhooks

Two events are sent for Connection Diagnostics. Both are thin: they say which report and how serious, and your system fetches the report from `link`.

| Event type | Sent when |
| --- | --- |
| `diagnostics.report.received` | A report is made under your brand |
| `diagnostics.report.problems` | A report finds at least one problem (severity `problem`) |

Create a webhook with `POST /platform/webhooks`, choosing these types; leave `types` out to receive every event your account can. The signing secret is returned once, when the webhook is created: store it then.

```
curl -X POST "https://partners.netavo.com/api/v1/platform/webhooks" \
  -H "Authorization: Bearer $NETAVO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://support.example.net/hooks/diagnostics",
       "description": "Connection reports into the helpdesk",
       "types": ["diagnostics.report.problems"]}'
```

Each event is an HTTPS `POST` of JSON to your URL:

```
{
  "id": "evt_0192a4c3e5f67b8d9e0f1a2b3c4d5e72",
  "type": "diagnostics.report.problems",
  "serviceArea": "diagnostics",
  "organisationId": "0f8b6c1e-5a2d-4c7e-9b3a-2d1e6f4a7c90",
  "occurredAt": "2026-11-06T10:15:32Z",
  "data": {
    "reference": "EF-9C2D-7KLP",
    "tenant": "examplefibre",
    "createdAt": "2026-11-06T10:15:30Z",
    "collector": "windows",
    "worstSeverity": "problem",
    "problemCount": 1,
    "warningCount": 0,
    "infoCount": 2,
    "demo": false,
    "link": "https://partners.netavo.com/api/v1/diagnostics/reports/EF-9C2D-7KLP",
    "deduplicationId": "diagnostics.report.problems:EF-9C2D-7KLP"
  }
}
```

Answer with any `2xx` quickly and do the work afterwards. The same event can arrive more than once, so ignore an `id` you have already handled. `demo` is `true` for a report made while your brand was in demo mode, which is not a real customer’s report. Missed events can be read back from `GET /platform/events?type=diagnostics.report.problems`.

### Verifying Signatures

Every request carries a `Webhook-Signature` header, `t=<unix seconds>,v1=<hex>`. The `v1` value is the lower-case hex HMAC-SHA256 of the timestamp, a full stop and the raw request body, keyed with your signing secret. While a secret is being rotated a second `v1` follows, signed with the previous secret; accept the request if either matches. Compute it over the body exactly as received, before parsing it, and reject requests more than five minutes old.

```
import hashlib, hmac, time

def verify_webhook(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = [p.strip().split("=", 1) for p in header.split(",") if "=" in p]
    timestamps = [v for k, v in parts if k == "t"]
    signatures = [v for k, v in parts if k == "v1"]
    if not timestamps or abs(time.time() - int(timestamps[0])) > tolerance:
        return False
    expected = hmac.new(secret.encode(), timestamps[0].encode() + b"." + raw_body,
                        hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in signatures)
```

## Connect Your Helpdesk

API access comes with both Partner Branded options. Talk to us about connecting reports to your ticketing system.

[Talk to Us](https://netavo.com/work-with-us)

[Connection Diagnostics](https://netavo.com/sectors/isps-and-network-operators/connection-diagnostics)

Source: https://netavo.com/sectors/isps-and-network-operators/connection-diagnostics/api
