{
  "openapi": "3.1.0",
  "info": {
    "title": "a.rvrwlk.net",
    "version": "2026-10-08",
    "summary": "Network tools, secret sharing and Riverwalk feeds over plain HTTPS.",
    "description": "Small HTTP tools from Riverwalk Consulting, one AWS Lambda per endpoint behind API Gateway in us-east-1. No API key is needed.\n\nThe network tools answer in plain text for `curl`, or in JSON with `?format=json` (or an `Accept: application/json` header). Their arguments can go in the query string or in the path: `/dig/example.com/MX/@1.1.1.1` is the same as `/dig?name=example.com&type=MX&server=1.1.1.1`.\n\nNetwork checks run from AWS us-east-1, so they show the view from there, not from your network. Lambda allows only TCP and UDP sockets, so there is no ICMP: `/tcpping` measures TCP handshakes, and `/traceroute` uses TCP or UDP probes.",
    "contact": {
      "name": "Riverwalk Consulting",
      "url": "https://riverwalkconsulting.com/"
    }
  },
  "servers": [
    {
      "url": "https://a.rvrwlk.net"
    }
  ],
  "tags": [
    {
      "name": "Network",
      "description": "Check addresses, ports, routes and DNS from AWS us-east-1."
    },
    {
      "name": "TLS",
      "description": "Check certificates and TLS servers, and search Certificate Transparency logs."
    },
    {
      "name": "Email",
      "description": "Send a message from your mail server and see how a receiver sees it."
    },
    {
      "name": "Secrets",
      "description": "Share a secret with a link that expires."
    },
    {
      "name": "Riverwalk",
      "description": "Onboarding and feeds for systems Riverwalk Consulting manages."
    }
  ],
  "paths": {
    "/ip": {
      "get": {
        "operationId": "ip",
        "tags": [
          "Network"
        ],
        "summary": "Your public IP address",
        "description": "Returns the source IP address of the request: the address servers on the internet see when you connect. The domain is dual-stack, so you get IPv6 if your connection prefers it. Use `curl -4` or `curl -6` to choose.\n\nTo get one family regardless of what the client prefers, use `https://ip4.rvrwlk.net/ip` (IPv4 only) or `https://ip6.rvrwlk.net/ip` (IPv6 only, so it fails without IPv6 connectivity). Those names serve only `/ip`.",
        "x-tool": "/ip/",
        "x-rate-limit": {
          "rate": 20,
          "burst": 40
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "Your IP address.",
            "content": {
              "text/plain": {
                "example": "203.0.113.7\n"
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ip": {
                      "type": "string",
                      "description": "Source IP address."
                    },
                    "version": {
                      "type": "integer",
                      "enum": [
                        4,
                        6
                      ]
                    }
                  }
                },
                "example": {
                  "ip": "203.0.113.7",
                  "version": 4
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tcpping": {
      "get": {
        "operationId": "tcpping",
        "tags": [
          "Network"
        ],
        "summary": "Check whether a TCP port is open",
        "description": "Opens full TCP connections to a host and port and times each handshake. The port is reported as:\n\n- `open`: a connection was established\n- `closed`: the host answered with a reset\n- `filtered`: nothing answered before the timeout, usually a firewall dropping packets\n- `unreachable`: the network reported the host as unreachable\n\nPath forms: `/tcpping/{host}/{port}` and `/tcpping/{host}:{port}`. Private, loopback and other non-public targets are refused.",
        "x-tool": "/tcpping/",
        "x-rate-limit": {
          "rate": 5,
          "burst": 10
        },
        "parameters": [
          {
            "name": "host",
            "in": "query",
            "required": true,
            "description": "Hostname or IP address. A hostname resolves to one public address, IPv4 first.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          },
          {
            "name": "port",
            "in": "query",
            "required": true,
            "description": "TCP port.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 65535
            },
            "example": 443
          },
          {
            "name": "count",
            "in": "query",
            "description": "Connection attempts.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10,
              "default": 3
            }
          },
          {
            "name": "timeout",
            "in": "query",
            "description": "Seconds to wait for each attempt.",
            "schema": {
              "type": "number",
              "minimum": 0.1,
              "maximum": 5,
              "default": 3
            }
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "The port's state and each attempt's connect time.",
            "content": {
              "text/plain": {
                "example": "TCPPING example.com (172.66.147.243) port 443\nseq=1 open time=1.7 ms\nseq=2 open time=2.1 ms\nseq=3 open time=1.9 ms\n\n--- example.com:443 ---\nport 443 is open: 3/3 connected\nconnect min/avg/max/mdev = 1.746/1.913/2.094/0.142 ms\n"
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "host": {
                      "type": "string"
                    },
                    "address": {
                      "type": "string",
                      "description": "The address that was probed."
                    },
                    "port": {
                      "type": "integer"
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "open",
                        "closed",
                        "filtered",
                        "unreachable"
                      ]
                    },
                    "attempts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "seq": {
                            "type": "integer"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "open",
                              "closed",
                              "timeout",
                              "unreachable",
                              "error"
                            ]
                          },
                          "rtt_ms": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "error": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "sent": {
                      "type": "integer"
                    },
                    "connected": {
                      "type": "integer"
                    },
                    "rtt_ms": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "min/avg/max/mdev of successful connects, in ms.",
                      "properties": {
                        "min": {
                          "type": "number"
                        },
                        "avg": {
                          "type": "number"
                        },
                        "max": {
                          "type": "number"
                        },
                        "mdev": {
                          "type": "number"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "host": "example.com",
                  "address": "172.66.147.243",
                  "port": 443,
                  "state": "open",
                  "attempts": [
                    {
                      "seq": 1,
                      "status": "open",
                      "rtt_ms": 1.746
                    },
                    {
                      "seq": 2,
                      "status": "open",
                      "rtt_ms": 2.094
                    }
                  ],
                  "sent": 2,
                  "connected": 2,
                  "rtt_ms": {
                    "min": 1.746,
                    "avg": 1.92,
                    "max": 2.094,
                    "mdev": 0.174
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/traceroute": {
      "get": {
        "operationId": "traceroute",
        "tags": [
          "Network"
        ],
        "summary": "Trace the network path to a host",
        "description": "Sends TCP (default, port 443) or UDP probes with rising TTLs and reports the routers that answer, the way Linux's unprivileged `tracepath` does. In TCP mode the destination is recognised by its SYN-ACK or reset, so it's found even when ICMP is filtered.\n\nAll hops of a round are probed in parallel, so a trace takes about `wait` × `queries` seconds, plus up to 2 seconds of reverse DNS. Intermediate hops depend on ICMP replies getting back into AWS. When they don't, hops show as `*` (or `null` in JSON).\n\nPath form: `/traceroute/{host}`. Private, loopback and other non-public targets are refused.",
        "x-tool": "/traceroute/",
        "x-rate-limit": {
          "rate": 2,
          "burst": 5
        },
        "parameters": [
          {
            "name": "host",
            "in": "query",
            "required": true,
            "description": "Hostname or IP address.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          },
          {
            "name": "protocol",
            "in": "query",
            "description": "Probe type.",
            "schema": {
              "type": "string",
              "enum": [
                "tcp",
                "udp"
              ],
              "default": "tcp"
            }
          },
          {
            "name": "port",
            "in": "query",
            "description": "Destination port. Defaults to 443 for TCP and 33434 for UDP (UDP adds one per hop, like classic traceroute).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 65535
            }
          },
          {
            "name": "max_hops",
            "in": "query",
            "description": "Highest TTL to probe.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 30,
              "default": 30
            }
          },
          {
            "name": "queries",
            "in": "query",
            "description": "Probes per hop.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 3,
              "default": 3
            }
          },
          {
            "name": "wait",
            "in": "query",
            "description": "Seconds to wait for each round of probes.",
            "schema": {
              "type": "number",
              "minimum": 0.5,
              "maximum": 3,
              "default": 2
            }
          },
          {
            "name": "resolve",
            "in": "query",
            "description": "Look up hostnames for hop addresses. `0` turns it off.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "The hops up to the destination, or up to `max_hops`.",
            "content": {
              "text/plain": {
                "example": "traceroute to 1.1.1.1 (1.1.1.1), 30 hops max, TCP port 443\n 1  169.254.100.5  0.625 ms  0.333 ms\n 2  169.254.100.1  0.595 ms  0.339 ms\n 3  169.254.240.2  0.563 ms  0.317 ms\n 4  *  *\n 5  one.one.one.one (1.1.1.1)  14.305 ms  1.706 ms\n"
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "host": {
                      "type": "string"
                    },
                    "address": {
                      "type": "string"
                    },
                    "protocol": {
                      "type": "string",
                      "enum": [
                        "tcp",
                        "udp"
                      ]
                    },
                    "port": {
                      "type": "integer"
                    },
                    "max_hops": {
                      "type": "integer"
                    },
                    "queries": {
                      "type": "integer"
                    },
                    "reached": {
                      "type": "boolean"
                    },
                    "hops": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "hop": {
                            "type": "integer"
                          },
                          "probes": {
                            "type": "array",
                            "description": "One entry per query; null when nothing answered.",
                            "items": {
                              "type": [
                                "object",
                                "null"
                              ],
                              "properties": {
                                "ip": {
                                  "type": "string"
                                },
                                "rtt_ms": {
                                  "type": "number"
                                },
                                "annotation": {
                                  "type": [
                                    "string",
                                    "null"
                                  ],
                                  "description": "ICMP unreachable code, like !H or !N."
                                },
                                "hostname": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "host": "1.1.1.1",
                  "address": "1.1.1.1",
                  "protocol": "tcp",
                  "port": 443,
                  "max_hops": 30,
                  "queries": 1,
                  "reached": true,
                  "hops": [
                    {
                      "hop": 1,
                      "probes": [
                        {
                          "ip": "169.254.100.5",
                          "rtt_ms": 0.709,
                          "annotation": null,
                          "hostname": null
                        }
                      ]
                    },
                    {
                      "hop": 2,
                      "probes": [
                        null
                      ]
                    },
                    {
                      "hop": 3,
                      "probes": [
                        {
                          "ip": "1.1.1.1",
                          "rtt_ms": 1.706,
                          "annotation": null,
                          "hostname": "one.one.one.one"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/dig": {
      "get": {
        "operationId": "dig",
        "tags": [
          "Network"
        ],
        "summary": "Look up DNS records",
        "description": "Sends one DNS query and returns every section of the response, like `dig`. Without `server`, the query goes to the AWS resolver in us-east-1. An IP address on its own does a reverse (PTR) lookup.\n\nPath form: `/dig/{name}/{type}/@{server}`, where the type and `@server` segments are optional. Only public servers can be queried. ANY, AXFR and other meta-types are refused.",
        "x-tool": "/dig/",
        "x-rate-limit": {
          "rate": 10,
          "burst": 20
        },
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "required": true,
            "description": "Domain name, or an IP address for a reverse lookup.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          },
          {
            "name": "type",
            "in": "query",
            "description": "Record type.",
            "schema": {
              "type": "string",
              "default": "A",
              "examples": [
                "A",
                "AAAA",
                "CNAME",
                "MX",
                "NS",
                "TXT",
                "SOA",
                "CAA",
                "SRV",
                "PTR",
                "HTTPS",
                "DS",
                "DNSKEY"
              ]
            }
          },
          {
            "name": "server",
            "in": "query",
            "description": "Resolver or nameserver to ask, by IP address or hostname.",
            "schema": {
              "type": "string"
            },
            "example": "1.1.1.1"
          },
          {
            "name": "tcp",
            "in": "query",
            "description": "Query over TCP instead of UDP. UDP falls back to TCP on its own when an answer is truncated.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "dnssec",
            "in": "query",
            "description": "Set the DO bit to get DNSSEC records (RRSIG and friends).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "norecurse",
            "in": "query",
            "description": "Clear the RD bit, like `dig +norecurse`.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "short",
            "in": "query",
            "description": "Plain text only: print just the answer data, like `dig +short`.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "The DNS response, including NXDOMAIN and SERVFAIL answers.",
            "content": {
              "text/plain": {
                "example": "; <<>> a.rvrwlk.net/dig <<>> @1.1.1.1 example.com. MX\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 47821\n;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n;example.com.\t\tIN\tMX\n\n;; ANSWER SECTION:\nexample.com.\t175\tIN\tMX\t0 .\n\n;; Query time: 15.9 msec\n;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)\n;; WHEN: 2026-10-07T14:42:12+00:00\n"
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "question": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        }
                      }
                    },
                    "server": {
                      "type": "string"
                    },
                    "protocol": {
                      "type": "string",
                      "enum": [
                        "udp",
                        "tcp"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "description": "Response code: NOERROR, NXDOMAIN, SERVFAIL, REFUSED…"
                    },
                    "opcode": {
                      "type": "string"
                    },
                    "id": {
                      "type": "integer"
                    },
                    "flags": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "answer": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Record"
                      }
                    },
                    "authority": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Record"
                      }
                    },
                    "additional": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Record"
                      }
                    },
                    "query_time_ms": {
                      "type": "number"
                    },
                    "when": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "question": {
                    "name": "example.com.",
                    "type": "MX"
                  },
                  "server": "1.1.1.1",
                  "protocol": "udp",
                  "status": "NOERROR",
                  "opcode": "QUERY",
                  "id": 47821,
                  "flags": [
                    "qr",
                    "rd",
                    "ra"
                  ],
                  "answer": [
                    {
                      "name": "example.com.",
                      "ttl": 175,
                      "class": "IN",
                      "type": "MX",
                      "data": "0 ."
                    }
                  ],
                  "authority": [],
                  "additional": [],
                  "query_time_ms": 15.9,
                  "when": "2026-10-07T14:42:12+00:00"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "502": {
            "description": "The query failed, for example the server refused the connection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "query to 203.0.113.53 failed: [Errno 111] Connection refused",
                  "server": "203.0.113.53"
                }
              }
            }
          },
          "504": {
            "description": "The server didn't answer within 5 seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "no response from 203.0.113.53 within 5s",
                  "server": "203.0.113.53"
                }
              }
            }
          }
        }
      }
    },
    "/httpcheck": {
      "get": {
        "operationId": "httpcheck",
        "tags": [
          "Network"
        ],
        "summary": "Follow a URL's redirects",
        "description": "Requests a URL from AWS us-east-1 and follows its redirects, reporting each hop's status, headers and timing (DNS, connect, TLS, time to first byte), plus the final response's security headers.\\n\\nEvery hop's host is checked: a URL or redirect that points at a private, loopback or other non-public address is refused. HTTPS is verified against the Mozilla CA bundle, and a certificate failure ends the chain with an error. Bodies are read to at most 64 KB and never returned. Requests are sent with `User-Agent: a.rvrwlk.net/httpcheck`.\\n\\nPath form: `/httpcheck/{host}/{path}`, which requests `https://{host}/{path}`.\\n\\nA failure part-way along the chain is still a 200: `error` says what went wrong, and the hops up to that point are included.",
        "x-tool": "/httpcheck/",
        "x-rate-limit": {
          "rate": 3,
          "burst": 6
        },
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "description": "`http://` or `https://` URL. Without a scheme, `https://` is assumed.",
            "schema": {
              "type": "string",
              "format": "uri"
            },
            "example": "http://github.com/"
          },
          {
            "name": "method",
            "in": "query",
            "description": "Request method.",
            "schema": {
              "type": "string",
              "enum": [
                "GET",
                "HEAD"
              ],
              "default": "GET"
            }
          },
          {
            "name": "follow",
            "in": "query",
            "description": "Follow redirects.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "max_redirects",
            "in": "query",
            "description": "Redirects to follow before giving up.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 20,
              "default": 10
            }
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "The redirect chain, or as much of it as could be followed.",
            "content": {
              "text/plain": {
                "example": "HTTPCHECK GET http://github.com/\n 1  301 Moved Permanently  http://github.com/\n      -> https://github.com/\n      140.82.114.4  dns 1.4 ms  connect 76 ms  ttfb 62 ms\n 2  200 OK  https://github.com/  TLSv1.3\n      140.82.114.4  dns 1.6 ms  connect 58.9 ms  tls 90 ms  ttfb 114.5 ms\n\nfinal: 200 https://github.com/ (1 redirect)\n\nheaders:\n  content-type: text/html; charset=utf-8\n  strict-transport-security: max-age=31536000; includeSubdomains; preload\n\nsecurity headers:\n  strict-transport-security  max-age=31536000; includeSubdomains; preload\n  content-security-policy    default-src 'none'; …\n  x-frame-options            deny\n  x-content-type-options     nosniff\n  referrer-policy            origin-when-cross-origin, strict-origin-when-cross-origin\n  permissions-policy         (missing)\n"
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "method": {
                      "type": "string"
                    },
                    "final_url": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "URL of the last hop that answered."
                    },
                    "status": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "Status of the last hop that answered."
                    },
                    "redirects": {
                      "type": "integer",
                      "description": "Redirects followed."
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Why the chain stopped early: a connection or certificate failure, a refused private address, or too many redirects."
                    },
                    "hops": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "url": {
                            "type": "string"
                          },
                          "method": {
                            "type": "string"
                          },
                          "status": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "reason": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "http_version": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "location": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "address": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The address that was connected to."
                          },
                          "tls": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "properties": {
                              "version": {
                                "type": "string"
                              },
                              "cipher": {
                                "type": "string"
                              },
                              "bits": {
                                "type": "integer"
                              }
                            }
                          },
                          "headers": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "string"
                            },
                            "description": "Lower-cased names; repeated headers joined with `, ` (Set-Cookie with a newline)."
                          },
                          "body_bytes": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "body_truncated": {
                            "type": [
                              "boolean",
                              "null"
                            ]
                          },
                          "timing_ms": {
                            "type": "object",
                            "properties": {
                              "dns": {
                                "type": [
                                  "number",
                                  "null"
                                ]
                              },
                              "connect": {
                                "type": [
                                  "number",
                                  "null"
                                ]
                              },
                              "tls": {
                                "type": [
                                  "number",
                                  "null"
                                ]
                              },
                              "ttfb": {
                                "type": [
                                  "number",
                                  "null"
                                ]
                              }
                            }
                          },
                          "error": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "security_headers": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "The final response's security headers; `null` values are missing headers.",
                      "additionalProperties": {
                        "type": [
                          "string",
                          "null"
                        ]
                      }
                    }
                  }
                },
                "example": {
                  "url": "http://github.com/",
                  "method": "GET",
                  "final_url": "https://github.com/",
                  "status": 200,
                  "redirects": 1,
                  "error": null,
                  "hops": [
                    {
                      "url": "http://github.com/",
                      "method": "GET",
                      "status": 301,
                      "reason": "Moved Permanently",
                      "http_version": "HTTP/1.1",
                      "location": "https://github.com/",
                      "address": "140.82.114.4",
                      "tls": null,
                      "headers": {
                        "location": "https://github.com/"
                      },
                      "body_bytes": 0,
                      "body_truncated": false,
                      "timing_ms": {
                        "dns": 1.4,
                        "connect": 76,
                        "tls": null,
                        "ttfb": 62
                      }
                    },
                    {
                      "url": "https://github.com/",
                      "method": "GET",
                      "status": 200,
                      "reason": "OK",
                      "http_version": "HTTP/1.1",
                      "location": null,
                      "address": "140.82.114.4",
                      "tls": {
                        "version": "TLSv1.3",
                        "cipher": "TLS_AES_128_GCM_SHA256",
                        "bits": 128
                      },
                      "headers": {
                        "content-type": "text/html; charset=utf-8",
                        "strict-transport-security": "max-age=31536000; includeSubdomains; preload"
                      },
                      "body_bytes": 65536,
                      "body_truncated": true,
                      "timing_ms": {
                        "dns": 1.6,
                        "connect": 58.9,
                        "tls": 90,
                        "ttfb": 114.5
                      }
                    }
                  ],
                  "security_headers": {
                    "strict-transport-security": "max-age=31536000; includeSubdomains; preload",
                    "content-security-policy": "default-src 'none'; …",
                    "x-frame-options": "deny",
                    "x-content-type-options": "nosniff",
                    "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
                    "permissions-policy": null
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/tlscheck": {
      "get": {
        "operationId": "tlscheck",
        "tags": [
          "TLS"
        ],
        "summary": "Check a server's TLS certificate",
        "description": "Connects to a TLS server and reports whether a normal client would trust it, with the reason when it wouldn't (`certificate has expired`, `Hostname mismatch…`, `unable to get local issuer certificate`…). Also returns the certificate chain exactly as the server sent it (PEM), the negotiated version, cipher and ALPN, which TLS versions the server accepts, and connect and handshake times.\\n\\nThe chain comes from a handshake with verification off, so broken certificates can still be inspected; the verdict comes from a separate verifying handshake against the Mozilla CA bundle. `protocols` values are `true`/`false`, or `null` when a version couldn't be tested from AWS.\\n\\nPath forms: `/tlscheck/{host}`, `/tlscheck/{host}/{port}` and `/tlscheck/{host}:{port}`. Private, loopback and other non-public targets are refused.",
        "x-tool": "/tlscheck/",
        "x-rate-limit": {
          "rate": 3,
          "burst": 6
        },
        "parameters": [
          {
            "name": "host",
            "in": "query",
            "required": true,
            "description": "Hostname or IP address. A hostname resolves to one public address, IPv4 first.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          },
          {
            "name": "port",
            "in": "query",
            "description": "TCP port.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 65535,
              "default": 443
            }
          },
          {
            "name": "servername",
            "in": "query",
            "description": "Name to send as SNI and verify the certificate against. Defaults to `host`; set it to test a name on an IP address.",
            "schema": {
              "type": "string"
            },
            "example": "www.example.com"
          },
          {
            "name": "starttls",
            "in": "query",
            "description": "Upgrade a plain-text connection first: `smtp` (ports 25, 587), `imap` (143) or `pop3` (110).",
            "schema": {
              "type": "string",
              "enum": [
                "smtp",
                "imap",
                "pop3"
              ]
            }
          },
          {
            "name": "protocols",
            "in": "query",
            "description": "Test which of TLS 1.0-1.3 the server accepts. `0` skips it, which is faster.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "revocation",
            "in": "query",
            "description": "Check whether the leaf certificate has been revoked, by OCSP or else the CRL. `0` skips it.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "The verdict, the chain as sent, and the negotiated parameters. An untrusted certificate is still a 200, with `trusted: false`.",
            "content": {
              "text/plain": {
                "example": "TLSCHECK example.com:443 (172.66.147.243) SNI example.com\nTLSv1.3 TLS_AES_256_GCM_SHA384 (256 bits), ALPN h2\ntrusted: yes (verified chain of 4)\nchain: 4 certificates sent\n  0 CN=example.com  expires 2026-12-25 (78 days)  issuer CN=Cloudflare TLS Issuing ECC CA 3\n  1 CN=Cloudflare TLS Issuing ECC CA 3  expires 2035-05-27 (3153 days)  issuer CN=SSL.com TLS Transit ECC CA R2\n  2 CN=SSL.com TLS Transit ECC CA R2  expires 2037-10-17 (4027 days)  issuer CN=SSL.com TLS ECC Root CA 2022\n  3 CN=SSL.com TLS ECC Root CA 2022  expires 2028-12-31 (815 days)  issuer CN=AAA Certificate Services\nrevocation: good (OCSP http://o.cf-i.ssl.com)\nprotocols: TLSv1 TLSv1.1 TLSv1.2 TLSv1.3\ntiming: connect 70.1 ms, handshake 71.8 ms\n"
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "host": {
                      "type": "string"
                    },
                    "port": {
                      "type": "integer"
                    },
                    "address": {
                      "type": "string",
                      "description": "The address that was connected to."
                    },
                    "servername": {
                      "type": "string"
                    },
                    "starttls": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "tls": {
                      "type": "object",
                      "properties": {
                        "version": {
                          "type": "string"
                        },
                        "cipher": {
                          "type": "string"
                        },
                        "bits": {
                          "type": "integer"
                        },
                        "alpn": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    },
                    "protocols": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "Per version: accepted, refused, or `null` for couldn't test. `null` overall with `protocols=0`.",
                      "additionalProperties": {
                        "type": [
                          "boolean",
                          "null"
                        ]
                      }
                    },
                    "trusted": {
                      "type": [
                        "boolean",
                        "null"
                      ],
                      "description": "`null` if the verifying connection couldn't be made at all."
                    },
                    "verify_error": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "chain": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "PEM certificates in the order the server sent them, leaf first."
                    },
                    "verified_chain_length": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "Length of the path to a trusted root, when trusted."
                    },
                    "revocation": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "The leaf certificate's revocation status; `null` with `revocation=0`.",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "good",
                            "revoked",
                            "unknown"
                          ]
                        },
                        "method": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "enum": [
                            "ocsp",
                            "crl",
                            null
                          ],
                          "description": "Which source answered."
                        },
                        "url": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "The OCSP responder or CRL that answered."
                        },
                        "revoked_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "reason": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Revocation reason, e.g. `key_compromise`, when the CA gave one."
                        },
                        "this_update": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "next_update": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "error": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Why the status is `unknown`."
                        }
                      }
                    },
                    "timing_ms": {
                      "type": "object",
                      "properties": {
                        "connect": {
                          "type": "number"
                        },
                        "handshake": {
                          "type": "number"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "host": "example.com",
                  "port": 443,
                  "address": "172.66.147.243",
                  "servername": "example.com",
                  "starttls": null,
                  "tls": {
                    "version": "TLSv1.3",
                    "cipher": "TLS_AES_256_GCM_SHA384",
                    "bits": 256,
                    "alpn": "h2"
                  },
                  "protocols": {
                    "TLSv1": true,
                    "TLSv1.1": true,
                    "TLSv1.2": true,
                    "TLSv1.3": true
                  },
                  "trusted": true,
                  "verify_error": null,
                  "chain": [
                    "-----BEGIN CERTIFICATE-----\nMIID5jCCA42gAwIBAgIQAe7mqrtSHV4U/DFf2ZhWkDAKBggqhkjOPQQDAjBRMQsw\n…\n-----END CERTIFICATE-----\n"
                  ],
                  "verified_chain_length": 4,
                  "revocation": {
                    "status": "good",
                    "method": "ocsp",
                    "url": "http://o.cf-i.ssl.com",
                    "revoked_at": null,
                    "reason": null,
                    "this_update": "2026-10-08T12:00:00Z",
                    "next_update": "2026-10-15T12:00:00Z",
                    "error": null
                  },
                  "timing_ms": {
                    "connect": 70.1,
                    "handshake": 71.8
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "502": {
            "description": "No TLS connection could be made: the port is closed, or it doesn't speak TLS.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "TLS handshake failed: wrong version number"
                }
              }
            }
          }
        }
      }
    },
    "/ctlog": {
      "get": {
        "operationId": "ctlog",
        "tags": [
          "TLS"
        ],
        "summary": "Search Certificate Transparency logs",
        "description": "Lists the certificates publicly logged for a domain, newest first: useful to find forgotten subdomains, spot unexpected issuance, or check that a renewal happened. Precertificate and certificate entries for the same serial are merged.\\n\\nResults come from crt.sh, which can take 10-20 seconds; if it fails, from SSLMate Cert Spotter instead (`source` says which, and Cert Spotter entries have no `id` or `serial`). Results are cached for 10 minutes.\\n\\nPath form: `/ctlog/{domain}`.",
        "x-tool": "/ctlog/",
        "x-rate-limit": {
          "rate": 1,
          "burst": 3
        },
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "description": "Domain or hostname. A leading `*.` is ignored.",
            "schema": {
              "type": "string"
            },
            "example": "example.com"
          },
          {
            "name": "subdomains",
            "in": "query",
            "description": "Include certificates for names under the domain, wildcards included.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "expired",
            "in": "query",
            "description": "Include expired certificates.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Certificates to return, newest first.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "Logged certificates, newest first.",
            "content": {
              "text/plain": {
                "example": "CTLOG rvrwlk.net and subdomains (source: crt.sh)\n2 certificates\n\n2026-07-28 to 2027-02-10  Amazon RSA 2048 M01  https://crt.sh/?id=28315813309\n  w.rvrwlk.net\n\n2026-01-16 to 2027-02-14  Amazon RSA 2048 M01  https://crt.sh/?id=23809538266\n  a.rvrwlk.net\n\n--- 2 names seen ---\na.rvrwlk.net\nw.rvrwlk.net\n"
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "domain": {
                      "type": "string"
                    },
                    "subdomains": {
                      "type": "boolean"
                    },
                    "expired": {
                      "type": "boolean"
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "crt.sh",
                        "certspotter"
                      ]
                    },
                    "total": {
                      "type": "integer",
                      "description": "Certificates found."
                    },
                    "count": {
                      "type": "integer",
                      "description": "Certificates returned, at most `limit`."
                    },
                    "certificates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "crt.sh certificate ID."
                          },
                          "serial": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "issuer": {
                            "type": "string"
                          },
                          "names": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "not_before": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "not_after": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "link": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The certificate on crt.sh."
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "domain": "rvrwlk.net",
                  "subdomains": true,
                  "expired": false,
                  "source": "crt.sh",
                  "total": 1,
                  "count": 1,
                  "certificates": [
                    {
                      "id": 23809538266,
                      "serial": "01bbf8c6ea961f37a9bc8471a8afcdfc",
                      "issuer": "C=US, O=Amazon, CN=Amazon RSA 2048 M01",
                      "names": [
                        "a.rvrwlk.net"
                      ],
                      "not_before": "2026-01-16T00:00:00Z",
                      "not_after": "2027-02-14T23:59:59Z",
                      "link": "https://crt.sh/?id=23809538266"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "502": {
            "description": "Neither crt.sh nor Cert Spotter answered in time.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Certificate Transparency search failed (crt.sh: timed out after 15s; certspotter: HTTP 429)"
                }
              }
            }
          }
        }
      }
    },
    "/emailtest": {
      "post": {
        "operationId": "emailtest-post",
        "tags": [
          "Email"
        ],
        "summary": "Get a test address",
        "description": "Returns a one-time address at `emailtest.rvrwlk.net` and the `id` that reads its results. Send a message to the address from the server you want to test; a few seconds after it arrives, `GET /emailtest/{id}` shows SPF, DKIM, DMARC, reverse DNS, the HELO name, DNS blocklists, the route it took and every header.\n\nThe address and the `id` are unrelated: the address ends up in your mail logs, but only the `id` reads the results. An address takes up to 10 messages and lasts 7 days. `local+anything@` reaches the same test.\n\nTo be told when a result arrives, either long-poll `GET /emailtest/{id}?wait=20`, or pass a `webhook`: each result is POSTed to it as JSON, with `X-Emailtest-Signature: sha256=<HMAC-SHA256 of \"<X-Emailtest-Timestamp>.<body>\">` keyed with the returned `webhook.secret`. Webhooks must be public `https://` URLs.\n\nThe body is optional and read as JSON whatever the Content-Type, so browsers can send it as `text/plain`.",
        "x-tool": "/emailtest/",
        "x-rate-limit": {
          "rate": 5,
          "burst": 10
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "webhook": {
                    "type": "string",
                    "description": "Optional public `https://` URL to POST each result to."
                  }
                }
              },
              "examples": {
                "plain": {
                  "summary": "No webhook",
                  "value": {}
                },
                "webhook": {
                  "summary": "With a webhook",
                  "value": {
                    "webhook": "https://hooks.example.com/emailtest"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The test. `webhook.secret` is only ever shown here.",
            "content": {
              "text/plain": {
                "example": "Send a message to:  k7m2p9qx@emailtest.rvrwlk.net\n\nid:       3f6c1b9e-5d2a-4c1e-9b7f-2a8d4e6f1c3b\nexpires:  2026-10-15T15:00:00Z (7 days)\n\nResults:  curl https://a.rvrwlk.net/emailtest/3f6c1b9e-5d2a-4c1e-9b7f-2a8d4e6f1c3b\nWait:     curl 'https://a.rvrwlk.net/emailtest/3f6c1b9e-5d2a-4c1e-9b7f-2a8d4e6f1c3b?wait=20'\n"
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Reads the results. Keep it to yourself."
                    },
                    "address": {
                      "type": "string",
                      "description": "Where to send the test message."
                    },
                    "created": {
                      "type": "string"
                    },
                    "expires": {
                      "type": "string"
                    },
                    "max_messages": {
                      "type": "integer"
                    },
                    "status_url": {
                      "type": "string"
                    },
                    "wait_url": {
                      "type": "string",
                      "description": "The status URL as a 20-second long poll."
                    },
                    "webhook": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "url": {
                          "type": "string"
                        },
                        "secret": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "id": "3f6c1b9e-5d2a-4c1e-9b7f-2a8d4e6f1c3b",
                  "address": "k7m2p9qx@emailtest.rvrwlk.net",
                  "created": "2026-10-08T15:00:00Z",
                  "expires": "2026-10-15T15:00:00Z",
                  "max_messages": 10,
                  "status_url": "https://a.rvrwlk.net/emailtest/3f6c1b9e-5d2a-4c1e-9b7f-2a8d4e6f1c3b",
                  "wait_url": "https://a.rvrwlk.net/emailtest/3f6c1b9e-5d2a-4c1e-9b7f-2a8d4e6f1c3b?wait=20",
                  "webhook": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/emailtest/{id}": {
      "get": {
        "operationId": "emailtest-get",
        "tags": [
          "Email"
        ],
        "summary": "Get a test's results",
        "description": "The test and every message it has received so far, oldest first. `status` is `waiting` until the first message arrives.\n\nWith `wait`, the request is held until a new message arrives (the message count goes above `since`) or `wait` seconds pass, whichever is first, so a loop of `?wait=20&since=<message_count>` requests gets each result as soon as it's in.",
        "x-tool": "/emailtest/",
        "x-rate-limit": {
          "rate": 5,
          "burst": 10
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The test's `id` from `POST /emailtest` (not the address).",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "3f6c1b9e-5d2a-4c1e-9b7f-2a8d4e6f1c3b"
          },
          {
            "name": "wait",
            "in": "query",
            "description": "Seconds to wait for a new message before answering.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 20,
              "default": 0
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "With `wait`: the message count you already have. Answers as soon as there are more.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "The test and its messages.",
            "content": {
              "text/plain": {
                "example": "Address:  k7m2p9qx@emailtest.rvrwlk.net\nExpires:  2026-10-15T15:00:00Z\n\n── Message 1 of 1, received 2026-10-08T15:02:11Z ──\nFrom:      Alice <alice@example.com>\nSubject:   Test\nEnvelope:  alice@example.com from mail.example.com [203.0.113.5]\n\n  PASS  SPF             pass for example.com\n  PASS  DKIM            Valid: example.com (2048-bit)\n  WARN  DMARC           pass, p=none\n        p=none only monitors. Once reports show all your mail passing, move to quarantine and then reject.\n  PASS  Reverse DNS     mail.example.com\n  PASS  HELO name       mail.example.com\n  PASS  Blocklists      Not listed (4 lists)\n  INFO  TLS             Final hop not recorded\n  PASS  Spam & virus    Clean\n  PASS  Message format  Well formed\n\nRoute, oldest first:\n   1  laptop.lan [198.51.100.7] -> mail.example.com  ESMTPSA TLSv1.3 TLS_AES_256_GCM_SHA384\n   2  mail.example.com [203.0.113.5] -> inbound-smtp.us-east-1.amazonaws.com  SMTP  +2s\n"
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "address": {
                      "type": "string"
                    },
                    "created": {
                      "type": "string"
                    },
                    "expires": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "waiting",
                        "received"
                      ]
                    },
                    "message_count": {
                      "type": "integer"
                    },
                    "max_messages": {
                      "type": "integer"
                    },
                    "webhook": {
                      "type": [
                        "object",
                        "null"
                      ]
                    },
                    "messages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "received_at": {
                            "type": "string"
                          },
                          "checks": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string",
                                  "description": "`spf`, `dkim`, `dmarc`, `rdns`, `helo`, `blocklists`, `tls`, `content` or `format`."
                                },
                                "label": {
                                  "type": "string"
                                },
                                "status": {
                                  "type": "string",
                                  "enum": [
                                    "pass",
                                    "warn",
                                    "fail",
                                    "info"
                                  ]
                                },
                                "summary": {
                                  "type": "string"
                                },
                                "notes": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "tone": {
                                        "type": "string",
                                        "description": "`ok`, `warn`, `bad`, or empty for neutral."
                                      },
                                      "text": {
                                        "type": "string"
                                      }
                                    }
                                  }
                                }
                              }
                            },
                            "description": "The summary, one entry per check."
                          },
                          "envelope": {
                            "type": "object",
                            "description": "MAIL FROM, HELO, client IP, recipients and size, as SES saw them."
                          },
                          "ses": {
                            "type": "object",
                            "description": "Amazon SES's own SPF, DKIM, DMARC, spam and virus verdicts, for comparison."
                          },
                          "spf": {
                            "type": "object",
                            "description": "Result, the term that matched, DNS lookups used, and the include tree."
                          },
                          "dkim": {
                            "type": "array",
                            "description": "Every DKIM-Signature: domain, selector, algorithm, key size, result and reason, alignment.",
                            "items": {
                              "type": "object"
                            }
                          },
                          "dmarc": {
                            "type": "object",
                            "description": "The record that applies, its policy, and whether SPF and DKIM align with the From domain."
                          },
                          "arc": {
                            "type": "object"
                          },
                          "ip": {
                            "type": "object",
                            "description": "Reverse DNS (forward-confirmed), the HELO name, and DNS blocklist results."
                          },
                          "route": {
                            "type": "array",
                            "items": {
                              "type": "object"
                            },
                            "description": "Received headers, oldest first: from, IP, by, protocol, TLS, time, delay."
                          },
                          "message": {
                            "type": "object",
                            "description": "Key headers, MIME structure and attachments."
                          },
                          "headers": {
                            "type": "array",
                            "items": {
                              "type": "object"
                            },
                            "description": "Every header, unfolded, with `ses: true` on the ones SES added."
                          },
                          "headers_raw": {
                            "type": "string"
                          },
                          "webhook_delivery": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "description": "What happened when the result was POSTed to the webhook."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "No such test: the id is wrong, it expired, or it was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "no such test (it may have expired)"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "operationId": "emailtest-delete",
        "tags": [
          "Email"
        ],
        "summary": "Delete a test",
        "description": "Deletes the test, its address and its results now instead of when they expire.",
        "x-tool": "/emailtest/",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The test's `id` from `POST /emailtest` (not the address).",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "3f6c1b9e-5d2a-4c1e-9b7f-2a8d4e6f1c3b"
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "example": {
                  "deleted": "3f6c1b9e-5d2a-4c1e-9b7f-2a8d4e6f1c3b"
                }
              }
            }
          },
          "404": {
            "description": "No such test: the id is wrong, it expired, or it was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "no such test (it may have expired)"
                }
              }
            }
          }
        }
      }
    },
    "/secret": {
      "post": {
        "operationId": "secret-post",
        "tags": [
          "Secrets"
        ],
        "summary": "Store or open a secret",
        "description": "Send `secret` to store one. It's encrypted with a key derived from `key` (a passphrase) or `ip` (the only address allowed to open it), and you get back an ID.\n\nSend `id` without `secret` to open one. With `key`, it's decrypted with that passphrase. Without `key`, the API uses your source IP, which opens secrets locked to your address. A one-time secret is deleted as it's opened.\n\nThe body is JSON whatever the Content-Type, so browsers can send it as `text/plain` and skip the CORS preflight. Keep passphrases out of URLs: a `key` in the query string is rejected.",
        "x-tool": "/secret/",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "title": "Store a secret",
                    "type": "object",
                    "required": [
                      "secret"
                    ],
                    "properties": {
                      "secret": {
                        "type": "string",
                        "description": "The text to protect."
                      },
                      "key": {
                        "type": "string",
                        "description": "Passphrase the recipient will need. Give this or `ip`."
                      },
                      "ip": {
                        "type": "string",
                        "description": "The only source IP that can open the secret. Give this or `key`."
                      },
                      "singleuse": {
                        "type": "boolean",
                        "default": false,
                        "description": "Delete the secret the first time it's opened."
                      },
                      "expire": {
                        "type": "integer",
                        "default": 86400,
                        "description": "Seconds until the secret expires."
                      }
                    }
                  },
                  {
                    "title": "Open a secret",
                    "type": "object",
                    "required": [
                      "id"
                    ],
                    "properties": {
                      "id": {
                        "type": "string",
                        "description": "The ID returned when the secret was stored."
                      },
                      "key": {
                        "type": "string",
                        "description": "The passphrase. Leave it out for an IP-locked secret."
                      }
                    }
                  }
                ]
              },
              "examples": {
                "store-passphrase": {
                  "summary": "Store, locked with a passphrase",
                  "value": {
                    "secret": "correct-horse-battery-staple",
                    "key": "tell-them-by-phone",
                    "singleuse": true,
                    "expire": 3600
                  }
                },
                "store-ip": {
                  "summary": "Store, locked to an IP",
                  "value": {
                    "secret": "correct-horse-battery-staple",
                    "ip": "203.0.113.24",
                    "singleuse": true,
                    "expire": 86400
                  }
                },
                "open-passphrase": {
                  "summary": "Open with a passphrase",
                  "value": {
                    "id": "66d0440d-9328-4f05-8527-00c567efb923",
                    "key": "tell-them-by-phone"
                  }
                },
                "open-ip": {
                  "summary": "Open an IP-locked secret",
                  "value": {
                    "id": "66d0440d-9328-4f05-8527-00c567efb923"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored (an `id`), or opened (the `secret`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "secret": {
                      "type": "string"
                    },
                    "sourceIp": {
                      "type": "string"
                    }
                  }
                },
                "examples": {
                  "stored": {
                    "summary": "Stored",
                    "value": {
                      "id": "66d0440d-9328-4f05-8527-00c567efb923"
                    }
                  },
                  "opened": {
                    "summary": "Opened",
                    "value": {
                      "secret": "correct-horse-battery-staple",
                      "sourceIp": "203.0.113.24"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing fields, or the passphrase or source IP doesn't decrypt the secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Missing secret or key/ip parameter"
                }
              }
            }
          },
          "404": {
            "description": "No such secret: it expired, was already opened, or the ID is wrong. `sourceIp` is the address the API sees.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Secret not found",
                  "sourceIp": "203.0.113.24"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "secret-get",
        "tags": [
          "Secrets"
        ],
        "summary": "Open an IP-locked secret",
        "description": "Opens a secret locked to your source IP, for scripts that can only make GET requests. Secrets locked with a passphrase need the POST form, so the passphrase stays out of the URL.",
        "x-tool": "/secret/",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The secret's ID.",
            "schema": {
              "type": "string"
            },
            "example": "66d0440d-9328-4f05-8527-00c567efb923"
          }
        ],
        "responses": {
          "200": {
            "description": "The secret.",
            "content": {
              "application/json": {
                "example": {
                  "secret": "correct-horse-battery-staple",
                  "sourceIp": "203.0.113.24"
                }
              }
            }
          },
          "400": {
            "description": "Your source IP doesn't decrypt this secret, or `key` was sent in the URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Secret not found",
                  "sourceIp": "203.0.113.24"
                }
              }
            }
          }
        }
      }
    },
    "/cfn-admin": {
      "get": {
        "operationId": "cfn-admin",
        "tags": [
          "Riverwalk"
        ],
        "summary": "CloudFormation template for the rvrwlk-admin role",
        "description": "Returns a CloudFormation template that creates an IAM role Riverwalk Consulting's AWS account can assume. The `ExternalId` parameter defaults to the `externalId` you pass, or to a new random one. Deploy it with `CAPABILITY_NAMED_IAM`.\n\nTemplate parameters: `RoleName` (default `rvrwlk-admin`), `ManagedPolicyType` (`AdministratorAccess`, `PowerUserAccess`, `ReadOnlyAccess`, `ViewOnlyAccess` or `NONE`), `GrantBillingAccess` (`true`/`false`), `CustomManagedPolicyArn` and `ExternalId`.\n\nWith `action=generate-id`, returns only a new External ID as JSON.",
        "x-tool": "/cfn-admin/",
        "parameters": [
          {
            "name": "externalId",
            "in": "query",
            "description": "External ID to put in the template. A random UUID when omitted.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "action",
            "in": "query",
            "description": "`generate-id` returns a new External ID instead of the template.",
            "schema": {
              "type": "string",
              "enum": [
                "generate-id"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The template (`attachment; filename=\"rvrwlk-admin.yaml\"`), or a new External ID.",
            "content": {
              "application/x-yaml": {
                "example": "AWSTemplateFormatVersion: '2010-09-09'\nDescription: >-\n  rvrwlk-admin Role\n…"
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "externalId": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "externalId": "2fdb22ad-8487-4a33-88dd-cd3861b2404b"
                }
              }
            }
          }
        }
      }
    },
    "/mgmtprefixes": {
      "get": {
        "operationId": "mgmtprefixes",
        "tags": [
          "Riverwalk"
        ],
        "summary": "Riverwalk management IP prefixes",
        "description": "The CIDR ranges Riverwalk Consulting connects from, one per line after a `#` comment. Firewalls that load IP lists from a URL can use it directly.",
        "x-tool": "/mgmtprefixes/",
        "responses": {
          "200": {
            "description": "One CIDR per line.",
            "content": {
              "text/plain": {
                "example": "# Last Sync: 2026-10-07 14:42:05.151458\n50.198.1.176/28\n199.202.154.0/24\n75.212.151.185/32\n"
              }
            }
          }
        }
      }
    },
    "/sshkeys": {
      "get": {
        "operationId": "sshkeys",
        "tags": [
          "Riverwalk"
        ],
        "summary": "Riverwalk SSH public keys",
        "description": "Riverwalk Consulting's SSH public keys in `authorized_keys` format, after a `#` comment line. For example: `curl -fsS https://a.rvrwlk.net/sshkeys >> ~/.ssh/authorized_keys`.",
        "responses": {
          "200": {
            "description": "authorized_keys lines.",
            "content": {
              "text/plain": {
                "example": "# Last Sync: 2026-10-07 14:42:07.867312\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA… rvrwlk-mac\n"
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "format": {
        "name": "format",
        "in": "query",
        "description": "`json` for JSON. Plain text otherwise, unless the `Accept` header asks for `application/json`.",
        "schema": {
          "type": "string",
          "enum": [
            "text",
            "json"
          ]
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "sourceIp": {
            "type": "string"
          },
          "server": {
            "type": "string"
          }
        }
      },
      "Record": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "ttl": {
            "type": "integer"
          },
          "class": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "data": {
            "type": "string",
            "description": "The record data in zone-file form."
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "A missing or invalid argument, or a target that isn't a public address.",
        "content": {
          "text/plain": {
            "example": "error: 10.0.0.1 resolves to 10.0.0.1, which is not a public address\n"
          },
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "port must be between 1 and 65535"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limit exceeded. Wait a moment and retry. This response comes from API Gateway itself, without CORS headers.",
        "content": {
          "application/json": {
            "example": {
              "message": "Too Many Requests"
            }
          }
        }
      }
    }
  }
}
