---
layout: default
title: API Reference
description: The proxy's management REST API — hosts, DNS providers, users, groups, and permissions.
---

# API Documentation

[← Back to Home](index.html)

All API endpoints require authentication unless otherwise noted. Three
authentication methods are supported:

- **`auth-token` header** — a browser-session token from `POST /api/auth/login`
  or the OIDC flow (below).
- **`Authorization: Bearer <token>` header** — a self-service API token (PAT,
  see [API Tokens](#api-tokens)), for scripts/CI without a browser session.
- **OIDC (browser)** — if the proxy is configured as an OIDC client of an SSO
  (`app_oidc__*` / `conf.oidc`, see [DEPLOYMENT.md](https://github.com/theta42/proxy/blob/master/DEPLOYMENT.md)),
  users can log in via `GET /api/auth/oidc/start` instead of posting a
  username/password.

The proxy can also be configured as a **direct LDAP client** (`app_ldap__*` /
`conf.ldap`) for looking up/validating users, independent of the OIDC flow —
see DEPLOYMENT.md for the full configuration reference.

Authenticated requests also carry **RBAC** (role-based access control):
global admins can manage everything; other users are scoped to `viewer` or
`manager` rights on specific domains via [Permissions](#permissions) and
[Groups](#groups).

Base URL: `https://your-proxy-host.com/api`

---

## Authentication

### Login

**POST** `/api/auth/login`

Authenticate a user and receive an auth token.

```bash
curl -H "Content-Type: application/json" \
  -X POST \
  -d '{"username": "myuser", "password": "mypassword"}' \
  https://proxy-host.com/api/auth/login
```

**Responses:**
- `200` `{"login": true, "token": "027d3964-7d81-4462-a6f9-2c1f9b40b4be", "message": "myuser logged in!"}`
- `401` `{"name": "LoginFailed", "message": "Invalid Credentials, login failed."}`

### Logout

**ALL** `/api/auth/logout`

Invalidate the current auth token.

```bash
curl -H "auth-token: your-token-here" \
  -X POST \
  https://proxy-host.com/api/auth/logout
```

**Responses:**
- `200` `{"message": "Bye"}`

### OIDC Login (start)

**GET** `/api/auth/oidc/start`

Begin the OIDC authorization-code flow: creates a PKCE + state challenge and
redirects the browser to the configured SSO's authorize endpoint. Only
available when `conf.oidc.enabled` is true.

**Query Parameters:**
- `redirect` - Internal path to return to after login (optional; sanitized to same-origin)

```bash
curl -i "https://proxy-host.com/api/auth/oidc/start?redirect=/hosts"
```

**Responses:**
- `302` Redirect to the SSO's authorization endpoint
- `404` `{"name": "OidcDisabled", "message": "OIDC login is not enabled."}`

### OIDC Callback

**GET** `/api/auth/oidc/callback`

Redirect target for the SSO after login. Validates the one-time `state`,
exchanges the authorization `code` for tokens, reads identity from the
userinfo endpoint, establishes a session, and redirects the browser back to
the login page with the app's own `auth-token` in a URL fragment.

**Query Parameters:**
- `code` (required) - Authorization code from the SSO
- `state` (required) - State value from the `start` step

```bash
# Not called directly — the SSO redirects the browser here after login.
```

**Responses:**
- `302` Redirect to `/login#token=...&redirect=...`
- `400` `{"name": "OidcCallbackInvalid", "message": "Missing code or state."}` or expired/unknown state

---

## API Tokens

Self-service personal access tokens (PATs) for scripting/CI without a browser
session. Every endpoint is owner-scoped: a user only sees/manages tokens they
created. Mounted at `/api/api-token`.

### List API Tokens

**GET** `/api/api-token`

List the current user's API tokens.

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/api-token
```

**Responses:**
- `200` `{"results": [{"id": "...", "name": "ci", ...}, ...]}`

### Create API Token

**POST** `/api/api-token`

Create a new API token. The raw token string is only returned once, at
creation.

**Parameters:**
- `name` (required) - Display name
- `description` (optional)
- `expires_in_days` (optional) - `0` or omitted means no expiry

```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X POST \
  -d '{"name": "ci", "expires_in_days": 90}' \
  https://proxy-host.com/api/api-token
```

**Responses:**
- `200` `{"results": {...}, "token": "prx_<id>_<secret>", "message": "API token 'ci' created. Save it now — it will not be shown again."}`

### Get API Token

**GET** `/api/api-token/:id`

Get a token's metadata (not the raw secret, which is never stored/returned again).

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/api-token/<id>
```

**Responses:**
- `200` `{"results": {...}}`
- `403` Not your token

### Update API Token

**PUT** `/api/api-token/:id`

Update a token's name/description/expiry.

```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X PUT \
  -d '{"name": "ci-updated"}' \
  https://proxy-host.com/api/api-token/<id>
```

**Responses:**
- `200` `{"results": {...}, "message": "API token 'ci-updated' updated."}`

### Delete (Revoke) API Token

**DELETE** `/api/api-token/:id`

Revoke a token immediately.

```bash
curl -H "auth-token: your-token-here" \
  -X DELETE \
  https://proxy-host.com/api/api-token/<id>
```

**Responses:**
- `200` `{"id": "<id>", "message": "API token 'ci' revoked."}`

### Rotate API Token

**POST** `/api/api-token/:id/rotate`

Issue a new secret for an existing token (same id, new raw value shown once).

```bash
curl -H "auth-token: your-token-here" \
  -X POST \
  https://proxy-host.com/api/api-token/<id>/rotate
```

**Responses:**
- `200` `{"token": "prx_<id>_<new-secret>", "message": "API token 'ci' rotated. Save it — it will not be shown again."}`

---

## Users

All user endpoints require authentication. `GET /me` and `PUT /password`
(self-service) work for any authenticated user; everything else (listing,
creating, deleting users, resetting another user's password) requires global
admin.

### List Users

**GET** `/api/user`

Get list of all users. Admin only.

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/user
```

**Query Parameters:**
- `detail` - Include full user details (optional)

**Responses:**
- `200` `{"results": ["user1", "user2"]}`
- `200` `{"results": [{"username": "user1", ...}, ...]}` (with `?detail=true`)
- `403` Not an admin

### Get Current User

**GET** `/api/user/me`

Get the currently authenticated user's identity and effective RBAC rights
(drives the web UI's nav/button gating).

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/user/me
```

**Responses:**
- `200` `{"username": "myuser", "groups": [...], "localGroups": [...], "externalGroups": [...], "isAdmin": false, "global": null, "domains": {...}}`

### Create User

**POST** `/api/user`

Create a new local user. Admin only.

```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X POST \
  -d '{"username": "newuser", "password": "newpassword"}' \
  https://proxy-host.com/api/user
```

**Responses:**
- `200` User created successfully
- `403` Not an admin
- `409` Username already exists
- `422` `{"name": "ObjectValidateError", "message": ...}` Validation error (also returned for weak passwords)

### Delete User

**DELETE** `/api/user/:username`

Delete a user account. Admin only.

```bash
curl -H "auth-token: your-token-here" \
  -X DELETE \
  https://proxy-host.com/api/user/olduser
```

**Responses:**
- `200` `{"username": "olduser", "results": ...}`
- `403` Not an admin
- `404` User not found

### Change Password (Self)

**PUT** `/api/user/password`

Change the password for the currently authenticated user.

```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X PUT \
  -d '{"password": "newpassword"}' \
  https://proxy-host.com/api/user/password
```

**Responses:**
- `200` `{"results": ...}` Password changed successfully
- `422` Weak password rejected by the password policy

### Change Password (Other User)

**PUT** `/api/user/password/:username`

Change the password for another user. Admin only.

```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X PUT \
  -d '{"password": "newpassword"}' \
  https://proxy-host.com/api/user/password/otheruser
```

**Responses:**
- `200` `{"results": ...}` Password changed successfully
- `403` Not an admin
- `404` User not found

---

## Permissions

RBAC: grants a `viewer` or `manager` role to a user or group, either globally
or scoped to one domain. Global-admin-only. Mounted at `/api/permission`.

### List Permissions

**GET** `/api/permission`

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/permission
```

**Responses:**
- `200` `{"results": [{"id": "...", "subjectType": "user", "subject": "alice", "role": "manager", "scope": "domain", "domain": "example.com", ...}, ...]}`

### List Permission Subjects

**GET** `/api/permission/subjects`

Autocomplete source for the "Subject" field: known usernames plus known group
names (local groups, groups already used in permissions, and groups from
`conf.auth.adminGroups` / `conf.auth.groupRoleMap`).

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/permission/subjects
```

**Responses:**
- `200` `{"users": ["alice", "bob"], "groups": ["ops", "sre"]}`

### Create Permission

**POST** `/api/permission`

Grant a role to a subject.

**Parameters:**
- `subjectType` (required) - `user` or `group`
- `subject` (required) - username or group name
- `role` (required) - `viewer` or `manager`
- `scope` (required) - `global` or `domain`
- `domain` (required if `scope` is `domain`)

```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X POST \
  -d '{"subjectType": "user", "subject": "alice", "role": "manager", "scope": "domain", "domain": "example.com"}' \
  https://proxy-host.com/api/permission
```

**Responses:**
- `200` `{"message": "Granted manager to user \"alice\" on example.com.", ...}`
- `422` Validation error

### Delete Permission

**DELETE** `/api/permission/:id`

```bash
curl -H "auth-token: your-token-here" \
  -X DELETE \
  https://proxy-host.com/api/permission/<id>
```

**Responses:**
- `200` `{"message": "Permission <id> removed."}`

---

## Groups

Local groups (independent of any SSO/LDAP groups) used as subjects for
permission grants. Global-admin-only. Mounted at `/api/group`.

### List Groups

**GET** `/api/group`

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/group
```

**Responses:**
- `200` `{"results": [{"name": "ops", "members": ["alice", "bob"], ...}, ...]}`

### Create Group

**POST** `/api/group`

**Parameters:**
- `name` (required)
- `members` (optional) - array of usernames

```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X POST \
  -d '{"name": "ops", "members": ["alice"]}' \
  https://proxy-host.com/api/group
```

**Responses:**
- `200` `{"message": "Group \"ops\" created.", ...}`

### Delete Group

**DELETE** `/api/group/:name`

```bash
curl -H "auth-token: your-token-here" \
  -X DELETE \
  https://proxy-host.com/api/group/ops
```

**Responses:**
- `200` `{"message": "Group \"ops\" removed."}`

### Add Group Member

**POST** `/api/group/:name/members`

**Parameters:**
- `username` (required)

```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X POST \
  -d '{"username": "bob"}' \
  https://proxy-host.com/api/group/ops/members
```

**Responses:**
- `200` `{"message": "Added \"bob\" to \"ops\".", ...}`

### Remove Group Member

**DELETE** `/api/group/:name/members/:username`

```bash
curl -H "auth-token: your-token-here" \
  -X DELETE \
  https://proxy-host.com/api/group/ops/members/bob
```

**Responses:**
- `200` `{"message": "Removed \"bob\" from \"ops\".", ...}`

---

## Hosts

Manage proxy host configurations.

### List Hosts

**GET** `/api/host`

Get list of all configured hosts.

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/host
```

**Query Parameters:**
- `detail` - Include full host details (optional)

**Responses:**
- `200` `{"results": ["example.com", "*.wildcard.com"]}`
- `200` `{"results": [{"host": "example.com", "ip": "192.168.1.10", ...}, ...]}` (with `?detail=true`)

### Get Host

**GET** `/api/host/:host`

Get configuration for a specific host.

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/host/example.com
```

**Responses:**
- `200` `{"item": "example.com", "results": {"host": "example.com", "ip": "192.168.1.10", "targetPort": 8080, ...}}`
- `404` `{"name": "HostNotFound", "message": "Host does not exists"}`

### Lookup Host

**GET** `/api/host/lookup/:domain`

Test the host lookup algorithm (supports wildcard matching).

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/host/lookup/sub.example.com
```

**Responses:**
- `200` `{"string": "sub.example.com", "results": {"host": "*.example.com", ...}}`
- `200` `{"string": "sub.example.com", "results": null}` (no match)

### Get Lookup Tree

**GET** `/api/host/lookupobj`

Get the internal lookup tree structure (for debugging).

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/host/lookupobj
```

**Responses:**
- `200` `{"results": {"com": {"example": {...}}}}`

### Create Host

**POST** `/api/host`

Add a new host configuration.

**Parameters:**
- `host` (required) - Domain name (e.g., `example.com`, `*.example.com`)
- `ip` (required) - Target IP address or FQDN
- `targetPort` (required) - Target port number (1-65535)
- `forcessl` (optional) - Force HTTPS redirect (default: true)
- `targetssl` (optional) - Use HTTPS to backend (default: false)
- `challengeType` (optional) - For wildcards: `DNS-01-wildcard` or `wildcardChild`

```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X POST \
  -d '{"host": "example.com", "ip": "192.168.1.10", "targetPort": 8080, "forcessl": true, "targetssl": false}' \
  https://proxy-host.com/api/host
```

**Responses:**
- `200` `{"message": "\"example.com\" added.", "host": "example.com", ...}`
- `409` `{"name": "HostNameUsed", "message": "Host already exists"}`
- `422` `{"name": "ObjectValidateError", "message": ...}` Validation error

### Update Host

**PUT** `/api/host/:host`

Update an existing host configuration.

**Parameters:** Same as Create Host (all optional)

```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X PUT \
  -d '{"ip": "192.168.1.20", "targetPort": 9000}' \
  https://proxy-host.com/api/host/example.com
```

**Responses:**
- `200` `{"message": "\"example.com\" updated.", ...}`
- `404` `{"name": "HostNotFound", "message": "Host does not exists"}`
- `422` Validation error

### Delete Host

**DELETE** `/api/host/:host`

Remove a host configuration.

```bash
curl -H "auth-token: your-token-here" \
  -X DELETE \
  https://proxy-host.com/api/host/example.com
```

**Responses:**
- `200` `{"message": "example.com deleted", ...}`
- `404` `{"name": "HostNotFound", "message": "Host does not exists"}`

### Clear Host Cache

**DELETE** `/api/host/cache`

Remove all cached wildcard-subdomain host lookups. Cache entries are created on
demand when a wildcard host serves a subdomain; clearing them forces the next
request for each subdomain to be resolved fresh through the lookup tree.
Admin only.

```bash
curl -H "auth-token: your-token-here" \
  -X DELETE \
  https://proxy-host.com/api/host/cache
```

**Responses:**
- `200` `{"message": "Cleared 3 cached hosts.", "count": 3}`

### Renew Wildcard Certificate

**PUT** `/api/host/:host/renew`

Manually trigger wildcard certificate renewal.

```bash
curl -H "auth-token: your-token-here" \
  -X PUT \
  https://proxy-host.com/api/host/*.example.com/renew
```

**Responses:**
- `200` `{"message": "Requesting wildcard cert for *.example.com"}`
- `404` Host not found

---

## DNS Providers

Manage DNS provider integrations for wildcard SSL certificates.

### List DNS Providers

**GET** `/api/dns`

Get list of configured DNS providers.

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/dns
```

**Query Parameters:**
- `detail` - Include full provider details (optional)

**Responses:**
- `200` `{"results": ["provider-id-1", "provider-id-2"]}`

### List Available Provider Types

**OPTIONS** `/api/dns`

Get list of supported DNS provider types and their configuration requirements.

```bash
curl -H "auth-token: your-token-here" \
  -X OPTIONS \
  https://proxy-host.com/api/dns
```

**Responses:**
- `200` `{"results": [{"name": "Cloudflare", "fields": {...}}, {"name": "DigitalOcean", ...}, {"name": "PorkBun", ...}, {"name": "DuckDns", ...}]}`

### Create DNS Provider

**POST** `/api/dns`

Configure a new DNS provider.

**Cloudflare:**
```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X POST \
  -d '{"name": "My Cloudflare", "dnsProvider": "Cloudflare", "token": "your-api-token"}' \
  https://proxy-host.com/api/dns
```

**DigitalOcean:**
```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X POST \
  -d '{"name": "My DO", "dnsProvider": "DigitalOcean", "token": "your-api-token"}' \
  https://proxy-host.com/api/dns
```

**PorkBun:**
```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X POST \
  -d '{"name": "My PorkBun", "dnsProvider": "PorkBun", "apiKey": "pk_xxx", "secretApiKey": "sk_xxx"}' \
  https://proxy-host.com/api/dns
```

**DuckDNS (free):**
```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X POST \
  -d '{"name": "My DuckDNS", "dnsProvider": "DuckDns", "token": "your-duckdns-token", "subdomains": "myhost,myhost2"}' \
  https://proxy-host.com/api/dns
```

`subdomains` is a comma-separated list of the subdomains you've registered at
[duckdns.org](https://www.duckdns.org) (e.g. `myhost` for
`myhost.duckdns.org`), since DuckDNS has no API to list them for you.
DuckDNS only supports one A/AAAA record and one TXT record per domain (no
arbitrary sub-records) — enough for dynamic DNS and DNS-01 wildcard certs.

**Responses:**
- `200` `{"message": "\"provider-id\" added.", ...}`
- `422` Validation error or invalid API credentials

### Get DNS Provider

**GET** `/api/dns/:id`

Get a specific DNS provider configuration.

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/dns/provider-id
```

**Responses:**
- `200` `{"item": "provider-id", "results": {...}}`
- `404` Provider not found

### Update DNS Provider

**PUT** `/api/dns/:id`

Update DNS provider configuration.

```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X PUT \
  -d '{"name": "Updated Name"}' \
  https://proxy-host.com/api/dns/provider-id
```

**Responses:**
- `200` `{"message": "\"provider-id\" updated.", ...}`
- `404` Provider not found

### Delete DNS Provider

**DELETE** `/api/dns/:id`

Remove a DNS provider and all associated domains.

```bash
curl -H "auth-token: your-token-here" \
  -X DELETE \
  https://proxy-host.com/api/dns/provider-id
```

**Responses:**
- `200` `{"message": "provider-id deleted", ...}`
- `404` Provider not found

### List Domains

**GET** `/api/dns/domain`

List all domains from all configured providers.

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/dns/domain
```

**Query Parameters:**
- `detail` - Include full domain details (optional)

**Responses:**
- `200` `{"results": ["example.com", "test.com"]}`

### Get Domain

**GET** `/api/dns/domain/:domain`

Get details for a specific domain.

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/dns/domain/example.com
```

**Responses:**
- `200` `{"results": [{"domain": "example.com", "zoneId": "...", ...}]}`
- `404` Domain not found

### Refresh Domains

**POST** `/api/dns/domain/refresh/:providerId`

Refresh the domain list from a DNS provider's API.

```bash
curl -H "auth-token: your-token-here" \
  -X POST \
  https://proxy-host.com/api/dns/domain/refresh/provider-id
```

**Responses:**
- `200` `{"results": ...}` Updated domain list
- `404` Provider not found

### Dynamic DNS

A-records kept automatically pointed at this box's public (WAN) IP. All
`/api/dns/dynamic*` routes are viewer/manager scoped to the record's domain
(via [Permissions](#permissions)), not admin-only like the rest of `/api/dns`.

#### Get Current Public IP

**GET** `/api/dns/dynamic/ip`

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/dns/dynamic/ip
```

**Responses:**
- `200` `{"ip": "203.0.113.5"}`

#### List Dynamic Records

**GET** `/api/dns/dynamic`

Lists records the caller may view (their own/granted domains, or all for admins).

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/dns/dynamic
```

**Responses:**
- `200` `{"results": [{"id": "...", "domain": "example.com", "name": "home", "last_status": "ok", ...}, ...]}`

#### Create Dynamic Record

**POST** `/api/dns/dynamic`

Requires `manager` rights on the target domain. Applies the record immediately
against the current public IP (best-effort — failures are recorded in
`last_status` and retried by the scheduler).

**Parameters:**
- `domain` (required)
- `name` (required) - sub-label, or `@` for the apex

```bash
curl -H "Content-Type: application/json" \
  -H "auth-token: your-token-here" \
  -X POST \
  -d '{"domain": "example.com", "name": "home"}' \
  https://proxy-host.com/api/dns/dynamic
```

**Responses:**
- `200` `{"message": "\"home.example.com\" added.", ...}`
- `403` Missing `manager` rights on the domain
- `422` Validation error

#### Refresh Dynamic Record

**POST** `/api/dns/dynamic/:id/refresh`

Force an immediate refresh of one record against the current public IP.
Requires `manager` rights on the record's domain.

```bash
curl -H "auth-token: your-token-here" \
  -X POST \
  https://proxy-host.com/api/dns/dynamic/<id>/refresh
```

**Responses:**
- `200` `{"message": "Refreshed \"home.example.com\".", "result": {...}}`
- `403` Missing `manager` rights on the domain

#### Delete Dynamic Record

**DELETE** `/api/dns/dynamic/:id`

Stop managing a record. Requires `manager` rights on the record's domain.
Leaves the provider's A record in place at its last value.

```bash
curl -H "auth-token: your-token-here" \
  -X DELETE \
  https://proxy-host.com/api/dns/dynamic/<id>
```

**Responses:**
- `200` `{"message": "home.example.com removed.", ...}`
- `403` Missing `manager` rights on the domain

---

## Certificates

Retrieve SSL certificate information.

### Get Certificate

**GET** `/api/cert/:host`

Get the SSL certificate for a host.

```bash
curl -H "auth-token: your-token-here" \
  https://proxy-host.com/api/cert/example.com
```

**Responses:**
- `200` Certificate data including `cert_pem`, `fullchain_pem`, `privkey_pem`, expiry information
- `404` Certificate not found

---

## Error Responses

All endpoints may return the following error responses:

- `401` `{"name": "LoginFailed", "message": "Invalid Credentials, login failed."}` - Authentication required or invalid
- `404` `{"name": "NotFound", "message": "..."}` - Resource not found
- `422` `{"name": "ObjectValidateError", "message": [...], "keys": [...]}` - Validation errors
- `500` Internal server error

## Notes

- All timestamps are in milliseconds since epoch
- Authenticated endpoints accept either the `auth-token` header (browser
  session / OIDC login) or an `Authorization: Bearer <token>` API token
- Host names support wildcards: `*` (single level) and `**` (multi-level)
- DNS providers are validated on creation - invalid API credentials will be rejected
- Wildcard certificates are automatically renewed 30 days before expiration
