# Omnigate API: guide for AI agents

> Tunnels, names, relays and access rules through plain HTTPS calls. This page is generated from the live API; the full reference is [/docs](/docs) and [/openapi.json](/openapi.json).

Base URL: `https://tnl.tkln.dev`. Short index: [/llms.txt](/llms.txt). One section at a time: [/ai/tunnels.md](/ai/tunnels.md), [/ai/domains.md](/ai/domains.md), [/ai/relays.md](/ai/relays.md), [/ai/audit.md](/ai/audit.md), [/ai/account.md](/ai/account.md), [/ai/organizations.md](/ai/organizations.md), [/ai/alerts.md](/ai/alerts.md), [/ai/pages.md](/ai/pages.md), [/ai/rules.md](/ai/rules.md), [/ai/invites.md](/ai/invites.md).

## Public pages and plans

No sign-in needed. Each page also exists as markdown by appending `.md`. Plans are informational: Free is available today, the others are coming soon, and the allowances shown are not enforced during the launch period (what an account may do is `GET /api/account/tier`).

- [About Takelan Omnigate](/info): what it is and what it does (markdown: [/info.md](/info.md))
- [How relays work](/info/how-it-works): client, edge, relays, regions and TLS (markdown: [/info/how-it-works.md](/info/how-it-works.md))
- [Private relays](/info/private-relays): bring your own relay (markdown: [/info/private-relays.md](/info/private-relays.md))
- [Features](/info/features): what is available today (markdown: [/info/features.md](/info/features.md))
- [Pricing](/pricing): plans; Free is available, the rest are coming soon (markdown: [/pricing.md](/pricing.md))
- [Contact](/contact): questions, plan interest, partnerships (markdown: [/contact.md](/contact.md))
- [Full public text](/llms-full.txt): all of the above in one markdown file
- [Plan catalog (JSON)](/api/plan-catalog): `GET /api/plan-catalog`, public, display-only (limits are not enforced during the launch period)

Prices in USD. Paid plans coming soon. Limits shown are the plan allowances we are launching with. During the launch period they are not enforced.

- Free (Available today): $0 forever. For testing and evaluation.
- Relay (Coming soon): $12/year, $12/year (about $1.00/mo). High availability on your own private relay.
- Personal (Coming soon): $5/month, $50/year (about $4.17/mo). For homelabs and individual developers.
- Pro (Coming soon): $15/month, $150/year (about $12.50/mo). For power users and production applications.
- Business (Coming soon): $29/month, $290/year (about $24.17/mo). For small teams and commercial use.
- Enterprise (Coming soon): Custom. For larger deployments and custom requirements.

## Authentication

Send one of:

- `X-API-Key: $OMNIGATE_API_KEY` (the API key from the Account page): the simplest, for scripts and agents.
- `Authorization: Bearer <access_token>` (the `access_token` returned by `POST /api/login`).
- the `access_token` cookie (the dashboard).

Examples read the key from the `OMNIGATE_API_KEY` environment variable and the address from `OMNIGATE_URL`. Never put a real key in a command you show, a file or a log.

## Errors

Failures are JSON: `{"detail": "...", "code": "stable_id", "hint": "what to do", "ref": "...", "field": "..."}`. Branch on `code`, show `detail` and `hint` to the person, quote `ref` (5xx) to support. HTTP statuses:

| status | meaning |
|---|---|
| 400 | The request is not valid (see `code` and `hint`). |
| 401 | Not signed in: send a Bearer token, an `X-API-Key` header or the session cookie. |
| 403 | Signed in, but not allowed (plan limit, locked account, or administrators only). |
| 404 | No such resource, or it is not yours. |
| 409 | Conflicts with the current state (name taken, already exists). |
| 410 | Gone: the link or token has expired. |
| 413 | Error (see `code`). |
| 415 | Error (see `code`). |
| 422 | The request body or parameters failed validation. |
| 429 | Too many requests: wait for `Retry-After` seconds. |
| 500 | Unexpected failure: quote the `ref`. |
| 501 | Error (see `code`). |
| 502 | A dependency (edge, relay, mail) did not answer. |
| 503 | Temporarily unavailable: try again shortly. |

Stable `code` values of tunnel and access calls:

| code | HTTP status | meaning |
|---|---|---|
| `abuse_limit_new_account` | 403 | New accounts can only hold a few tunnels. |
| `abuse_limit_rate` | 429 | You have created many tunnels in the last hour. |
| `abuse_risk_rate_limited` | 429 | Too many requests. |
| `account_banned` | 403 | This account has been closed. |
| `account_banned_use_unban` | 409 | This account is banned. |
| `account_disabled` | 403 | This account is disabled. |
| `account_locked` | 403 | This account is locked. |
| `acme_disabled` | 409 | Let's Encrypt issuance is not switched on on this server. |
| `admin_required` | 403 | Only administrators can do this. |
| `admit_rate_limited` | 429 | Too many requests to open this tunnel. |
| `admit_refused` | 403 | Your address is not allowed to reach this tunnel. |
| `already_in_organization` | 409 | This account already belongs to an organization. |
| `already_reserved` | 409 | You already reserved that name. |
| `anomaly_closed` | 409 | That anomaly is already resolved. |
| `anomaly_not_found` | 404 | No such relay anomaly. |
| `bandwidth_grant_invalid` | 422 | That bandwidth grant is not valid. |
| `bandwidth_limit_invalid` | 422 | That bandwidth limit is not valid. |
| `bandwidth_limit_reached` | 403 | Monthly bandwidth limit reached. |
| `bandwidth_monthly_exhausted` | 409 | Your monthly bandwidth and credits are used up. |
| `bandwidth_not_paused` | 409 | That tunnel is not paused by a bandwidth rule. |
| `bandwidth_order_limit` | 409 | You already have the maximum number of pending orders. |
| `bandwidth_order_not_found` | 404 | No such bandwidth order. |
| `bandwidth_order_state` | 409 | That order is not pending. |
| `bandwidth_pack_invalid` | 422 | That bandwidth pack is not valid. |
| `bandwidth_pack_not_found` | 404 | No such bandwidth pack. |
| `cannot_ban_admin` | 409 | An administrator cannot be banned. |
| `cannot_change_self` | 409 | You cannot lock or demote your own account. |
| `captcha_failed` | 400 | The captcha check failed. |
| `cdn_private_relay` | 409 | The CDN cache is not available on tunnels served only by private relays. |
| `cert_failed` | 502 | The certificate could not be obtained. |
| `cert_mismatch` | 409 | That is not a valid certificate issued for this relay. |
| `cert_not_found` | 404 | There is no issued certificate of that name. |
| `cert_not_managed` | 409 | This certificate is not managed here. |
| `cert_owned` | 409 | Another owner already holds a certificate for these names. |
| `cert_too_soon` | 429 | A certificate was ordered a moment ago. |
| `cert_unavailable` | 503 | Certificates are not available on this server. |
| `cert_wrong_kind` | 400 | That action does not apply to this kind of certificate. |
| `claim_captcha_failed` | 400 | The verification did not pass. |
| `claim_full` | 409 | This tunnel cannot admit more visitors right now. |
| `claim_invalid` | 404 | This link is not valid any more. |
| `claim_mode_required` | 409 | This tunnel is not in claim link mode. |
| `claim_rate_limited` | 429 | Too many attempts. |
| `contact_delivery_not_configured` | 409 | Contact delivery is not configured on this server. |
| `contact_message_not_found` | 404 | That contact message does not exist. |
| `custom_domain_check_too_soon` | 429 | That domain was checked a moment ago. |
| `custom_domain_disabled` | 403 | This domain was disabled by an administrator. |
| `custom_domain_exists` | 409 | You already added that domain. |
| `custom_domain_ip` | 400 | An IP address cannot be used as a custom domain. |
| `custom_domain_limit_reached` | 403 | You have reached the limit of custom domains on your plan. |
| `custom_domain_not_allowed` | 403 | That domain name cannot be used. |
| `custom_domain_not_found` | 404 | That custom domain was not found. |
| `custom_domain_platform` | 400 | That name belongs to the platform. |
| `custom_domain_taken` | 409 | That domain is already in use on this platform. |
| `custom_domain_unsupported_tunnel` | 409 | Custom domains work with HTTP tunnels on the shared edge. |
| `custom_domain_wildcard` | 400 | Wildcard domains are not supported. |
| `direct_disabled` | 409 | Direct mode is off for this tunnel. |
| `direct_http_admit` | 400 | Web tunnels need no admit. |
| `direct_no_hostname` | 409 | This tunnel has no direct host name, so there is no certificate for it. |
| `direct_unavailable` | 409 | The direct path of this tunnel is not available right now. |
| `dns_failed` | 502 | The DNS record for the tunnel could not be set up. |
| `edge_not_ready` | 503 | The web server did not start serving the new tunnel in time. |
| `email_required` | 400 | Copyright reports need a contact e-mail. |
| `email_taken` | 409 | An account with that e-mail address already exists. |
| `enrollment_invalid` | 401 | The enrollment token is invalid, expired or already used. |
| `exception_not_allowed` | 409 | No exception can be made for that port. |
| `expiry_exceeds_plan` | 403 | That lifetime is longer than your plan allows. |
| `ha_no_relays` | 409 | No relays are available for high availability yet. |
| `ha_not_in_plan` | 403 | High availability is not part of your plan. |
| `hold_not_found` | 404 | No such held request: it was already decided or has expired. |
| `hold_off` | 409 | Hold mode is not on for this tunnel. |
| `hostname_not_yours` | 403 | That host name does not belong to this relay. |
| `http_header_too_long` | 400 | A header value is too long. |
| `http_only` | 400 | That setting only works for HTTP tunnels. |
| `impersonation_chain` | 403 | You cannot start another view while viewing as someone. |
| `impersonation_disabled` | 403 | Impersonation is not switched on for that person's organization. |
| `impersonation_ended` | 401 | This view has ended. |
| `impersonation_forbidden` | 403 | This action is not allowed while viewing as another person. |
| `impersonation_quota` | 429 | The daily limit of viewing as this person is used up. |
| `inspector_entry_not_found` | 404 | No such request: it may have expired or been cleared. |
| `inspector_http_only` | 409 | The inspector works for HTTP tunnels only. |
| `inspector_not_in_plan` | 403 | Your plan does not include the request inspector. |
| `inspector_off` | 503 | The request inspector is switched off on this server. |
| `inspector_replay_failed` | 502 | The replay could not reach the tunnel. |
| `inspector_replay_not_http` | 409 | Only HTTP tunnels can be replayed. |
| `inspector_replay_rate` | 429 | Too many replays. |
| `inspector_replay_truncated` | 409 | This request cannot be replayed: its body was not kept completely. |
| `inspector_replay_unsafe` | 409 | This request cannot be replayed. |
| `inspector_store_unavailable` | 503 | The inspector storage is not answering. |
| `inspector_unauthorized` | 401 | Not the edge. |
| `internal_error` | 500 | We could not create the tunnel because of an unexpected problem on our side. |
| `invalid_abuse_request` | 422 | That request is not valid. |
| `invalid_abuse_settings` | 400 | Those abuse settings are not valid. |
| `invalid_access_rules` | 422 | Some of the access rules are not valid. |
| `invalid_announce` | 400 | The announced direct endpoint is not valid. |
| `invalid_basic_auth` | 400 | The password protection settings are not valid. |
| `invalid_cdn_glob` | 400 | A path pattern is not valid. |
| `invalid_cdn_mode` | 400 | The cache mode must be off, auto or custom. |
| `invalid_cdn_path` | 400 | The purge request is not valid. |
| `invalid_cdn_range` | 400 | The range must be 24h, 7d or 30d. |
| `invalid_cdn_request` | 422 | Some of the CDN settings are not valid. |
| `invalid_cdn_stale` | 400 | A stale-serving window is not valid. |
| `invalid_cdn_ttl` | 400 | The cache lifetime must be a whole number of seconds from 1 to 2592000 (30 days). |
| `invalid_cdn_types` | 400 | A file type is not valid. |
| `invalid_cidr` | 400 | That is not an IP address or range. |
| `invalid_claim_link` | 400 | That claim link setting is not valid. |
| `invalid_csr` | 400 | The certificate request is not acceptable. |
| `invalid_direct_request` | 400 | That request is not valid. |
| `invalid_direct_settings` | 422 | Some of the direct settings are not valid. |
| `invalid_edge_policy` | 422 | The edge rules are not in the expected shape. |
| `invalid_expiry` | 400 | That tunnel lifetime is not valid. |
| `invalid_geo` | 400 | The country rule is not valid. |
| `invalid_granularity` | 422 | That granularity is not valid. |
| `invalid_hold` | 422 | Some hold settings are not valid. |
| `invalid_host_header` | 400 | That Host header is not valid. |
| `invalid_hostname` | 400 | That is not a valid domain name. |
| `invalid_hostnames` | 400 | Those are not host names a certificate can be issued for. |
| `invalid_http_header` | 400 | A header name or value is not valid. |
| `invalid_http_settings` | 400 | The header settings are not valid. |
| `invalid_idle_timeout` | 422 | That is not a time you can choose. |
| `invalid_inspector` | 422 | Some inspector settings are not valid. |
| `invalid_invite_request` | 422 | That invite code request is not valid. |
| `invalid_lock_message` | 422 | That message is not valid. |
| `invalid_maintenance` | 400 | That maintenance setting is not valid. |
| `invalid_max_connections` | 400 | The connection limit must be a whole number from 1 to 10000. |
| `invalid_max_uses` | 400 | The use limit must be a whole number from 1 to 1000000. |
| `invalid_metrics` | 400 | The metrics batch is not valid. |
| `invalid_mirror` | 400 | The mirror is not valid. |
| `invalid_org_recording` | 400 | The organization's recording policy is not valid. |
| `invalid_org_request` | 422 | Some of the organization settings are not valid. |
| `invalid_org_role` | 422 | The role must be owner, admin or member. |
| `invalid_password_label` | 400 | The label must be 1 to 80 characters. |
| `invalid_password_request` | 422 | Some of the password settings are not valid. |
| `invalid_password_secret` | 400 | The password must be 8 to 72 bytes long. |
| `invalid_pause` | 400 | That pause is not valid. |
| `invalid_port` | 400 | That local port is not valid. |
| `invalid_port_rule` | 422 | The port rule is not valid. |
| `invalid_proxy_protocol` | 400 | PROXY protocol must be off or v2. |
| `invalid_range` | 422 | That time range is not valid. |
| `invalid_rate` | 400 | The rate limit is not valid. |
| `invalid_rate_limit` | 400 | The rate limit must be a whole number from 1 to 10000 requests per second. |
| `invalid_recording` | 400 | The recording settings are not valid. |
| `invalid_relay` | 422 | The relay settings are not valid. |
| `invalid_relay_failover` | 422 | That is not a choice you can make. |
| `invalid_relay_policy` | 422 | That relay policy is not valid. |
| `invalid_replay` | 422 | The replay edit is not valid. |
| `invalid_report` | 422 | That report is not valid. |
| `invalid_request` | 400 | Some of the tunnel settings are not valid. |
| `invalid_route` | 400 | A path route is not valid. |
| `invalid_satellite_request` | 422 | The routing settings are not valid. |
| `invalid_schedule` | 400 | The schedule is not valid. |
| `invalid_schedule_spec` | 400 | That schedule text could not be read. |
| `invalid_security_mode` | 400 | That security mode is not valid. |
| `invalid_session_minutes` | 400 | The session length must be a whole number of minutes from 5 to 1440 (24 hours). |
| `invalid_ssh_key` | 400 | That SSH public key is not valid. |
| `invalid_sso_request` | 422 | Some of the SSO settings are not valid. |
| `invalid_subdomain` | 400 | That subdomain is not valid. |
| `invalid_theme` | 422 | That is not a theme you can choose. |
| `invalid_tier` | 422 | The tier settings are not valid. |
| `invalid_timezone` | 400 | That is not a time zone we know. |
| `invalid_user_update` | 422 | That change is not valid. |
| `invalid_validity` | 400 | The end of the validity must come after its start. |
| `invalid_visitor_allow` | 400 | An entry of the allow list is not an e-mail address or an @domain. |
| `invalid_visitor_auth` | 400 | That sign-in mode is not valid. |
| `invalid_visitor_bypass` | 400 | A bypass path is not valid. |
| `invalid_visitor_passcode` | 400 | That passcode is not valid. |
| `invalid_visitor_session` | 400 | That session length is not valid. |
| `invalid_webhook` | 400 | The webhook check is not valid. |
| `invite_code_taken` | 409 | That code already exists. |
| `invite_downgrade` | 409 | This code would not improve your plan. |
| `invite_in_use` | 409 | This invite code has been used. |
| `invite_invalid` | 400 | That invite code is not valid. |
| `invite_not_found` | 404 | No such invite code. |
| `invite_rate_limited` | 429 | Too many attempts with invite codes. |
| `ip_invalid` | 400 | That is not an IP address. |
| `ip_lookup_disabled` | 503 | IP geolocation is not set up. |
| `ip_lookup_failed` | 404 | No location is known for that address. |
| `ip_lookup_rate_limited` | 429 | Too many IP lookups right now. |
| `ip_lookup_unavailable` | 503 | The IP lookup service is not reachable right now. |
| `ip_not_public` | 422 | Only public IP addresses can be located. |
| `knock_mode_required` | 409 | This tunnel is not in knock mode. |
| `knock_unavailable` | 409 | Port knocking is not available on this relay. |
| `last_admin` | 409 | This is the last active administrator. |
| `last_owner` | 409 | An organization needs at least one owner. |
| `limit_exceeds_parent` | 422 | A limit cannot be larger than what the parent organization has left. |
| `metrics_too_large` | 413 | The metrics batch is too large. |
| `name_not_allowed` | 403 | That name is not available. |
| `name_reserved` | 409 | That name is reserved by another account. |
| `name_taken` | 409 | That name is already taken. |
| `no_restriction` | 409 | No automatic restriction is in force on that account. |
| `no_review_needed` | 409 | No temporary limit applies to your account, or you already asked for a review. |
| `not_banned` | 409 | This account is not banned. |
| `not_impersonating` | 409 | You are not viewing as another person. |
| `open_requires_paid` | 403 | Your plan does not allow open TCP/UDP tunnels. |
| `org_account_locked` | 409 | An administrator locked this account. |
| `org_cannot_change_self` | 403 | You cannot change your own role, organization or limits this way. |
| `org_cycle` | 409 | An organization cannot be moved into itself or into one of its own children. |
| `org_disabled` | 409 | This organization is disabled. |
| `org_limit_exceeded` | 409 | That would exceed a limit of this organization or of one above it. |
| `org_max_depth` | 422 | Organizations can be nested 8 levels deep at most. |
| `org_rate_limited` | 429 | Too many changes in a short time. |
| `org_sso_unavailable` | 409 | Takelan SSO sign-in is not available on this server. |
| `org_tunnel_not_found` | 404 | That tunnel is not in this organization. |
| `org_user_not_found` | 404 | That account is not in this organization. |
| `organization_limit_reached` | 403 | You have reached the number of organizations your plan allows. |
| `organization_not_empty` | 409 | The organization still has child organizations or members. |
| `organization_not_found` | 404 | Organization not found. |
| `organizations_not_allowed` | 403 | Your plan does not include organizations. |
| `password_not_found` | 404 | That password does not exist on this tunnel. |
| `password_required` | 401 | This tunnel is protected. |
| `payload_too_large` | 413 | The request or response is larger than this plan allows. |
| `port_blocked` | 403 | That port is blocked. |
| `port_exception_not_found` | 404 | That exception does not exist. |
| `port_not_allowed_for_tier` | 403 | Your plan does not allow that port. |
| `port_rule_not_found` | 404 | That port rule does not exist. |
| `port_taken` | 409 | That port is already in use. |
| `ports_exhausted` | 503 | The server has no free tunnel ports right now. |
| `pull_not_configured` | 409 | The daily pull from the central certificate host is not configured. |
| `quic_disabled` | 503 | The Omnigate client (QUIC) connection is not enabled on this server. |
| `rate_limited` | 429 | Too many reports from this address. |
| `recording_http_only` | 409 | Only web (HTTP) tunnels can be recorded. |
| `recording_not_found` | 404 | No such tunnel, or you may not read its audit log. |
| `relay_already_disabled` | 409 | That relay is already disabled. |
| `relay_exists` | 409 | You already have a relay with this slug. |
| `relay_limit_reached` | 403 | Private relay limit reached. |
| `relay_locked_by_admin` | 403 | An administrator disabled this relay. |
| `relay_main_protected` | 409 | The platform relay (main) cannot be disabled or deleted. |
| `relay_name_taken` | 409 | Another relay already has that name. |
| `relay_not_available` | 422 | That relay cannot take this tunnel. |
| `relay_not_disabled` | 409 | That relay is not disabled. |
| `relay_not_drained` | 409 | Drain the relay before deleting it. |
| `relay_not_found` | 404 | Relay not found. |
| `relay_not_yours` | 403 | That relay does not belong to this tunnel's account. |
| `relay_policy_admin_only` | 403 | Only an administrator can change this setting. |
| `relay_ports_exhausted` | 503 | The relay has no free public ports left. |
| `relay_reason_required` | 422 | Give a reason of 3 to 500 characters. |
| `relay_rename_rate_limited` | 429 | You renamed relays too often. |
| `report_has_no_owner` | 409 | This report does not point at an account. |
| `report_has_no_tunnel` | 409 | This report does not point at a tunnel. |
| `report_not_found` | 404 | There is no such report. |
| `reservation_not_found` | 404 | That name is not reserved by you. |
| `reserve_limit_reached` | 403 | You have reached the limit of reserved names on your plan. |
| `reserved_http_header` | 400 | That header cannot be set or removed. |
| `role_not_allowed` | 403 | You cannot give that role. |
| `sample_tunnel` | 409 | Sample tunnels cannot have access rules. |
| `satellite_not_available` | 400 | That satellite cannot be used. |
| `satellite_revoked` | 403 | This satellite has been disabled. |
| `satellite_unauthorized` | 401 | The satellite credential is missing or wrong. |
| `schedule_http_only` | 409 | Only web (HTTP) tunnels can have opening hours. |
| `scim_token_limit` | 409 | An organization can hold 5 active SCIM tokens at most. |
| `scim_token_not_found` | 404 | No such SCIM token on this organization. |
| `server_storage` | 500 | The server could not write the tunnel's configuration. |
| `slug_taken` | 409 | That organization address (slug) is taken. |
| `ssh_server_busy` | 503 | The SSH server did not release the previous session of this tunnel. |
| `sso_api_key_blocked` | 403 | Your organization requires Takelan SSO and does not allow API keys for this account. |
| `sso_domain_exists` | 409 | That domain is already on this organization. |
| `sso_domain_invalid` | 422 | That is not a valid domain name. |
| `sso_domain_limit` | 409 | An organization can hold 20 domains at most. |
| `sso_domain_not_found` | 404 | No such domain on this organization. |
| `sso_domain_required` | 409 | Add and verify at least one e-mail domain first. |
| `sso_domain_taken` | 409 | That domain is already verified by another organization. |
| `sso_recovery_rate_limited` | 429 | Too many recovery sign-ins. |
| `sso_required` | 403 | Your organization requires Takelan SSO sign-in for this account. |
| `sso_session_required` | 409 | Turn enforcement on while signed in with Takelan SSO. |
| `sso_unavailable` | 409 | Takelan SSO sign-in is not set up on this server. |
| `state_unavailable` | 503 | The server's state store is not reachable. |
| `subdomain_taken` | 400 | Subdomain already in use. |
| `system_rule` | 409 | System rules cannot be changed. |
| `tcp_only` | 400 | PROXY protocol is only available for TCP tunnels. |
| `tier_exists` | 409 | A tier with this slug exists. |
| `tier_feature_cdn` | 403 | The CDN cache is not part of your plan. |
| `tier_in_use` | 409 | Accounts are still on this tier. |
| `tier_limit_tcp` | 403 | Your plan's TCP tunnel limit is reached. |
| `tier_limit_tunnels` | 403 | Your plan's tunnel limit is reached. |
| `tier_limit_udp` | 403 | Your plan's UDP tunnel limit is reached. |
| `tier_not_found` | 404 | That tier does not exist. |
| `too_many_cert_requests` | 429 | Too many certificate requests for this tunnel today. |
| `too_many_http_headers` | 400 | Too many headers. |
| `too_many_passwords` | 400 | This tunnel already has the maximum of 50 passwords. |
| `too_many_rules` | 400 | Too many entries in one list. |
| `too_many_visitor_allow` | 400 | The allow list is too long. |
| `transport_mismatch` | 400 | TCP and UDP tunnels need the Omnigate client (QUIC). |
| `tunnel_limit_reached` | 403 | Tunnel limit reached. |
| `tunnel_not_assigned` | 404 | That hostname is not served by this satellite. |
| `tunnel_not_found` | 404 | That tunnel does not exist. |
| `tunnel_suspended` | 403 | This tunnel was suspended by the platform. |
| `tunnel_suspended_by_admin` | 409 | An administrator took this tunnel down. |
| `use_passwords_api` | 400 | Single basic-auth passwords were replaced by access passwords. |
| `use_secret_endpoint` | 400 | The webhook secret is set on its own endpoint. |
| `user_not_found` | 404 | That account does not exist. |
| `visitor_allow_required` | 400 | Sign-in by e-mail code needs an allow list. |
| `visitor_passcode_required` | 400 | Passcode sign-in needs a passcode. |
| `webhook_secret_required` | 400 | A webhook check needs a signing secret. |
| `would_lock_out` | 409 | These rules would block your own address. |
| `wrong_password` | 401 | That password is not right. |

