# CNF API — brief for an agent

You can call the CNF API at `https://api.c.nf`. Version 2.0.0. This document is generated from the live route table, so it describes the API as it is right now.

## Authentication

Send `Authorization: Bearer cnf_<32 hex characters>`.
Tokens are issued at https://console.c.nf/account/api and each one carries a fixed
set of scopes. A token acts only for the account that issued it. There is no cross-user or administrative override anywhere in this API.

Endpoints under `/v1/public` take no token at all — do not send one.

If you do not know what your token can do, call `GET /v1/account/me` first. It works
with any valid token and returns both the granted scopes and the exact list of
endpoints they open. That is cheaper than discovering the limits one 403 at a time.

## Reading a response

Every JSON response has the same shape.

```json
{"success": true,  "data": { … },                    "request_id": "req_…"}
{"success": false, "error": "forbidden_scope", "message": "…", "request_id": "req_…"}
```

Branch on `error`, never on `message`. The codes are stable; the messages are prose
and may be translated or reworded. Quote `request_id` if you report a problem.

Codes you should handle:

- `bad_request` (HTTP 400)
- `unauthorized` (HTTP 401)
- `forbidden_scope` (HTTP 403)
- `forbidden_zone` (HTTP 403)
- `not_found` (HTTP 404)
- `method_not_allowed` (HTTP 405)
- `unprocessable` (HTTP 422)
- `upstream_error` (HTTP 502)

## Rules that will otherwise surprise you

1. A token sees only its owner's data. There is no administrator override, no
   impersonation parameter, and no cross-account listing. Asking for someone else's
   zone returns `forbidden_zone`, and no amount of retrying changes that.
2. DNS records are only reachable inside a zone the account has **verified**. Claim
   the zone, publish the TXT record it hands you, then call verify. Until the zone
   flips to verified, every record call on it fails.
3. A key created before a scope existed does not have that scope. An empty scope
   column means `dns:read` and nothing else — least privilege, deliberately.
4. Write actions on files and packages are POST with the identifier in the body
   (`POST /v1/files/delete` with `{"id": 42}`), not `DELETE /v1/files/42`.
   The DNS endpoints are the resource-shaped ones. Follow the paths listed below
   rather than inferring them.
5. Rates, WHOIS results and similar upstream data are cached. `cached: true` is
   normal and not a reason to retry.

## Endpoints

### Public utilities — `public`

Small dependable endpoints that need no account: time, address, country, exchange rates and hashes.

- **`GET /v1/public/time`** — The current UTC time in ISO 8601, RFC 2822 and Unix form. _(no auth)_
    - also answers at `/time`
- **`GET /v1/public/timestamp`** — The current Unix timestamp, in seconds and milliseconds. _(no auth)_
    - also answers at `/timestamp`
- **`GET /v1/public/ip`** — The address this request came from. _(no auth)_
    - also answers at `/ip`
- **`GET /v1/public/geo`** — The country this request came from, as resolved at the edge. _(no auth)_
    - also answers at `/ip-geolocation`
- **`GET /v1/public/fx`** — Exchange rates for a base currency, refreshed hourly. _(no auth)_
    - `base` (query, string) — ISO 4217 base currency. Defaults to USD.
    - `symbols` (query, string) — Comma-separated ISO 4217 codes to return. Defaults to all.
    - also answers at `/fx/rate`
- **`GET /v1/public/hash`** — Hash a string with SHA-256 or another supported algorithm. _(no auth)_
    - `value` (query, string, required) — The string to hash. Maximum 8192 bytes.
    - `algo` (query, string) — One of sha256, sha512, sha1, md5, crc32b. Defaults to sha256.
    - also answers at `/hash/sha256`

### Account — `account`

Who a token belongs to, and what it is allowed to do.

- **`GET /v1/account/me`** — The account behind the token, its scopes, and the endpoints they open. _(any token)_
- **`GET /v1/account/sessions`** — Every browser session currently signed in to this account. _(account:read)_
- **`POST /v1/account/sessions/revoke`** — Sign one session out. _(account:write)_
    - `id` (body, integer, required) — The session identifier from /v1/account/sessions.
- **`POST /v1/account/sessions/revoke-all`** — Sign every session out. _(account:write)_
- **`POST /v1/account/profile`** — Change your display name. _(account:write)_
    - `field` (body, string, required) — Only nickname may be changed here.
    - `value` (body, string, required) — The new display name, up to 64 characters.

### DNS — `dns`

Claim a domain, prove you control it, then manage its records.

- **`GET /v1/dns/zones`** — Every zone this account has claimed. _(dns:read)_
    - `status` (query, string) — verified, pending, or all. Defaults to all.
- **`POST /v1/dns/zones`** — Claim a zone. An uncontested claim is instant; a contested one gets a TXT challenge. _(dns:write)_
    - `zone` (body, string, required) — The apex domain to claim, for example example.com.
