Docs / HTTP API

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 languageThe local agent API, started by dynv run
Read them from Node with live updatesThe SDK
Change variables or manage tokensThe CLI, the dashboard, or MCP
Build a client of the server APIThe 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

sh
curl -s -H "Authorization: Bearer $DYNV_AGENT_KEY" "$DYNV_AGENT_URL/v1/env"
response
{
  "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 in managed but not in vars has 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:

python
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 plain http except to loopback.
  • Errors: 401 a bad or revoked token, or a master whose role cannot write, 404 unknown id, 409 a stale expected_version (re-read and retry), 410 an invite that expired or was used, 429 throttled, 400 a malformed request. The body is {"error": "…"}.

Endpoints

Service read (what dynv run calls)

MethodPathAuthPurpose
GET/v1/selfservice tokenThe 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=Gservice tokenLong-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.

MethodPathAuthPurpose
GET/v1/processesmasterList services (ciphertext).
GET/v1/processes/{id}masterOne service.
POST/v1/processesownerCreate a service. The id is generated by the client.
DELETE/v1/processes/{id}ownerDelete a service and revoke its tokens.
PUT/v1/processes/{id}/blobownerReplace the encrypted document. Compare-and-swap on expected_version: a stale write is a 409.
POST/v1/processes/{id}/tokensownerRegister a new service token (the client generated it and sends only a verifier plus ciphertext).
POST/v1/processes/{id}/rotateownerRotate the service's data key; optionally revoke tokens in the same write.
DELETE/v1/tokens/{id}ownerRevoke a service token.
GET/v1/audit?limit=NmasterAudit events, newest first. limit 1 to 1000, default 100.

Account, members and sign-in (used by the dashboard)

MethodPathAuthPurpose
GET/v1/accountmasterYour account and the keyring sealed to your token.
GET/v1/account/mastersmasterList master tokens (devices) with role, member and last use.
POST/v1/account/mastersownerAdd a master token (a recovery kit or another device).
DELETE/v1/account/masters/{id}masterRevoke one device. Viewers can revoke only their own.
DELETE/v1/account/members/{id}masterRevoke all of a member's devices and cancel their invites.
POST/v1/account/email/start, /v1/account/email/verifyownerLink an email to an account created before email sign-up. Once only.
GET/v1/invitesmasterList invites.
POST/v1/invitesownerCreate an invite. The secret is in the link's #fragment and never reaches us.
DELETE/v1/invites/{id}ownerCancel an invite.
GET/v1/invites/self, POST /v1/invites/self/acceptinvite tokenPreview and accept an invite.
POST/v1/passkeys/challengenone (rate limited)Get a single-use WebAuthn challenge.
POST/v1/passkeysownerEnroll a passkey.
POST/v1/passkeys/loginnone (WebAuthn proof)Sign in with a passkey.
POST/v1/signup/start, /v1/signup/verify, /v1/signup/complete, /v1/auth/startnoneEmail 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/metanoneWhich sign-in options the server offers.
POST/v1/bootstrapsetup secretOne-time account creation on a server you run yourself.
GET/healthznoneReturns 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:

sh
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.