An unknown Dev Help section answers 404 with code `unknown_section`.

## Rate limits

Too many requests answer `429` with a `Retry-After` header (seconds). Wait that long, then retry once; do not loop. Some calls have their own limit (sign-in, sign-up, locating an edge, the MCP endpoint: 60 calls per minute per account).

## Safety rules for agents

- Read first, change second. Prefer the read calls to understand the state before any write.
- Confirm with the person before every destructive call (`DELETE`, releasing a name, deleting a tunnel) and say exactly what will be removed.
- Never print, log, store or repeat an API key, access token, relay token or claim-link token. Responses that contain one (`GET /api/account`, creating a tunnel or a claim link) are secrets.
- Handle `429` by honouring `Retry-After`; handle `401` by asking the person to refresh the key, not by guessing credentials.
- A write is audited under the person's account. Do not repeat a failed write blindly: read the `code` and `hint` first.
- Organization calls act on other people's accounts. Name the person and the effect and get a yes first; never ask for, repeat or store a reset token, a view token or an API key (the MCP tools never return them); there is no tool to view the dashboard as someone else.

## Host header, extra headers and the test endpoint

If a visitor sees `400 Invalid host header` (or `Blocked request. This host is not allowed`), the app behind the tunnel refuses the public name it is sent as `Host` (Starlette `TrustedHostMiddleware`, Django `ALLOWED_HOSTS`, webpack, Vite or Angular dev servers). The fix is Host header `local`, which sends `localhost:<port>` instead and keeps the public name in `X-Forwarded-Host`: `tlomnigate tunnel 8000 --host-header local` or `PUT /api/tunnels/{tunnel_ref}/http-settings` with `{"host_header": "local"}`. `GET .../http-settings` shows a `hint` once the edge has seen the app reject the Host header; nothing is changed automatically.

- `host_header`: `preserve` (default), `local`, or a host name with an optional port.
- `request_headers` / `response_headers`: up to 20 each, name = HTTP token, value = printable ASCII up to 1024 characters (line breaks are refused). `remove_request_headers` / `remove_response_headers`: up to 20 names.
- Never settable or removable: `Host` (use `host_header`), hop-by-hop headers, `Content-Length`, anything starting `x-tunnel-` or `x-omnigate-`; `Set-Cookie` cannot be set on responses; `X-Forwarded-For`, `X-Real-IP` and `Forwarded` are the platform's.
- Rules apply to what is proxied to and from the app, never to the platform's own pages. Header values are stored data: never log them, and do not put secrets you would not store in a database in them.
- With Host rewriting on, `Location` headers and cookie `Domain`s that point at the rewritten host are changed back to the public address. `Origin` and `Referer` are not touched.

Every web tunnel answers `GET` and `HEAD /test-tunnel` itself (the app is not asked): JSON by default, a small page for `Accept: text/html`, `200` when up and `503` with the same JSON when down, never cached. It reports `ok`, the relay connection and app state, the relay and versions, transport, the Host mode and header counts (never values), time left, bandwidth state, the certificate's expiry and the server time. `?probe=1` adds `upstream_probe` (a `HEAD /` through the tunnel to the app, 2 s timeout, at most once a second per tunnel). It sits behind the tunnel's sign-in. The path is reserved and shadows an app route of the same name; set `test_endpoint` to `false` to give it back. `tlomnigate test [name] [--probe]` prints a summary and exits 0 or 1.

## Tunnels

List, create and delete tunnels, choose who may reach them (security mode, access rules, claim links), set the Host header and extra headers your app gets, and read their logs.

### List your tunnels

`GET /api/tunnels` (risk: read)

Every tunnel of your account with its address, protocol, security mode and DNS status.

There is no single-tunnel call: pick the entry whose subdomain you want from this list.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Check a tunnel's health

`GET /api/tunnels/{tunnel_ref}/health` (risk: read)

The latest synthetic check of one tunnel (is it reachable, how fast).

tunnel_ref is the subdomain or the numeric id.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/health" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Read a tunnel's Host header, extra headers and test endpoint

`GET /api/tunnels/{tunnel_ref}/http-settings` (risk: read)

The Host header your app is sent (preserve, local or a custom name), the headers added or removed on requests and responses, whether /test-tunnel is on, and `hint` when the edge saw your app reject the public Host header.

Every web tunnel also answers GET or HEAD /test-tunnel itself (JSON; add ?probe=1 to time a round trip to your app, at most once a second). The path is reserved: it shadows an app route of the same name unless test_endpoint is false.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/http-settings" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Fix 'Invalid host header' from your app

`PUT /api/tunnels/{tunnel_ref}/http-settings` (risk: write)

Apps behind a Starlette TrustedHostMiddleware, Django ALLOWED_HOSTS, or a webpack, Vite or Angular dev server answer 400 Invalid host header because they are sent the public name. Host header `local` sends localhost:<your port> instead; the original stays in X-Forwarded-Host.

Command line: tlomnigate tunnel 8000 --host-header local. Only the fields you send change. Location headers and cookie domains that point at the local host are changed back to the public address.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/http-settings" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "host_header": "local"
}'
```

### Add and remove headers on requests and responses

`PUT /api/tunnels/{tunnel_ref}/http-settings` (risk: write)

request_headers and response_headers add or replace headers (up to 20 each; values are printable ASCII up to 1024 characters); the remove_* lists drop headers. Applies to your app's traffic only, never to the platform's own pages.

Refused: Host (use host_header), hop-by-hop headers, Content-Length, Set-Cookie on responses, names starting x-tunnel- or x-omnigate-. Values are stored data: they are never logged or audited.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/http-settings" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "request_headers": {
    "X-Env": "dev"
  },
  "response_headers": {
    "X-Robots-Tag": "noindex"
  },
  "remove_response_headers": [
    "server"
  ]
}'
```

### Create an HTTP tunnel

`POST /api/tunnels` (risk: write)

Opens a tunnel from a local port to https://<name>.<your domain>. The response carries the relay address and the one-session token your client connects with.

Errors name the cause in `code` (subdomain_taken, tunnel_limit, ...). The relay_token is a secret: do not log it.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "local_port": 3000,
  "subdomain": "myapp",
  "transport": "quic",
  "protocol": "http",
  "is_persistent": true
}'
```

### Create a TCP or UDP tunnel with a security mode

`POST /api/tunnels` (risk: write)

Exposes a raw TCP (or UDP) service. security_mode decides who may connect: allowlist (default, nobody until admitted), claim_link, knock or open (paid plans only).

Use "protocol": "udp" for UDP. remote_port asks for a specific public port; leave it out to get one assigned.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "local_port": 22,
  "transport": "quic",
  "protocol": "tcp",
  "security_mode": "allowlist"
}'
```

### Delete a tunnel

`DELETE /api/tunnels/{subdomain}` (risk: destructive)

Closes the tunnel and frees its name and port. This cannot be undone.

Destructive: ask the person for confirmation before calling it.

Confirm with the person before calling this.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Read a tunnel's security mode

`GET /api/tunnels/{tunnel_ref}/security-mode` (risk: read)

The mode of a TCP or UDP tunnel and which modes your plan and the relay allow.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/tcp-4821/security-mode" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Set a tunnel's security mode

`PUT /api/tunnels/{tunnel_ref}/security-mode` (risk: write)

Switches a TCP or UDP tunnel between allowlist, claim_link, knock and open. Takes effect on the relay within seconds.

Codes: invalid_security_mode, http_only, open_requires_paid, knock_unavailable.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/tcp-4821/security-mode" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "security_mode": "claim_link"
}'
```

### Read a tunnel's access rules

`GET /api/tunnels/{tunnel_ref}/access` (risk: read)

Allow and deny lists, rate limit, connection limit and PROXY protocol setting.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/access" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Replace a tunnel's access rules

`PUT /api/tunnels/{tunnel_ref}/access` (risk: write)

Sets the complete rule set: addresses (CIDR) that may or may not connect, requests per second and concurrent connections. Fields you leave out are cleared; use PATCH to change only some.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/access" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "allow_cidrs": [
    "203.0.113.0/24"
  ],
  "deny_cidrs": [],
  "rate_limit_rps": 20,
  "max_connections": 100,
  "proxy_protocol": "off"
}'
```

### Read a tunnel's edge rules

`GET /api/tunnels/{tunnel_ref}/edge-policy` (risk: read)

Country rule, rate limits with burst, webhook signature check, path routes and the request mirror of an HTTP tunnel. The webhook secret is never returned (only whether one is set).

geo.available is false when the server has no country database: the rule is saved but not enforced.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/edge-policy" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Change a tunnel's edge rules

`PATCH /api/tunnels/{tunnel_ref}/edge-policy` (risk: write)

Sets any of: country rule (allow or deny list of ISO codes), per-address and whole-tunnel rate limits (429 with Retry-After), a webhook signature check (report_only or enforce), up to 10 path routes to other local ports and a request mirror. Sections you leave out keep their value; null switches one off. Enforced at the web edge before traffic reaches your computer.

Codes: invalid_geo, invalid_rate, invalid_webhook, webhook_secret_required, invalid_route, invalid_mirror, would_lock_out (send force: true). Run your client with the same ports (`tlomnigate tunnel 3000 --route /api=8000 --mirror 9000`): it only serves ports it was started with. PUT replaces every section instead.

```bash
curl -sS -X PATCH "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/edge-policy" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "geo": {
    "mode": "deny",
    "countries": [
      "KP"
    ]
  },
  "rate": {
    "per_ip_rps": 20,
    "per_ip_burst": 40
  },
  "routes": [
    {
      "path_prefix": "/api",
      "local_port": 8000,
      "strip_prefix": false
    }
  ],
  "mirror": {
    "port": 9000,
    "percent": 100
  }
}'
```

### Set a tunnel's webhook signing secret

`PUT /api/tunnels/{tunnel_ref}/edge-policy/webhook-secret` (risk: write)

Stores the provider's signing secret (encrypted, never shown again) so the edge can verify webhooks. Then PATCH the edge rules with webhook {provider, mode}; start with report_only to see failures without refusing anything.

Treat the secret as a credential: do not log it. DELETE the same path to forget it.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/edge-policy/webhook-secret" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "secret": "whsec_example_secret"
}'
```

### Create a claim link

`POST /api/tunnels/{tunnel_ref}/claim-links` (risk: write)

A link a person opens to admit their own IP address to a tunnel in claim_link mode. The token is shown only in this response.

The tunnel must be in claim_link mode (409 claim_mode_required otherwise). Treat url and token as secrets.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/tcp-4821/claim-links" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "expires_minutes": 60,
  "max_uses": 1,
  "allow_minutes": 240,
  "label": "laptop"
}'
```

### Show when a tunnel expires

`GET /api/tunnels/{tunnel_ref}/expiry` (risk: read)

The deadline of a tunnel (null = none), the seconds left and the longest lifetime your plan allows.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/expiry" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Extend, shorten or clear a tunnel's expiry

`PUT /api/tunnels/{tunnel_ref}/expiry` (risk: write)

Expire the tunnel `ttl` (90s, 30m, 2h, 7d) or `expires_in_seconds` from now, within your plan's maximum. `ttl: never` (or `expires_in_seconds: null`) removes the deadline on plans without a maximum.