- **`GET /v1/dns/zones/{zone}`** — One zone, with its verification instructions if it is still pending. _(dns:read)_
    - `zone` (path, string, required) — The zone apex.
- **`POST /v1/dns/zones/{zone}/verify`** — Run the verification lookup and mark the zone verified if it matches. _(dns:write)_
    - `zone` (path, string, required) — The zone apex.
- **`DELETE /v1/dns/zones/{zone}`** — Release a claim. The records themselves are left alone. _(dns:write)_
    - `zone` (path, string, required) — The zone apex.
- **`GET /v1/dns/zones/{zone}/records`** — Every record in a verified zone. _(dns:read)_
    - `zone` (path, string, required) — The zone apex.
    - `type` (query, string) — Filter by record type, for example A or MX.
- **`POST /v1/dns/zones/{zone}/records`** — Add a record to a verified zone. _(dns:write)_
    - `zone` (path, string, required) — The zone apex.
    - `type` (body, string, required) — A, AAAA, CNAME, MX, TXT, NS, SRV, CAA.
    - `host` (body, string) — Subdomain label. Empty or @ means the apex.
    - `value` (body, string, required) — The record value.
    - `ttl` (body, integer) — Seconds. Defaults to 3600.
    - `priority` (body, integer) — Required for MX and SRV.
- **`GET /v1/dns/zones/{zone}/records/{id}`** — A single record. _(dns:read)_
    - `zone` (path, string, required) — The zone apex.
    - `id` (path, integer, required) — The record identifier.
- **`PATCH /v1/dns/zones/{zone}/records/{id}`** — Change part of a record; whatever you omit keeps its value. _(dns:write)_
    - `zone` (path, string, required) — The zone apex.
    - `id` (path, integer, required) — The record identifier.
    - `value` (body, string) — New value.
    - `ttl` (body, integer) — New TTL in seconds.
    - `priority` (body, integer) — New priority for MX and SRV.
- **`DELETE /v1/dns/zones/{zone}/records/{id}`** — Remove a record. _(dns:write)_
    - `zone` (path, string, required) — The zone apex.
    - `id` (path, integer, required) — The record identifier.

### Credentials — `cred`

The private credential store behind the credentials tool.

- **`GET /v1/cred`** — Stored credentials, optionally filtered by platform. _(cred:read or cred:write)_
    - `platform` (query, string) — Case-insensitive substring filter on the platform name.
- **`POST /v1/cred`** — Store a credential. _(cred:write)_
    - `platform` (body, string, required) — Site or service name. Maximum 255 characters.
    - `username` (body, string, required) — Login name. Maximum 255 characters.
    - `password` (body, string, required) — Secret. Maximum 4096 characters.
    - `email` (body, string) — Defaults to the username.
    - `remarks` (body, string) — Free-text note. Maximum 2000 characters.

### Files — `files`

Personal file storage: list, upload, edit, share and delete.

- **`GET /v1/files`** — Your stored files, newest first. _(files:read or files:write)_
    - `category` (query, string) — Filter by category.
    - `visibility` (query, string) — private or unlisted.
    - `limit` (query, integer) — 1 to 500. Defaults to 100.
    - `offset` (query, integer) — Rows to skip. Defaults to 0.
- **`GET /v1/files/{id}`** — One file, including its share link if it has one. _(files:read or files:write)_
    - `id` (path, integer, required) — The file identifier.
- **`GET /v1/files/download`** — Download a file. _(files:read or files:write)_
    - `id` (query, integer, required) — The file identifier.
- **`POST /v1/files/upload`** — Upload a file. _(files:write)_
    - `file` (form, file, required) — The file part. Size is capped per account.
    - `category` (form, string) — Optional grouping label.
    - `description` (form, string) — Optional description.
- **`POST /v1/files/update`** — Edit a file’s details, including whether it is shared. _(files:write)_
    - `id` (body, integer, required) — The file identifier.
    - `display_name` (body, string) — Shown in the console and on share pages.
    - `original_name` (body, string) — Filename offered on download.
    - `description` (body, string) — Free-text description.
    - `category` (body, string) — Grouping label.
    - `visibility` (body, string) — private or unlisted. Switching to unlisted mints a share token.
- **`POST /v1/files/rename`** — Change the name a file downloads as. _(files:write)_
    - `id` (body, integer, required) — The file identifier.
    - `original_name` (body, string, required) — New name. No slashes. Maximum 255 characters.
- **`POST /v1/files/delete`** — Delete a file and its contents. _(files:write)_
    - `id` (body, integer, required) — The file identifier.
