---
title: "Check where one bound domain points and whether this machine is reachable from the internet"
method: GET
path: "/api/nodes/{node_id}/services/{service}/domains/{domain}/check"
tags: ["nodes"]
---

# Check where one bound domain points and whether this machine is reachable from the internet

`GET /api/nodes/{node_id}/services/{service}/domains/{domain}/check`

## Request

- Base URL: `https://app.nerdit.ai` (the document declares no server; this is the origin it was published from)
- URL: `https://app.nerdit.ai/api/nodes/{node_id}/services/{service}/domains/{domain}/check`
- Auth: none declared

Look, once, live, and remember nothing (task 12 D2).

**The order below is the security property**, not an implementation
convenience, so it is written out rather than left to be read off the
code:

1. the service grammar, then the transport class — the same two refusals
   the writes make, before a tunnel byte;
2. the rate limit, before a tunnel byte too: a check is two daemon reads
   and up to three network operations, and the cheapest place to refuse
   a flood is before any of them;
3. the node's **own list of bound names**, and a 404 for anything not on
   it. This is what keeps the route from being a request-forgery
   primitive: the caller cannot get this far with a name the machine
   does not already serve;
4. only then the capabilities read, the DNS lookup, and the probes;
5. and the probes go to **the address the relay observed this machine
   dialling out from**, canonicalized, and only when that address is
   globally routable. Never to a DNS answer. The caller controls the
   name and therefore controls the answer; it does not control what this
   deployment observed.

A read, so no standing check, no audit row, and nothing written — the
one departure being the 409 below.

## Path parameters

- `service` string, required
- `domain` string, required
- `node_id` string, uuid, required

## Response `200`

Successful Response

- DomainCheckView: One live look at one bound name. Nothing here is remembered. ``checked_at`` is on the view rather than implied by the request because the console renders "checked N seconds ago": a finding with no age reads as a standing fact about a name, and none of these are.
  - `service_name` string, required
  - `domain` string, required
  - `checked_at` string, date-time, required
  - `resolved` ResolvedView, required: Where the name points, as this cloud sees it from where a visitor sits. Split in two on purpose. ``addresses`` carries only globally routable answers; a private answer is *counted* and never rendered, because the web role's resolver inside compose is Docker's embedded DNS and printing what it said would describe this deployment's own network in a console panel. ``resolved`` therefore means "the lookup answered something", whether or not any of it is renderable. ``addresses`` is also capped, so a name behind a long round-robin shows a sample of its answer rather than the whole of it. The verdict below is computed from the whole answer and never from this sample.
    - `state` 'resolved' | 'unresolved' | 'timed_out', required
    - `addresses` string[], required
    - `private_count` integer, required
  - `observed` ObservedView, required: Where this machine dialled out from, as the relay saw it. ``unavailable`` covers both "no connection to have observed" and "the address we saw is not one the internet routes to" — a machine behind the same NAT as the relay, or, in the dev stack, a fake daemon on the compose network. The two are one state because the console's answer is the same: we cannot compare it against DNS, so we do not pretend to. Rendered live and stored nowhere (D-NC10). It is personal data.
    - `state` 'observed' | 'unavailable', required
    - `address` string, nullable, required
  - `verdict` 'points_here' | 'points_elsewhere' | 'unresolved' | 'unknown', required
  - `reachability` ReachabilityView, required: Whether the internet can open a connection to this machine at all. ``not_probed`` is not a failure: it is what an unavailable observed address forces, and it is the state the dev stack always shows.
    - `state` 'reachable' | 'partial' | 'unreachable' | 'not_probed', required
    - `http` ProbeView, required: One TCP connect to one port of the observed address.
      - `open` boolean, required
      - `timed_out` boolean, required
    - `https` ProbeView, required: One TCP connect to one port of the observed address.
      - `open` boolean, required
      - `timed_out` boolean, required
    - `https_port` integer, required
  - `certificate` CertificateView, required: The ACME preconditions, each from the source that actually knows it. ``requested`` and ``cert_state`` come from the node's own row. ``acme_enabled`` comes from ``GET /api/capabilities`` — see :data:`CAPABILITIES_PATH` for why the row cannot answer it — and is ``None`` when that read could not be made or could not be read, which is a fault on one precondition and never a refusal of the whole check. ``http_reachable`` is the HTTP-01 precondition, and ``None`` when nothing was probed.
    - `requested` boolean, required
    - `cert_state` 'internal' | 'disabled' | 'pending' | 'issued' | 'expired', required
    - `acme_enabled` boolean, nullable, required
    - `http_reachable` boolean, nullable, required

## Other responses

- `401`: Unauthorized
- `404`: Not Found
- `409`: Conflict
- `422`: Unprocessable Content
- `429`: Too Many Requests
- `502`: Bad Gateway

---

[API](https://skmtc.dev/nerdit/apis/nerdit-cloud-web.md) · [All operations](https://skmtc.dev/nerdit/apis/nerdit-cloud-web/llms.txt) · [OpenAPI document](https://skmtc.dev/nerdit/apis/nerdit-cloud-web/revisions/06e3b44b58be?raw)
