Skip to main content

Targets

A target is one host and one check protocol. Agents measure every target every 15 seconds. This page describes the target model and the endpoints that manage targets and their agent assignments.

All examples assume that OBSERVIANT_URL and OBSERVIANT_API_KEY are set, as shown in Authentication.

The target object​

{
"target_id": "01M4MZ5TNNHHA3B7J2C52XWBCD",
"label": "example.com HTTPS",
"host": "example.com",
"protocol": "http",
"port": 443,
"family": "auto",
"category": "Websites",
"scheme": "https",
"path": "/",
"method": "GET",
"expect_status": 200
}
FieldTypeDescription
target_idstringServer-assigned ID. Read-only.
labelstringRequired. Name shown in the dashboard. Labels do not have to be unique.
hoststringRequired. IPv4 address, IPv6 address or hostname. See Host rules.
protocolstringRequired. icmp, tcp, tls, http or dns. See Protocols.
portintegerDestination port for tcp, tls and http. 0 or omitted selects the protocol default: 80 for HTTP, 443 for HTTPS. Always 0 for icmp and dns.
familystringAddress family agents use when they resolve a hostname: auto (default), ipv4 or ipv6. Applies to icmp, tcp and tls targets.
categorystringOptional grouping label, up to 64 characters. Surrounding whitespace is removed. An empty string means the target is Ungrouped.
record_typestringdns only. A (default), AAAA, SOA or NS.
schemestringhttp only. http (default) or https.
pathstringhttp only. Request path starting with /, optionally with a query string. Default /.
methodstringhttp only. GET (default) or HEAD.
expect_statusintegerhttp only. Exact status code required for success, 100–599. 0 or omitted accepts any 2xx or 3xx status.

Responses always include target_id, label, host, protocol, port, family and category. Protocol-specific fields appear only on targets that use them.

Protocols​

protocolWhat each measurement doesLatencySuccessDefault port
icmpSends 10 ICMP echo requests, one second apart. Also collects traceroutes.Average round-trip time of the repliesPercentage of requests answered—
tcpOpens a TCP connection to host:port.Time to establish the connectionConnection established within 5 seconds80
tlsOpens a TCP connection and completes a TLS handshake (TLS 1.2 or later). host is sent as the server name (SNI).Time to connect and complete the handshakeHandshake completed within 5 seconds443
httpSends one GET or HEAD request to scheme://host:port/path, following up to 10 redirects.Time to the first response byteStatus 2xx/3xx, or exactly expect_status, within 5 seconds80 for HTTP; 443 for HTTPS
dnsResolves host for record_type with the agent's configured DNS resolver.Time to receive the answerAt least one record returned within 5 seconds—

Keep the following in mind when you choose a protocol:

  • HTTPS defaults to port 443 for new targets. Send "scheme": "https" and omit port or set it to 0 for the standard port. Existing targets with an explicit port keep that port until updated.
  • Certificates are not validated. tls and http checks succeed against expired, self-signed or mismatched certificates, so they measure reachability and timing, not certificate validity.
  • A dns target's host is the name to resolve, such as www.example.com, not the address of a DNS server. Each agent queries its own configured resolver.
  • Only icmp targets get traceroutes. Traceroutes, path changes and correlated events need ICMP. To trace the path to a service you check with another protocol, create a second, ICMP target for the same host.

Host rules​

  • A host is an IPv4 or IPv6 address, or a hostname of up to 253 characters. Each dot-separated part of a hostname contains letters, digits and hyphens, is at most 63 characters, and does not start or end with a hyphen. A single trailing dot is allowed.
  • Agents resolve hostnames themselves and resolve them again periodically. A hostname served by anycast or GeoDNS can resolve to a different address for each agent. Every traceroute records the address it actually reached.
  • System agents measure public, globally routable addresses only. They do not measure private (RFC 1918), shared (100.64.0.0/10), loopback, link-local, documentation, benchmarking or multicast addresses, and those measurements fail. Use user agents for hosts inside your networks, and assign private targets to them.

