HTTP API
There are two HTTP APIs. Your app almost certainly wants the first one, and it is three lines of code. The second is real but encrypted end to end, so most people should leave it to the CLI and SDK.
| I want to… | Use |
|---|---|
| Read my variables from any language | The local agent API, started by dynv run |
| Read them from Node with live updates | The SDK |
| Change variables or manage tokens | The CLI, the dashboard, or MCP |
| Build a client of the server API | The server API below, and its caveats |
Local agent API: for your app
When you run your app with dynv run, it starts a tiny HTTP server on loopback (127.0.0.1, a random port) and gives your app two environment variables: DYNV_AGENT_URL and DYNV_AGENT_KEY. It has already fetched, verified and decrypted your variables, so you get plain JSON and no cryptography. It refuses to listen on anything but loopback.
GET /v1/env
curl -s -H "Authorization: Bearer $DYNV_AGENT_KEY" "$DYNV_AGENT_URL/v1/env"{
"version": 42,
"vars": { "PROXY_SERVER": "http://proxy.example:8000", "LOG_LEVEL": "info" },
"apply": { "PROXY_SERVER": "hot", "LOG_LEVEL": "restart" },
"managed": ["LOG_LEVEL", "OLD_KEY", "PROXY_SERVER"]
}vars: current values.apply: each variable's mode.managed: every key Dynv controls, including recently removed ones. A key inmanagedbut not invarshas been deleted: treat it as unset, not as whatever your container inherited.- Wrong or missing key:
401.
GET /v1/env/watch?since=V
A 30-second long-poll. It returns 200 with the same body as soon as the version is no longer V, or 204 with no body when nothing changed. Loop on it with the version you last saw:
import json, os, urllib.request
base, key = os.environ["DYNV_AGENT_URL"], os.environ["DYNV_AGENT_KEY"]
def fetch(path):
req = urllib.request.Request(base + path, headers={"Authorization": "Bearer " + key})
with urllib.request.urlopen(req, timeout=40) as r:
return None if r.status == 204 else json.load(r)
snap = fetch("/v1/env")
while True:
new = fetch(f"/v1/env/watch?since={snap['version']}")
if new:
snap = new
print("changed:", snap["vars"].get("PROXY_SERVER"))This is exactly what the Node SDK does. An official Python or Go SDK is Coming soon; until then these two GETs are all you need.
Server API: encrypted end to end
This is what the CLI, MCP server and dashboard speak to https://api.dynv.dev. It is JSON over HTTPS.
Authentication
Authorization: Dynv <kind>.<token_id>.<auth_key>kind is pt (service token) or mt (master token). token_id is the part of dynv_pt_<id>.<secret> before the dot. auth_key is derived from the token's secret with HKDF-SHA256 (label auth) and base64url-encoded: your raw secret is never sent, and the server stores only a SHA-256 of the derived key. A service token is accepted only on /v1/self and /v1/self/watch; a master token on everything else it is allowed.
- Roles: owner (writer) can do everything; recovery and viewer masters can read. A write by anyone else is refused.
- Failed authentication is throttled per IP and per token id. Request bodies are limited to 1 MiB. Responses are not cacheable.
- Use
https. The CLI itself refuses plainhttpexcept to loopback. - Errors:
401a bad or revoked token, or a master whose role cannot write,404unknown id,409a staleexpected_version(re-read and retry),410an invite that expired or was used,429throttled,400a malformed request. The body is{"error": "…"}.
Endpoints
Service read (what dynv run calls)
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /v1/self | service token | The service's encrypted env: account_id, process_id, envelope_ct (the data key, sealed to your token) and blob (the signed, encrypted document). |
GET | /v1/self/watch?version=V&gen=G | service token | Long-poll. Returns 200 with the same body as soon as the version or key generation differs from V/G, or 204 after at most 50 s. |
Services and tokens (what dynv admin calls)
Reads accept any master token. Writes need an owner (writer) master token.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /v1/processes | master | List services (ciphertext). |
GET | /v1/processes/{id} | master | One service. |
POST | /v1/processes | owner | Create a service. The id is generated by the client. |
DELETE | /v1/processes/{id} | owner | Delete a service and revoke its tokens. |
PUT | /v1/processes/{id}/blob | owner | Replace the encrypted document. Compare-and-swap on expected_version: a stale write is a 409. |
POST | /v1/processes/{id}/tokens | owner | Register a new service token (the client generated it and sends only a verifier plus ciphertext). |
POST | /v1/processes/{id}/rotate | owner | Rotate the service's data key; optionally revoke tokens in the same write. |
DELETE | /v1/tokens/{id} | owner | Revoke a service token. |
GET | /v1/audit?limit=N | master | Audit events, newest first. limit 1 to 1000, default 100. |
Account, members and sign-in (used by the dashboard)
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /v1/account | master | Your account and the keyring sealed to your token. |
GET | /v1/account/masters | master | List master tokens (devices) with role, member and last use. |
POST | /v1/account/masters | owner | Add a master token (a recovery kit or another device). |
DELETE | /v1/account/masters/{id} | master | Revoke one device. Viewers can revoke only their own. |
DELETE | /v1/account/members/{id} | master | Revoke all of a member's devices and cancel their invites. |
POST | /v1/account/email/start, /v1/account/email/verify | owner | Link an email to an account created before email sign-up. Once only. |
GET | /v1/invites | master | List invites. |
POST | /v1/invites | owner | Create an invite. The secret is in the link's #fragment and never reaches us. |
DELETE | /v1/invites/{id} | owner | Cancel an invite. |
GET | /v1/invites/self, POST /v1/invites/self/accept | invite token | Preview and accept an invite. |
POST | /v1/passkeys/challenge | none (rate limited) | Get a single-use WebAuthn challenge. |
POST | /v1/passkeys | owner | Enroll a passkey. |
POST | /v1/passkeys/login | none (WebAuthn proof) | Sign in with a passkey. |
POST | /v1/signup/start, /v1/signup/verify, /v1/signup/complete, /v1/auth/start | none | Email sign-up and email-first sign-in. They answer the same way for any address, so they cannot be used to find out who has an account. |
GET | /v1/meta | none | Which sign-in options the server offers. |
POST | /v1/bootstrap | setup secret | One-time account creation on a server you run yourself. |
GET | /healthz | none | Returns ok when the database answers. |
Shared variable groups have their own endpoints under /v1/shared (create, read, update, attach, detach, rotate). Like everything else, their payloads are encrypted and signed client-side, so use the CLI or the TypeScript crypto library rather than calling them by hand.
Example: read one service, raw
Fetching is the one call you can make without writing crypto, but what comes back is still ciphertext:
curl -s https://api.dynv.dev/v1/self \
-H "Authorization: Dynv pt.$TOKEN_ID.$AUTH_KEY"
# {"account_id":"…","process_id":"…","envelope_ct":"…","blob":{"version":42,"dek_gen":1,"ct":"…","sig":"…","signer":"…"}}To turn that into variables you must open envelope_ct with a key derived from your token, check the signature against the signers inside it, then decrypt blob.ct. That is the work dynv run does for you.