Sign in Get API keys
API Reference · v1

QuickZTNA API

The QuickZTNA API is organized around REST. It uses action-based dispatch inside a single-handler pattern, returns JSON-encoded responses wrapped in a consistent envelope, and takes JWT bearer tokens or org-scoped API keys. 57 public endpoints across 10 logical groups.

POST https://login.quickztna.com/api/key-management

Creates a new auth key — the credential used by the ztna agent installer to enrol a device. Reusable keys cover fleet rollouts (100 devices in 2 minutes via MDM or Ansible); ephemeral keys are ideal for CI/CD runners that disappear when idle.

Authentication

Every authenticated request carries a bearer token in the Authorization header. QuickZTNA supports three credential types:

  • JWT access token — ES256-signed, 1-hour TTL. Issued by /api/auth/login and rotated via /api/auth/refresh. Used by the dashboard SPA.
  • API key — long-lived, revocable, org-scoped. Used by SCIM, Terraform, and your own automation. Created via action: "create_api_key".
  • Node key — machine-to-server credential issued at first registration. Used only by the ztna agent for heartbeat and key exchange.
Keep your keys safe API keys carry tenant-wide privileges. Never expose them in client-side code, mobile apps, or public repos. Use rotate_api_key on a 90-day rotation schedule.

Response envelope

Every response is wrapped in a consistent envelope. Application-level success is in success, payload in data, and failures populate error with a machine-readable code and a human-readable message.

{
  "success": true,
  "data": { /* payload */ },
  "error": null
}
{
  "success": false,
  "data": null,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable"
  }
}

Create an auth key

Creates an auth key that the ztna agent uses to enrol devices into your tailnet. The key appears in the ZTNA_AUTH_KEY environment variable read by the installer.

POST /api/key-management · action: "create_auth_key"

Body parameters

action string required

Must be "create_auth_key". Do not use the generic "create" — that form does not exist and will 404.

org_id string required

The organization ID this key belongs to. You must be an admin or owner of this org.

reusable boolean optional

If true, the key can enrol multiple machines. Required for fleet rollouts. Default false (one-shot key).

ephemeral boolean optional

If true, enrolled machines auto-deregister when they go offline. Useful for CI runners and containers. Default false.

expires_in integer optional

Seconds until the key becomes invalid. Minimum 300, maximum 2592000 (30 days). Default 3600 (1 hour).

tags array of strings optional

Tags applied to machines enrolled with this key. Prefix with tag: — e.g. ["tag:laptop", "tag:prod"]. Used in ACL rules.

description string optional

Optional human-readable note for audit. Max 255 chars.

Returns

Returns an auth_key object on success. The key field is only returned on creation — you cannot retrieve it again later.

Authentication endpoints

POST /api/auth/signup

Create a new user account. Returns JWT pair + refresh cookie on success. Rate-limited to 5 per IP per 5 min.

$ curl https://login.quickztna.com/api/auth/signup \
    -H "Content-Type: application/json" \
    -d '{
      "email": "you@company.com",
      "password": "Str0ngP@ssw0rd!",
      "full_name": "Your Name"
    }'
201 Created Response · 412ms
{
  "success": true,
  "data": {
    "user_id": "user_5k4aLw3Jm",
    "email": "you@company.com",
    "email_verified": false,
    "token": "eyJhbGciOiJFUzI1NiIs...",
    "refresh_token": "rt_7K9xqP4BmT...",
    "expires_in": 3600
  },
  "error": null
}

// Set-Cookie: __Host-refresh_token=rt_...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=2592000
Anti-enumeration If the email is already registered, signup returns 201 with a placeholder UUID — not a 409 conflict. This prevents attackers from enumerating registered emails.

POST /api/auth/login

Exchange email + password for JWT pair. TOTP required if MFA is enabled (returns 403 MFA_REQUIRED if code missing). Rate-limited 10 per IP and 10 per email per 5 min.

$ curl https://login.quickztna.com/api/auth/login \
    -H "Content-Type: application/json" \
    -d '{
      "email": "you@company.com",
      "password": "Str0ngP@ssw0rd!",
      "totp_code": "487392"
    }'
200 OK Response · 186ms
{
  "success": true,
  "data": {
    "user_id": "user_5k4aLw3Jm",
    "email": "you@company.com",
    "token": "eyJhbGciOiJFUzI1NiIs...",
    "refresh_token": "rt_7K9xqP4BmT...",
    "expires_in": 3600
  },
  "error": null
}

POST /api/auth/refresh