List targets​

GET /api/targets

Returns every target in your environment as a JSON array, sorted by label. An environment without targets returns [].

curl --header "Authorization: Bearer $OBSERVIANT_API_KEY" \
"$OBSERVIANT_URL/api/targets"
[
{
"target_id": "01M4MZ5T3A76E4RBFKGBD5MP8V",
"protocol": "icmp",
"host": "1.1.1.1",
"port": 0,
"label": "Cloudflare DNS",
"family": "auto",
"category": "Public DNS"
},
{
"target_id": "01M4MZ5TYKK2A9GY8TMPZSME86",
"protocol": "tls",
"host": "example.com",
"port": 443,
"label": "example.com TLS",
"family": "auto",
"category": ""
}
]

Get a target​

GET /api/targets/{target_id}

Returns one target, or 404 Not Found with {"error": "Target not found"}.

curl --header "Authorization: Bearer $OBSERVIANT_API_KEY" \
"$OBSERVIANT_URL/api/targets/01M4MZ5T3A76E4RBFKGBD5MP8V"

Create a target​

POST /api/targets

Send a target object without target_id. Only label, host and protocol are required. The response is 200 OK with the created target, including its new target_id and the defaults Observiant filled in.

curl --header "Authorization: Bearer $OBSERVIANT_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"label": "Cloudflare DNS",
"host": "1.1.1.1",
"protocol": "icmp",
"category": "Public DNS"
}' \
"$OBSERVIANT_URL/api/targets"
{
"target_id": "01M4MZ5T3A76E4RBFKGBD5MP8V",
"protocol": "icmp",
"host": "1.1.1.1",
"port": 0,
"label": "Cloudflare DNS",
"family": "auto",
"category": "Public DNS"
}

More request bodies:

HTTPS check that requires status 200
{"label": "example.com HTTPS", "host": "example.com", "protocol": "http",
"scheme": "https", "port": 443, "path": "/", "expect_status": 200}
TCP port check over IPv4 only
{"label": "SSH bastion", "host": "bastion.example.net", "protocol": "tcp", "port": 22, "family": "ipv4"}
TLS handshake on the default port 443
{"label": "example.com TLS", "host": "example.com", "protocol": "tls"}
DNS resolution of an AAAA record
{"label": "www AAAA", "host": "www.example.com", "protocol": "dns", "record_type": "AAAA"}

New targets are measured within about a minute.

note

Creating a target is not idempotent. If you retry a request whose response you did not receive, you can create a duplicate. Labels are not unique, so match on host, protocol and port when you check whether a target already exists. The inventory example shows one way to do this.

Errors:

StatusExample messageCause
400label not specified, host not specified, protocol not specifiedA required field is missing or empty.
400unsupported protocol specifiedprotocol is not one of the five supported values.
400invalid host: host "-x" starts with a dashhost breaks the host rules.
400family must be auto, ipv4 or ipv6Invalid family.
400record_type must be A, AAAA, SOA, or NS for DNS targetsInvalid record_type.
400method must be GET or HEAD, path must be an origin path starting with /Invalid HTTP settings.
400Failed to decode bodyThe body is not a single JSON object, or contains an unknown field.
403Target limit reached (100 of 100). Upgrade your plan to monitor more targets.Your plan's target limit is reached.

Update a target​

PUT /api/targets/{target_id}

Replaces the target's settings with the request body and returns 204 No Content. PUT is a full replacement: send every field you want to keep. Optional fields you omit return to their defaults, so leaving out category, for example, moves the target to Ungrouped. A target_id in the body is ignored.

The target keeps its ID, measurement history and agent assignments. If you change the host or protocol, the existing history stays with the target and new measurements continue the same series. To start a clean history, create a new target and delete the old one instead.

To change a single field, read the target, modify it, and send the whole object back:

This example uses jq to edit the JSON:

TARGET_ID=01M4MZ5T3A76E4RBFKGBD5MP8V

curl --silent --header "Authorization: Bearer $OBSERVIANT_API_KEY" \
"$OBSERVIANT_URL/api/targets/$TARGET_ID" \
| jq '.category = "Resolvers"' \
| curl --silent --show-error \
--header "Authorization: Bearer $OBSERVIANT_API_KEY" \
--header "Content-Type: application/json" \
--request PUT --data @- \
"$OBSERVIANT_URL/api/targets/$TARGET_ID"

Validation errors are the same as for creating a target. An unknown ID returns 404 Not Found.

Delete a target​

DELETE /api/targets/{target_id}

Deletes the target and returns 204 No Content. Deletion is permanent. It also removes:

  • the target's measurement history, traceroutes and path changes
  • its agent assignments
curl --header "Authorization: Bearer $OBSERVIANT_API_KEY" \
--request DELETE \
"$OBSERVIANT_URL/api/targets/01M4MZ5T3A76E4RBFKGBD5MP8V"

An unknown ID returns 404 Not Found. If the response is 500 with Failed to delete target measurements; please retry, the target still exists. Repeat the same request.

Agent assignments​

By default, every agent measures every target. Assigning a target to one or more agents restricts it to those agents. Removing the last assignment returns the target to all agents.

Use assignments to:

  • send private targets only to the user agents that can reach them
  • measure a target from selected locations only

Assignments refer to agents by label. Changes take effect within about a minute.

warning

Observiant does not check that an agent with the label exists. A misspelled label silently removes the target from every other agent, and no agent measures it. After you assign agents, confirm that measurements arrive with latency and loss.

List assignments​

GET /api/targets/{target_id}/agents

curl --header "Authorization: Bearer $OBSERVIANT_API_KEY" \
"$OBSERVIANT_URL/api/targets/01M4MZ5T3A76E4RBFKGBD5MP8V/agents"
{
"target_id": "01M4MZ5T3A76E4RBFKGBD5MP8V",
"agents": ["dc1-probe", "sys-amsterdam"],
"all_agents": false
}
FieldDescription
agentsLabels of the assigned agents, sorted. Empty when the target has no assignments.
all_agentstrue when there are no assignments and every agent measures the target.

An unknown target returns 404 Not Found.

Assign an agent​

PUT /api/targets/{target_id}/agents/{agent_label}

Adds an assignment and returns 204 No Content. Assigning an agent that is already assigned succeeds without changing anything. URL-encode the label; a space becomes %20.

curl --header "Authorization: Bearer $OBSERVIANT_API_KEY" \
--request PUT \
"$OBSERVIANT_URL/api/targets/01M4MZ5T3A76E4RBFKGBD5MP8V/agents/dc1-probe"

An unknown target returns 404 Not Found. A label with characters other than letters, digits, spaces and . , : ; _ - returns 403 Forbidden.

Remove an assignment​

DELETE /api/targets/{target_id}/agents/{agent_label}

Removes one assignment and returns 204 No Content. If no such assignment exists, the response is 404 Not Found with {"error": "Assignment not found"}.

curl --header "Authorization: Bearer $OBSERVIANT_API_KEY" \
--request DELETE \
"$OBSERVIANT_URL/api/targets/01M4MZ5T3A76E4RBFKGBD5MP8V/agents/dc1-probe"

Look up a target's address​

GET /api/targets/{target_id}/ipinfo

Resolves the target's host now, using its family, and returns the address with its network and country. The lookup is made from Observiant's servers, so for anycast or GeoDNS hostnames the address can differ from the ones your agents use. Each traceroute records the address its agent actually reached.

curl --header "Authorization: Bearer $OBSERVIANT_API_KEY" \
"$OBSERVIANT_URL/api/targets/01M4MZ5TCN74PFRTXDKXYE4XWX/ipinfo"
{
"ip": "8.8.8.8",
"family": "ipv4",
"asn": "AS15169",
"as_name": "Google LLC",
"country_code": "US",
"country": "United States"
}