- **`POST /v1/files/upload-chunk`** — Send one part of a large upload. _(files:write)_
    - `chunk` (form, file, required) — One part of the file, up to about 50 MB.
    - `index` (form, integer, required) — Zero-based part number.
    - `total` (form, integer, required) — How many parts there are, 1 to 400.
    - `filename` (form, string) — Required on the first part.
    - `id` (form, string) — Upload session id; omit on the first part and reuse what comes back.
- **`POST /v1/files/upload-finalize`** — Assemble the parts into a file. _(files:write)_
    - `id` (body, string, required) — The upload session id.
    - `display_name` (body, string) — Label shown in the console.
    - `description` (body, string) — Free-text description.
    - `category` (body, string) — Grouping label.
    - `visibility` (body, string) — private or unlisted.

### CDN — `cdn`

Edge caching and traffic for the files you share.

- **`GET /v1/cdn/stats`** — Cache settings and request counts for your shared files. _(cdn:read)_
- **`POST /v1/cdn/config`** — Set how long the edge keeps a file, or stop serving it. _(cdn:write)_
    - `file_id` (body, integer, required) — One of your files, shared as unlisted.
    - `ttl` (body, integer) — Edge cache lifetime in seconds, 0 to 31536000.
    - `enabled` (body, integer) — 1 to serve through the CDN, 0 to stop.
- **`POST /v1/cdn/purge`** — Drop a file from the edge cache now. _(cdn:write)_
    - `file_id` (body, integer, required) — One of your files.

### SMS forwarder — `sms`

Messages relayed from your phone, and the keys that let it in.

- **`GET /v1/sms/messages`** — Messages forwarded to your account. _(sms:read)_
    - `since` (query, integer) — Return only messages with an id above this one.
    - `limit` (query, integer) — How many to return.
- **`GET /v1/sms/keys`** — The forwarder keys your devices use. _(sms:read or sms:write)_
- **`POST /v1/sms/keys`** — Issue a forwarder key. Five at a time. _(sms:write)_
    - `name` (body, string) — A label for the device. Five keys maximum.
- **`DELETE /v1/sms/keys`** — Revoke a forwarder key. _(sms:write)_
    - `id` (body, integer, required) — The forwarder key identifier.

### Static sites — `sites`

Upload a zip, serve a site.

- **`GET /v1/sites`** — Your static sites, most recently deployed first. _(sites:read or sites:write)_
- **`POST /v1/sites/deploy`** — Deploy a zip as a site. _(sites:write)_
    - `file` (form, file, required) — A zip of the site, up to 50 MB. It is expanded into your storage.
    - `slug` (form, string, required) — 3 to 63 lowercase letters, digits or hyphens. Unique across all accounts.
    - `name` (form, string, required) — Display name, up to 128 characters.
- **`POST /v1/sites/delete`** — Delete a site and its files. _(sites:write)_
    - `id` (body, integer, required) — The site identifier.

### Deployments — `deploy`

The form, webhook and shortlink endpoints you have deployed.

- **`GET /v1/deployments`** — Your deployments and how often each has been used. _(deploy:read or deploy:write)_
- **`POST /v1/deployments/toggle`** — Enable or disable a deployment. _(deploy:write)_
    - `id` (body, integer, required) — The deployment identifier.
    - `enabled` (body, integer, required) — 1 to enable, 0 to disable.
- **`POST /v1/deployments/delete`** — Delete a deployment. _(deploy:write)_
    - `id` (body, integer, required) — The deployment identifier.

### Domains — `domain`

Your domain portfolio and its registration data.

- **`GET /v1/domains`** — Your domains, soonest to expire first. _(domain:read or domain:write)_
    - `status` (query, string) — Filter by status, for example active.
    - `expiring` (query, integer) — Only domains expiring within this many days.
    - `limit` (query, integer) — 1 to 1000. Defaults to 200.
    - `offset` (query, integer) — Rows to skip.
- **`POST /v1/domains/whois-sync`** — Refresh one domain from its registry record. _(domain:write)_
    - `udid` (body, string, required) — The portfolio identifier of one of your domains.

### Packages — `packages`

Publish releases and read back their versions and download counts.

- **`GET /v1/packages`** — Your packages, with version counts and download totals. _(packages:read or packages:write)_
    - `visibility` (query, string) — public, unlisted, or private.
- **`GET /v1/packages/{slug}`** — One package, with every version and artifact. _(packages:read or packages:write)_
    - `slug` (path, string, required) — The package slug.
- **`POST /v1/packages/publish`** — Publish a version, creating the package if it is new. _(packages:write)_
    - `slug` (form, string, required) — URL-safe identifier, unique within the account.
    - `version` (form, string, required) — Version string, for example 1.2.0.
    - `file` (form, file, required) — The artifact to publish.
    - `name` (form, string) — Display name. Defaults to the slug.
    - `description` (form, string) — Short description.
    - `category` (form, string) — Grouping label.
    - `visibility` (form, string) — public, unlisted, or private.
    - `changelog` (form, string) — Notes for this version.
    - `label` (form, string) — Artifact label when a version has several files.