Codes: invalid_expiry (400), expiry_exceeds_plan (403). When the time is up the tunnel is deleted (audit event tunnel.expired). POST /api/tunnels takes the same ttl / expires_in_seconds.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/expiry" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "ttl": "2h"
}'
```

### List a tunnel's recent logs

`GET /api/tunnels/{tunnel_ref}/logs` (risk: read)

Recent requests and connection events, newest first. Filter by level and limit the count.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/logs" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "limit=50" \
  --data-urlencode "level=error"
```

### Replay a captured request

`POST /api/tunnels/{tunnel_ref}/inspect/{entry_id}/replay` (risk: write)

Sends a request from the inspector through the edge to your app again, marked with X-Omnigate-Replay, and returns the new entry (linked to the original by replay_of). Redacted headers and query values are never sent; the inspector must be on and the tunnel must be HTTP.

10 replays a minute per tunnel. A body that was cut at capture or is not text is refused (inspector_replay_truncated) unless you send body. Not allowed while viewing as another account.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/inspect/42/replay" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Replay a captured request with changes

`POST /api/tunnels/{tunnel_ref}/inspect/{entry_id}/replay` (risk: write)

Same, but change the method, the path and query, some headers or the text body first. Header changes are {name, value}; value null removes a header. A sensitive header (Authorization, Cookie, keys) is sent only with include_credentials: true, once, and is never stored.

The edit never appears in the audit log, only which parts were edited.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/inspect/42/replay" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "method": "POST",
  "path": "/hook?v=2",
  "headers": [
    {
      "name": "X-Test",
      "value": "1"
    }
  ],
  "body": "{\"amount\": 5}"
}'
```

### Copy a captured request as curl

`GET /api/tunnels/{tunnel_ref}/inspect/{entry_id}/curl` (risk: read)

A curl command that sends the request again to the tunnel's address. Secrets are left out (a comment names them).

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/inspect/42/curl" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Export captured requests as HAR

`GET /api/tunnels/{tunnel_ref}/inspect/export/har` (risk: read)

A HAR 1.2 file of the entries you pick (up to 100), for browser dev tools or HAR viewers.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/inspect/export/har" \
  -o myapp.har \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "ids=41,42,43"
```

### Provider presets for webhook replays

`GET /api/inspect/presets` (risk: read)

Content type and header hints for Stripe, GitHub and Slack. Hints only: Omnigate never signs a request.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/inspect/presets" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Show hold mode and the held requests

`GET /api/tunnels/{tunnel_ref}/hold` (risk: read)

Whether 'Hold incoming requests' is on for a tunnel, when it switches itself off, and the requests waiting for a decision (secrets in headers hidden).

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/hold" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Hold incoming requests

`PUT /api/tunnels/{tunnel_ref}/hold` (risk: write)

Parks incoming non-GET requests (mode all: every request) at the edge for you to forward, edit or drop. Switches itself off after 10 minutes; at most 20 requests, 120 seconds each. A request nobody decides on is forwarded unchanged (on_timeout drop: dropped).

The sender waits while a request is held. Turn it off with enabled false.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/hold" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "enabled": true,
  "mode": "non_get"
}'
```

### Forward a held request

`POST /api/tunnels/{tunnel_ref}/hold/{hold_id}/forward` (risk: write)

Sends a held request on to your app, unchanged (no body) or edited (method, path, header changes, text body).

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/hold/19a3f2c1-0/forward" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "headers": [
    {
      "name": "X-Reviewed",
      "value": "yes"
    }
  ]
}'
```

### Drop a held request

`POST /api/tunnels/{tunnel_ref}/hold/{hold_id}/drop` (risk: destructive)

Does not send the request to your app. The sender is answered 502 'dropped by the tunnel owner' (or 200, 202 or 410 if you pick it).

Destructive: ask the person for confirmation before calling it.

Confirm with the person before calling this.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/hold/19a3f2c1-0/drop" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "status": 410
}'
```

### Show a tunnel's opening hours

`GET /api/tunnels/{tunnel_ref}/schedule` (risk: read)

The weekly hours, one-off windows, exceptions, any pause and the maintenance switch, and whether the tunnel is open right now with the next time that changes (worked out on the server, daylight saving time included).

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/schedule" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Set a tunnel's opening hours

`PUT /api/tunnels/{tunnel_ref}/schedule` (risk: write)

Visitors only reach the tunnel inside the windows; outside them the edge shows a 'closed' page (503 + Retry-After) and your client stays connected. `spec` is the compact form; `weekly`, `windows` (one-off) and `exceptions` give the full control. `notify_open` alerts you when a window opened and your client is not connected.

Codes: invalid_schedule (field names the part), invalid_timezone, invalid_schedule_spec, schedule_http_only (409). Audit event tunnel.schedule.updated. The server cannot start your client: that is what notify_open is for.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/schedule" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "spec": "Mon-Fri 09:00-18:00 Europe/Madrid",
  "notify_open": true
}'
```

### Remove a tunnel's opening hours

`DELETE /api/tunnels/{tunnel_ref}/schedule` (risk: destructive)

The tunnel is reachable at all times again. Removes the schedule, any pause and your closed-page wording; the maintenance switch stays.

Destructive: ask the person for confirmation before calling it.

204 with no body; idempotent. Audit event tunnel.schedule.cleared.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/schedule" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Close a tunnel for a while

`POST /api/tunnels/{tunnel_ref}/schedule/pause` (risk: write)

Quick action: visitors see the 'closed' page for `minutes` (or until `until`), whatever the schedule says. Your client stays connected.

Code: invalid_pause (400). At most 30 days. DELETE the same path ends it. Audit event tunnel.schedule.paused.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/schedule/pause" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "minutes": 120,
  "reason": "deploying"
}'
```

### End a pause now

`DELETE /api/tunnels/{tunnel_ref}/schedule/pause` (risk: destructive)

The tunnel follows its schedule again (open when it has none).

Destructive: ask the person for confirmation before calling it.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/schedule/pause" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Turn maintenance mode on or off

`PUT /api/tunnels/{tunnel_ref}/maintenance` (risk: write)

While on, every request gets a maintenance page: HTTP 503 with Retry-After and your message. It wins over the schedule and a pause; your client stays connected.

Code: invalid_maintenance (400). Audit event tunnel.maintenance.updated.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/maintenance" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "enabled": true,
  "message": "Upgrading the database. Back in 15 minutes.",
  "retry_after_seconds": 900
}'
```

## Domain Name Service

Reserve names (subdomains) for your tunnels, check whether one is free and release the ones you no longer need, and serve a tunnel on your own domain (custom domains). How many you may hold depends on your plan.

### List your reserved names

`GET /api/domains` (risk: read)

Your reserved names and the names of your tunnels, with your plan's limit and how many you use.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/domains" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Check whether a name is free

`GET /api/domains/check` (risk: read)

Tells whether you could reserve this name now, and why not when you cannot.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/domains/check" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "name=myapp"
```

### Reserve a name

`POST /api/domains` (risk: write)

Holds the name for your account so no one else can take it, within your plan's limit.

Codes: domain_taken, domain_limit, invalid name (see `field`).

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/domains" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "myapp",
  "note": "staging site"
}'
```

### Release a reserved name

`DELETE /api/domains/{name}` (risk: destructive)

Gives the name up; anyone may take it afterwards. A tunnel using the name is not deleted.

Destructive: ask the person for confirmation before calling it.

Confirm with the person before calling this.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/domains/myapp" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### List your reserved subdomains (CLI vocabulary)

`GET /api/subdomains/reservations` (risk: read)

Your reservations with label and whether a tunnel uses them, the plan's limit, and the names you released that are still in the 24 hour quarantine. `tlomnigate reservations` calls this.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/subdomains/reservations" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Reserve a subdomain (CLI vocabulary)

`POST /api/subdomains/reservations` (risk: write)

Holds the name so only your account can create a tunnel on it, within your plan's limit. `tlomnigate reserve NAME` calls this.

Errors: invalid_subdomain, name_not_allowed, name_taken (also while another account's release is in quarantine), already_reserved, reserve_limit_reached.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/subdomains/reservations" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "myapp",
  "label": "staging"
}'
```

### Release a reserved subdomain (CLI vocabulary)

`DELETE /api/subdomains/reservations/{name}` (risk: destructive)

Gives the name up. Only you can take it again for 24 hours. A tunnel using the name is not deleted. `tlomnigate release NAME` calls this.

Destructive: ask the person for confirmation before calling it.

Confirm with the person before calling this.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/subdomains/reservations/myapp" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Schema of the tlomnigate.yml profile file

`GET /tlomnigate.schema.json` (risk: read)

The JSON Schema of the client's tunnel profile file, for editor completion. Public, no sign-in. The file defines named tunnels for `tlomnigate up [name...]`, with `defaults:` shared by all.

File format: `tunnels: {web: {port: 3000, subdomain: myapp, protocol: http, ttl: 8h, auth: sso, allow: ["@corp.com"]}, db: {port: 5432, protocol: tcp}}`. Keys: port (required), host, subdomain, protocol, ttl, auth, allow, passcode, persistent, transport, proxy_protocol; host_header, headers, ip_allow, ip_deny, routes, mirror and webhook_verify are sent to the server as written. Values may use ${ENV_VAR}; a literal passcode or token is refused unless --allow-literal-secrets. `tlomnigate config check` names every unknown key; `tlomnigate config init` writes an example.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/tlomnigate.schema.json" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### List your custom domains

`GET /api/custom-domains` (risk: read)

Your own host names with their status, the exact DNS records to create and the certificate state.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/custom-domains" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Add a custom domain

`POST /api/custom-domains` (risk: write)

Claims a host name you own. The answer lists the TXT record that proves ownership and the CNAME to create once a tunnel is attached.

Codes: invalid_hostname, custom_domain_wildcard, custom_domain_ip, custom_domain_platform, custom_domain_not_allowed, custom_domain_taken, custom_domain_limit_reached.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/custom-domains" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "hostname": "shop.example.org"
}'
```

### Check a custom domain's DNS records

`POST /api/custom-domains/{domain_id}/verify` (risk: write)

Looks the records up now. A found TXT record verifies the domain; the answer says in words what is still missing.

Answers 429 custom_domain_check_too_soon when called again within a few seconds.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/custom-domains/7/verify" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Attach a custom domain to a tunnel

`POST /api/custom-domains/{domain_id}/attach` (risk: write)

Serves the domain from one of your HTTP tunnels once it is verified and has a certificate.

Only HTTP tunnels on the shared edge: custom_domain_unsupported_tunnel.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/custom-domains/7/attach" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "tunnel_id": 12
}'
```

### Detach a custom domain

`POST /api/custom-domains/{domain_id}/detach` (risk: write)

Stops serving the domain at once and deletes its certificate; the domain stays yours.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/custom-domains/7/detach" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Delete a custom domain

`DELETE /api/custom-domains/{domain_id}` (risk: destructive)

Removes the domain, its route and its certificate. Your DNS records are not touched.

Destructive: ask the person for confirmation before calling it.

Confirm with the person before calling this.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/custom-domains/7" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

## My relays

See where the edges are, list your own private relays, read their traffic by protocol, anomalies and history, find an edge on the map, and disable or delete a relay (administrators also see every relay's traffic and health under /api/admin/relays/traffic and /api/admin/relay-anomalies).

### List the regions and edges

`GET /api/regions` (risk: read)

Where the edges are: the main site, shared satellites and your own relays, with their status and load.

scope=all is for administrators.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/regions" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "scope=mine"
```

### Your tunnels as arrows on the Regions map

`GET /api/regions/map` (risk: read)

For each tunnel: the relay serving it, an approximate client location (address masked), distance, estimated latency, whether the client is connected, requests of the last five minutes and why this relay (whether it is the closest available).

New HTTP tunnels are placed on the closest shared relay to the client (or the main site when it is the closest or no location is known); private relays are used only when you choose them.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/regions/map" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### List your private relays

`GET /api/relays` (risk: read)

The relays you registered, with their addresses, status and the tunnels running on them.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/relays" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Traffic through one of your relays

`GET /api/relays/{relay_id}/traffic` (risk: read)

Bytes in and out and requests, connections or flows of a relay as a series per protocol (http, tcp, udp), with totals. Administrators may read any relay.

range is 1h, 6h, 24h, 7d or 30d. HTTP is counted at the platform edge, so a private relay shows tcp and udp.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/relays/4/traffic" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "range=24h" \
  --data-urlencode "bucket=auto"
```

### Anomalies on one of your relays

`GET /api/relays/{relay_id}/anomalies` (risk: read)

Traffic spikes, silent relays, stale heartbeats and the like the detectors found on a relay, with what automation did about them.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/relays/4/anomalies" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "status=active"
```

### What happens automatically on an anomaly

`GET /api/relays/{relay_id}/policy` (risk: read)

notify (default on), auto_pause_offender and auto_isolate (both default off; the last is for administrators).

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/relays/4/policy" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### History of one of your relays

`GET /api/relays/{relay_id}/events` (risk: read)

Who disabled, enabled, isolated or deleted the relay and why, newest first.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/relays/4/events" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Disable one of your relays

`POST /api/relays/{relay_id}/disable` (risk: destructive)

Takes the relay out of service: no new tunnels, out of DNS, its token and config sync refused. Needs a reason of 3 to 500 characters. Reverse it with enable.

Destructive: ask the person for confirmation before calling it.

Confirm with the person before calling this.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/relays/4/disable" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "Moving the server to a new host."
}'
```

### Rename a relay

`PUT /api/relays/{relay_id}/name` (risk: write)

Changes only the display name (1 to 64 characters, unique among your relays). The slug, DNS name, keys and tokens do not change. Administrators may rename any relay; the owner is told. Up to 10 renames an hour.

Answers 409 relay_name_taken for a duplicate name and 404 for a relay that is not yours.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/relays/4/name" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Home server (Berlin)"
}'
```

### Delete one of your relays

`DELETE /api/relays/{relay_id}` (risk: destructive)

Disables and purges the relay in one step; its credentials stop working. Needs a reason of 3 to 500 characters in the JSON body. Cannot be undone.

Destructive: ask the person for confirmation before calling it.

Confirm with the person before calling this.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/relays/4" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "Server retired."
}'
```

### Locate an edge on the map

`POST /api/regions/{slug}/locate` (risk: write)

Looks up the geographic position of one edge now. Rate limited per account.

Answers 429 ip_lookup_rate_limited with Retry-After when called too often.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/regions/default/locate" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

## Audit log

Read and export what happened on your account, and the traffic totals of your tunnels.

### List your audit log

`GET /api/audit` (risk: read)

Account and tunnel events, newest first. Page with `before` set to next_before from the previous answer.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/audit" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "limit=20"
```

### Filter the audit log by action

`GET /api/audit` (risk: read)

Only the listed actions (comma separated), for example access-rule and security-mode changes.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/audit" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "action=tunnel.access_rules.updated,tunnel.security_mode.updated" \
  --data-urlencode "limit=50"
```

### Export the audit log as CSV

`GET /api/audit/export` (risk: read)

Downloads the log as a file, csv (safe to open in a spreadsheet) or json.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/audit/export" \
  -o audit.csv \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "format=csv"
```

### Traffic totals

`GET /api/metrics/summary` (risk: read)

Requests, bytes, status classes and latency per tunnel and overall, for 1h, 24h or 7d.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/metrics/summary" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "range=24h"
```

### Show a tunnel's session recording

`GET /api/tunnels/{tunnel_ref}/recording` (risk: read)

Whether the edge records this tunnel's sessions (metadata only, never bodies), where that comes from, how long records are kept and the head of the tamper-evident hash chain.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/recording" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Turn session recording on or off

`PUT /api/tunnels/{tunnel_ref}/recording` (risk: write)

`metadata` keeps an append-only, hash-chained log: time, event, truncated source IP, country, method, path template, allow-listed query keys, status, bytes, duration and the visitor's sign-in identity. No bodies, headers or cookies, ever. The dashboard shows 'recording is ON (metadata only)' while it is on.

Codes: invalid_recording (400), recording_http_only (409). Audit event tunnel.recording.updated. Not available while viewing as someone else.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/recording" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "mode": "metadata",
  "allow_query": [
    "page"
  ],
  "ip_mode": "truncated"
}'
```

### List a tunnel's recorded sessions

`GET /api/tunnels/{tunnel_ref}/recording/sessions` (risk: read)

Newest first, with filters (time, event kind, status or class, method, address prefix, country, visitor, path). Page with `before`.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/recording/sessions" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "limit=50" \
  --data-urlencode "ev=refused,signature_failed" \
  --data-urlencode "status=4"
```

### Verify a tunnel's recording chain

`GET /api/tunnels/{tunnel_ref}/recording/verify` (risk: read)

Walks the hash chain and reports the first broken link (a changed or missing record, a changed order or a truncated end).

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/recording/verify" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Download a tunnel's recorded sessions

`GET /api/tunnels/{tunnel_ref}/recording/export` (risk: read)

Streamed CSV or NDJSON, oldest first, with the same filters as the list. Each export is recorded in the audit log.

Owner, organization owner/admin above the owner, or administrator. Audit event tunnel.recording.exported.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/recording/export" \
  -o sessions.ndjson \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "format=ndjson" \
  --data-urlencode "since=2026-10-01T00:00:00Z"
```

## Account

Your account, plan and usage, two-step sign-in status and whether AI agents (MCP) may make changes.

### Read your account

`GET /api/account` (risk: read)

Your e-mail, limits and totals. The answer includes your API key: do not log or share it.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Read your plan and usage

`GET /api/account/tier` (risk: read)

Your plan, what it allows (tunnels per protocol, private relays, reserved names) and what you use.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account/tier" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Read your usage against the plan limits

`GET /api/account/usage` (risk: read)

Bandwidth this month, tunnels (all and per protocol), reserved names and custom domains as used and limit, with a percent, a status and the next plan that raises each. The same numbers the 80 % and 100 % alerts use.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account/usage" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Read the public plan catalog

`GET /api/plan-catalog` (risk: read)

The plans Takelan Omnigate is launching with, with prices in USD, features and the comparison numbers. Free is available today; Personal, Pro, Business and Enterprise are coming soon. Public, needs no key. Display only: the allowances are not enforced during the launch period; your actual plan is GET /api/account/tier.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/plan-catalog" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Read the public pages as markdown

`GET /llms-full.txt` (risk: read)

Every public page (about, how relays work, private relays, features, pricing, contact) and the FAQ as one markdown document. Public, needs no key.