family is ipv4 or ipv6. Network and country fields are empty strings when no data is available for the address. An unknown target returns 404 Not Found. A hostname that cannot be resolved returns 502 Bad Gateway.

Example: sync targets from an inventory​

This script makes your environment match a list of targets kept in version control or exported from your source of truth. It creates targets that are missing and updates targets whose settings differ. With --prune, it also deletes targets that are not in the list.

Targets are matched on protocol, host and port, so you can rename a target by changing its label. Because PUT replaces the whole target, the script sends the complete desired settings for each target it updates.

targets.json
[
{"label": "Cloudflare DNS", "host": "1.1.1.1", "protocol": "icmp", "category": "Public DNS"},
{"label": "Google DNS", "host": "8.8.8.8", "protocol": "icmp", "category": "Public DNS"},
{"label": "example.com HTTPS", "host": "example.com", "protocol": "http",
"scheme": "https", "port": 443, "expect_status": 200, "category": "Websites"}
]
sync_targets.py
"""Make Observiant targets match a JSON inventory file.

Usage: python sync_targets.py targets.json [--prune]
Requires OBSERVIANT_URL and OBSERVIANT_API_KEY in the environment.
"""
import json
import os
import sys

import requests

DEFAULT_PORTS = {"icmp": 0, "dns": 0, "tcp": 80, "tls": 443, "http": 80}
DEFAULTS = {"family": "auto", "category": ""}
HTTP_DEFAULTS = {"scheme": "http", "path": "/", "method": "GET", "expect_status": 0}


def normalize(target):
"""Return the settings Observiant stores, so comparisons are exact."""
result = {**DEFAULTS, **target}
result["protocol"] = result["protocol"].lower()
if result["protocol"] == "http":
result = {**HTTP_DEFAULTS, **result}
default_port = (443 if result["protocol"] == "http" and result["scheme"] == "https"
else DEFAULT_PORTS[result["protocol"]])
result["port"] = result.get("port") or default_port
if result["protocol"] == "dns":
result["record_type"] = result.get("record_type", "A").upper()
result.pop("target_id", None)
return result


def key(target):
return (target["protocol"], target["host"].lower(), target["port"])


def main(path, prune):
base_url = os.environ["OBSERVIANT_URL"]
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['OBSERVIANT_API_KEY']}"

with open(path, encoding="utf-8") as handle:
desired = {}
for item in json.load(handle):
target = normalize(item)
desired[key(target)] = target

response = session.get(f"{base_url}/api/targets", timeout=30)
response.raise_for_status()
current = {key(normalize(t)): t for t in response.json()}

for target_key, target in desired.items():
existing = current.get(target_key)
if existing is None:
response = session.post(f"{base_url}/api/targets", json=target, timeout=30)
response.raise_for_status()
print(f"created {response.json()['target_id']} {target['label']}")
elif normalize(existing) != target:
target_id = existing["target_id"]
response = session.put(f"{base_url}/api/targets/{target_id}", json=target, timeout=30)
response.raise_for_status()
print(f"updated {target_id} {target['label']}")

if prune:
for target_key, existing in current.items():
if target_key not in desired:
target_id = existing["target_id"]
response = session.delete(f"{base_url}/api/targets/{target_id}", timeout=60)
response.raise_for_status()
print(f"deleted {target_id} {existing['label']}")


if __name__ == "__main__":
main(sys.argv[1], "--prune" in sys.argv[2:])
$ python sync_targets.py targets.json
created 01M4N0CZ2DCB0CWTYS3MT4KJY8 Google DNS
updated 01M4MZ5TNNHHA3B7J2C52XWBCD example.com HTTPS

Run the script without --prune first. Pruning deletes targets, and their history, that are not in the file. The script assumes that the file contains each protocol, host and port combination at most once and that your environment does too.