Skip to main content

Remnawave Python SDK — fork

Asynchronous Python client for the Remnawave panel API, built on httpx + pydantic v2, with orjson for fast serialization.

[!IMPORTANT] This is a fork of remnawave/python-sdk. It is published to PyPI as remnactual (the upstream remnawave project belongs to the Remnawave community). The import name is unchanged — import remnawave. The fork exists to track the Remnawave 3.x API closely and to fix the divergences that the migration from 2.7.x left behind — see What this fork changes. Attribution for the original work is at the bottom of this file.


Compatibility

SDK version Remnawave panel Backend contract
3.0.0 >= 3.0.0 @remnawave/backend-contract 3.0.0
2.8.1 >= 2.8.0, < 3.0 @remnawave/backend-contract 2.8.35

Every endpoint, request body and response model in this fork is verified against libs/contract of remnawave/backend at tag 3.0.0.

Remnawave 3.0 is not backwards compatible with 2.8 — users are identified by a numeric id instead of a uuid, /api/ip-control became /api/connections, and many endpoints answer with an empty body. Pin 2.8.1 if your panel is still on the 2.8 line.

Requirements: Python >= 3.11, < 3.15.

Installation

pip install remnactual

The import name stays remnawave:

from remnawave import RemnawaveSDK

A specific branch straight from git:

pip install "git+https://github.com/IceOne-i/remnactual.git@feat/api-3.0"

Quick start

import asyncio
import os

from remnawave import RemnawaveSDK
from remnawave.models import GetAllUsersResponseDto


async def main() -> None:
    sdk = RemnawaveSDK(
        # https://panel.example.com  or  http://127.0.0.1:3000
        base_url=os.environ["REMNAWAVE_BASE_URL"],
        # Bearer token from the panel: Settings -> API Tokens
        token=os.environ["REMNAWAVE_TOKEN"],
    )

    page: GetAllUsersResponseDto = await sdk.users.get_all_users(start=0, size=50)
    print(f"total: {page.total}")
    for user in page.users:
        # 3.0: users are identified by a numeric id; there is no `uuid` field any more
        print(user.id, user.username, user.status, user.expire_at)


asyncio.run(main())

The transport unwraps the API's {"response": ...} envelope for you, so responses are the payload itself.

Client options

sdk = RemnawaveSDK(
    base_url="https://panel.example.com",
    token="...",
    caddy_token="...",                      # X-Api-Key when the panel sits behind Caddy
    ssl_ignore=False,                       # skip TLS verification (self-signed certs)
    custom_headers={"X-Real-IP": "1.2.3.4"},
    cookies={"session": "..."},             # reverse-proxy auth
)

You can also pass a pre-configured client and manage its lifetime yourself:

import httpx

async with httpx.AsyncClient(
    base_url="https://panel.example.com/api",
    headers={"Authorization": "Bearer ..."},
) as client:
    sdk = RemnawaveSDK(client=client)

base_url is normalised automatically: a trailing slash is stripped and /api is appended when missing.

Controllers