- **`POST /v1/packages/update`** — Edit a package’s details. _(packages:write)_
    - `slug` (body, string, required) — The package slug. The slug itself cannot be changed.
    - `name` (body, string) — Display name.
    - `description` (body, string) — Short description.
    - `category` (body, string) — Grouping label.
    - `icon` (body, string) — Icon identifier.
    - `visibility` (body, string) — public, unlisted, or private.
- **`POST /v1/packages/delete`** — Delete a package, every version and every artifact. _(packages:write)_
    - `slug` (body, string, required) — The package slug.

### WHOIS — `whois`

Registration data for a domain, cached and shared with the domain tools.

- **`GET /v1/whois`** — Registration details for a domain. _(whois:read)_
    - `domain` (query, string, required) — The domain to look up. Unicode domains are punycoded for you.

## What this API deliberately cannot do

These exist as console features and are reachable only from a signed-in
session. They are listed so you do not spend turns looking for them, and so
you can tell the person you are helping why, rather than reporting a 404.

- **account/api-keys.php** — Minting API keys with an API key is privilege escalation: a token holding one narrow scope could issue itself a token holding every scope. Keys come from a signed-in session only.
- **account/passkey-delete.php** — Removing an authentication factor is not something a long-lived bearer credential should be able to do.
- **account/passkey-rename.php** — Same surface as passkey deletion; kept behind the session for the same reason.
- **admin/redirects.php** — Platform administration, not account data.
- **admin/seo-settings.php** — Platform administration, not account data.
- **admin/seo-overrides.php** — Platform administration, not account data.
- **admin/seo-sitemap-status.php** — Platform administration, not account data.
- **admin/seo-indexnow-ping.php** — Platform administration, not account data.
- **dns/admin-claims.php** — Reads other accounts' zone claims. This API has no cross-user surface at all, and adding one for administrators would make that claim untrue.
- **dns/admin-status.php** — Cross-user administration; see dns/admin-claims.
- **dns/zone-approve.php** — Approves another account's zone claim.
- **downloads/item-add.php** — Publishes to the public download catalogue on d.c.nf.
- **downloads/item-update.php** — Publishes to the public download catalogue on d.c.nf.
- **downloads/item-patch.php** — Publishes to the public download catalogue on d.c.nf.
- **downloads/item-delete.php** — Publishes to the public download catalogue on d.c.nf.
- **downloads/upload.php** — Publishes to the public download catalogue on d.c.nf.
- **downloads/upload-chunk.php** — Publishes to the public download catalogue on d.c.nf.
- **downloads/upload-finalize.php** — Publishes to the public download catalogue on d.c.nf.
- **downloads/upload-status.php** — Publishes to the public download catalogue on d.c.nf.
- **downloads/files-scan.php** — Lists the server filesystem under the download root.
- **deploy-recv/webhook.php** — A public receiver for deployed endpoints, authenticated by its own deployment token. Not account API.
- **deploy-recv/form.php** — Public receiver; see deploy-recv/webhook.
- **deploy-recv/shortlink.php** — Public receiver; see deploy-recv/webhook.
- **files/shared.php** — Serves a file to anyone holding its share token. That is already published at cdn.c.nf and needs no key.
- **whois/client.php** — A library, not an endpoint.
- **domain/registrar-map.php** — Writes registrar_map, which has no owner column: it is one table shared by every account's domain tool. A token editing it would be changing other people's data, and this API has no shared-state writes anywhere else. The per-domain registrar name and URL come back from GET /v1/domains instead.

The two worth understanding: a token cannot mint another token, and a token
cannot remove an authentication factor. If either were possible, the scopes
on every other endpoint would be advisory.

## Worked example

```bash
# 1. What can this token do?
curl -s -H "Authorization: Bearer $CNF_TOKEN" https://api.c.nf/v1/account/me

# 2. Which zones are verified?
curl -s -H "Authorization: Bearer $CNF_TOKEN" "https://api.c.nf/v1/dns/zones?status=verified"

# 3. Point api.example.com at an address.
curl -s -X POST -H "Authorization: Bearer $CNF_TOKEN" -H "Content-Type: application/json" \
  -d '{"type":"A","host":"api","value":"192.0.2.10","ttl":300}' \
  https://api.c.nf/v1/dns/zones/example.com/records
```

## Machine-readable forms

- Tool definitions (Anthropic and OpenAI shapes): <https://api.c.nf/v1/ai?format=json>
- OpenAPI 3.1: <https://api.c.nf/v1/openapi.json>
- JSON index: <https://api.c.nf/v1>
- Human reference: <https://api.c.nf/v1/docs>