Rotate the access token. Browsers send __Host-refresh_token cookie automatically; CLI sends refresh_token in body. Server deletes old refresh token and issues a new pair (single-use rotation).

$ curl https://login.quickztna.com/api/auth/refresh \
    -X POST \
    -b "__Host-refresh_token=rt_..." \
    -d '{}'

# CLI / non-browser: POST /api/auth/refresh with {"refresh_token":"rt_..."}
200 OK Response · 42ms
{
  "success": true,
  "data": {
    "token": "eyJhbGciOiJFUzI1NiIs...",
    "refresh_token": "rt_8Ly4rN2xQ...",
    "expires_in": 3600
  },
  "error": null
}

GET /api/auth/me

Return the authenticated user's profile. Checks JWT, checks blacklist (returns 401 if password was changed).

$ curl https://login.quickztna.com/api/auth/me \
    -H "Authorization: Bearer $TOKEN"
200 OK Response · 18ms
{
  "success": true,
  "data": {
    "id": "user_5k4aLw3Jm",
    "email": "you@company.com",
    "full_name": "Your Name",
    "avatar_url": null
  },
  "error": null
}

POST /api/auth/mfa/setup

Generate a new TOTP secret. Scan the returned URI with an authenticator, then call /api/auth/mfa/verify with the first 6-digit code to activate + receive 10 backup codes.

$ curl https://login.quickztna.com/api/auth/mfa/setup \
    -X POST \
    -H "Authorization: Bearer $TOKEN"
200 OK Response · 24ms
{
  "success": true,
  "data": {
    "secret": "JBSWY3DPEHPK3PXP",
    "uri": "otpauth://totp/QuickZTNA:you@company.com?secret=JBSWY3DPEHPK3PXP&issuer=QuickZTNA"
  },
  "error": null
}

API keys

POST /api/key-management (action: "create_api_key")

Create a long-lived, revocable API key for machine-to-machine use (SCIM, Terraform, CI/CD). Admin-only.

$ curl https://login.quickztna.com/api/key-management \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "action": "create_api_key",
      "org_id": "org_9fX2kRmBnqT4",
      "name": "terraform-ci",
      "scopes": ["machines:write", "acl:write", "dns:write"]
    }'
200 OK Response · 38ms
{
  "success": true,
  "data": {
    "id": "ak_Pq7r2wXK",
    "key": "qz-api-live-4N8mR3w...",
    "name": "terraform-ci",
    "scopes": ["machines:write", "acl:write", "dns:write"],
    "created_at": "2026-04-19T18:00:00Z",
    "revoked": false
  },
  "error": null
}

Machines

POST /api/register-machine

Agent-side registration. Called by the ztna installer with an auth key. Server generates node_key, allocates tailnet IP, returns machine ID.

Field name gotcha Use name (not hostname). The backend silently ignores hostname.
$ curl https://login.quickztna.com/api/register-machine \
    -H "Authorization: Bearer tskey-auth-abc123" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "laptop-alex",
      "os": "linux",
      "wireguard_pubkey": "abcdef0123...",
      "pqc_pubkey": "ML-KEM-768 bytes..."
    }'
200 OK Response · 124ms
{
  "success": true,
  "data": {
    "machine_id": "m_Pq7r2wXK",
    "node_key": "nk_live_...",
    "tailnet_ip": "100.64.1.42",
    "magicdns_name": "laptop-alex.acme.zt.net",
    "derp_region": "blr",
    "status": "online"
  },
  "error": null
}

POST /api/machine-heartbeat

Called by the agent every 60 seconds. Returns peer list, DNS config, ACL rules, and any pending admin commands (wipe, lock, config push).

$ curl https://login.quickztna.com/api/machine-heartbeat \
    -H "Authorization: Bearer $NODE_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "status": "online",
      "endpoint": "52.34.12.88:41824",
      "derp_region": "blr",
      "posture": { "os_version": "24.04", "disk_encryption": true, "firewall": "ufw" }
    }'
200 OK Response · 58ms
{
  "success": true,
  "data": {
    "peers": [ /* 99 peer objects */ ],
    "dns_config": {
      "nameservers": ["100.100.100.100"],
      "search_domain": "acme.zt.net"
    },
    "acl_rules": [ /* evaluated rules */ ],
    "pending_commands": [],
    "posture_verdict": "compliant",
    "quarantined": false
  },
  "error": null
}

POST /api/machine-admin (action: "approve")

Approve a machine pending admin review. Required when org has approval_required enabled.

