Contact us Partner portal

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.

ScopeAllows
diagnostics.reports:readList reports, and fetch one as JSON or PDF
diagnostics.reports:writeErase a report
platform.webhooks:manageCreate and change webhooks
platform.events:readRead 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.

RequestReturnsScope
GET /diagnostics/reportsA page of report summaries, newest firstdiagnostics.reports:read
GET /diagnostics/reports/{reference}The full report as JSONdiagnostics.reports:read
GET /diagnostics/reports/{reference}/pdfThe report as your branded PDFdiagnostics.reports:read
DELETE /diagnostics/reports/{reference}204; the report is eraseddiagnostics.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 parameterMeaning
limitReports per page, 1 to 100; 25 if left out
startingAfterThe previous page’s nextCursor, for the next page
receivedFromOnly reports received at or after this time (UTC)
receivedToOnly reports received before this time (UTC)
severityOnly reports whose worst finding is ok, info, warn or problem
collectorOnly reports from web, windows, macos or other
ipOnly reports whose public IPv4 or IPv6 address contains this text
searchOnly 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.

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 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"
}
StatusCodeWhen
400validation-failedA filter or paging value is invalid; errors names the field
401unauthorizedThe token is missing, expired or revoked
403insufficient-scopeThe token lacks the endpoint’s scope
404not-foundNo report with that reference in your organisation
429rate-limitedToo 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 typeSent when
diagnostics.report.receivedA report is made under your brand
diagnostics.report.problemsA 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.