Documentation
REST API reference
Every REST endpoint with its method, path, scope and purpose, plus pagination, errors, rate limits and request bodies.
Overview
The REST API gives scripts and integrations access to assets, vulnerabilities, cloud resources, flows, reports and scans, plus device recognition and the Partner API. Requests and responses use JSON (CSV for exports, the report file for report downloads).
- Base URL:
https://<your DeviceQuest host>/api/v1 - Authentication: an API key in the
X-API-Keyheader. See Authentication. - Scope: decided by the HTTP method. See Scopes.
- Tenant: taken from the API key. Every endpoint returns only your organisation’s data.
The Developer page in the app also lists these endpoints in its REST API Reference section, with a search box and the required scope of each endpoint.
Endpoints
Assets
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /assets | read | List assets, newest lastSeen first. Paginated. Filters: type, search. |
GET | /assets/search | read | Search assets with q. Paginated. |
GET | /assets/export | read | Download all assets as CSV. Requires format=csv. |
GET | /assets/{id} | read | Get one asset. |
GET | /assets/{id}/export | read | Alias of /assets/export; returns the same all-assets CSV. |
Query parameters:
| Parameter | Endpoint | Description |
|---|---|---|
type | /assets | Exact asset type: device, virtual_machine, hypervisor, cluster, datacenter, datastore, cloud_resource, ot_device, software, saas_account or user. |
search | /assets | Case-insensitive text matched anywhere in the asset ID, hostname, IP address or serial number. |
q | /assets/search | Same matching as search. |
limit, offset | /assets, /assets/search | See Pagination. |
format | /assets/export | Must be csv. |
Asset object:
{
"id": "6f1c2b9e-4d3a-4f8e-9b7a-2c5d8e1f0a3b",
"name": "web-01.example.local",
"assetType": "device",
"ipAddress": "10.20.0.15",
"macAddress": "00:1b:21:3a:4f:5c",
"os": "Ubuntu 22.04.4 LTS",
"riskScore": 7.4,
"lastSeen": "2026-10-13T05:42:10Z",
"tags": ["production", "web"]
}
riskScore uses the same risk algorithm as the app; an asset with no open vulnerabilities scores 0. Empty strings mean the value was not collected.
curl -H "X-API-Key: $DEVICEQUEST_API_KEY" \
"$DEVICEQUEST_BASE_URL/api/v1/assets?type=device&search=web&limit=25"
Vulnerabilities
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /vulnerabilities | read | List vulnerability findings (one per CVE and asset), highest CVSS first. Paginated. |
GET | /vulnerabilities/export | read | Download findings as CSV. Requires format=csv. |
GET | /vulnerabilities/{id} | read | Get one finding. |
Query parameters for /vulnerabilities:
| Parameter | Description |
|---|---|
severity | critical, high, medium, low or informational. Matches the finding’s effective severity, including any severity override set in the app. |
cvss_min, cvss_max | Inclusive CVSS score bounds, for example cvss_min=7.0. |
fixed | true lists resolved findings. Without it (or with any other value) the list contains open findings. |
limit, offset | See Pagination. |
Vulnerability object:
{
"id": "1b7e0c3d-9a52-4f1e-8c6d-3e2f1a0b9c87",
"cveId": "CVE-2024-6387",
"severity": "high",
"cvssScore": 8.1,
"description": "A signal handler race condition was found in OpenSSH's server (sshd)...",
"assetId": "6f1c2b9e-4d3a-4f8e-9b7a-2c5d8e1f0a3b",
"affectedAssets": 1
}
curl -H "X-API-Key: $DEVICEQUEST_API_KEY" \
"$DEVICEQUEST_BASE_URL/api/v1/vulnerabilities?severity=critical&cvss_min=9&limit=50"
Cloud resources
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /cloud-resources | read | List discovered cloud resources across AWS, Azure and Google Cloud. Paginated. |
Each item has id, provider (AWS, Azure or GCP), name, type (compute, database, storage, kubernetes or function), region and status. The response also carries meta.count (the total) and meta.tenant.
curl -H "X-API-Key: $DEVICEQUEST_API_KEY" \
"$DEVICEQUEST_BASE_URL/api/v1/cloud-resources?limit=100"
{
"data": [
{ "id": "5e2d7c1a-8b4f-4a3e-9d6c-0f1b2a3c4d5e", "provider": "AWS", "name": "db-prod-01", "type": "database", "region": "eu-central-1", "status": "available" }
],
"meta": { "count": 1, "tenant": "00000000-0000-0000-0000-000000000001" }
}
Flows
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /flows | read | List automation flows with their trigger type and last run time. Paginated. |
Each item has id, name, description, enabled, triggerType and lastTriggeredAt (RFC 3339, or an empty string if the flow never ran). The response also carries meta.count and meta.tenant.
curl -H "X-API-Key: $DEVICEQUEST_API_KEY" "$DEVICEQUEST_BASE_URL/api/v1/flows"
Reports
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /reports | read | List report definitions with the status of their latest generation. Paginated. |
GET | /reports/{id} | read | Get one report. |
POST | /reports | write | Create a report and queue its first generation. |
GET | /reports/{id}/download | read | Download the latest generated file of a report. |
Report object:
{
"id": "4f6a2b8c-3d5e-4f6a-9b8c-7d6e5f4a3b2c",
"name": "Weekly inventory",
"type": "inventory_summary",
"format": "pdf",
"status": "generating",
"createdAt": "2026-10-13T06:00:00Z"
}
status | Meaning |
|---|---|
pending | The report exists but has not been generated yet. |
generating | A generation is queued or running. |
available | The latest generation succeeded. GET /reports/{id}/download returns the file. |
failed | The latest generation failed. |
Create a report
POST /reports body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Report name. Must not be blank. |
type | string | Yes | inventory_summary, compliance_report, asset_audit, software_license, topology, budget_forecast or custom. |
format | string | No | csv (default), pdf, excel or xml. excel produces an .xlsx workbook. |
asset_ids | array of strings | No | Limit the report to these asset UUIDs. |
period | string | No | Reporting period, stored with the report’s filter. |
curl -X POST \
-H "X-API-Key: $DEVICEQUEST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Weekly inventory", "type": "inventory_summary", "format": "pdf"}' \
"$DEVICEQUEST_BASE_URL/api/v1/reports"
The response is 201 Created with the report object and "status": "generating". DeviceQuest generates the report in the background, usually within about a minute, and stores the result as a snapshot.
Download a report
GET /reports/{id}/download returns the latest snapshot as a file:
| Response | Meaning |
|---|---|
200 OK | The most recent successful snapshot, as a file. Content-Type matches the format (application/pdf, text/csv, application/xml or the .xlsx type). Content-Disposition names the file report-<id>-<UTC timestamp>.<ext>. X-Report-Snapshot-Id and X-Report-Generated-At identify the snapshot. |
409 Conflict, error: report_not_ready | The first generation is still running. The response has Retry-After: 60; try again after that many seconds. |
404 Not Found, error: not_found | The report doesn’t exist, or has never produced a file. |
# Poll until the file is ready, then save it.
until curl -sf -o weekly-inventory.pdf \
-H "X-API-Key: $DEVICEQUEST_API_KEY" \
"$DEVICEQUEST_BASE_URL/api/v1/reports/4f6a2b8c-3d5e-4f6a-9b8c-7d6e5f4a3b2c/download"
do
sleep 60
done
Scans
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /scans | read | List scan jobs, with their status. Paginated. |
POST | /scans | write | Queue a scan of an existing scan target. |
DELETE | /scans/{id} | admin | Cancel a queued or running scan. |
Scan object:
{
"scanId": "2d0c8f4e-6b1a-4e7d-9c3f-5a8b7e6d4c21",
"status": "pending",
"startedAt": "",
"message": "Scan job queued for target 10.20.0.0/24"
}
status is pending, running, completed, failed or cancelled. startedAt is empty until the scan starts. For failed scans, message holds the error.
Start a scan
POST /scans body:
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Scan type: sweep, ssh, snmp, wmi, vmware or hypervisor (Proxmox). See Scan types. |
target | string | Yes | The UUID of an existing scan target, not a hostname or IP address. Create targets on the Targets tab (see Targets). |
Rules:
- The target must be runnable with the requested type. An IP range (CIDR) target needs
sweep; the other types scan one host at a time. - Credentials come from the credentials configured in DeviceQuest for the target. The request has no credential fields.
- DeviceQuest queues the job as
pending; the scan scheduler picks it up and runs it.
curl -X POST \
-H "X-API-Key: $DEVICEQUEST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "sweep", "target": "8a3e5c71-2f4b-4d9a-b6e8-1c0d7f2a9e45"}' \
"$DEVICEQUEST_BASE_URL/api/v1/scans"
The response is 201 Created with the scan object. An unknown target returns 404; an unsupported type or a target/type mismatch returns 400 invalid_request with the reason.
Cancel a scan
curl -X DELETE -H "X-API-Key: $DEVICEQUEST_API_KEY" \
"$DEVICEQUEST_BASE_URL/api/v1/scans/2d0c8f4e-6b1a-4e7d-9c3f-5a8b7e6d4c21"
The response is 200 OK with {"message": "Scan cancelled successfully"}. A scan that already finished (completed, failed or cancelled) returns 409 conflict.
Device recognition
The recognition endpoints identify a device type and vendor from network signals such as a MAC address, IP address or fingerprint. They evaluate the signals you send; they don’t look up your asset inventory (use GET /assets/search?q=<ip> for that).
All recognition endpoints need the recognition:read scope (or admin), whatever the method.
| Method | Path | Scope | Purpose |
|---|---|---|---|
POST | /recognition/recognize | recognition:read | Recognise a device from any combination of signals. |
POST | /recognition/recognize/mac | recognition:read | Recognise a device from its MAC address. |
POST | /recognition/recognize/ip | recognition:read | Recognise a device from its IP address. |
POST | /recognition/recognize/fingerprint | recognition:read | Recognise a device from fingerprint attributes. |
GET | /recognition/device-types | recognition:read | List the device types recognition can return. |
Request bodies:
| Endpoint | Body |
|---|---|
/recognize | Any of mac_address, ip_address, vendor, model, hostname and fingerprints (object). At least one of mac_address, ip_address or fingerprints is required; vendor, model and hostname refine the result. |
/recognize/mac | {"mac_address": "00:1b:21:3a:4f:5c"} (required, must be a valid MAC address). |
/recognize/ip | {"ip_address": "10.20.0.1"} (required, must be a valid IPv4 or IPv6 address). |
/recognize/fingerprint | {"fingerprint": {...}} with at least one attribute. vendor, model and protocol keys are used as hints. |
curl -X POST \
-H "X-API-Key: $DEVICEQUEST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mac_address": "00:00:0c:12:34:56", "vendor": "Cisco", "hostname": "core-sw-01"}' \
"$DEVICEQUEST_BASE_URL/api/v1/recognition/recognize"
Response:
{
"device_type": "switch",
"vendor": "Cisco",
"model": "",
"confidence": 0.9,
"matches": [
{ "device_type": "switch", "vendor": "Cisco", "model": "", "confidence": 0.3, "source": "mac_oui" }
],
"source": "fallback"
}
confidence runs from 0 to 1; 0.8 and above is a high-confidence match. GET /recognition/device-types returns {"device_types": ["workstation", "server", "router", "switch", "firewall", "printer", "camera", "sensor", "gateway", "access_point", "load_balancer", "storage", "unknown"]}.
Partner API
The Partner API is for Builder Partners who provision and manage DeviceQuest for their customers. Send exactly one of these headers:
| Header | Key | Acts as |
|---|---|---|
X-Partner-Key | A partner key, pk_ followed by 64 hexadecimal characters. | The partner the key belongs to, limited to the key’s scopes. |
X-API-Key | A tenant API key with the admin scope. | Your tenant: it manages the partners your tenant owns. |
A request with both headers gets 400. A partner key that is unknown, revoked or expired, or whose partner has the API turned off, gets 401 with the message Invalid or expired credentials.
Getting started. With your tenant admin X-API-Key, create a partner (POST /partners) and issue its first partner key (POST /partners/{id}/keys). Give that key to the partner; from then on the partner uses X-Partner-Key.
Partner keys. A partner key is pk_ plus 32 random bytes in hex. DeviceQuest shows it once, in the api_key field of the create response, and stores only its SHA-256 hash and a short display prefix. Every key needs at least one scope.
What each key can reach.
Creating, listing and deleting partners needs the tenant admin
X-API-Key; a partner key gets403.GET /partnersreturns your tenant’s partners.On the per-partner routes (
/partners/{id}/...), a partner key works only on its own partner. Any other{id}returns404withpartner not found. A tenant admin key works on the partners its tenant owns.Partner key scopes:
Scope Allows partner:readView the partner, its keys and its quota. partner:writeCreate and revoke partner keys. Includes partner:read.webhook:readView the partner’s webhooks. webhook:writeRegister and delete the partner’s webhooks. Includes webhook:read.A partner key can issue new keys only with scopes it holds itself.
| Method | Path | Partner key scope | Purpose |
|---|---|---|---|
POST | /partners | Tenant admin key | Create a partner owned by your tenant. Body: name, slug, email (required), quota_limit (default 10000 assets), rate_limit (default 100 requests per minute). A slug already in use returns 409. |
GET | /partners | Tenant admin key | List your tenant’s partners. Query: limit (1 to 100, default 20), offset. Returns data, total, limit, offset. |
GET | /partners/{id} | partner:read | Get a partner. |
DELETE | /partners/{id} | Tenant admin key | Delete a partner owned by your tenant. |
POST | /partners/{id}/keys | partner:write | Create a partner key. Body: name, scopes (required, at least one), expires_in_days. Returns the key once in api_key. |
GET | /partners/{id}/keys | partner:read | List the partner’s keys (display prefix only). |
DELETE | /partners/{id}/keys/{keyId} | partner:write | Revoke a partner key. |
POST | /partners/{id}/webhooks | webhook:write | Register a partner webhook. Body: name, url, events (required), secret, headers. |
GET | /partners/{id}/webhooks | webhook:read | List the partner’s webhooks. |
DELETE | /partners/{id}/webhooks/{webhookId} | webhook:write | Delete a partner webhook. |
GET | /partners/{id}/quota/{tenantId} | partner:read | Quota usage for the partner’s own tenant: asset_count, quota_limit, quota_used_pct, requests_today, rate_limit, reset_at. Any other tenant ID returns 404. |
Partner and quota responses include tenant_id, the tenant that owns the partner.
# Tenant admin: create a partner, then issue its first partner key.
curl -X POST \
-H "X-API-Key: $DEVICEQUEST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Acme MSP", "slug": "acme-msp", "email": "[email protected]"}' \
"$DEVICEQUEST_BASE_URL/api/v1/partners"
curl -X POST \
-H "X-API-Key: $DEVICEQUEST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Acme automation", "scopes": ["partner:write", "webhook:write"], "expires_in_days": 90}' \
"$DEVICEQUEST_BASE_URL/api/v1/partners/<partner id>/keys"
# Partner: read its own partner record with the partner key.
curl -H "X-Partner-Key: $DEVICEQUEST_PARTNER_KEY" \
"$DEVICEQUEST_BASE_URL/api/v1/partners/<partner id>"
Pagination
List endpoints accept limit and offset:
| Parameter | Default | Rules |
|---|---|---|
limit | 50 | 1 to 1000. Values outside that range use the default. |
offset | 0 | 0 or more. Number of items to skip. |
Paginated responses wrap the items in data and describe the page in headers:
| Header | Meaning |
|---|---|
X-Total-Count | Total number of items that match your filters. |
X-Total-Pages | Number of pages at the current limit. |
X-Page | Current page number, starting at 1. |
X-Per-Page | The limit in effect. |
X-Next-Cursor | offset=<n> for the next page. Absent on the last page. |
Read every page in a loop:
offset=0
while :; do
headers=$(mktemp)
curl -s -D "$headers" -o "assets-$offset.json" \
-H "X-API-Key: $DEVICEQUEST_API_KEY" \
"$DEVICEQUEST_BASE_URL/api/v1/assets?limit=500&offset=$offset"
next=$(grep -i '^x-next-cursor:' "$headers" | sed 's/.*offset=//' | tr -d '\r')
rm -f "$headers"
[ -z "$next" ] && break
offset=$next
done
Exports
/assets/export?format=csv and /vulnerabilities/export?format=csv return a CSV file (Content-Type: text/csv, Content-Disposition: attachment; filename=assets.csv or vulnerabilities.csv).
- Any other
formatreturns400witherror: invalid_format. - An export holds at most 10,000 rows. When there are more, the response has the header
X-Export-Truncated: true; page through the list endpoint to read everything. - Values that a spreadsheet would run as a formula (starting with
=,+,-,@, a tab or a carriage return) are prefixed with an apostrophe, so files are safe to open in Excel or Google Sheets.
curl -H "X-API-Key: $DEVICEQUEST_API_KEY" -o assets.csv \
"$DEVICEQUEST_BASE_URL/api/v1/assets/export?format=csv"
Errors
Errors return an HTTP status and a flat JSON body with a machine-readable error code and a readable message:
{
"error": "invalid_request",
"message": "'target' must be the UUID of an existing scan target"
}
| Status | error | Meaning |
|---|---|---|
400 | invalid_request | The body is not valid JSON, a required field is missing, or a value is not allowed. The message names the field. |
400 | invalid_format | An export was requested without format=csv. |
401 | unauthorized | Missing, unknown, revoked or expired API key. |
403 | forbidden | The key lacks the scope this request needs. |
404 | not_found | The resource doesn’t exist in your organisation. |
409 | conflict | The request conflicts with the resource’s state, for example cancelling a finished scan. |
409 | report_not_ready | The report is still generating. Retry after the Retry-After seconds. |
429 | rate_limit_exceeded | Too many requests. See Rate limits. |
500 | internal_error | DeviceQuest could not complete the request. Retry later; if it persists, contact your administrator. |
Rate limits
Each API key can make 100 requests per minute, counted across all REST endpoints. DeviceQuest counts requests by the X-API-Key header; requests without that header (for example Partner API requests that use X-Partner-Key) are counted by client IP address.
Every response carries the current state:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per window (100). |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | When the window resets, as a Unix timestamp in seconds. |
When you go over the limit, DeviceQuest answers 429 Too Many Requests:
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again later.",
"retry_after": 41.87
}
retry_after is the number of seconds to wait (may include a fraction). Slow down when X-RateLimit-Remaining gets low, and wait until X-RateLimit-Reset after a 429.