# Public API on heedeo.com

These endpoints are live on heedeo.com so you can check how Heedeo behaves without signing up. The machine-readable spec is at https://heedeo.com/openapi.json. `/api/visitor`, `/api/decide` and `/api/policy` send `Access-Control-Allow-Origin: *`.

## GET /api/visitor

Classifies the caller and really verifies Web Bot Auth signatures. See https://heedeo.com/docs/security.md for every check.

```bash
curl https://heedeo.com/api/visitor
```

Real output for plain curl:

```json
{"kind":"agent","name":"script","signed":false,"verified":false,"verifiedFor":null,"summary":"Not signed. Classified from the user agent only.","signature":{"present":false,"valid":false,"trusted":false,"verified":false,"agentOrigin":null,"keyId":null,"reason":"no signature headers"}}
```

| Field | Meaning |
|---|---|
| `kind` | `"human"`, `"agent"` or `"crawler"`. |
| `name` | The recognised agent, for example `"ChatGPT agent"`, or `"script"` for plain HTTP clients like curl. |
| `signed` | Signature headers are present. Not a verification. |
| `verified` | A valid signature and a trusted operator. |
| `verifiedFor` | The operator origin when verified, otherwise null. |
| `summary` | One sentence on how the request was classified. |
| `signature.present` | Signature headers were present. |
| `signature.valid` | The signature checked out against the operator's published key, and was not a replay. |
| `signature.trusted` | The operator is on `trustedOperators`. |
| `signature.verified` | `valid` and `trusted`. |
| `signature.agentOrigin` | The operator origin from `Signature-Agent`. |
| `signature.keyId` | The `keyid` the signature names. |
| `signature.reason` | Why it did or didn't verify, for example `"no signature headers"`, `"expired"`, `"key not found in directory"`, `"replayed signature"`. |

A request recognised by user agent but not verified (`name` set, `verified` false) is what the docs call **detected**; rules treat it as unknown.

## GET or POST /api/decide

Runs the real policy engine on the demo policy (https://heedeo.com/docs/rules.md).

Input, as query parameters (GET) or a JSON body (POST):

| Field | Required | Meaning |
|---|---|---|
| `route` | yes | A path starting with `/`, up to 512 characters. Normalized before matching. |
| `client` | yes | `"human"` or `"agent"`. |
| `signed` | no | `true` when the Web Bot Auth signature is valid. Accepts `true`, `false`, `1`, `0`. |
| `operator` | no | The signing operator's host, for example `chatgpt.com`. `signed=true` counts as verified only if the operator is on `trustedOperators`. |
| `principal` | no | Who the request acts for, up to 120 characters. Echoed back, never put in `reason`. |
| `count` | no | Requests by this principal on this route in the current window, counting this one. A whole number. Needs `principal`. |
| `method` | no | An HTTP method. Accepted; the demo policy has no per-method rules. |

Response fields: `verdict`, `status` (200, 401, 429 or 403), `retryAfter` (seconds, throttle only, otherwise null), `reason`, `rule` (always set: `path`, `line`, `text`), `principal` (echoed), `policy` (`"demo"`) and `note`.

```bash
curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=true&operator=chatgpt.com&principal=maya&count=7"
```

Real output:

```json
{"verdict":"throttle","status":429,"retryAfter":3600,"reason":"This person is at 7/5 per day on /signup, over the cap. Rate-limited to 1/hour up to 10, then blocked.","rule":{"path":"/signup","line":13,"text":"overCap: { throttle: 5, rate: \"1/hour\", then: \"block\" },"},"principal":"maya","policy":"demo","note":"Same engine and rules shown on heedeo.com. Your own rules are set up during onboarding."}
```

A valid signature from an operator that isn't trusted:

```bash
curl "https://heedeo.com/api/decide?route=/signup&client=agent&signed=true&operator=agent.example-operator.com"
```

```json
{"verdict":"challenge","status":401,"retryAfter":null,"reason":"Unknown agent on /signup: challenge. The signature is valid, but agent.example-operator.com is not in trustedOperators, so the agent counts as unknown.","rule":{"path":"/signup","line":10,"text":"unknown: { challenge: \"passkey\" },"},"principal":null,"policy":"demo","note":"Same engine and rules shown on heedeo.com. Your own rules are set up during onboarding."}
```

POST works the same:

```bash
curl -X POST https://heedeo.com/api/decide \
  -H "Content-Type: application/json" \
  -d '{"route":"/checkout","client":"agent","signed":false}'
```

```json
{"verdict":"block","status":403,"retryAfter":null,"reason":"Unknown agent on /checkout: block. The request is not signed.","rule":{"path":"/checkout","line":15,"text":"\"/checkout\": { agents: { verified: \"allow\", unknown: \"block\" } },"},"principal":null,"policy":"demo","note":"Same engine and rules shown on heedeo.com. Your own rules are set up during onboarding."}
```

Bad input returns 400 with `ok: false`, an `error` and a `help` string. For example, a `count` without a `principal` returns the error `"count" needs a "principal": the cap is counted per principal.`

## GET /api/policy

Returns the demo policy: `source` (the 18 lines that `rule.line` refers to) and `rules` (the same, parsed).

```bash
curl https://heedeo.com/api/policy
```

## POST /api/book

Request a call for a person. Only `email` is required.

```bash
curl -X POST https://heedeo.com/api/book \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","via":"agent","agent":"Your agent name","site":"example.com","pain":"what agents are doing on the site"}'
```

| Field | Type | Meaning |
|---|---|---|
| `email` | string, required | The person who wants access. |
| `site` | string | Their site. |
| `pain` | string | What agents are doing on their site. |
| `via` | `"human"` or `"agent"` | `"agent"` when an agent asks for someone. Defaults to `"human"`. |
| `agent` | string | The agent's name, up to 80 characters. |

- Requests from agents, and from plain scripts like curl, stay `pending` until the person confirms.
- Success is HTTP 200 with `ok`, `status`, `existing` and a `message` you can pass on as is. Errors are HTTP 400 with an `error` field.
- `GET /api/book` returns 405 with `Allow: POST`.
- `POST /api/waitlist` is an older alias with the same contract.

## Documents

| Path | Content |
|---|---|
| `GET /openapi.json` | OpenAPI spec for `/api/book`, `/api/waitlist`, `/api/decide`, `/api/policy` and `/api/visitor` |
| `GET /index.md` | The landing page as markdown. `GET /` with `Accept: text/markdown` returns the same. |
| `GET /llms.txt` | Map of the site for language models |
| `GET /llms-full.txt` | llms.txt plus every doc, in one file |
| `GET /docs.md` | Docs index |

---

Docs index: https://heedeo.com/docs.md. How to verify our claims: https://heedeo.com/trust.md. Book a call: https://cal.com/heedeo/30min

Updated: 2026-10-02
