02 · REST API

28 endpoints, six tags.

View as Markdown

All responses are JSON. Every request must carry an Authorization: Bearer $DONUT_API_KEY header — see Authentication. Set the env var in your shell once and every example below will pick it up.

profiles

Manage isolated browser profiles — create, list, update, delete, launch, kill, drive.

GET /v1/profiles

Returns every profile in the local install.

Request

curl -sS -X GET http://127.0.0.1:10108/v1/profiles \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

{
  "profiles": [
    {
      "id": "ce490dd0-02fe-4be5-b91d-fedec7550b28",
      "name": "Work · Chromium",
      "browser": "wayfern",
      "version": "147.0.7727.138",
      "release_type": "stable",
      "group_id": "75f2b42f-8c94-4291-8af7-7a3d79122507",
      "proxy_id": null,
      "vpn_id": null,
      "tags": [],
      "is_running": true,
      "process_id": 29300,
      "last_launch": 1779106515,
      "camoufox_config": null,
      "launch_hook": null,
      "proxy_bypass_rules": []
    },
    {
      "id": "c20221fa-be36-485d-a4e1-5537254fa863",
      "name": "Personal · Firefox",
      "browser": "camoufox",
      "version": "v150.0.2-beta.25",
      "release_type": "stable",
      "group_id": null,
      "proxy_id": null,
      "vpn_id": null,
      "tags": [
        "personal"
      ],
      "is_running": false,
      "process_id": null,
      "last_launch": 1779226206,
      "camoufox_config": {},
      "launch_hook": null,
      "proxy_bypass_rules": []
    }
  ]
}
POST /v1/profiles

Both browser engines need their own config object. Pass `wayfern_config: {}` and `camoufox_config: {}` for sensible defaults.

Request

curl -sS -X POST http://127.0.0.1:10108/v1/profiles \
  -H "Authorization: Bearer $DONUT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "name": "client-1",
      "browser": "chromium",
      "version": "latest",
      "proxy_id": "c5617755-0709-4820-832a-6b97a0a24f3c",
      "tags": [
        "client-1",
        "prod"
      ],
      "wayfern_config": {},
      "camoufox_config": {}
    }'

Response

{
  "profile": {
    "id": "prf_01J8...",
    "name": "client-1",
    "browser": "chromium",
    "version": "latest",
    "release_type": "stable",
    "group_id": null,
    "proxy_id": "c5617755-0709-4820-832a-6b97a0a24f3c",
    "vpn_id": null,
    "tags": [
      "client-1",
      "prod"
    ],
    "is_running": false,
    "process_id": null,
    "last_launch": null,
    "camoufox_config": {},
    "launch_hook": null,
    "proxy_bypass_rules": []
  }
}
GET /v1/profiles/{id}

Fetches the full profile object by ID.

Request

curl -sS -X GET http://127.0.0.1:10108/v1/profiles/{id} \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

{
  "profile": {
    "id": "ce490dd0-02fe-4be5-b91d-fedec7550b28",
    "name": "Work · Chromium",
    "browser": "wayfern",
    "version": "147.0.7727.138",
    "release_type": "stable",
    "group_id": "75f2b42f-8c94-4291-8af7-7a3d79122507",
    "proxy_id": null,
    "vpn_id": null,
    "tags": [],
    "is_running": true,
    "process_id": 29300,
    "last_launch": 1779106515,
    "camoufox_config": null,
    "launch_hook": null,
    "proxy_bypass_rules": []
  }
}
PUT /v1/profiles/{id}

Any subset of fields can be sent. `camoufox_config` is required by the spec; pass the current value if you don't want to change it.

Request

curl -sS -X PUT http://127.0.0.1:10108/v1/profiles/{id} \
  -H "Authorization: Bearer $DONUT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "name": "Work · Chromium — renamed",
      "proxy_id": "c5617755-0709-4820-832a-6b97a0a24f3c",
      "tags": [
        "work",
        "client-1"
      ],
      "camoufox_config": {}
    }'

Response

{
  "profile": {
    "id": "...",
    "name": "Work · Chromium — renamed",
    "...": "..."
  }
}
DELETE /v1/profiles/{id}

Hard delete. The profile data on disk is removed.

Request

curl -sS -X DELETE http://127.0.0.1:10108/v1/profiles/{id} \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

204 No Content
POST /v1/profiles/{id}/run
Pro

Spawns the browser process for the profile and returns the CDP port. Optionally headless and/or opening at a specific URL.

Request

curl -sS -X POST http://127.0.0.1:10108/v1/profiles/{id}/run \
  -H "Authorization: Bearer $DONUT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "url": "https://whoer.net",
      "headless": false
    }'

Response

{
  "cdp_port": 9222,
  "process_id": 41218
}