Each page is also markdown on its own: append .md (/info/features.md, /pricing.md). The contact form is an HTML page at /contact (a form post with an anti-spam question); there is no JSON endpoint for it.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/llms-full.txt" \
  -o llms-full.md \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Is two-step sign-in on?

`GET /api/account/totp` (risk: read)

Whether TOTP is enabled and how many recovery codes are left.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account/totp" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Can AI agents make changes?

`GET /api/account/mcp` (risk: read)

Whether the MCP endpoint may run write tools for your account (off by default: read-only).

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account/mcp" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Allow or forbid changes by AI agents

`PUT /api/account/mcp` (risk: write)

Turns the MCP write tools on or off for your account. Destructive tools also need confirm: true on every call. The change is recorded in the audit log.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account/mcp" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "writes_enabled": true
}'
```

### How long do I stay signed in?

`GET /api/account/session` (risk: read)

Your idle timeout in minutes, the choices you may pick and the cap set by the server operator.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account/session" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Choose how long to stay signed in

`PUT /api/account/session` (risk: write)

Signs you out after this many minutes without activity: 15, 30, 60, 240, 480, 1440 or 10080 (not above the server's cap). It applies from your next renewal or sign-in, and is recorded in the audit log.

Activity keeps a session alive (the dashboard renews it with POST /api/session/touch). The page refreshing itself does not, and API keys never expire this way.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account/session" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "minutes": 240
}'
```

### Which colour theme do I use?

`GET /api/account/theme` (risk: read)

Your saved colour theme: system (follow the operating system, the default), light or dark.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account/theme" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Choose the colour theme

`PUT /api/account/theme` (risk: write)

Saves system, light or dark on your account so every browser you sign in from starts with it. Recorded in the audit log as account.theme_changed.

Not available while an administrator views the account as you. The dashboard also keeps the choice in the browser (localStorage tk-theme) so there is no flash on load.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account/theme" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "theme": "dark"
}'
```

### Is a temporary limit on my account?

`GET /api/account/abuse-review` (risk: read)

A neutral notice when creating new tunnels is temporarily limited while the account's activity is reviewed. Never a score or a reason; restricted is false for almost everyone.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account/abuse-review" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Ask for a review of the temporary limit

`POST /api/account/abuse-review` (risk: write)

Opens an item for the administrators with your optional note. Once per limit.

Code no_review_needed: no limit applies, or you already asked.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account/abuse-review" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "message": "This is my staging app for a client."
}'
```

## Organizations

Optional. Group accounts into organizations and child organizations, manage the accounts below you (invite, reset, disable, move, change role), stop or delete their tunnels, read usage and audit, and (only where an organization owner allows it) view the dashboard as a member with a reason. You never need an organization; parents can reset or replace your secrets but never read them.

### List your organizations

`GET /api/orgs` (risk: read)

Your organization and, if you own or administer it, everything below it, as a flat list with parent_id. Empty for accounts without an organization (they are optional).

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "limit=50"
```

### Create an organization

`POST /api/orgs` (risk: write)

A top-level organization: allowed only when you have none and your plan includes organizations. You become its owner.

Codes: organizations_not_allowed, organization_limit_reached, already_in_organization, slug_taken.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Acme Inc",
  "slug": "acme"
}'
```

### Create a child organization

`POST /api/orgs` (risk: write)

Below an organization you own or administer, at most 8 levels deep. Its limits cannot exceed what the parent has left.

Codes: org_max_depth, limit_exceeds_parent, org_limit_exceeded.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Acme Berlin",
  "parent_id": 1,
  "max_users": 20
}'
```

### Read an organization

`GET /api/orgs/{org_id}` (risk: read)

Name, limits, settings, counts and your role in it. Anything outside your subtree is a 404.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Read the tree below an organization

`GET /api/orgs/{org_id}/tree` (risk: read)

The organization and its children as a nested tree.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/tree" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Rename an organization

`PATCH /api/orgs/{org_id}` (risk: write)

Owners and admins can rename an organization below them.

```bash
curl -sS -X PATCH "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Acme Berlin GmbH"
}'
```

### Change the settings of an organization

`PATCH /api/orgs/{org_id}` (risk: write)

admins_can_inspect (request contents, off by default), share_tunnels_default and allow_impersonation (only owners of that organization; every member is told by e-mail).

```bash
curl -sS -X PATCH "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "settings": {
    "share_tunnels_default": true
  }
}'
```

### Disable (or enable) an organization

`PATCH /api/orgs/{org_id}` (risk: destructive)

disabled: true signs out everyone in it and below it and stops their tunnels; false undoes it. Confirm with the person first.

Destructive: ask the person for confirmation before calling it.

```bash
curl -sS -X PATCH "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "disabled": true
}'
```

### Move an organization under another one

`POST /api/orgs/{org_id}/move` (risk: write)

Re-parent it with everything below it, inside your own subtree. Cycles and trees deeper than 8 levels are refused.

Codes: org_cycle, org_max_depth, org_limit_exceeded.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/move" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "new_parent_id": 2
}'
```

### Delete an empty organization

`DELETE /api/orgs/{org_id}` (risk: destructive)

Only an organization with no child organizations and no members except its single owner. Confirm with the person before calling this.

Destructive: ask the person for confirmation before calling it.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### List the accounts of an organization

`GET /api/orgs/{org_id}/users` (risk: read)

Owners and admins only. descendants=true includes the organizations below. No passwords, keys or secrets.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "descendants=true" \
  --data-urlencode "q=sam" \
  --data-urlencode "limit=50"
```

### Create an account and send an invitation

`POST /api/orgs/{org_id}/users` (risk: write)

The person gets an e-mail with a link to choose their own password. No password is ever returned.

Codes: email_taken, role_not_allowed (admins can only create members), org_limit_exceeded.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "sam@example.com",
  "name": "Sam Lee",
  "role": "member"
}'
```

### Read one account

`GET /api/orgs/{org_id}/users/{user_id}` (risk: read)

Profile, role, plan, limits and status. No secrets.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users/21" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Change an account's role

`PATCH /api/orgs/{org_id}/users/{user_id}` (risk: write)

Owners can give any role; admins only member. You cannot change yourself, an equal or higher role, or the last owner. The person is told by e-mail.

Codes: role_not_allowed, last_owner, org_cannot_change_self.

```bash
curl -sS -X PATCH "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users/21" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "role": "admin"
}'
```

### Move an account to another organization

`PATCH /api/orgs/{org_id}/users/{user_id}` (risk: write)

To an organization you manage. The role becomes member unless you send one.

```bash
curl -sS -X PATCH "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users/21" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "organization_id": 5
}'
```

### Change an account's plan or limits

`PATCH /api/orgs/{org_id}/users/{user_id}` (risk: write)

tier_id (not above your own plan) and the overrides max_tunnels and max_bandwidth_gb (not above your own).

```bash
curl -sS -X PATCH "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users/21" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "tier_id": 2,
  "max_tunnels": 10
}'
```

### Remove an account from the organization

`DELETE /api/orgs/{org_id}/users/{user_id}` (risk: destructive)

The account stays as it is; it just no longer belongs to an organization. Confirm first.

Destructive: ask the person for confirmation before calling it.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users/21" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Send a password reset link

`POST /api/orgs/{org_id}/users/{user_id}/reset-password` (risk: write)

E-mails the person a reset link (1 hour, works once). You never see a password, and they are told.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users/21/reset-password" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "deliver": "email"
}'
```

### Get a one-time password reset token

`POST /api/orgs/{org_id}/users/{user_id}/reset-password` (risk: destructive)

For automation: the token is returned to you once, as reset_token. Send it to POST /web/reset-password. Treat it as a secret; the AI/MCP tools never return it.

Destructive: ask the person for confirmation before calling it.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users/21/reset-password" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "deliver": "token"
}'
```

### Turn off an account's two-step sign-in

`POST /api/orgs/{org_id}/users/{user_id}/disable-2fa` (risk: destructive)

Clears the authenticator secret and recovery codes (lost device). The person is told by e-mail.

Destructive: ask the person for confirmation before calling it.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users/21/disable-2fa" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Disable an account

`POST /api/orgs/{org_id}/users/{user_id}/disable` (risk: destructive)

It cannot sign in and its sessions end. Tunnels and data are kept.

Destructive: ask the person for confirmation before calling it.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users/21/disable" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Enable an account

`POST /api/orgs/{org_id}/users/{user_id}/enable` (risk: write)

Lets a disabled account sign in again (not one an administrator locked).

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users/21/enable" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Sign an account out everywhere

`POST /api/orgs/{org_id}/users/{user_id}/revoke-sessions` (risk: destructive)

Every session ends; API keys keep working.

Destructive: ask the person for confirmation before calling it.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users/21/revoke-sessions" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Replace an account's API key

`POST /api/orgs/{org_id}/users/{user_id}/rotate-api-key` (risk: destructive)

The old key stops working. The new key is not shown to anyone unless you send return_key: true (then to you, once). You can never read the existing key.

Destructive: ask the person for confirmation before calling it.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users/21/rotate-api-key" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "return_key": false
}'
```

### Start a view as an account (impersonation)

`POST /api/orgs/{org_id}/users/{user_id}/impersonate` (risk: write)

Needs allow_impersonation on the person's organization and a reason (10-300 characters). Returns a 30 minute Bearer token; every request made with it is audited and the person is e-mailed. Account-security actions stay refused.

Codes: impersonation_disabled, impersonation_quota, impersonation_chain. Not available through MCP.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/users/21/impersonate" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "Ticket 4821: the tunnel does not start"
}'
```

### End a view early

`POST /api/impersonation/stop` (risk: write)

Call it with the view token. The person is e-mailed a summary.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/impersonation/stop" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### List the tunnels of an organization

`GET /api/orgs/{org_id}/tunnels` (risk: read)

Tunnels of the accounts in the organization and below (owners and admins); members see only what colleagues shared. No access passwords or keys.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/tunnels" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "limit=50"
```

### Stop a member's tunnel

`POST /api/orgs/{org_id}/tunnels/{tunnel_id}/stop` (risk: write)

Takes it down until an owner or admin of the organization starts it. Visitors get the unavailable page.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/tunnels/77/stop" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Start a stopped tunnel

`POST /api/orgs/{org_id}/tunnels/{tunnel_id}/start` (risk: write)

Lifts a stop made through an organization (not a takedown by a platform administrator).

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/tunnels/77/start" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Share a tunnel with the organization

`PATCH /api/orgs/{org_id}/tunnels/{tunnel_id}` (risk: write)

Members of the owner's organization can then see it (admins can also stop and start it).

```bash
curl -sS -X PATCH "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/tunnels/77" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "shared_with_org": true
}'
```

### Delete a member's tunnel

`DELETE /api/orgs/{org_id}/tunnels/{tunnel_id}` (risk: destructive)

Permanently deletes it. You must outrank its owner. Confirm with the person first.

Destructive: ask the person for confirmation before calling it.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/tunnels/77" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Read the limits of an organization

`GET /api/orgs/{org_id}/limits` (risk: read)

Accounts, child organizations and tunnels allowed, what is in use and what is left to hand to a child.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/limits" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Set the limits of an organization

`PUT /api/orgs/{org_id}/limits` (risk: write)

Owners of an organization above it only. A value cannot exceed the parent's headroom nor drop below the current use; null removes a limit.

Codes: limit_exceeds_parent, org_limit_exceeded, org_cannot_change_self.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/limits" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "max_users": 50,
  "max_child_orgs": 5,
  "max_tunnels_total": 200
}'
```

### Usage of an organization

`GET /api/orgs/{org_id}/usage` (risk: read)

Accounts, tunnels and traffic of the last 30 days, in total and per child organization.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/usage" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Audit events of an organization

`GET /api/orgs/{org_id}/audit` (risk: read)

Events of every account in the organization and below, newest first. Page with before.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/audit" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "limit=50"
```

### Export accounts, tunnels or audit

`GET /api/orgs/{org_id}/export` (risk: read)

Downloads a file: what is users, tunnels or audit; format is csv or json.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/export" \
  -o organization-users.csv \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "what=users" \
  --data-urlencode "format=csv"
```

### Read the SSO rule, domains and SCIM summary

`GET /api/orgs/{org_id}/sso` (risk: read)

Whether Takelan SSO is required, the e-mail domains with the DNS record that proves each, how many accounts the rule touches, and the SCIM base URL.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/sso" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Add an e-mail domain to prove

`POST /api/orgs/{org_id}/sso/domains` (risk: write)

Enforcement only covers accounts on a verified domain. Create the returned TXT record, then verify.

Codes: sso_domain_invalid, sso_domain_exists, sso_domain_taken, sso_domain_limit.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/sso/domains" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "domain": "example.com"
}'
```

### Check the DNS record of a domain

`POST /api/orgs/{org_id}/sso/domains/{domain_id}/verify` (risk: write)

Looks up the TXT record and marks the domain verified when it matches. definitive false means DNS timed out.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/sso/domains/3/verify" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Require Takelan SSO for the organization

`PUT /api/orgs/{org_id}/sso` (risk: write)

Accounts on a verified domain must then sign in with Takelan SSO; password sign-in answers 403 sso_required, API keys keep working unless block_api_keys is true. You must be signed in with Takelan SSO yourself when turning it on.

Codes: sso_session_required, sso_domain_required, org_sso_unavailable. Turning it off is always allowed. Owners keep an audited, rate-limited recovery sign-in (header X-SSO-Recovery: 1).

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/sso" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "require_sso": true,
  "apply_to_subtree": false,
  "block_api_keys": false,
  "exempt_user_ids": []
}'
```

### Create a SCIM token for your identity provider

`POST /api/orgs/{org_id}/scim/tokens` (risk: write)

The secret is in the answer once; only a hash is stored. Give the identity provider the scim_base_url and this token (Authorization: Bearer ...) and send application/scim+json to /scim/v2.

The SCIM endpoints use this token, not your API key. Codes: scim_token_limit.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/scim/tokens" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Okta",
  "expires_days": 365
}'
```

### List the SCIM tokens

`GET /api/orgs/{org_id}/scim/tokens` (risk: read)

Newest first, with when and from where each was last used. Secrets are never shown again.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/scim/tokens" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Replace a SCIM token

`POST /api/orgs/{org_id}/scim/tokens/{token_id}/rotate` (risk: write)

Issues a replacement and revokes the old token at once. The new secret is shown once.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/scim/tokens/2/rotate" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Revoke a SCIM token

`DELETE /api/orgs/{org_id}/scim/tokens/{token_id}` (risk: destructive)

The identity provider's next call is refused; accounts it created stay.

Destructive: ask the person for confirmation before calling it.

Confirm with the person before calling this.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/scim/tokens/2" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Recent SCIM provisioning

`GET /api/orgs/{org_id}/scim/log` (risk: read)

What the identity provider created, changed, deactivated or re-roled, newest first.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/scim/log" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "limit=50"
```

### Read an organization's recording policy

`GET /api/orgs/{org_id}/recording` (risk: read)

The organization's own policy, what applies below it and how many tunnels record now.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/recording" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Set an organization's recording policy

`PUT /api/orgs/{org_id}/recording` (risk: write)

`metadata` makes every tunnel of the accounts below record (metadata only) unless a child organization or the tunnel sets its own value; `enforce` stops them switching it off.

Code: invalid_org_recording (400). Audit event org.recording.updated.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/recording" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "mode": "metadata",
  "enforce": false
}'
```

### List the recorded sessions of an organization

`GET /api/orgs/{org_id}/recording/sessions` (risk: read)

The org-level audit view: the recorded exchanges of every recording tunnel in the subtree, newest first.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/recording/sessions" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "limit=50" \
  --data-urlencode "ev=refused"
```

### Download an organization's recorded sessions

`GET /api/orgs/{org_id}/recording/export` (risk: read)

Streamed CSV or NDJSON of the whole subtree. Recorded in the audit log.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/recording/export" \
  -o sessions.csv \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "format=csv"
```

## Alerts

Traffic alert rules: be told by e-mail, signed webhook, Slack or Discord when a tunnel sees too much of an event (refused visitors, rate limits, 5xx errors, failed sign-ins, the tunnel going down). Rules live on a tunnel, on your account (all tunnels) or on an organization (inherited by its members); nothing is sent unless a rule exists. Channels themselves are managed on the Alerts page.

### List the events a rule can watch

`GET /api/traffic-alerts/events` (risk: read)

Each event with a description, where its count comes from and whether it is available. Reserved names (waf_blocked, geo_denied, ...) are listed with available false and never fire yet. Also the message variables.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/traffic-alerts/events" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### List the rules of a tunnel

`GET /api/tunnels/{tunnel_ref}/traffic-alerts` (risk: read)

Its own rules, those of your account and those inherited from your organization (inherited true), with the last time each fired and whether it is muted.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/traffic-alerts" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Add a rule to a tunnel

`POST /api/tunnels/{tunnel_ref}/traffic-alerts` (risk: write)

Alert when the event happens threshold times within window_minutes, at most once per cooldown_minutes (default 15). channels are ids of your alert channels (null = all verified ones); digest true batches into one message an hour; message may use {{tunnel_name}} {{event}} {{count}} {{window}} {{top_sources}} {{link}}.

Codes: invalid_traffic_event, invalid_traffic_alert (field names the value), invalid_alert_channel, traffic_alert_limit.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/traffic-alerts" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Flood",
  "event": "rate_limited",
  "threshold": 100,
  "window_minutes": 10,
  "severity": "warning"
}'
```

### Change a rule of a tunnel

`PATCH /api/tunnels/{tunnel_ref}/traffic-alerts/{rule_id}` (risk: write)

Send only the fields that change. Rules inherited from your account or organization are changed there.

```bash
curl -sS -X PATCH "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/traffic-alerts/3" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "threshold": 200,
  "enabled": true
}'
```

### Delete a rule of a tunnel

`DELETE /api/tunnels/{tunnel_ref}/traffic-alerts/{rule_id}` (risk: destructive)

Stops the rule; its history stays for 30 days.

Destructive: ask the person for confirmation before calling it.

Confirm with the person before calling this.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/traffic-alerts/3" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Send a test notification

`POST /api/tunnels/{tunnel_ref}/traffic-alerts/{rule_id}/test` (risk: write)

Queues a message marked [TEST] with sample numbers to the rule's channels. It does not count as an alert.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/traffic-alerts/3/test" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Mute a rule on a tunnel

`POST /api/tunnels/{tunnel_ref}/traffic-alerts/{rule_id}/mute` (risk: write)