$ curl https://login.quickztna.com/api/machine-admin \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "action": "approve",
      "org_id": "org_9fX2kR",
      "machine_id": "m_Pq7r2w"
    }'
200 OK Response · 32ms
{
  "success": true,
  "data": {
    "machine_id": "m_Pq7r2w",
    "status": "online",
    "approved_at": "2026-04-19T18:00:00Z",
    "approved_by": "user_5k4aLw"
  },
  "error": null
}

POST /api/machine-admin (action: "quarantine")

Isolate a machine. Its tailnet IP stays allocated but ACL evaluation denies all traffic until unquarantined. Used for compromised devices or posture failures.

$ curl https://login.quickztna.com/api/machine-admin \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "action": "quarantine",
      "org_id": "org_9fX2kR",
      "machine_id": "m_Pq7r2w",
      "reason": "Posture failure: OS version outdated"
    }'
200 OK Response · 28ms
{
  "success": true,
  "data": { "machine_id": "m_Pq7r2w", "status": "quarantined" },
  "error": null
}

POST /api/machine-admin (action: "wipe_device")

Issue a remote wipe command. Delivered via next heartbeat. The agent wipes local config, state, tokens, and uninstalls the service. Audit-logged with actor identity.

$ curl https://login.quickztna.com/api/machine-admin \
    -H "Authorization: Bearer $TOKEN" \
    -d '{
      "action": "wipe_device",
      "org_id": "org_9fX2kR",
      "machine_id": "m_Pq7r2w",
      "confirm": "WIPE-m_Pq7r2w"
    }'

Policies (ACL)

POST /api/acl-evaluate

Evaluate a hypothetical connection against current ACLs. Returns allow/deny + the matched rule (or why no rule matched).

$ curl https://login.quickztna.com/api/acl-evaluate \
    -H "Authorization: Bearer $TOKEN" \
    -d '{
      "org_id": "org_9fX2kR",
      "source_machine_id": "m_laptop_alex",
      "destination_machine_id": "m_db_primary",
      "port": 5432,
      "protocol": "tcp",
      "time_hour": 14,
      "day_of_week": "thu",
      "source_country": "IN"
    }'
200 OK Response · 18ms
{
  "success": true,
  "data": {
    "verdict": "ALLOW",
    "matched_rule_id": "acl_7K9xq",
    "rule_name": "laptops-to-prod-business-hours",
    "evaluated_conditions": {
      "src_tag": "tag:laptop ✓",
      "dst_tag": "tag:prod ✓",
      "port": "5432 in 22,443,5432 ✓",
      "time": "14:00 in 09-18 ✓",
      "source_country": "IN ✓"
    }
  },
  "error": null
}

DNS

POST /api/dns-management

Manage MagicDNS configuration — nameservers, search domain, resolver behaviour.

Action name gotcha Use get_settings (not list_records). The older name is documented elsewhere but will 404.
$ curl https://login.quickztna.com/api/dns-management \
    -H "Authorization: Bearer $TOKEN" \
    -d '{
      "action": "get_settings",
      "org_id": "org_9fX2kR"
    }'
200 OK Response · 22ms
{
  "success": true,
  "data": {
    "magic_dns": true,
    "search_domain": "acme.zt.net",
    "nameservers": ["1.1.1.1", "1.0.0.1"],
    "override_local_dns": false
  },
  "error": null
}

POST /api/dns-filter (action: "update_policy")

Update DNS blocklist + allowlist policy. Feature-gated by dns_filtering (free on all plans).

$ curl https://login.quickztna.com/api/dns-filter \
    -H "Authorization: Bearer $TOKEN" \
    -d '{
      "action": "update_policy",
      "org_id": "org_9fX2kR",
      "blocklist_categories": ["malware", "phishing", "ads"],
      "allow_domains": ["github.com", "*.github.com"],
      "block_domains": ["tiktok.com"]
    }'
200 OK Response · 42ms
{
  "success": true,
  "data": {
    "policy_id": "dnsp_K9xq",
    "blocklist_categories": ["malware", "phishing", "ads"],
    "allow_domains": ["github.com", "*.github.com"],
    "block_domains": ["tiktok.com"],
    "updated_at": "2026-04-19T18:00:00Z"
  },
  "error": null
}

AI assistant

POST /api/ai-assist (action: "chat")

Conversational Q&A about your tenant. Claude-powered. Gated by ai_chat (free on all plans).

$ curl https://login.quickztna.com/api/ai-assist \
    -H "Authorization: Bearer $TOKEN" \
    -d '{
      "action": "chat",
      "org_id": "org_9fX2kR",
      "message": "Which machines went offline in the last hour?",
      "conversation_id": null
    }'