Attribute Covers
sdk.users CRUD, lookups by id / short-uuid / username, actions (incl. extend), tags, stream (cursor pagination + filters), accessible nodes
sdk.users_bulk_actions bulk/* and bulk/all/* operations
sdk.nodes CRUD, enable/disable/restart, reorder, reset traffic, bulk actions, tags
sdk.hosts / sdk.hosts_bulk_actions CRUD, reorder, tags, bulk enable/disable/delete/update
sdk.config_profiles Config profiles, inbounds, computed config, reorder
sdk.inbounds Inbounds under /config-profiles/inbounds
sdk.internal_squads / sdk.external_squads Squads, membership bulk actions, accessible nodes, reorder
sdk.subscription Public /sub/{shortUuid} endpoints
sdk.subscriptions Admin subscription lookups, raw subscription, connection keys
sdk.subscriptions_settings / sdk.subscriptions_template / sdk.subscription_page_config Subscription settings, templates, subscription page configs
sdk.subscription_request_history Request history and its stats
sdk.hwid HWID devices, stats, top users
sdk.connections Per-user / per-node connection jobs, drop connections (was sdk.ip_control)
sdk.node_plugins Node plugins, executor, torrent-blocker reports
sdk.infra_billing Providers, billing nodes, billing history
sdk.bandwidthstats Per-node and per-user bandwidth stats (incl. legacy endpoints)
sdk.system Stats, digest, HTTP counters, health, metrics, recap, x25519, SRR matcher
sdk.auth / sdk.passkeys / sdk.api_tokens_management Login, OAuth2, passkeys, scoped API tokens
sdk.remnawave_settings / sdk.snippets / sdk.keygen / sdk.metadata Panel settings, snippets, node secret key, user/node metadata
sdk.webhook_utility Webhook signature validation and payload parsing

Request bodies and null

Request models are serialized with model_dump(exclude_unset=True), i.e. exactly the fields you set are sent. Several fields can only be cleared by sending an explicit null:

from remnawave.models import UpdateUserBodyDto

# `telegram_id` is cleared, `email` is left untouched
await sdk.users.update_user(
    UpdateUserBodyDto(id=user_id, telegram_id=None)
)

Fields you do not pass are omitted from the payload, so the server's own defaults apply.

Endpoints without a response body

In 3.0, 43 endpoints answer 204 No Content or 202 Accepted with an empty body: every DELETE, the asynchronous bulk operations, node restarts, squad membership changes and the plugin executor. Their SDK methods return None — success is "no exception raised", and affected-row counts are no longer reported by the API.

await sdk.users.delete_user(user_id)          # -> None (204)
await sdk.users_bulk_actions.bulk_delete_users(
    BulkDeleteUsersBodyDto(user_ids=[1, 2, 3])
)                                             # -> None (204)
await sdk.nodes.restart_node(node_uuid, RestartNodeBodyDto(force_restart=True))  # -> None (202)

Error handling

Every non-2xx response raises a subclass of ApiError. The exception class is derived from the httpCode declared for that error code in the backend contract.

from remnawave.exceptions import ApiError, NotFoundError, ConflictError

try:
    user = await sdk.users.get_user_by_id(user_id)
except NotFoundError as e:
    print(e.code, e.message)      # A025 User not found
except ConflictError:
    ...
except ApiError as e:
    print(e.status_code, e.error)

remnawave.enums.ErrorCode contains every error code of the contract; ERROR_HTTP_CODES and ERROR_MESSAGES in remnawave.enums.error_code expose the declared status and default message.

Webhooks

from remnawave import RemnawaveSDK

sdk = RemnawaveSDK(base_url=..., token=...)

# `raw_body` must be the RAW request body (bytes or str) — re-serializing a parsed dict
# can change the byte string and break the signature check.
payload = sdk.webhook_utility.parse_webhook(raw_body, request_headers, webhook_secret)
if payload is None:
    ...  # invalid signature

if sdk.webhook_utility.is_user_event(payload.event):
    user = payload.data
    if payload.event == "user.expiration":
        # single event replacing user.expires_in_*; hours are in meta
        print(payload.meta.expiration)

What this fork changes

This fork tracks the Remnawave API closely and fixes the divergences each upstream migration left behind.

Migration to 3.0

  • Users are identified by a numeric id. The uuid field is gone from the user model, all {uuid} path params became {userId} (integers), and bulk bodies take userIds.
  • /api/ip-control became /api/connections — sdk.ip_control is now sdk.connections, with by-user / by-node jobs and drop.
  • 43 endpoints answer with an empty body (204/202) and their methods return None; the Delete*ResponseDto / Bulk*ResponseDto models were removed.
  • Removed upstream, removed here — GET /users/by-email, /by-tag, /by-telegram-id, /by-id, both legacy bandwidth-stats endpoints, the docs object of GET /tokens, and the profileTitle / profileUpdateInterval / supportLink / isProfileWebpageUrlEnabled / happAnnounce / happRouting subscription settings (they moved to response headers).
  • Added — POST /users/{userId}/actions/extend, POST /tokens/ott, GET /system/stats/digest, GET /system/stats/http, POST /bandwidth-stats/nodes/usage, internal-squad usage stats and targeted squad membership bulk actions, GET /subscriptions/by-id/{userId}.
  • Renamed — keygen returns secretKey instead of pubKey; external squads split responseHeaders into responseHeadersAdd / responseHeadersRemove; request models follow the contract's *BodyDto naming, with the old *RequestDto names kept as aliases.

Earlier: 2.8 compliance

  • Removed the endpoints 2.8 dropped (/xray, /inbounds/bulk/*, /sub/outline/..., POST /nodes/actions/reset-traffic, POST /auth/oauth2/tg/callback, GET /bandwidth-stats/nodes/realtime).
  • userUuid → userId in HWID devices and subscription request history; hwidCheckup replaced isHwidLimited; resolvedProxyConfigs replaced rawHosts.
  • Error codes regenerated from the contract, exceptions mapped from the declared httpCode.
  • Name collisions in remnawave.models resolved (webhook models are prefixed Webhook*).
  • Serialization switched from exclude_none to exclude_unset, so an explicit null reaches the API; keys the contract requires unconditionally are always emitted.

Development

The project is managed with uv — there is no lock file, because a library should resolve against whatever its consumers already have.

uv sync --group dev

# offline tests (3.0 contract regressions, models, enums, controller surface)
uv run pytest tests/test_3_0_compliance.py tests/test_models_validation.py \
              tests/test_enums.py tests/test_controllers_completeness.py

# full suite — requires a live panel
#   REMNAWAVE_BASE_URL, REMNAWAVE_TOKEN and the REMNAWAVE_* fixtures in tests/conftest.py
uv run pytest

# build locally — the version comes from the git tag
uv build

Releasing

The version lives in exactly one place: the git tag. pyproject.toml carries no version number — hatch-vcs derives it from git describe at build time, so a release is a single action:

git tag v3.0.1
git push origin v3.0.1

Pushing to a branch never publishes anything. The Publish Python Package workflow then builds the wheel and sdist (their version comes straight from the tag), runs the offline test suite, asserts that the built version equals the tag, publishes to PyPI via Trusted Publishing (OIDC — no tokens), and finally creates the GitHub Release with the artifacts attached.

Between tags the version is a PEP 440 development version derived from the last tag, e.g. 3.0.1.post7.dev0+g1a2b3c4, and remnawave.__version__ reports whatever was installed:

import remnawave
print(remnawave.__version__)

To rehearse without touching PyPI: Actions → Publish Python Package → Run workflow → target testpypi.

Credits

This SDK is a fork of the official remnawave/python-sdk, maintained by the Remnawave community.

Upstream history:

The Remnawave panel itself lives at remnawave/backend; API documentation is at docs.rw.

License

MIT — see LICENSE. The original license and copyright of the upstream project are preserved.

Metadata

Release files for remnactual 3.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for remnactual 3.0.0
File Size Uploaded
remnactual-3.0.0.tar.gz 114.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for remnactual 3.0.0
File Interpreter ABI Platform
remnactual-3.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 237.4 kB

Release files / remnactual-3.0.0.tar.gz

Download URL remnactual-3.0.0.tar.gz
Size 114.0 kB
Tags Source
SHA-256 checksum
How to use checksums
937fa4e56ea22b141594db728ca8fb3d282c914cd2c938e44134232d60df0581
BLAKE2b-256 checksum
How to use checksums
b0ea0cdfbc17dab7424f15aca33ac487783f9a2f47006c4da3e22d70b77f7b70
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.

Transparency log

Release files / remnactual-3.0.0-py3-none-any.whl

Download URL remnactual-3.0.0-py3-none-any.whl
Size 123.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0f9124d61d5d52434f3fca0602c6a3aab0845f9f0312d6e36c39f9fd12a02e33
BLAKE2b-256 checksum
How to use checksums
f6409558a7432e7feac7028749515db24e79e3962a83785fddc494f46e7c7704
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.

Transparency log

Release history Release notifications | RSS feed

3.4.5

2 release files

3.4.4

2 release files

3.4.3

2 release files

3.3.2

2 release files

3.2.3

2 release files

3.2.1

2 release files

This release

3.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page