Silences the notifications of a rule on this tunnel for minutes (1-10080); 0 unmutes. A mandatory organization rule cannot be muted (traffic_alert_mandatory).

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/traffic-alerts/3/mute" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "minutes": 120
}'
```

### What fired on a tunnel

`GET /api/tunnels/{tunnel_ref}/traffic-alerts/history` (risk: read)

Newest first, kept 30 days: the event, count, window, result and the masked top sources.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/traffic-alerts/history" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "limit=20"
```

### List your account-wide rules

`GET /api/account/traffic-alerts` (risk: read)

Rules that apply to every tunnel of your account.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account/traffic-alerts" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Add an account-wide rule

`POST /api/account/traffic-alerts` (risk: write)

The same body as for a tunnel; it is checked on each of your tunnels separately.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account/traffic-alerts" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "event": "upstream_error_5xx",
  "threshold": 20,
  "window_minutes": 5,
  "severity": "critical"
}'
```

### List the rules of an organization

`GET /api/orgs/{org_id}/traffic-alerts` (risk: read)

Defaults every tunnel of the organization and below inherits.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/traffic-alerts" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Add a rule to an organization

`POST /api/orgs/{org_id}/traffic-alerts` (risk: write)

Owners and administrators. channels are channel kinds (email, webhook, slack, discord) used from each member's own channels; mandatory true stops members from muting it.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/4/traffic-alerts" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "event": "ip_denied",
  "threshold": 500,
  "window_minutes": 60,
  "digest": true,
  "mandatory": true,
  "channels": [
    "email"
  ]
}'
```

## Pages and branding

Change the pages visitors of your tunnels see (offline, password required, wrong password, sign-in, access denied, too many requests, closed, expired, upstream error), add a logo and colours, show a terms page once before the app, and choose robots.txt. Set it per tunnel, per account or per organization; a tunnel beats the account, which beats the organization, unless an organization locks a page.

### List the pages and their state

`GET /api/pages/{scope}/{scope_id}` (risk: read)

Every page kind with its state at a scope: default (the platform's page), customised, inherited from your account or an organization, or locked by an organization. scope is tunnel, account (me) or org.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/pages/tunnel/my-app" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Read one page

`GET /api/pages/{scope}/{scope_id}/{kind}` (risk: read)

Your definition at this scope, the definition that applies (and where it comes from), the platform's default wording per language and WCAG contrast warnings.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/pages/tunnel/my-app/offline" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Customise a page

`PUT /api/pages/{scope}/{scope_id}/{kind}` (risk: write)

Heading, message (plain text with **bold**, *italic*, [links](https://example.com), line breaks; variables such as {{tunnel_name}} {{status}} {{retry_after}} {{request_id}}), colours (hex), logo (an id from the upload call), button (https or mailto), footer, support link and language. No HTML. Fields you leave out take their default.

Upload a logo first with POST /api/pages/assets: send the raw PNG, JPEG or WebP bytes as the body (Content-Type image/png, up to 256 KB, 8 to 2048 pixels per side), then put the returned `name` in `logo`. Codes: page_too_long, page_no_html, invalid_page_color, invalid_page_url, invalid_page_asset, unknown_asset, page_locked.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/pages/tunnel/my-app/offline" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "heading": "Back in a moment",
  "message": "We are **updating** the demo. [Status](https://example.com/status)",
  "accent": "#632ca6",
  "background": "#ffffff",
  "text_color": "#1d1b29",
  "language": "en"
}'
```

### Lock a page for an organization

`PUT /api/pages/{scope}/{scope_id}/{kind}` (risk: write)

Organization administrators can set a page for every tunnel below the organization and lock it, so members cannot override it.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/pages/org/4/access_denied" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "heading": "Members only",
  "message": "Contact IT at {{support_email}}.",
  "support_email": "it@example.com",
  "locked": true
}'
```

### Show a terms page before the app

`PUT /api/pages/{scope}/{scope_id}/{kind}` (risk: write)

The interstitial page: browsers see it once (never API clients, webhooks or WebSockets), acceptance is remembered in a signed cookie for cookie_hours (1 to 720).

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/pages/tunnel/my-app/interstitial" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "enabled": true,
  "heading": "Terms of use",
  "message": "By continuing you accept the [terms](https://example.com/terms).",
  "button_label": "I agree",
  "cookie_hours": 24
}'
```

### Choose robots.txt and indexing

`PUT /api/pages/{scope}/{scope_id}/{kind}` (risk: write)

mode: default (unchanged), block_all, allow_all or custom (with text). Only used when your app does not answer /robots.txt itself. noindex adds X-Robots-Tag: noindex to every response.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/pages/tunnel/my-app/robots" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "mode": "block_all",
  "noindex": true
}'
```

### Reset a page to the default

`DELETE /api/pages/{scope}/{scope_id}/{kind}` (risk: destructive)

Remove your definition at this scope; the page falls back to the account or organization above, or the platform's own.

Destructive: ask the person for confirmation before calling it.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/pages/tunnel/my-app/offline" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Copy a page from another scope

`POST /api/pages/{scope}/{scope_id}/{kind}/copy` (risk: write)

For example your account's page onto one tunnel.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/pages/tunnel/my-app/offline/copy" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "from_scope": "account",
  "from_id": "me"
}'
```

### What a tunnel shows

`GET /api/pages/effective/tunnel/{tunnel_ref}` (risk: read)

Each page kind for a tunnel with the definition that applies and its source (tunnel, account, organization, or locked by an organization). Read-only.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/pages/effective/tunnel/my-app" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Preview a page as a visitor sees it

`GET /api/pages/preview` (risk: read)

The saved page for a scope as HTML, with dummy request values. Open it in a browser or an iframe sandbox.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/pages/preview" \
  -o offline-preview.html \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "scope=tunnel" \
  --data-urlencode "scope_id=my-app" \
  --data-urlencode "kind=offline"
```

## Traffic rules

One YAML or JSON document per web tunnel that the edge runs on every request: deny, respond, redirect, rewrite the path or query, set request and response headers and the Host, tag requests and set variables, in the phases early, request, response and log. Save a draft, validate, dry-run a request (nothing is sent), publish, compare revisions and roll back. Shared rulesets of your account or organization are included with include: [account:<slug>, org:<slug>]; organizations can also apply them to every tunnel below them and lock them. If a published revision cannot be loaded the edge keeps the last good one and you get the rules.load_failed alert. Writes are refused in a "view as" session; MCP offers read, validate and test only.

### Show a tunnel's traffic rules

`GET /api/tunnels/{tunnel_ref}/rules` (risk: read)

The published revision and the draft with their source, canonical YAML and document, what the edge is serving (status: current, last_good, closed or open), your plan's values and the read-only settings rules made from the Host & headers tab.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Check a rules document

`POST /api/tunnels/{tunnel_ref}/rules/validate` (risk: read)

Parses and checks the document exactly as a draft would be (plan gates included) and lists every problem with its line and column. Stores nothing.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/validate" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "format": "yaml",
  "source": "version: 1\nearly:\n  - id: old-blog\n    match: {path: {prefix: /blog/}}\n    do:\n      - redirect: {to: \"https://news.example.com${req.path}\", status: 301, keep_query: true}\n"
}'
```

### Save the rules as a draft

`PUT /api/tunnels/{tunnel_ref}/rules/draft` (risk: write)

Validates and keeps the document as the tunnel's draft (replacing an earlier draft). Nothing reaches the edge until you publish. base_rev is the published revision you started from: someone else's newer publish answers 409 rules_conflict.

Codes: invalid_rules (with diagnostics), rules_too_many, rules_phase_action, rules_waf_not_in_plan, rules_script_not_in_plan, rules_conflict, rules_validator_unavailable.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/draft" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "format": "yaml",
  "source": "version: 1\nearly:\n  - id: old-blog\n    match: {path: {prefix: /blog/}}\n    do:\n      - redirect: {to: \"https://news.example.com${req.path}\", status: 301, keep_query: true}\n",
  "base_rev": 3
}'
```

### Publish the draft

`POST /api/tunnels/{tunnel_ref}/rules/publish` (risk: write)

Validates the draft again and sends it to the edge before answering. If the edge cannot load it, the tunnel keeps the last good revision and GET .../rules shows the error.

This changes how live visitors are served: confirm with the person first.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/publish" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "rev": 4,
  "message": "Redirect the old blog"
}'
```

### List the revisions

`GET /api/tunnels/{tunnel_ref}/rules/revisions` (risk: read)

Newest first, without their text.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/revisions" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Show one revision

`GET /api/tunnels/{tunnel_ref}/rules/revisions/{rev}` (risk: read)

One revision with its source as submitted.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/revisions/3" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Compare two revisions

`GET /api/tunnels/{tunnel_ref}/rules/diff` (risk: read)

The rule keys (phase/id) added, removed and changed, and a unified diff of the canonical YAML. Without from and to: the published revision against the draft.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/diff" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "from=3" \
  --data-urlencode "to=4"
```

### Publish an earlier revision again

`POST /api/tunnels/{tunnel_ref}/rules/rollback` (risk: write)

A new published revision with the text of an older one, validated again against today's limits.

This changes how live visitors are served: confirm with the person first.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/rollback" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "rev": 2
}'
```

### Rate-limit an endpoint

`PUT /api/tunnels/{tunnel_ref}/rules/draft` (risk: write)

A draft that allows five sign-in attempts per minute per visitor address: the sixth POST within a minute is answered 429 with Retry-After (the blocked page, or JSON for API clients). Buckets are per edge node and keyed by tunnel; key parts: client.ip, header:NAME, cookie:NAME, query:NAME, var:NAME, visitor.subject, tunnel, rule. Your plan's own rate and connection limits still run first.

Publish the draft for it to take effect.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/draft" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "format": "yaml",
  "source": "version: 1\nearly:\n  - id: login-limit\n    match: {all: [{method: POST}, {path: {exact: /login}}]}\n    do:\n      - rate_limit: {key: [client.ip], rate: 5/min, burst: 5, status: 429}\n      - tag: {labels: [login]}\n",
  "base_rev": 3
}'
```

### Throttle downloads and compress JSON

`POST /api/tunnels/{tunnel_ref}/rules/validate` (risk: read)