200 OK Response · 1248ms
{
  "success": true,
  "data": {
    "conversation_id": "conv_K9xq",
    "answer": "Two machines went offline in the last hour: legacy-vpc (offline 47min ago, reason: heartbeat timeout) and eu-edge-07 (offline 12min ago, reason: scheduled maintenance). No others.",
    "context_used": ["machines", "heartbeats"]
  },
  "error": null
}

POST /api/ai-assist (action: "generate_acl")

English → ACL rule. Returns a ready-to-apply JSON policy. Gated by nl_acl_builder (free on all plans).

$ curl https://login.quickztna.com/api/ai-assist \
    -H "Authorization: Bearer $TOKEN" \
    -d '{
      "action": "generate_acl",
      "org_id": "org_9fX2kR",
      "description": "Engineers can SSH to prod servers 9-6 IST on weekdays from India"
    }'
200 OK Response · 1802ms
{
  "success": true,
  "data": {
    "rule": {
      "name": "eng-ssh-prod-business-hours",
      "src": "tag:engineer",
      "dst": "tag:prod",
      "proto": "tcp",
      "ports": "22",
      "time_hour": "09-18",
      "day_of_week": "mon-fri",
      "source_country": "IN"
    },
    "explanation": "Generated ACL matches tag:engineer source to tag:prod dest on SSH (port 22), restricted to weekdays 09:00–18:00 IST from India."
  },
  "error": null
}

Governance

POST /api/governance (action: "jit_request")

Request time-bounded access to a resource. An approver gets notified; on approval the grant is active for the window and auto-revokes.

$ curl https://login.quickztna.com/api/governance \
    -H "Authorization: Bearer $TOKEN" \
    -d '{
      "action": "jit_request",
      "org_id": "org_9fX2kR",
      "resource_type": "machine",
      "resource_id": "m_db_primary",
      "requested_duration": 3600,
      "justification": "Debugging elevated-privilege migration issue #4782"
    }'
200 OK Response · 68ms
{
  "success": true,
  "data": {
    "grant_id": "jit_Pq7r2w",
    "status": "pending",
    "approvers_notified": ["alice@acme.com", "bob@acme.com"],
    "expires_at": "2026-04-19T19:00:00Z",
    "requested_at": "2026-04-19T18:00:00Z"
  },
  "error": null
}

Errors

QuickZTNA uses conventional HTTP status codes. 2xx = success, 4xx = client error, 5xx = server-side. Every non-2xx response populates error.code with a machine-readable string.

Status
Code
Meaning
200
Everything worked as expected.
400
VALIDATION_ERROR
Request body failed zod validation — often a missing or out-of-range parameter.
400
INVALID_JSON
Request body is not valid JSON.
401
UNAUTHORIZED
No token, expired token, or token blacklisted after password change.
401
INVALID_CREDENTIALS
Email + password did not match. Returned for non-existent emails too (timing-safe, anti-enumeration).
403
FORBIDDEN
Valid token, wrong role — the action requires admin or owner on the org.
403
FEATURE_GATED
The feature is not included on your plan. See pricing.
403
CSRF_REJECTED
Origin header did not match an allowed origin. State-changing browser requests only.
404
NOT_FOUND
Resource doesn't exist, or you don't have access to the org it belongs to.
409
CONFLICT
Resource already exists or concurrent update conflict.
429
RATE_LIMITED
Too many requests — see rate limits below.
5xx
INTERNAL_ERROR
Something failed on our side. Logged and alerted automatically.

Pagination

List endpoints use limit + offset pagination. Pass limit and range_from/range_to as query parameters.

limit integer optional

Page size, 1–100. Default 20.

range_from integer optional

Offset to start from. Default 0.

range_to integer optional

Offset to end at (exclusive).

order string optional

Column to sort by. Must be an indexed column.

ascending boolean optional

Sort direction. Default true.

Rate limits

Rate limits are applied per IP and per email on authentication endpoints, and per org on everything else. The client IP is detected via cf-connecting-ip first (not spoofable), falling back to x-real-ip.

10
login attempts / 5 min
Per IP · Per email
5
signups / 5 min
Per IP
200
WebSocket connections
Per org
10
WS broadcasts / min
Per client
5
MFA attempts / 5 min
Per user
5
password resets / 5 min
Per IP

Exceeding a limit returns 429 RATE_LIMITED. Login lockout after 10 failed password attempts per email = 15-minute block; unlock is automatic at the expiry timestamp.