Without an active Pro subscription this endpoint returns HTTP 402 Payment Required.

POST /v1/profiles/{id}/kill

Terminates the browser process for the profile.

Request

curl -sS -X POST http://127.0.0.1:10108/v1/profiles/{id}/kill \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

204 No Content
POST /v1/profiles/{id}/open-url
Pro

Opens the given URL as a new tab in a profile that's already running.

Request

curl -sS -X POST http://127.0.0.1:10108/v1/profiles/{id}/open-url \
  -H "Authorization: Bearer $DONUT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "url": "https://news.ycombinator.com"
    }'

Response

200 OK

Without an active Pro subscription this endpoint returns HTTP 402 Payment Required.

groups

Organize profiles into named groups for bulk actions.

GET /v1/groups

Every group, with its current profile count.

Request

curl -sS -X GET http://127.0.0.1:10108/v1/groups \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

[
  {
    "id": "75f2b42f-8c94-4291-8af7-7a3d79122507",
    "name": "Work",
    "profile_count": 12
  },
  {
    "id": "bfda155d-0c04-4fc2-ab62-30651557a510",
    "name": "Personal",
    "profile_count": 3
  }
]
POST /v1/groups

Create a named group profiles can be assigned to.

Request

curl -sS -X POST http://127.0.0.1:10108/v1/groups \
  -H "Authorization: Bearer $DONUT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "name": "Client – Acme"
    }'

Response

{
  "id": "grp_01J8...",
  "name": "Client – Acme",
  "profile_count": 0
}
GET /v1/groups/{id}

Fetches a single group by ID.

Request

curl -sS -X GET http://127.0.0.1:10108/v1/groups/{id} \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

{
  "id": "bfda155d-0c04-4fc2-ab62-30651557a510",
  "name": "Personal",
  "profile_count": 3
}
PUT /v1/groups/{id}

Updates the group name.

Request

curl -sS -X PUT http://127.0.0.1:10108/v1/groups/{id} \
  -H "Authorization: Bearer $DONUT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "name": "Client – Acme (2026)"
    }'

Response

{
  "id": "grp_01J8...",
  "name": "Client – Acme (2026)",
  "profile_count": 8
}
DELETE /v1/groups/{id}

Profiles assigned to the group are not deleted — their `group_id` is set to null.

Request

curl -sS -X DELETE http://127.0.0.1:10108/v1/groups/{id} \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

204 No Content

tags

Flat list of every tag used across your profiles.

GET /v1/tags

Flat deduplicated list of every tag string used across your profiles.

Request

curl -sS -X GET http://127.0.0.1:10108/v1/tags \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

[
  "client-1",
  "prod",
  "personal",
  "burner"
]

proxies

HTTP, HTTPS, SOCKS4, SOCKS5 proxy definitions. Attach to a profile via `proxy_id`.

GET /v1/proxies

Every proxy definition stored locally.

Request

curl -sS -X GET http://127.0.0.1:10108/v1/proxies \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

[
  {
    "id": "c5617755-0709-4820-832a-6b97a0a24f3c",
    "name": "Albania",
    "proxy_settings": {
      "proxy_type": "https",
      "host": "proxy.example.com",
      "port": 8080,
      "username": "user",
      "password": "secret"
    }
  }
]
POST /v1/proxies

`proxy_type` ∈ `http` | `https` | `socks4` | `socks5`. `username`/`password` are optional for unauthenticated proxies.

Request

curl -sS -X POST http://127.0.0.1:10108/v1/proxies \
  -H "Authorization: Bearer $DONUT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "name": "us-residential",
      "proxy_settings": {
        "proxy_type": "socks5",
        "host": "proxy.example.com",
        "port": 1080,
        "username": "user",
        "password": "secret"
      }
    }'

Response

{
  "id": "pxy_01J8...",
  "name": "us-residential",
  "proxy_settings": {
    "proxy_type": "socks5",
    "host": "proxy.example.com",
    "port": 1080,
    "username": "user",
    "password": "secret"
  }
}
GET /v1/proxies/{id}

Returns the single proxy definition matching the ID.

Request

curl -sS -X GET http://127.0.0.1:10108/v1/proxies/{id} \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

{
  "id": "c5617755-0709-4820-832a-6b97a0a24f3c",
  "name": "Albania",
  "proxy_settings": {
    "proxy_type": "https",
    "host": "proxy.example.com",
    "port": 8080,
    "username": "user",
    "password": "secret"
  }
}
PUT /v1/proxies/{id}

Send the full updated `proxy_settings` and optionally a new `name`.

Request

curl -sS -X PUT http://127.0.0.1:10108/v1/proxies/{id} \
  -H "Authorization: Bearer $DONUT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "name": "us-residential-rotating",
      "proxy_settings": {
        "proxy_type": "socks5",
        "host": "rotate.example.com",
        "port": 1080
      }
    }'