Checks a document that slows /downloads/ to 2 MiB/s per visitor and compresses JSON answers with br or gzip (levels at most gzip 6 and br 5). bandwidth_limit: rate (at least 8 KiB/s), per request|ip|tunnel, direction down|up|both; it never applies to WebSockets and the bytes are metered as before. compress: enabled, algorithms, types (globs such as text/*), min_bytes, level; without a compress action the edge compresses as it always did. Stores nothing.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/validate" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "format": "yaml",
  "source": "version: 1\nrequest:\n  - id: dl-throttle\n    match: {path: {prefix: /downloads/}}\n    do:\n      - bandwidth_limit: {rate: 2 MiB/s, per: ip, direction: down}\nresponse:\n  - id: compress-json\n    match: {response_header: {name: content-type, prefix: application/json}}\n    do:\n      - compress: {algorithms: [br, gzip], min_bytes: 1024, level: {br: 4, gzip: 6}}\n"
}'
```

### Skip the cache for an API, inflate gzip uploads

`POST /api/tunnels/{tunnel_ref}/rules/validate` (risk: read)

Checks a document that bypasses the CDN cache for /api/ (cache: {ttl: N} shortens it instead, never beyond the CDN tab's TTL) and inflates gzip request bodies for the app (bounded by your plan's payload cap and max_ratio, 413 beyond). The warning limit_bounds names the cap; cache_inactive means the CDN is off. Stores nothing.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/validate" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "format": "yaml",
  "source": "version: 1\nrequest:\n  - id: inflate-uploads\n    match: {header: {name: content-encoding, exact: gzip}}\n    do:\n      - decompress_request: {algorithms: [gzip], max_ratio: 100}\nupstream:\n  - id: api-no-cache\n    match: {path: {prefix: /api/}}\n    do:\n      - cache: {bypass: true}\n"
}'
```

### Dry-run a request through the rules

`POST /api/tunnels/{tunnel_ref}/rules/test` (risk: read)

Runs one request through the rules with the edge's own engine and shows each phase's rules (matched, not matched, disabled, not reached), the verdict and the request your app would get. Nothing is sent and nothing is stored. The request can be a curl command, raw HTTP/1.1 text, an inspector entry or its parts; the rules are the draft by default, a revision (rev) or a document you pass (format + source).

At most 30 dry runs a minute. Inspector entries are the owner's and keep secrets as [redacted].

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/test" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "request": {
    "curl": "curl -A sqlmap https://myapp.example.com/login"
  },
  "response": {
    "status": 200
  }
}'
```

### Per-rule hit counters

`GET /api/tunnels/{tunnel_ref}/rules/stats` (risk: read)

How often each rule fired over 24h, 7d or 30d, how often the edge answered with an error while it fired, when it last fired and the 95th percentile evaluation time.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/stats" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "range=24h"
```

### List shared rulesets

`GET /api/rulesets` (risk: read)

Your account's shared rulesets (include them as account:<slug>) and those of your organizations (org:<slug>; inherited and locked ones apply without an include).

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/rulesets" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Create a shared ruleset

`POST /api/rulesets` (risk: write)

A new, empty account ruleset. Save a draft and publish it like a tunnel's rules (PUT .../draft, POST .../publish under /api/rulesets/{slug}), then add include: [account:<slug>] to a tunnel's rules.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/rulesets" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "slug": "cors",
  "name": "CORS"
}'
```

### Save a shared ruleset's draft

`PUT /api/rulesets/{slug}/draft` (risk: write)

Validates the document (rules, vars, waf, scripts; never forward) and keeps it as the draft.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/rulesets/cors/draft" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "format": "yaml",
  "source": "version: 1\nresponse:\n  - id: cors\n    do:\n      - response_header: {set: {access-control-allow-origin: \"https://app.example.com\"}}\n",
  "base_rev": 0
}'
```

### Publish a shared ruleset

`POST /api/rulesets/{slug}/publish` (risk: write)

Publishes the draft and re-flattens every tunnel that includes it, after checking each of them: if one would break, nothing changes and the answer lists the tunnels.

This changes how live visitors of every tunnel that includes it are served: confirm first.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/rulesets/cors/publish" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "rev": 1
}'
```

### Lock an organization ruleset

`PATCH /api/orgs/{org_id}/rulesets/{slug}` (risk: write)

enforced: true makes an organization ruleset run first in every phase of every web tunnel of the organization and the ones below it; members cannot remove, skip or loosen it. Organization administrators only; every affected tunnel is checked first.

This changes how live visitors of every tunnel of the organization are served: confirm first.

```bash
curl -sS -X PATCH "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/orgs/12/rulesets/baseline" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "enforced": true
}'
```

### Get the rules JSON Schema

`GET /api/rules/schema` (risk: read)

The JSON Schema of a document, every action with the phases it may run in and every condition. Public.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/rules/schema" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Check a script (and dry-run it)

`POST /api/tunnels/{tunnel_ref}/rules/scripts/validate` (risk: read)

Checks one Starlark module the way a draft save would (no load, constant-only top level, no recursion, the entry def ENTRY(req, ctx), the actions it can return in the phase, literal forward targets, sizes) and lists problems with the module's line and column. With test, the entry runs once on the sample request through the edge's evaluator and limits: steps, heap peak, time, returned actions, print output. Stores nothing; paid plans (Free answers rules_script_not_in_plan).

Scripts are saved and published inside the rules document (scripts: with | blocks), with PUT .../rules/draft and POST .../rules/publish.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/scripts/validate" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "source": "SALT = \"ab-2026-10:\"\n\ndef pick(req, ctx):\n    uid = req.cookies.get(\"uid\")\n    if uid == None:\n        return None\n    if fnv1a32(SALT + uid) % 100 < 10:\n        return [forward(tunnel = \"shop-canary\"), tag(\"canary\")]\n    return [tag(\"stable\")]\n",
  "name": "ab",
  "entry": "pick",
  "kind": "action",
  "phase": "upstream",
  "forward": {
    "tunnels": [
      "shop-canary"
    ]
  },
  "test": {
    "request": {
      "method": "GET",
      "path": "/",
      "cookies": {
        "uid": "carol"
      }
    }
  }
}'
```

### List a tunnel's scripts

`GET /api/tunnels/{tunnel_ref}/rules/scripts` (risk: read)

The script modules of the published and draft revisions, the rules that call each, the plan's script limits and what the edge reports (isolation, quarantined modules after repeated worker crashes).

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/scripts" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### List forward domain proofs

`GET /api/domain-proofs` (risk: read)

Your domain proofs with the DNS record each needs and its state. A rule's forward_url may only reach hosts that are verified here (or are a verified custom domain of the account), on every plan.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/domain-proofs" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Start a forward domain proof

`POST /api/domain-proofs` (risk: write)

Answers the DNS record to create for the host: TXT _omnigate-verify.<host> = omnigate-forward=<token> (or a CNAME of that label with method cname, or the host's A/AAAA at your private relay with method relay_addr).

Codes: invalid_hostname, forward_target_platform, forward_domain_exists, forward_domain_limit.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/domain-proofs" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "hostname": "api.payments.example"
}'
```

### Check a forward domain proof now

`POST /api/domain-proofs/{proof_id}/verify` (risk: write)

Looks the record up now. Proofs are also re-checked every 6 hours; after 3 failed checks in a row the rules that forward to the host are turned off (and turned on again when the record is back).

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/domain-proofs/7/verify" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Remove a forward domain proof

`DELETE /api/domain-proofs/{proof_id}` (risk: destructive)

Refused with 409 forward_domain_in_use while a published rule lists the host in forward.allow_hosts.

Destructive: ask the person for confirmation before calling it.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/domain-proofs/7" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Show a tunnel's WAF

`GET /api/tunnels/{tunnel_ref}/waf` (risk: read)

The WAF settings of the published rules with their defaults, whether the draft changes them, the plan's WAF level and evidence retention, whether the plan lets this tunnel use the WAF, the active rule bundle, and the hits of the last 24 hours. The WAF itself is set in the rules document (waf: block). Paid plans only.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/waf" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### List a tunnel's WAF events

`GET /api/tunnels/{tunnel_ref}/waf/events` (risk: read)

The newest WAF events (at most 200 are kept per tunnel) with the rule, target and masked fragment (64 bytes at most) of each match. Kept your plan's waf_evidence_days (7 by default, at most 30); nothing older is returned.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/waf/events" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "limit=20" \
  --data-urlencode "action=block"
```

### Count WAF hits per rule

`GET /api/tunnels/{tunnel_ref}/waf/hits` (risk: read)

Detected and blocked hits per rule, per hour and per path template (numbers and ids as *), from counters without fragments (kept 30 days).

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/waf/hits" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "range=7d"
```

### List the WAF exceptions

`GET /api/tunnels/{tunnel_ref}/waf/exceptions` (risk: read)

The waf.exceptions of the draft, or of the published rules when there is no draft.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/waf/exceptions" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Allow a WAF rule for a path

`POST /api/tunnels/{tunnel_ref}/rules/waf-exception` (risk: write)

A false positive? Make an exception from a WAF event (its rule, path and parameter) or from a rule id and a path. scope path: exactly that path; param: that parameter everywhere; pattern: paths like it (numbers and ids become *). It is saved as the draft: publish it to change what the edge does.

Not available while an administrator views the account as you. Publish with POST .../rules/publish.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/tunnels/myapp/rules/waf-exception" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "event_id": "1842",
  "scope": "path"
}'
```

### List the WAF rule bundles (administrators)

`GET /api/admin/waf/bundles` (risk: read)

The built-in bundle and uploaded ones, which is active, and what the edge reports it runs.

Administrators only.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/admin/waf/bundles" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Upload a WAF rule bundle (administrators)

`POST /api/admin/waf/bundles` (risk: write)

Validated by the rules helper: it must compile, keep every rule id of the active bundle (or mark it retired) and pass its own test vectors. Stored inactive; activate it next.

Administrators only, never from a "view as" session. New rules detect only for 7 days after activation.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/admin/waf/bundles" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "format": "yaml",
  "source": "bundle_version: '\''2026.11.0'\''\nrules:\n  - ...",
  "note": "November rules"
}'
```

### Activate a WAF rule bundle (administrators)

`POST /api/admin/waf/bundles/{bundle_id}/activate` (risk: write)

Writes the bundle for every edge (or goes back to the built-in one with id builtin). An edge that cannot compile it keeps its current bundle and reports the error.

Administrators only. This changes what every WAF-enabled tunnel blocks: confirm first.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/admin/waf/bundles/3/activate" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### List addresses attacking WAF tunnels (administrators)

`GET /api/admin/waf/sources` (risk: read)

Per source address (/64 for IPv6): hits and how many WAF-enabled tunnels it hit, most tunnels first.

Administrators only.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/admin/waf/sources" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "hours=24" \
  --data-urlencode "min_tunnels=3"
```

### Grant the WAF to one tunnel (administrators)

`PUT /api/admin/tunnels/{tunnel_ref}/rules/gate-overrides` (risk: write)

Lets one tunnel's rules use the WAF whatever the owner's plan (waf: true), or removes the grant (waf: false).

Administrators only, never from a "view as" session.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/admin/tunnels/myapp/rules/gate-overrides" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "waf": true
}'
```

## Invite codes, lock messages and bans

Redeem an invite code to get a plan at sign-up or from your account. Administrators create and manage codes, set the public message a locked person sees, and lock or ban accounts. A locked or banned account answers every call with 403 `account_locked` or `account_banned` and the message in `message`.

### Redeem an invite code

`POST /api/account/invite` (risk: write)

Moves your plan up at once. A code for your plan or a lower one is refused, and a plan an administrator gave you is replaced only by a higher one. Temporary codes (grant_days) return you to the default plan afterwards.

Codes: invite_invalid (400, the same for unknown, inactive, expired and used-up codes), invite_downgrade (409), invite_rate_limited (429, 10 attempts an hour per address and per account). Not available in a 'view as' session. Audit events invite.redeemed and invite.failed (never the code). At sign-up send `invite_code` to POST /api/register, or run `tlomnigate register --invite CODE`.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/account/invite" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "code": "TLK-ABCD-EFGH-JKLM"
}'
```

### List invite codes (administrators)

`GET /api/admin/invite-codes` (risk: read)

Every code with its uses against the limit and its status (active, inactive, scheduled, expired, exhausted). The code itself is never returned: only a hint.

```bash
curl -sS -G "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/admin/invite-codes" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  --data-urlencode "status=active"
```

### Create an invite code (administrators)

`POST /api/admin/invite-codes` (risk: write)

Grants `tier_id` to whoever redeems it. Leave `code` out for a generated one or choose your own (8-64 characters: A-Z a-z 0-9 - _). The full code is in this answer ONLY; afterwards just a hint is kept.

Optional: valid_from, org_id (new accounts join that organization as members), note_public. Codes: tier_not_found, organization_not_found (404), invite_code_taken (409), invalid_invite_request (422). Audit event invite.created. Refused in a 'view as' session.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/admin/invite-codes" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "tier_id": 3,
  "label": "Conference 2026",
  "uses_max": 100,
  "expires_at": "2026-12-31T00:00:00Z",
  "grant_days": 90
}'
```

### Deactivate or reactivate an invite code (administrators)

`PATCH /api/admin/invite-codes/{invite_id}` (risk: write)

`active: false` stops new redemptions at once and keeps the history; accounts that redeemed it keep their plan. The same call changes the label, public note, use limit (not below the uses so far) and dates.

Audit events invite.deactivated, invite.reactivated, invite.updated.

```bash
curl -sS -X PATCH "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/admin/invite-codes/12" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "active": false
}'
```

### Delete an unused invite code (administrators)

`DELETE /api/admin/invite-codes/{invite_id}` (risk: destructive)

Only a code that was never redeemed can be deleted (409 invite_in_use otherwise): deactivate it instead.

Destructive: ask the person for confirmation before calling it.

204 with no body. Audit event invite.deleted.

```bash
curl -sS -X DELETE "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/admin/invite-codes/12" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### See who redeemed an invite code (administrators)

`GET /api/admin/invite-codes/{invite_id}/redemptions` (risk: read)

The accounts that redeemed it, newest first, with the plan before and after.

```bash
curl -sS "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/admin/invite-codes/12/redemptions" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json"
```

### Set the public message of a locked account (administrators)

`PUT /api/admin/users/{user_id}/lock-message` (risk: write)

The text the locked or banned person sees: on a refused sign-in, in the error of any API call, in the dashboard banner and in the command line client. Plain text, at most 1000 characters, line breaks kept; empty = the neutral default. It is shown to that person only.

Audit event user.lock_message_changed. Refused in a 'view as' session.

```bash
curl -sS -X PUT "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/admin/users/41/lock-message" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "message": "Your account is locked while we check a payment. Write to billing@example.com."
}'
```

### Lock an account and stop its tunnels (administrators)

`POST /api/admin/users/{user_id}/lock` (risk: destructive)

Signs the person out everywhere, refuses sign-in with the public message and stops every tunnel (kept, with the suspend reason `locked`; persistent ones reconnect after an unlock). `reason` is internal; `message` is public.

Destructive: ask the person for confirmation before calling it.

Audit event user.locked with tunnels_stopped. Cannot lock yourself or the last active administrator.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/admin/users/41/lock" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "Chargeback under review",
  "message": "Your account is locked while we check a payment."
}'
```

### Ban an account (administrators)

`POST /api/admin/users/{user_id}/ban` (risk: destructive)

A lock that also hides the account from the Users list, organization member lists and searches (use `show_banned=true` or `status=banned` to see them). The e-mail stays reserved; the person cannot delete or recover the account. `reason` is required and internal.

Destructive: ask the person for confirmation before calling it.

Audit event user.banned. Not for administrators or yourself; refused in a 'view as' session.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/admin/users/41/ban" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "Repeated phishing pages",
  "message": "This account was closed for abuse."
}'
```

### Lift a ban (administrators)

`POST /api/admin/users/{user_id}/unban` (risk: write)

`state` is `locked` (still refused, listed again) or `active` (released at once; a new API key is issued).

Audit event user.unbanned.

```bash
curl -sS -X POST "${OMNIGATE_URL:-https://tnl.tkln.dev}/api/admin/users/41/unban" \
  -H "X-API-Key: $OMNIGATE_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "state": "locked",
  "reason": "Appeal under review"
}'
```

## MCP endpoint

`POST /mcp` (full address `https://tnl.tkln.dev/mcp`) speaks the Model Context Protocol (JSON-RPC 2.0 over streamable HTTP, one JSON response per request). Authenticate with `Authorization: Bearer <api key>` or `X-API-Key`; it is the same account, limits and errors as the REST API. `GET` and `DELETE` answer 405.

Policy: read-only until the owner enables writes (Account page, or `PUT /api/account/mcp` with `{"writes_enabled": true}`). With writes off, write and destructive tools are not listed and calling one returns the tool error `writes_disabled`. Destructive tools also need `confirm: true` in their arguments. There are no administrator tools.

| tool | kind | REST operation | purpose |
|---|---|---|---|
| `list_tunnels` | read | `GET /api/tunnels` | List your tunnels. |
| `get_tunnel` | read | `GET /api/tunnels` | One tunnel of your list by subdomain. |
| `list_tunnel_logs` | read | `GET /api/tunnels/{tunnel_ref}/logs` | Recent requests and events of a tunnel. |
| `get_tunnel_access` | read | `GET /api/tunnels/{tunnel_ref}/access` | Access rules of a tunnel. |
| `get_edge_policy` | read | `GET /api/tunnels/{tunnel_ref}/edge-policy` | Country, rate, webhook, route and mirror rules of a tunnel (never the secret). |
| `get_tunnel_http_settings` | read | `GET /api/tunnels/{tunnel_ref}/http-settings` | Host header, extra headers and test endpoint of a web tunnel. |
| `rules_get` | read | `GET /api/tunnels/{tunnel_ref}/rules` | Traffic rules of a web tunnel (published and draft). |
| `rules_validate` | read | `POST /api/tunnels/{tunnel_ref}/rules/validate` | Check a traffic rules document without saving it (no rules write tool exists). |
| `rules_test` | read | `POST /api/tunnels/{tunnel_ref}/rules/test` | Dry-run one request (curl, raw HTTP or an inspector entry) through the rules; nothing is sent. |
| `rules_stats` | read | `GET /api/tunnels/{tunnel_ref}/rules/stats` | Per-rule hits, blocks, last hit, p95. |
| `list_rulesets` | read | `GET /api/rulesets` | Your shared traffic rulesets and your organizations'. |
| `list_domains` | read | `GET /api/domains` | Your reserved names and plan limit. |
| `check_domain` | read | `GET /api/domains/check` | Whether a name can be reserved. |
| `list_relays` | read | `GET /api/regions` | Regions and edges. |
| `list_private_relays` | read | `GET /api/relays` | Your private relays. |
| `get_relay_traffic` | read | `GET /api/relays/{relay_id}/traffic` | Traffic of a private relay by protocol. |
| `list_relay_anomalies` | read | `GET /api/relays/{relay_id}/anomalies` | Anomalies found on a private relay. |
| `get_relay_policy` | read | `GET /api/relays/{relay_id}/policy` | What happens automatically on an anomaly. |
| `list_relay_events` | read | `GET /api/relays/{relay_id}/events` | Who disabled, enabled or deleted a relay, and why. |
| `list_audit` | read | `GET /api/audit` | Your audit log. |
| `get_audit_metrics` | read | `GET /api/metrics/summary` | Traffic totals. |
| `get_account` | read | `GET /api/account` | Your account (the API key is masked). |
| `get_plan_usage` | read | `GET /api/account/tier` | Your plan and usage. |
| `get_docs_section` | read | `(this guide)` | The markdown of one section, as /ai/{section}.md. |
| `create_tunnel` | write | `POST /api/tunnels` | Create a tunnel. |
| `set_security_mode` | write | `PUT /api/tunnels/{tunnel_ref}/security-mode` | Change who may reach a tunnel. |
| `put_access_rules` | write | `PUT /api/tunnels/{tunnel_ref}/access` | Replace access rules. |
| `create_claim_link` | write | `POST /api/tunnels/{tunnel_ref}/claim-links` | Create a claim link. |
| `reserve_domain` | write | `POST /api/domains` | Reserve a name. |
| `delete_tunnel` | destructive | `DELETE /api/tunnels/{subdomain}` | Delete a tunnel (needs confirm: true). |
| `release_domain` | destructive | `DELETE /api/domains/{name}` | Release a name (needs confirm: true). |
| `list_organizations` | read | `GET /api/orgs` | Your organization and the ones below it. |
| `get_organization` | read | `GET /api/orgs/{org_id}` | One organization. |
| `get_org_tree` | read | `GET /api/orgs/{org_id}/tree` | The tree below an organization. |
| `list_org_users` | read | `GET /api/orgs/{org_id}/users` | Accounts of an organization (no secrets). |
| `list_org_tunnels` | read | `GET /api/orgs/{org_id}/tunnels` | Tunnels of an organization. |
| `get_org_usage` | read | `GET /api/orgs/{org_id}/usage` | Accounts, tunnels and traffic. |
| `get_org_audit` | read | `GET /api/orgs/{org_id}/audit` | Audit events of an organization. |
| `create_organization` | write | `POST /api/orgs` | Create an organization. |
| `create_org_user` | write | `POST /api/orgs/{org_id}/users` | Create an account and e-mail an invitation. |
| `set_org_limits` | write | `PUT /api/orgs/{org_id}/limits` | Replace an organization's limits. |
| `move_org_user` | write | `PATCH /api/orgs/{org_id}/users/{user_id}` | Move an account to another organization. |
| `set_org_user_role` | write | `PATCH /api/orgs/{org_id}/users/{user_id}` | Change an account's role. |
| `disable_org_user` | write | `POST /api/orgs/{org_id}/users/{user_id}/disable` | Disable an account. |
| `enable_org_user` | write | `POST /api/orgs/{org_id}/users/{user_id}/enable` | Enable an account. |
| `reset_user_password` | destructive | `POST /api/orgs/{org_id}/users/{user_id}/reset-password` | E-mail a reset link to the person (needs confirm: true; the token is never returned). |
| `disable_user_2fa` | destructive | `POST /api/orgs/{org_id}/users/{user_id}/disable-2fa` | Turn off two-step sign-in (needs confirm: true). |
| `revoke_user_sessions` | destructive | `POST /api/orgs/{org_id}/users/{user_id}/revoke-sessions` | Sign an account out everywhere (needs confirm: true). |
| `rotate_user_api_key` | destructive | `POST /api/orgs/{org_id}/users/{user_id}/rotate-api-key` | Replace an API key (needs confirm: true; the key is never shown). |
| `delete_org_tunnel` | destructive | `DELETE /api/orgs/{org_id}/tunnels/{tunnel_id}` | Delete a member's tunnel (needs confirm: true). |
| `delete_organization` | destructive | `DELETE /api/orgs/{org_id}` | Delete an empty organization (needs confirm: true). |

## API index

Every operation, generated from the OpenAPI schema. Details, parameters and schemas: [/docs](/docs).

### abuse

- `POST /api/abuse/reports`: Report abuse of a tunnel
- `GET /api/abuse/reports/{ref}/status`: Get the state of a report you sent
- `GET /api/admin/abuse-scores`: List accounts by abuse risk
- `GET /api/admin/abuse-scores/settings`: Read the abuse risk thresholds
- `PUT /api/admin/abuse-scores/settings`: Change the abuse risk thresholds
- `GET /api/admin/abuse-scores/{account_id}`: Explain one account's abuse risk
- `POST /api/admin/abuse-scores/{account_id}/ack`: Dismiss or allow-list an account for up to 30 days
- `DELETE /api/admin/abuse-scores/{account_id}/ack`: End a dismissal early
- `POST /api/admin/abuse-scores/{account_id}/lift-restriction`: End the automatic restriction of an account
- `POST /api/admin/abuse-scores/{account_id}/recompute`: Score one account now
- `GET /api/admin/abuse/reports`: List the abuse report queue
- `GET /api/admin/abuse/reports/{report_id}`: Get one abuse case
- `PATCH /api/admin/abuse/reports/{report_id}`: Change a report's status or notes
- `POST /api/admin/abuse/reports/{report_id}/dismiss`: Dismiss an abuse report
- `POST /api/admin/abuse/reports/{report_id}/lock-account`: Lock the reported tunnel's owner and resolve the case
- `POST /api/admin/abuse/reports/{report_id}/resolve`: Resolve a report without a takedown
- `POST /api/admin/abuse/reports/{report_id}/suspend-tunnel`: Suspend the reported tunnel and resolve the case
- `POST /api/admin/abuse/scan`: Run the abuse detector now
- `GET /api/admin/abuse/settings`: Read the abuse settings
- `PUT /api/admin/abuse/settings`: Change abuse settings (only the fields sent)
- `POST /api/admin/abuse/tunnels/{tunnel_id}/unsuspend`: Lift a tunnel suspension

### access

- `GET /api/tunnels/{tunnel_ref}/access`: Get a tunnel's access rules
- `PUT /api/tunnels/{tunnel_ref}/access`: Replace a tunnel's access rules
- `PATCH /api/tunnels/{tunnel_ref}/access`: Change some of a tunnel's access rules
- `GET /api/tunnels/{tunnel_ref}/access/blocked`: Get the visitors blocked by a tunnel's rules
- `GET /api/tunnels/{tunnel_ref}/allowed-ips`: List addresses admitted for a while
- `DELETE /api/tunnels/{tunnel_ref}/allowed-ips/{entry_id}`: Remove an admitted address now
- `GET /api/tunnels/{tunnel_ref}/claim-links`: List a tunnel's claim links
- `POST /api/tunnels/{tunnel_ref}/claim-links`: Create a claim link for a tunnel
- `DELETE /api/tunnels/{tunnel_ref}/claim-links/{link_id}`: Withdraw a claim link
- `GET /api/tunnels/{tunnel_ref}/passwords`: List a tunnel's access passwords
- `POST /api/tunnels/{tunnel_ref}/passwords`: Add an access password (the secret is returned once)
- `PATCH /api/tunnels/{tunnel_ref}/passwords/{pid}`: Change an access password
- `DELETE /api/tunnels/{tunnel_ref}/passwords/{pid}`: Delete an access password
- `POST /api/tunnels/{tunnel_ref}/passwords/{pid}/reset`: Give an access password a new secret (returned once)
- `GET /api/tunnels/{tunnel_ref}/security-mode`: Show a tunnel's security mode
- `PUT /api/tunnels/{tunnel_ref}/security-mode`: Change a tunnel's security mode
- `GET /api/tunnels/{tunnel_ref}/visitor-auth`: Get the sign-in in front of a tunnel
- `PUT /api/tunnels/{tunnel_ref}/visitor-auth`: Set the sign-in in front of a tunnel

### account

- `GET /api/account`: Get your account and usage
- `DELETE /api/account`: Delete your account and everything on it
- `GET /api/account/abuse-review`: Is a temporary limit on my account?
- `POST /api/account/abuse-review`: Ask for a review of the temporary limit
- `POST /api/account/invite`: Redeem an invite code
- `GET /api/account/mcp`: Can MCP clients change things for my account?
- `PUT /api/account/mcp`: Allow or forbid MCP write tools for my account
- `GET /api/account/relay-failover`: Do I get an automatic relay failover?
- `PUT /api/account/relay-failover`: Turn automatic relay failover on or off
- `GET /api/account/session`: How long do I stay signed in?
- `PUT /api/account/session`: Choose how long to stay signed in
- `GET /api/account/theme`: Which colour theme do I use?
- `PUT /api/account/theme`: Choose the colour theme
- `GET /api/account/usage`: Get your usage against the plan limits

### admin

- `GET /api/admin/invite-codes`: List invite codes
- `POST /api/admin/invite-codes`: Create an invite code
- `GET /api/admin/invite-codes/{invite_id}`: Get one invite code
- `PATCH /api/admin/invite-codes/{invite_id}`: Change, deactivate or reactivate an invite code
- `DELETE /api/admin/invite-codes/{invite_id}`: Delete an unused invite code
- `GET /api/admin/invite-codes/{invite_id}/redemptions`: Who redeemed an invite code
- `GET /api/admin/stats`: Get system-wide statistics (administrators)
- `GET /api/admin/users`: Find accounts (search, filters, pages)
- `GET /api/admin/users/{user_id}`: Get one account with its tunnels and events
- `PATCH /api/admin/users/{user_id}`: Change an account's name, plan, role or lock
- `POST /api/admin/users/{user_id}/ban`: Ban an account (lock it and hide it from the list)
- `POST /api/admin/users/{user_id}/lock`: Lock an account and stop its tunnels
- `PUT /api/admin/users/{user_id}/lock-message`: Set the public message of a locked account
- `POST /api/admin/users/{user_id}/revoke-sessions`: Sign an account out everywhere (optionally replace its API key)
- `POST /api/admin/users/{user_id}/unban`: Lift a ban
- `POST /api/admin/users/{user_id}/unlock`: Unlock an account

### alerts

- `GET /api/alerts/channels`: List your alert channels
- `POST /api/alerts/channels`: Add an alert channel
- `PATCH /api/alerts/channels/{channel_id}`: Rename, enable or disable an alert channel
- `DELETE /api/alerts/channels/{channel_id}`: Delete an alert channel
- `POST /api/alerts/channels/{channel_id}/test`: Send a test message to a channel
- `POST /api/alerts/channels/{channel_id}/verify`: Confirm an e-mail channel with the code we sent
- `GET /api/alerts/deliveries`: Show the alert delivery log
- `GET /api/alerts/events`: List the events you can be alerted about
- `GET /api/tunnels/{tunnel_ref}/alerts`: Show which alerts a tunnel sends
- `PUT /api/tunnels/{tunnel_ref}/alerts`: Choose the events and channels of a tunnel
- `POST /api/tunnels/{tunnel_ref}/app-state`: Report that the local app is down or back up
- `GET /api/tunnels/{tunnel_ref}/down-page`: Show a tunnel's down page and app state
- `PUT /api/tunnels/{tunnel_ref}/down-page`: Customise the page shown when a tunnel is down
- `DELETE /api/tunnels/{tunnel_ref}/down-page`: Go back to the platform's down page

### audit

- `GET /api/audit`: List your audit log, newest first
- `GET /api/audit/export`: Download your audit log as CSV or JSON
- `GET /api/logs/export`: Download recent log entries as CSV or JSON
- `GET /api/metrics/endpoints`: List the busiest request paths
- `GET /api/metrics/export`: Download per-minute traffic as CSV or JSON
- `GET /api/metrics/summary`: Get traffic totals per tunnel and overall
- `GET /api/metrics/timeseries`: Get traffic over time, zero-filled
- `GET /api/tunnels/{tunnel_ref}/logs`: List recent requests and connection events of a tunnel
- `GET /api/tunnels/{tunnel_ref}/logs/stream`: Follow a tunnel's log live (Server-Sent Events)
- `GET /api/tunnels/{tunnel_ref}/recording`: Show a tunnel's session recording setting
- `PUT /api/tunnels/{tunnel_ref}/recording`: Turn session recording on or off for a tunnel
- `GET /api/tunnels/{tunnel_ref}/recording/export`: Download a tunnel's recorded sessions
- `GET /api/tunnels/{tunnel_ref}/recording/sessions`: List a tunnel's recorded sessions
- `GET /api/tunnels/{tunnel_ref}/recording/verify`: Verify a tunnel's recording chain

### auth

- `POST /api/cli/token`: Exchange a browser sign-in code for tokens
- `POST /api/login`: Sign in with e-mail and password
- `POST /api/login/email-code`: Ask for a sign-in code by e-mail (API)
- `POST /api/login/email-code/verify`: Finish a sign-in with the e-mailed code (API)
- `POST /api/login/method`: Ask which sign-in an account uses
- `POST /api/login/totp`: Finish a two-step sign-in with a code
- `POST /api/logout`: End the web session
- `POST /api/register`: Create an account
- `POST /api/resend-verification`: Send a new verification e-mail
- `GET /api/session`: When does my session end?
- `POST /api/session/touch`: Keep me signed in (renew my session)
- `GET /api/verify-email`: Verify an e-mail address with the token from the e-mail
- `POST /web/forgot-password`: Ask for a password reset e-mail
- `POST /web/login/email-code`: Ask for a sign-in code by e-mail
- `POST /web/login/email-code/verify`: Finish a sign-in with the e-mailed code
- `POST /web/login/passkey`: Sign in with a passkey
- `POST /web/login/passkey/options`: Start a passkey sign-in
- `POST /web/login/recovery`: Sign in with a passkey recovery code
- `POST /web/reset-password`: Set a new password with a reset token

### bandwidth

- `GET /api/account/bandwidth`: Get your bandwidth: month so far, allowance, credits, packs
- `GET /api/account/bandwidth/orders`: List your bandwidth orders
- `POST /api/account/bandwidth/orders`: Order a bandwidth pack
- `POST /api/account/bandwidth/orders/{order_id}/cancel`: Cancel one of your pending orders
- `GET /api/account/bandwidth/packs`: List the bandwidth packs you can order
- `GET /api/account/traffic`: Get your account's traffic with a per-tunnel breakdown
- `POST /api/admin/bandwidth/grants`: Give an account bandwidth credit
- `GET /api/admin/bandwidth/orders`: List bandwidth orders
- `POST /api/admin/bandwidth/orders/{order_id}/cancel`: Cancel a pending order
- `POST /api/admin/bandwidth/orders/{order_id}/fulfil`: Mark an order paid and give the credit
- `GET /api/admin/bandwidth/packs`: List every bandwidth pack, active or not
- `POST /api/admin/bandwidth/packs`: Add a bandwidth pack
- `PATCH /api/admin/bandwidth/packs/{pack_id}`: Change or switch off a bandwidth pack
- `DELETE /api/admin/bandwidth/packs/{pack_id}`: Delete a bandwidth pack
- `GET /api/admin/traffic`: Rank accounts by the traffic they moved
- `GET /api/tunnels/{tunnel_ref}/bandwidth`: Get a tunnel's hourly cap and whether it is paused
- `PUT /api/tunnels/{tunnel_ref}/bandwidth`: Set a tunnel's hourly cap and automatic resume
- `POST /api/tunnels/{tunnel_ref}/bandwidth/resume`: Re-enable a tunnel paused by a bandwidth rule
- `GET /api/tunnels/{tunnel_ref}/traffic`: Get a tunnel's traffic as a series with totals

### cdn

- `GET /api/tunnels/{tunnel_ref}/cdn`: Show a tunnel's CDN cache settings
- `PUT /api/tunnels/{tunnel_ref}/cdn`: Turn the CDN cache on or off, or customise it
- `POST /api/tunnels/{tunnel_ref}/cdn/purge`: Purge cached responses: all, a path prefix or one path
- `GET /api/tunnels/{tunnel_ref}/cdn/stats`: Show cache hits, misses and bytes saved

### certificates

- `GET /api/admin/certs`: List the managed certificates and their state
- `POST /api/admin/certs/pull`: Pull the company certificate from the central host now
- `POST /api/admin/certs/request`: Order a certificate for hostnames or for a relay
- `GET /api/admin/certs/status`: Show the state of the daily pull, ACME and the store
- `GET /api/admin/certs/{name}`: Show one certificate with its history
- `PATCH /api/admin/certs/{name}`: Switch automatic renewal of a certificate on or off
- `GET /api/admin/certs/{name}/bundle`: Fetch the chain and private key of a managed certificate
- `POST /api/admin/certs/{name}/deploy`: Install a certificate again where it is used
- `GET /api/admin/certs/{name}/events`: List the history of one certificate
- `POST /api/admin/certs/{name}/renew`: Force a new certificate now

### contact

- `GET /api/admin/contact-messages`: List the messages sent through the contact form
- `POST /api/admin/contact-messages/{message_id}/resend`: Deliver a stored contact message again
- `POST /contact`: Send a message through the contact form

### custom-domains

- `GET /api/admin/custom-domains`: List custom domains of all accounts
- `POST /api/admin/custom-domains/{domain_id}/disable`: Take a custom domain down (abuse)
- `POST /api/admin/custom-domains/{domain_id}/enable`: Lift the takedown of a custom domain
- `GET /api/custom-domains`: List your custom domains
- `POST /api/custom-domains`: Add a custom domain
- `GET /api/custom-domains/{domain_id}`: Read one custom domain
- `DELETE /api/custom-domains/{domain_id}`: Delete a custom domain
- `POST /api/custom-domains/{domain_id}/attach`: Attach a custom domain to a tunnel
- `POST /api/custom-domains/{domain_id}/detach`: Detach a custom domain from its tunnel
- `POST /api/custom-domains/{domain_id}/verify`: Check the DNS records now

### demo

- `POST /api/demo/seed`: Replace your sample data with fresh demo tunnels
- `DELETE /api/demo/seed`: Remove your sample data

### dev-help

- `GET /ai.md`: The whole guide for AI agents (markdown)
- `GET /ai/{section}.md`: One section of the guide for AI agents (markdown)
- `GET /api/dev-help`: List the Dev Help sections
- `GET /api/dev-help/{section}`: One Dev Help section with ready-to-paste snippets
- `GET /llms-full.txt`: The full public text for AI agents (llms-full.txt)
- `GET /llms.txt`: Index of the guides for AI agents (llms.txt)
- `GET /tlomnigate.schema.json`: JSON Schema of tlomnigate.yml

### direct

- `POST /api/tunnels/{name}/direct/admit-request`: Admit your address to a TCP/UDP tunnel's direct port
- `GET /api/tunnels/{tunnel_ref}/direct`: Show a tunnel's direct path status
- `PUT /api/tunnels/{tunnel_ref}/direct`: Turn a tunnel's direct path on or off
- `POST /api/tunnels/{tunnel_ref}/direct-metrics`: Report requests and connections served on the direct path
- `POST /api/tunnels/{tunnel_ref}/direct/announce`: Announce the client's direct endpoint
- `POST /api/tunnels/{tunnel_ref}/direct/csr`: Order a certificate for the tunnel's direct host
- `POST /api/tunnels/{tunnel_ref}/direct/heartbeat`: Send a direct-path heartbeat and get the current rules

### dns

- `GET /api/admin/dns`: Show DNS automation status and queue
- `POST /api/admin/dns/reconcile`: Reconcile DNS with the tunnels now

### domains

- `GET /api/admin/domains`: List reservations across all accounts
- `DELETE /api/admin/domains/{name}`: Release any account's reservation
- `GET /api/domains`: List your reserved names and tunnel names
- `POST /api/domains`: Reserve a name
- `GET /api/domains/check`: Check whether a name can be reserved
- `DELETE /api/domains/{name}`: Release a reserved name
- `GET /api/subdomains/reservations`: List your reserved subdomains
- `POST /api/subdomains/reservations`: Reserve a subdomain
- `DELETE /api/subdomains/reservations/{name}`: Release a reserved subdomain

### edge-policy

- `GET /api/tunnels/{tunnel_ref}/edge-policy`: Get a tunnel's edge rules
- `PUT /api/tunnels/{tunnel_ref}/edge-policy`: Replace a tunnel's edge rules
- `PATCH /api/tunnels/{tunnel_ref}/edge-policy`: Change some of a tunnel's edge rules
- `PUT /api/tunnels/{tunnel_ref}/edge-policy/webhook-secret`: Set a tunnel's webhook signing secret
- `DELETE /api/tunnels/{tunnel_ref}/edge-policy/webhook-secret`: Forget a tunnel's webhook secret

### feedback

- `POST /api/feedback`: Send product feedback

### inspector

- `GET /api/admin/inspector`: Show the request inspector's global switch and storage ceiling
- `PUT /api/admin/inspector`: Switch the request inspector off or on for the whole server, or set the ceiling
- `GET /api/inspect/presets`: Header hints for replaying a Stripe, GitHub or Slack webhook
- `POST /api/internal/hold/sync`: Edge sync: announce held requests and fetch decisions (internal)
- `POST /api/internal/inspect/ingest`: Receive a batch of captured requests from the edge (internal)
- `GET /api/tunnels/{tunnel_ref}/hold`: Show hold mode of a tunnel and the requests held right now
- `PUT /api/tunnels/{tunnel_ref}/hold`: Start or stop holding incoming requests on a tunnel
- `POST /api/tunnels/{tunnel_ref}/hold/{hold_id}/drop`: Drop a held request and answer the sender instead
- `POST /api/tunnels/{tunnel_ref}/hold/{hold_id}/forward`: Send a held request on to your app, as it is or edited
- `GET /api/tunnels/{tunnel_ref}/inspect`: Show the request inspector of a tunnel and its newest requests
- `PUT /api/tunnels/{tunnel_ref}/inspect`: Turn the request inspector on or off, or change how much it captures
- `DELETE /api/tunnels/{tunnel_ref}/inspect`: Delete every captured request of a tunnel
- `GET /api/tunnels/{tunnel_ref}/inspect/export/har`: Export selected captured requests as a HAR file
- `GET /api/tunnels/{tunnel_ref}/inspect/{entry_id}`: Show one captured request with its headers and body previews
- `GET /api/tunnels/{tunnel_ref}/inspect/{entry_id}/curl`: Show a captured request as a curl command
- `POST /api/tunnels/{tunnel_ref}/inspect/{entry_id}/replay`: Send a captured request again (optionally edited) and return the new entry

### mcp

- `POST /mcp`: Model Context Protocol endpoint (JSON-RPC 2.0)

### meta

- `GET /health`: Check that the service is running

### organizations

- `GET /api/admin/orgs`: List every organization (administrators)
- `GET /api/impersonation`: Am I viewing as someone else?
- `POST /api/impersonation/stop`: End the view as another account
- `GET /api/orgs`: List my organization and the ones below it
- `POST /api/orgs`: Create an organization
- `GET /api/orgs/{org_id}`: Get one organization
- `PATCH /api/orgs/{org_id}`: Rename, change settings, disable or enable
- `DELETE /api/orgs/{org_id}`: Delete an empty organization
- `GET /api/orgs/{org_id}/audit`: List the audit events of an organization's accounts
- `GET /api/orgs/{org_id}/export`: Download users, tunnels or audit as CSV/JSON
- `GET /api/orgs/{org_id}/limits`: Get the limits of an organization
- `PUT /api/orgs/{org_id}/limits`: Set the limits of an organization
- `POST /api/orgs/{org_id}/move`: Move an organization under another one
- `GET /api/orgs/{org_id}/recording`: Show an organization's session recording policy
- `PUT /api/orgs/{org_id}/recording`: Set an organization's session recording policy
- `GET /api/orgs/{org_id}/recording/export`: Download the recorded sessions of an organization's tunnels
- `GET /api/orgs/{org_id}/recording/sessions`: List the recorded sessions of an organization's tunnels
- `GET /api/orgs/{org_id}/scim/log`: Recent SCIM provisioning
- `GET /api/orgs/{org_id}/scim/tokens`: List the SCIM tokens of an organization
- `POST /api/orgs/{org_id}/scim/tokens`: Create a SCIM token
- `DELETE /api/orgs/{org_id}/scim/tokens/{token_id}`: Revoke a SCIM token
- `POST /api/orgs/{org_id}/scim/tokens/{token_id}/rotate`: Replace a SCIM token
- `GET /api/orgs/{org_id}/sso`: Read the SSO rule and SCIM summary of an organization
- `PUT /api/orgs/{org_id}/sso`: Change the SSO enforcement of an organization
- `POST /api/orgs/{org_id}/sso/domains`: Add an e-mail domain to prove
- `DELETE /api/orgs/{org_id}/sso/domains/{domain_id}`: Remove an e-mail domain
- `POST /api/orgs/{org_id}/sso/domains/{domain_id}/verify`: Check the DNS record of a domain
- `GET /api/orgs/{org_id}/tree`: Get the tree below an organization
- `GET /api/orgs/{org_id}/tunnels`: List the tunnels of an organization
- `PATCH /api/orgs/{org_id}/tunnels/{tunnel_id}`: Share a tunnel with the organization (or stop sharing)
- `DELETE /api/orgs/{org_id}/tunnels/{tunnel_id}`: Delete a member's tunnel
- `POST /api/orgs/{org_id}/tunnels/{tunnel_id}/start`: Start a stopped tunnel
- `POST /api/orgs/{org_id}/tunnels/{tunnel_id}/stop`: Stop a member's tunnel
- `GET /api/orgs/{org_id}/usage`: Get accounts, tunnels and traffic per child
- `GET /api/orgs/{org_id}/users`: List the accounts of an organization
- `POST /api/orgs/{org_id}/users`: Create an account and e-mail an invitation
- `GET /api/orgs/{org_id}/users/{user_id}`: Get one account of an organization
- `PATCH /api/orgs/{org_id}/users/{user_id}`: Change an account's role, plan, limits or organization
- `DELETE /api/orgs/{org_id}/users/{user_id}`: Remove an account from the organization
- `POST /api/orgs/{org_id}/users/{user_id}/disable`: Disable an account
- `POST /api/orgs/{org_id}/users/{user_id}/disable-2fa`: Turn off an account's two-step sign-in
- `POST /api/orgs/{org_id}/users/{user_id}/enable`: Enable an account
- `POST /api/orgs/{org_id}/users/{user_id}/impersonate`: Start a view as an account (impersonation)
- `POST /api/orgs/{org_id}/users/{user_id}/reset-password`: Reset an account's password (link or one-time token)
- `POST /api/orgs/{org_id}/users/{user_id}/revoke-sessions`: Sign an account out everywhere
- `POST /api/orgs/{org_id}/users/{user_id}/rotate-api-key`: Replace an account's API key

### pages

- `GET /api/pages/asset/{name}`: Fetch an uploaded image
- `POST /api/pages/assets`: Upload a logo or favicon
- `GET /api/pages/effective/tunnel/{tunnel_ref}`: What each page resolves to for a tunnel
- `GET /api/pages/preview`: Open a page as a visitor sees it
- `POST /api/pages/preview`: Preview an unsaved page
- `GET /api/pages/{scope}/{scope_id}`: List the pages you can customise
- `GET /api/pages/{scope}/{scope_id}/{kind}`: Read one page
- `PUT /api/pages/{scope}/{scope_id}/{kind}`: Customise a page
- `DELETE /api/pages/{scope}/{scope_id}/{kind}`: Reset a page to the default
- `POST /api/pages/{scope}/{scope_id}/{kind}/copy`: Copy a page from another scope

### relays

- `GET /api/admin/relay-anomalies`: Relay anomalies (health panel)
- `POST /api/admin/relay-anomalies/{anomaly_id}/ack`: Acknowledge a relay anomaly
- `POST /api/admin/relay-anomalies/{anomaly_id}/resolve`: Resolve a relay anomaly
- `GET /api/admin/relay-events`: History of every relay, deleted ones included
- `GET /api/admin/relays/traffic`: Traffic of every relay (overview)
- `GET /api/capacity/relays`: Show free public ports in the pool and per relay
- `GET /api/geo/visitors`: Show where your tunnels' visitors come from
- `GET /api/ip-lookup/{ip}`: Locate one public IP address
- `GET /api/regions`: List the edges and which of your tunnels use them
- `GET /api/regions/map`: Show your tunnels as arrows from their client to their relay
- `POST /api/regions/{slug}/locate`: Locate an edge on the map now
- `GET /api/relays`: List your private relays
- `POST /api/relays`: Add a private relay
- `GET /api/relays/{relay_id}`: Show one private relay
- `PATCH /api/relays/{relay_id}`: Edit a private relay
- `DELETE /api/relays/{relay_id}`: Delete a private relay, with a reason
- `GET /api/relays/{relay_id}/anomalies`: Anomalies of one relay
- `POST /api/relays/{relay_id}/disable`: Disable a private relay, with a reason
- `POST /api/relays/{relay_id}/drain`: Drain a private relay before maintenance or deletion
- `POST /api/relays/{relay_id}/enable`: Enable a disabled private relay
- `POST /api/relays/{relay_id}/enrollment-token`: Create an enrollment token for a private relay
- `GET /api/relays/{relay_id}/events`: History of a relay (disable, enable, delete, isolation)
- `PUT /api/relays/{relay_id}/name`: Rename a relay
- `GET /api/relays/{relay_id}/policy`: Show what happens automatically on an anomaly
- `PUT /api/relays/{relay_id}/policy`: Change what happens automatically on an anomaly
- `GET /api/relays/{relay_id}/traffic`: Traffic through a relay, by protocol
- `POST /api/relays/{relay_id}/undrain`: Put a drained private relay back in service
- `GET /api/satellites`: List every satellite and private relay
- `POST /api/satellites`: Add a shared satellite
- `PATCH /api/satellites/{satellite_id}`: Edit a shared satellite
- `DELETE /api/satellites/{satellite_id}`: Delete any satellite or private relay, with a reason
- `POST /api/satellites/{satellite_id}/disable`: Disable a satellite or private relay, with a reason
- `POST /api/satellites/{satellite_id}/enable`: Enable a satellite or private relay again
- `POST /api/satellites/{satellite_id}/enrollment-token`: Create an enrollment token for a shared satellite
- `GET /api/tunnels/{tunnel_ref}/satellites`: Show where a tunnel is served
- `PUT /api/tunnels/{tunnel_ref}/satellites`: Choose which satellites serve a tunnel
- `POST /api/tunnels/{tunnel_ref}/satellites/{slug}/token`: Get a relay token for one of a tunnel's satellites
- `POST /sat/v1/cert`: Issue or renew the certificate of a tunnel host
- `GET /sat/v1/config`: Fetch a satellite's signed configuration
- `POST /sat/v1/enroll`: Enroll a satellite with its one-time token
- `POST /sat/v1/heartbeat`: Report a satellite's health
- `GET /sat/v1/relay-cert`: Download this relay's certificate and key
- `POST /sat/v1/relay-cert/loaded`: Report the certificate this relay now serves
- `POST /sat/v1/relay-cert/request`: Ask the central server for this relay's certificate
- `POST /sat/v1/rotate`: Rotate a satellite's identity certificate
- `POST /sat/v1/traffic`: Report the bytes a satellite relayed for raw TCP and UDP tunnels

### scim

- `GET /scim/v2/Groups`: List the groups (roles)
- `POST /scim/v2/Groups`: Create a group (not supported)
- `GET /scim/v2/Groups/{group_id}`: Get one group
- `PUT /scim/v2/Groups/{group_id}`: Replace the members of a group
- `PATCH /scim/v2/Groups/{group_id}`: Add or remove members of a group
- `DELETE /scim/v2/Groups/{group_id}`: Delete a group (not supported)
- `GET /scim/v2/ResourceTypes`: List the SCIM resource types
- `GET /scim/v2/ResourceTypes/{type_id}`: Get one SCIM resource type
- `GET /scim/v2/Schemas`: List the SCIM schemas
- `GET /scim/v2/Schemas/{schema_id}`: Get one SCIM schema
- `GET /scim/v2/ServiceProviderConfig`: SCIM service provider configuration
- `GET /scim/v2/Users`: List or search users
- `POST /scim/v2/Users`: Create a user
- `GET /scim/v2/Users/{user_id}`: Get one user
- `PUT /scim/v2/Users/{user_id}`: Replace a user
- `PATCH /scim/v2/Users/{user_id}`: Change parts of a user
- `DELETE /scim/v2/Users/{user_id}`: Deactivate a user

### security

- `GET /api/account/passkeys`: List passkeys
- `POST /api/account/passkeys`: Register a passkey
- `POST /api/account/passkeys/challenges`: Start passkey registration
- `DELETE /api/account/passkeys/{credential_id}`: Delete a passkey
- `GET /api/account/totp`: Get the state of two-step sign-in
- `POST /api/account/totp/disable`: Turn two-step sign-in off
- `POST /api/account/totp/enable`: Confirm two-step sign-in with a code
- `POST /api/account/totp/setup`: Start setting up two-step sign-in
- `GET /api/captcha`: Get a captcha challenge for sign-up or password reset
- `POST /api/tunnels/{tunnel_ref}/knock-secret`: Rotate the tunnel's knock secret (returned this once)

### takelan

- `GET /api/account/takelan`: Show this account's Takelan SSO link
- `DELETE /api/account/takelan`: Unlink the Takelan SSO account
- `POST /api/account/takelan/link`: Link a Takelan SSO account
- `POST /api/account/takelan/link/2fa`: Finish linking a Takelan SSO account
- `GET /api/admin/takelan`: Show the Takelan integration status
- `POST /api/admin/takelan/backfill`: Queue existing accounts for Takelan provisioning
- `GET /api/admin/takelan/crm`: Show the Takelan CRM outbox status
- `POST /api/admin/takelan/crm/backfill`: Queue existing accounts for the Takelan CRM
- `GET /api/admin/takelan/crm/items`: List Takelan CRM outbox items
- `POST /api/admin/takelan/crm/retry`: Requeue failed Takelan CRM outbox items
- `GET /api/admin/takelan/items`: List Takelan provisioning items
- `POST /api/admin/takelan/retry`: Requeue failed Takelan provisioning items
- `GET /api/auth/takelan/config`: Show whether Takelan SSO sign-in is offered
- `POST /api/takelan/2fa`: Finish a Takelan SSO sign-in (tokens)
- `POST /api/takelan/login`: Sign in with Takelan SSO (tokens)
- `POST /api/takelan/webhooks`: Receive a signed TakeLan account event
- `POST /web/takelan/2fa`: Finish a Takelan SSO sign-in (session cookie)
- `POST /web/takelan/login`: Sign in with Takelan SSO (session cookie)

### tiers

- `GET /api/account/tier`: Get your plan and your usage against its limits
- `GET /api/admin/port-exceptions`: List per-user port exceptions
- `POST /api/admin/port-exceptions`: Let one account use a blocked port range
- `DELETE /api/admin/port-exceptions/{exception_id}`: Remove a per-user port exception
- `GET /api/admin/port-rules`: List every port rule
- `POST /api/admin/port-rules`: Block a port or a port range
- `GET /api/admin/port-rules/check`: Test a port, optionally as one account
- `PATCH /api/admin/port-rules/{rule_id}`: Edit or switch off a port rule
- `DELETE /api/admin/port-rules/{rule_id}`: Delete a port rule you added
- `GET /api/admin/tiers`: List every plan with its number of accounts
- `POST /api/admin/tiers`: Add a plan
- `PATCH /api/admin/tiers/{tier_id}`: Change a plan's limits, flags or price
- `DELETE /api/admin/tiers/{tier_id}`: Delete an unused plan
- `PUT /api/admin/users/{user_id}/tier`: Move an account to a plan
- `GET /api/plan-catalog`: The public plan catalog (display only)
- `GET /api/ports/check`: Check whether you can use a public port

### traffic-alerts

- `GET /api/account/traffic-alerts`: List your account-wide traffic alert rules
- `POST /api/account/traffic-alerts`: Add an account-wide traffic alert rule
- `PATCH /api/account/traffic-alerts/{rule_id}`: Change an account-wide rule
- `DELETE /api/account/traffic-alerts/{rule_id}`: Delete an account-wide rule
- `GET /api/orgs/{org_id}/traffic-alerts`: List the traffic alert rules of an organization
- `POST /api/orgs/{org_id}/traffic-alerts`: Add a traffic alert rule to an organization
- `PATCH /api/orgs/{org_id}/traffic-alerts/{rule_id}`: Change an organization rule
- `DELETE /api/orgs/{org_id}/traffic-alerts/{rule_id}`: Delete an organization rule
- `GET /api/traffic-alerts/events`: List the events a traffic alert rule can watch
- `GET /api/tunnels/{tunnel_ref}/traffic-alerts`: List the traffic alert rules of a tunnel
- `POST /api/tunnels/{tunnel_ref}/traffic-alerts`: Add a traffic alert rule to a tunnel
- `GET /api/tunnels/{tunnel_ref}/traffic-alerts/history`: What traffic alerts fired on a tunnel
- `PATCH /api/tunnels/{tunnel_ref}/traffic-alerts/{rule_id}`: Change one of the tunnel's own rules
- `DELETE /api/tunnels/{tunnel_ref}/traffic-alerts/{rule_id}`: Delete one of the tunnel's own rules
- `POST /api/tunnels/{tunnel_ref}/traffic-alerts/{rule_id}/mute`: Mute a rule on this tunnel for a while
- `POST /api/tunnels/{tunnel_ref}/traffic-alerts/{rule_id}/test`: Send a test notification for a rule

### traffic-rules

- `PUT /api/admin/tunnels/{tunnel_ref}/rules/gate-overrides`: Grant or remove the WAF for one tunnel's rules
- `GET /api/admin/waf/bundles`: List the WAF rule bundles
- `POST /api/admin/waf/bundles`: Upload a WAF rule bundle
- `POST /api/admin/waf/bundles/{bundle_id}/activate`: Activate a WAF rule bundle
- `GET /api/admin/waf/sources`: List addresses attacking WAF-enabled tunnels
- `GET /api/domain-proofs`: List your forward domain proofs
- `POST /api/domain-proofs`: Start a forward domain proof
- `DELETE /api/domain-proofs/{proof_id}`: Remove a forward domain proof
- `POST /api/domain-proofs/{proof_id}/verify`: Check a forward domain proof now
- `GET /api/orgs/{org_id}/rulesets`: List an organization's shared traffic rulesets
- `POST /api/orgs/{org_id}/rulesets`: Create an organization traffic ruleset
- `GET /api/orgs/{org_id}/rulesets/{slug}`: Show an organization traffic ruleset
- `PATCH /api/orgs/{org_id}/rulesets/{slug}`: Change an organization traffic ruleset's name, inheritance or lock
- `DELETE /api/orgs/{org_id}/rulesets/{slug}`: Delete an organization traffic ruleset
- `GET /api/orgs/{org_id}/rulesets/{slug}/diff`: Compare two revisions of an organization traffic ruleset
- `PUT /api/orgs/{org_id}/rulesets/{slug}/draft`: Save an organization traffic ruleset as the draft
- `POST /api/orgs/{org_id}/rulesets/{slug}/publish`: Publish an organization traffic ruleset
- `GET /api/orgs/{org_id}/rulesets/{slug}/revisions`: List an organization traffic ruleset's revisions
- `GET /api/orgs/{org_id}/rulesets/{slug}/revisions/{rev}`: Show one revision of an organization traffic ruleset
- `POST /api/orgs/{org_id}/rulesets/{slug}/rollback`: Publish an earlier revision of an organization traffic ruleset again
- `POST /api/orgs/{org_id}/rulesets/{slug}/validate`: Check an organization traffic ruleset without saving it
- `GET /api/rules/schema`: Get the traffic rules JSON Schema
- `GET /api/rulesets`: List your shared traffic rulesets
- `POST /api/rulesets`: Create a shared traffic ruleset
- `GET /api/rulesets/{slug}`: Show a shared traffic ruleset
- `PATCH /api/rulesets/{slug}`: Rename a shared traffic ruleset
- `DELETE /api/rulesets/{slug}`: Delete a shared traffic ruleset
- `GET /api/rulesets/{slug}/diff`: Compare two revisions of a shared traffic ruleset
- `PUT /api/rulesets/{slug}/draft`: Save a shared traffic ruleset as the draft
- `POST /api/rulesets/{slug}/publish`: Publish a shared traffic ruleset
- `GET /api/rulesets/{slug}/revisions`: List a shared traffic ruleset's revisions
- `GET /api/rulesets/{slug}/revisions/{rev}`: Show one revision of a shared traffic ruleset
- `POST /api/rulesets/{slug}/rollback`: Publish an earlier revision of a shared traffic ruleset again
- `POST /api/rulesets/{slug}/validate`: Check a shared traffic ruleset without saving it
- `GET /api/tunnels/{tunnel_ref}/rules`: Show a tunnel's traffic rules
- `GET /api/tunnels/{tunnel_ref}/rules/diff`: Compare two traffic rules revisions
- `PUT /api/tunnels/{tunnel_ref}/rules/draft`: Save a tunnel's traffic rules as the draft
- `POST /api/tunnels/{tunnel_ref}/rules/publish`: Publish a tunnel's draft traffic rules
- `GET /api/tunnels/{tunnel_ref}/rules/revisions`: List a tunnel's traffic rules revisions
- `GET /api/tunnels/{tunnel_ref}/rules/revisions/{rev}`: Show one traffic rules revision
- `POST /api/tunnels/{tunnel_ref}/rules/rollback`: Publish an earlier traffic rules revision again
- `GET /api/tunnels/{tunnel_ref}/rules/scripts`: List a tunnel's traffic rules scripts
- `POST /api/tunnels/{tunnel_ref}/rules/scripts/validate`: Check a traffic rules script, optionally with a dry run
- `GET /api/tunnels/{tunnel_ref}/rules/stats`: Show per-rule hit counters of a tunnel's traffic rules
- `POST /api/tunnels/{tunnel_ref}/rules/test`: Dry-run one request through a tunnel's traffic rules
- `POST /api/tunnels/{tunnel_ref}/rules/validate`: Check a traffic rules document without saving it
- `POST /api/tunnels/{tunnel_ref}/rules/waf-exception`: Allow a WAF rule for a path (adds an exception to the draft)
- `GET /api/tunnels/{tunnel_ref}/waf`: Show a tunnel's WAF settings and state
- `GET /api/tunnels/{tunnel_ref}/waf/events`: List a tunnel's recent WAF events
- `GET /api/tunnels/{tunnel_ref}/waf/exceptions`: List a tunnel's WAF exceptions
- `GET /api/tunnels/{tunnel_ref}/waf/hits`: Count a tunnel's WAF hits per rule and hour

### tunnels

- `GET /api/health/overview`: Count your tunnels by up, degraded and down
- `POST /api/internal/edge/hint`: Receive a hint from the edge: an app rejected the Host header (internal)
- `GET /api/subdomain-check`: Check whether a tunnel name is free
- `GET /api/tunnels`: List your tunnels
- `POST /api/tunnels`: Create or re-key a tunnel
- `DELETE /api/tunnels/{subdomain}`: Delete a tunnel
- `POST /api/tunnels/{subdomain}/clear-cache`: Clear a tunnel's cache
- `GET /api/tunnels/{tunnel_ref}/expiry`: Show when a tunnel expires
- `PUT /api/tunnels/{tunnel_ref}/expiry`: Extend, shorten or clear a tunnel's expiry
- `GET /api/tunnels/{tunnel_ref}/health`: Get a tunnel's keep-alive state, checks and 24h uptime
- `GET /api/tunnels/{tunnel_ref}/http-settings`: Show a tunnel's Host header, extra headers and test endpoint
- `PUT /api/tunnels/{tunnel_ref}/http-settings`: Change a tunnel's Host header, extra headers and test endpoint
- `DELETE /api/tunnels/{tunnel_ref}/http-settings`: Go back to the default Host header and headers
- `PUT /api/tunnels/{tunnel_ref}/maintenance`: Turn a tunnel's maintenance mode on or off
- `GET /api/tunnels/{tunnel_ref}/path`: Show where a tunnel's client and relay are
- `GET /api/tunnels/{tunnel_ref}/schedule`: Show a tunnel's opening hours and whether it is open now
- `PUT /api/tunnels/{tunnel_ref}/schedule`: Set a tunnel's opening hours
- `DELETE /api/tunnels/{tunnel_ref}/schedule`: Remove a tunnel's opening hours
- `POST /api/tunnels/{tunnel_ref}/schedule/pause`: Close a tunnel until a time
- `DELETE /api/tunnels/{tunnel_ref}/schedule/pause`: End a pause now