WebSocket realtime

QuickZTNA exposes a single WebSocket at /api/realtime for live updates on machines, members, audit, keys, posture, threats, certs, DNS, ACL, billing, and remote desktop channels.

Token via subprotocol, not URL Token is passed in the Sec-WebSocket-Protocol header as access_token.<jwt> — never in the URL. This prevents tokens from leaking to proxy logs, CDN caches, or browser history.
const ws = new WebSocket(
  'wss://login.quickztna.com/api/realtime?org_id=org_...',
  [`access_token.${jwt}`]
);

ws.onopen = () => {
  ws.send(JSON.stringify({
    type: 'subscribe',
    channels: ['machines', 'audit']
  }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  // { type: 'event', channel: 'machines', event: 'UPDATE', payload: {...} }
};

Deprecated fields

Some older field/action names silently fail or 404. Use the forms on the left — not the forms on the right.

Correct
Wrong
Context
create_auth_key
create
/api/key-management action field
platform
os
/api/client-version action: "check"
/api/export
/api/export-data
Export endpoint
/api/ai-assist
/api/functions/ai-assist
AI handler
get_settings
list_records
/api/dns-management action
name
new_name
Machine rename
name
hostname
Machine registration
/api/manage-subscription
/api/customer-portal
Billing management
Request
$ curl https://login.quickztna.com/api/key-management \
    -H "Authorization: Bearer $QZ_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "action": "create_auth_key",
      "org_id": "org_9fX2kRmBnqT4",
      "reusable": true,
      "ephemeral": false,
      "expires_in": 3600,
      "tags": ["tag:laptop", "tag:prod"]
    }'
import { QuickZTNA } from '@quickztna/node';

const qz = new QuickZTNA(process.env.QZ_API_KEY);

const key = await qz.keys.createAuthKey({
  orgId: 'org_9fX2kRmBnqT4',
  reusable: true,
  ephemeral: false,
  expiresIn: 3600,
  tags: ['tag:laptop', 'tag:prod'],
});

// key.key = "tskey-auth-abc123..."
from quickztna import QuickZTNA
import os

qz = QuickZTNA(api_key=os.environ["QZ_API_KEY"])

key = qz.keys.create_auth_key(
    org_id="org_9fX2kRmBnqT4",
    reusable=True,
    ephemeral=False,
    expires_in=3600,
    tags=["tag:laptop", "tag:prod"],
)
# key.key -> "tskey-auth-abc123..."
package main

import "github.com/quickztna/quickztna-go"

qz := quickztna.NewClient(os.Getenv("QZ_API_KEY"))

key, err := qz.Keys.CreateAuthKey(ctx, &quickztna.AuthKeyParams{
    OrgID:     "org_9fX2kRmBnqT4",
    Reusable:  true,
    Ephemeral: false,
    ExpiresIn: 3600,
    Tags:      []string{"tag:laptop", "tag:prod"},
})
$ ztna auth-key create \
    --reusable \
    --expiry 1h \
    --tags tag:laptop,tag:prod

# Output: tskey-auth-abc123...
resource "quickztna_auth_key" "fleet_rollout" {
  org_id     = "org_9fX2kRmBnqT4"
  reusable   = true
  ephemeral  = false
  expires_in = 3600
  tags = ["tag:laptop", "tag:prod"]
}
Response
200 OK Response · 38ms
{
  "success": true,
  "data": {
    "id": "ak_9fX2kRmBnqT4",
    "key": "tskey-auth-abc123def456...",
    "org_id": "org_9fX2kRmBnqT4",
    "reusable": true,
    "ephemeral": false,
    "expires_at": "2026-04-19T18:00:00Z",
    "tags": ["tag:laptop", "tag:prod"],
    "created_by": "user_5k4aLw3Jm",
    "created_at": "2026-04-19T17:00:00Z",
    "revoked": false
  },
  "error": null
}
400 Bad Request Response · 12ms
{
  "success": false,
  "data": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "expires_in must be between 300 and 2592000 seconds"
  }
}
401 Unauthorized Response · 8ms
{
  "success": false,
  "data": null,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or missing bearer token"
  }
}
403 Forbidden Response · 10ms
{
  "success": false,
  "data": null,
  "error": {
    "code": "FORBIDDEN",
    "message": "Only admins can create auth keys"
  }
}
429 Too Many Requests Response · 5ms
{
  "success": false,
  "data": null,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests. Retry after 3s."
  }
}