Response

{
  "id": "pxy_01J8...",
  "name": "us-residential-rotating",
  "proxy_settings": {
    "proxy_type": "socks5",
    "host": "rotate.example.com",
    "port": 1080,
    "username": null,
    "password": null
  }
}
DELETE /v1/proxies/{id}

Profiles that referenced the proxy will have their `proxy_id` set to null.

Request

curl -sS -X DELETE http://127.0.0.1:10108/v1/proxies/{id} \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

204 No Content

vpns

WireGuard VPN configs. Attach to a profile via `vpn_id`.

GET /v1/vpns

Every VPN config stored locally.

Request

curl -sS -X GET http://127.0.0.1:10108/v1/vpns \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

[
  {
    "id": "22034ceb-7b76-456e-9cfe-a4b207751f8d",
    "name": "Test WG",
    "vpn_type": "WireGuard",
    "created_at": 1777203528,
    "last_used": null
  }
]
POST /v1/vpns

`config_data` is the raw WireGuard `.conf` content. Newlines must be preserved.

Request

curl -sS -X POST http://127.0.0.1:10108/v1/vpns \
  -H "Authorization: Bearer $DONUT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "name": "DE — Frankfurt",
      "vpn_type": "WireGuard",
      "config_data": "[Interface]\nPrivateKey = <base64>\nAddress = 10.0.0.2/32\n\n[Peer]\nPublicKey = <base64>\nEndpoint = de.example.com:51820\nAllowedIPs = 0.0.0.0/0"
    }'

Response

{
  "id": "vpn_01J8...",
  "name": "DE — Frankfurt",
  "vpn_type": "WireGuard",
  "created_at": 1779226206,
  "last_used": null
}
POST /v1/vpns/import

Same as `POST /v1/vpns`, but you pass the file `content` plus `filename` (used to derive `name` if omitted).

Request

curl -sS -X POST http://127.0.0.1:10108/v1/vpns/import \
  -H "Authorization: Bearer $DONUT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "filename": "de-frankfurt.conf",
      "name": "DE — Frankfurt",
      "content": "[Interface]\nPrivateKey = <base64>\n…\n[Peer]\nPublicKey = <base64>\nEndpoint = de.example.com:51820"
    }'

Response

{
  "id": "vpn_01J8...",
  "name": "DE — Frankfurt",
  "vpn_type": "WireGuard",
  "created_at": 1779226206,
  "last_used": null
}
GET /v1/vpns/{id}

Returns metadata for a single VPN config. The raw key material is not exposed.

Request

curl -sS -X GET http://127.0.0.1:10108/v1/vpns/{id} \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

{
  "id": "22034ceb-7b76-456e-9cfe-a4b207751f8d",
  "name": "Test WG",
  "vpn_type": "WireGuard",
  "created_at": 1777203528,
  "last_used": null
}
PUT /v1/vpns/{id}

Currently only the `name` field is updatable on existing VPNs.

Request

curl -sS -X PUT http://127.0.0.1:10108/v1/vpns/{id} \
  -H "Authorization: Bearer $DONUT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "name": "DE — Frankfurt (primary)"
    }'

Response

{
  "id": "vpn_01J8...",
  "name": "DE — Frankfurt (primary)",
  "vpn_type": "WireGuard",
  "created_at": 1779226206,
  "last_used": null
}
DELETE /v1/vpns/{id}

Profiles that referenced the VPN will have their `vpn_id` set to null.

Request

curl -sS -X DELETE http://127.0.0.1:10108/v1/vpns/{id} \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

204 No Content

browsers

Trigger browser-binary downloads and check installed versions.

POST /v1/browsers/download

Triggers a download of the named browser at the named version (Chromium via Wayfern, Firefox via Camoufox).

Request

curl -sS -X POST http://127.0.0.1:10108/v1/browsers/download \
  -H "Authorization: Bearer $DONUT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "browser": "wayfern",
      "version": "147.0.7727.138"
    }'

Response

{
  "started": true,
  "browser": "wayfern",
  "version": "147.0.7727.138"
}
GET /v1/browsers/{browser}/versions

Versions are returned newest first.

Request

curl -sS -X GET http://127.0.0.1:10108/v1/browsers/{browser}/versions \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

[
  "147.0.7727.138",
  "147.0.7727.137"
]
GET /v1/browsers/{browser}/versions/{version}/downloaded

Returns `true` when the browser+version pair is on disk.

Request

curl -sS -X GET http://127.0.0.1:10108/v1/browsers/{browser}/versions/{version}/downloaded \
  -H "Authorization: Bearer $DONUT_API_KEY"

Response

true