No Comments

Introducing Impreza DevKit

Introducing Impreza DevKit Labeled Featured Image with Impreza's Character, Jake, fixing a Spaceship with Tools, made by Impreza Team, 2026

The Impreza Host REST API has been powering programmatic access to domains, hosting, VPS, email, billing, and webhook deliveries for a while. We’ve been using it internally and quietly with a handful of customers; what was missing was first-class tooling that any developer could pip install or curl and have working in a minute.

Today we’re shipping that — open-source, MIT-licensed, on GitHub at imprezahost/impreza-devkit. Three packages built against the same OpenAPI 3.1 contract, plus the contract itself (and the AsyncAPI 3.0 webhook contract that goes with it).

This post is for the developer who’s going to actually integrate against it. We’ll cover what’s in the box, the design decisions we made, and the places where we deliberately stopped short of competitor parity.

What ships

Artifact Version Where What it gives you
impreza-sdk 0.3.2 PyPI Python SDK — sync + async, fully typed
impreza-cli 0.3.2 PyPI Reference CLI built on the SDK
impreza-cli-go 0.1.1 GitHub Releases Static Go binary, no runtime
openapi/openapi.yaml repo OpenAPI 3.1 REST contract
openapi/asyncapi.yaml repo AsyncAPI 3.0 webhook contract

Both Python packages are co-released in lock-step — pip show impreza-sdk and pip show impreza-cli always carry matching versions. The Go CLI ships independently under the cli-go-v* tag track because Go binaries don’t need the Python lock-step.

Verb surface: 86 commands across 10 resource groups — same on both CLIs:

account     5     info / balance / services / topup / topup-status
catalog     4     products / product / product-groups / tlds
context     5     create / use / list / current / delete
doctor      1     5-check reachability + auth + IP + config probe
domain     17     show / check / pricing / register / transfer /
                  set-nameservers / lock / unlock / id-protection /
                  raa-verify / gdpr-auth / transfer-approval
                  + dns: list / add / update / delete / activate
invoice     2     list / show
key         1     whoami
order       4     list / show / create / upgrade
service     1     cancel
vps        32     base 12 + proxmox 11 + cloud 9 (rdns hidden — see below)
webhook     8     list / show / create / update / delete /
                  rotate-secret / deliveries / event-types

The Go binary and the Python CLI parse the same TOML config file, expose the same flags, accept the same --output json|yaml|table switches, and produce byte-identical JSON output for the same call. You can install both side-by-side and pick whichever wins $PATH per shell.

Architecture

github.com/imprezahost/impreza-devkit
├── openapi/openapi.yaml       OpenAPI 3.1, source of truth for REST
├── openapi/asyncapi.yaml      AsyncAPI 3.0, source of truth for webhooks
├── sdk-python/                impreza-sdk: typed HTTP client + resources
├── cli-python/                impreza-cli: typer/rich CLI on top of the SDK
├── cli-go/                    impreza-cli-go: hand-written client + Cobra CLI
└── examples/                  curl + Python recipes

Three things to call out about this layout:

The OpenAPI spec is in the same repo, not a separate one. That means SDK regenerations stay reviewable against the spec change that motivated them, and the spec ships in every Go-CLI release archive (bundled alongside the binary) so users have the contract on disk without a second download.

The Python CLI depends on the Python SDK as a path dep. No code is duplicated. The CLI wraps the SDK’s typed resources with roughly 3,000 lines of Typer/Rich glue, while the SDK handles authentication, retries, and validation.

The Go CLI does NOT depend on the OpenAPI spec at build time. We tried — oapi-codegen v2 doesn’t yet handle OpenAPI 3.1’s type: [X, null] nullable form, and pivoting to OpenAPI 3.0 would have lost expressive power we use elsewhere. So the Go client is hand-written. It re-implements the typed-request, typed-response, retry, and error-mapping layers in idiomatic Go (about 1,200 lines under cli-go/internal/client/).

Python SDK design

impreza-sdk is the reference implementation. Everything in the public surface is type-checked, every response is a Pydantic v2 model, both sync and async clients share extractor helpers, and there is no string-typed Any floating around customer code.

from impreza import Client

with Client.from_env() as c:
    me = c.account.get()                  # AccountInfo
    print(me.balance, me.currency)        # float, str

    for vps in c.vps.list():              # list[VPS]
        print(vps.id, vps.hostname, vps.status)

    new = c.domains.register(
        domain="example.com",
        years=2,
        nameservers=["ns1.foo.example.com", "ns2.foo.example.com"],
    )                                     # DomainRegistration
    print(new.order_id, new.invoice_id, new.amount)

Async surface is identical except for await and AsyncClient:

import asyncio
from impreza import AsyncClient

async def main():
    async with AsyncClient.from_env() as c:
        me = await c.account.get()
        vps_list = await c.vps.list()
        print(me.balance, len(vps_list))

asyncio.run(main())

A few specific design calls we made:

  • mypy --strict clean. The SDK ships py.typed. We dogfood this with our own integration code; the CLI test suite uses mypy --strict impreza_cli as a gate.
  • VPS dispatch. c.vps.get(id) returns a VPS object whose backend-specific operations are exposed via sub-resources (.proxmox.snapshots, .cloud.images, .rdns). Operations that don’t apply on the current backend raise BackendNotSupported with a clear message, not a silent error. No more “is this a Proxmox or a Cloud VPS?” branching in customer code.
  • Operation polling. Long-running calls (vps.reinstall(), vps.migrate(), vps.cloud.images.create()) return an Operation future. Operation.wait() polls GET /v1/operations/{id} until a terminal state with a sensible default timeout per op type — 600 s for fast ops (snapshot, image), 1800 s for slow ops (reinstall, migrate, restore).
  • Tor / SOCKS5 first-class. Pass proxy="socks5://127.0.0.1:9050", use_tor=True, or set IMPREZA_USE_TOR=1. Uses httpx[socks] under the hood.
# Three equivalent ways to route through Tor:
Client(api_key=..., api_secret=..., use_tor=True)
Client(api_key=..., api_secret=..., proxy="socks5://127.0.0.1:9050")
# IMPREZA_USE_TOR=1 in env + Client.from_env()
  • Crypto top-up with ETA. This is the one we haven’t seen any other cloud provider expose via API. Top up your account balance in BTC / XMR / TRX / USDT, return a typed TopupInvoice future, optionally wait_until_paid() to block until the gateway confirms:
invoice = c.account.topup(amount=50, method="xmr")
print(invoice.invoice_url)         # BTCPay invoice URL
print(invoice.btc_address)         # or xmr_address / trx_address etc.
invoice.wait_until_paid(timeout=7200)  # default 2h

Python CLI design

impreza-cli is built on typer + rich. It mirrors the SDK surface 1:1; if c.foo.bar(...) exists, impreza foo bar ... does too.

Three things that aren’t obvious from the verb list:

Multi-context config. Multiple API keys, multiple environments, switch between them with one command:

impreza context create personal --key imp_... --secret ...
impreza context create company  --key imp_... --secret ...
impreza context use company                # set default
impreza --context personal vps list        # one-off override
impreza context list                       # show all

Config lives in TOML at the platform-native path:

OS Path
Linux $XDG_CONFIG_HOME/impreza/config.toml (default ~/.config/impreza/config.toml)
macOS ~/Library/Application Support/impreza/config.toml
Windows %APPDATA%\impreza\config.toml

Override with IMPREZA_CONFIG=/path/to/config.toml. On POSIX the file is chmod 0600 after every write — the SDK enforces this on save.

Three output formats with stable contracts. --output table (default, human), --output json (scriptable, stable field names), --output yaml (round-trip of the JSON shape). The same fields show up regardless of format. The YAML output uses the JSON tags, not Python attribute names — service_id, not serviceId, not service-id. Whatever you pipe into jq is what you’ll see in yq.

Confirmation prompts on destructive operations + cost-charging operations. --yes / -y skips them in scripts; without it, the CLI prints what’s about to happen and waits for y/N. Cost-charging verbs (register, transfer, top-up, id-protection, order create, order upgrade) also do a best-effort price lookup so the prompt shows the actual charge upfront:

$ impreza domain register example.com --years 2
Register 'example.com' for 2 year(s) — 24.00 USD from your balance
(balance: 100.00 USD). Continue? [y/N]

InsufficientCredit (HTTP 402) gets a tailored hint pointing at impreza account topup — same pattern in both CLIs.

Go CLI design

Why a Go CLI when the Python one already exists? Three reasons.

  1. Drop-in deployment. Single static binary, around 8 MB stripped, no Python runtime, no pip, no virtualenv. The Hetzner / DigitalOcean / Vultr CLIs all follow this shape and ops people expect it.
  2. Faster cold start. impreza --version returns in under 10 ms in Go vs about 150 ms in Python (interpreter spin-up). Matters in CI pipelines that fan out 50 invocations.
  3. No dependency footprint on production hosts. You don’t need to taint your server’s Python with pip installs just to interact with the API.

The Go binary is built with cobra (commands), BurntSushi/toml + yaml.v3 (config + output), fatih/color + jedib0t/go-pretty (palette + tables), x/term (no-echo password prompts), and x/net/proxy (SOCKS5 / Tor).

Five platforms ship per release:

impreza-cli-go_0.1.1_Linux_x86_64.tar.gz    3.21 MB
impreza-cli-go_0.1.1_Linux_arm64.tar.gz     2.91 MB
impreza-cli-go_0.1.1_Darwin_x86_64.tar.gz   3.24 MB
impreza-cli-go_0.1.1_Darwin_arm64.tar.gz    3.00 MB
impreza-cli-go_0.1.1_Windows_x86_64.zip     3.32 MB
+ impreza-cli-go_0.1.1_checksums.txt        (SHA-256, one line per archive)

Each archive bundles the binary, README.md, LICENSE, and copies of both openapi.yaml and asyncapi.yaml. You get the contract on disk without a second download — and you can pipe it to oapi-codegen for your own language if you want.

Builds are reproducible: CGO_ENABLED=0, -trimpath, -ldflags="-s -w -X main.version=...". Verify a download against the published checksum:

sha256sum -c impreza-cli-go_0.1.1_checksums.txt

The Go CLI reads and writes the same TOML config file as the Python CLI. Same field names (default, contexts, key, secret, use_tor, proxy, url), same types, same ordering. You can install both side-by-side:

$ impreza context create personal --key imp_... --secret ...   # via Python CLI
$ impreza --version                                            # whichever wins $PATH
impreza-cli 0.3.2

$ impreza-go context list                                      # if you renamed the Go binary
NAME      KEY PREFIX  USE_TOR  CURRENT
personal  imp_...     no       *

The contracts

OpenAPI 3.1 (REST) lives at openapi/openapi.yaml. Covers every endpoint the SDK exposes — auth scheme, response envelope ({success, data, error, meta}), typed errors per HTTP status, the full account / catalog / domain / vps / order / invoice / webhook surface. Use it with any OpenAPI-aware tool: client codegen, request validation in your reverse proxy, contract testing in your CI.

AsyncAPI 3.0 (webhooks) lives at openapi/asyncapi.yaml. Covers:

  • 16 event types organized in dot-namespaces:
    • webhook.test — delivery probe on subscription create
    • topup.paid
    • invoice.created / invoice.paid
    • order.created
    • service.activated / service.suspended / service.cancelled
    • domain.registered / domain.transferred / domain.expiring_soon / domain.expired
    • vps.power_state_changed / vps.backup_completed / vps.snapshot_created / vps.reinstall_completed
  • 3 wildcard patterns: * (all events), vps.* (all VPS events), domain.* (all domain events)
  • EventEnvelope shape, identical across every event: {id, type, timestamp, data}
  • Per-event payload schemas under components.messages — autocompletes correctly in your AsyncAPI-aware editor
  • X-Impreza-Signature HMAC-SHA256 security scheme + standard delivery headers (X-Impreza-Event, X-Impreza-Delivery, X-Impreza-Timestamp)

Honesty: as of this release only webhook.test is actively fired by the server. We document the other 15 event types as part of the public contract, and we’re progressively wiring the server-side hooks that trigger them. The wire shape is stable — you can register a subscription for any event today and you’ll start receiving them as the hooks land. We decided that documenting the contract first was better than waiting until integrations had already calcified around inconsistent shapes.

Webhook signature verification

Every outbound delivery carries X-Impreza-Signature: sha256=<hex> computed as HMAC-SHA256(secret, body). Both SDKs ship a constant-time verifier so you don’t have to roll your own:

Python:

from impreza.webhooks import verify_signature

@app.post("/webhooks/impreza")
async def handle(request):
    body = await request.body()
    sig  = request.headers["X-Impreza-Signature"]
    if not verify_signature(secret=SECRET, body=body, signature=sig):
        return Response(status_code=401)
    event = parse_event(body)
    ...

Go:

import "github.com/imprezahost/impreza-devkit/cli-go/internal/client"

func handler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    sig := r.Header.Get("X-Impreza-Signature")
    if !client.VerifySignature(secret, body, sig) {
        http.Error(w, "bad signature", 401)
        return
    }
    // ... handle event
}

Both use hmac.compare_digest / hmac.Equal — constant-time comparison to avoid timing-attack signature recovery.

Auth model

API keys are issued in your Client Area under API Management. Each key is a pair: a 32-character imp_... public id and a 64-character hex secret. The secret is HMAC-signed at request time, not sent in the clear.

Two security guardrails worth knowing about:

  1. IP allowlist per key. Each key has an allowlist of source IPs (or CIDR ranges); requests from other IPs return 403 with code IP_NOT_WHITELISTED. Manage via Client Area → API Management → <key> → Allowed IPs. Empty allowlist = key is disabled. impreza key whoami shows the current allowlist.
  2. Per-key scopes. Read-only keys exist if you want to feed monitoring without exposing write paths. The SDK surfaces the scope via c.key.whoami() so your code can sanity-check before attempting a write.

impreza doctor runs a 5-check probe end-to-end:

$ impreza doctor
[OK]   Config loaded — context: company (api_key imp_f4b8a7...)
[OK]   API reachable — 200 OK on GET /v1 (62ms)
[OK]   Key authenticated — 200 OK on GET /v1/key/whoami
[OK]   IP whitelisted — caller 198.51.100.42 in allowed list
[OK]   Account profile readable — balance 100.00 USD

All checks passed.

[FAIL] / [WARN] / [SKIP] for the failure modes. Exit code 0 if all green, non-zero otherwise. Wire it into a Nagios / Prometheus / your-monitor-of-choice check and you’ll know when your CI key rotates out of an IP allowlist before your deploy tries.

Production concerns

Retry with exponential backoff is built into both clients. Transient failures (HTTP 502 / 503 / 504, plus DNS / connect errors) get retried up to 3 times with 100ms * 2^n backoff + jitter. Idempotent verbs (GET, PUT, DELETE of the same record) are always retried; POST only on connect failures (not on 5xx) to avoid double-charging if the server received but failed to acknowledge the response.

HTTP timeout is 180 seconds. Cloud upstream operations (boot-order changes, instance resize, image create) sometimes take 60-120 s to return; Proxmox reads are sub-second. 180 s is the upper bound that still catches a stuck CI / network failure quickly without aborting valid slow operations.

Typed errors map 1:1 with HTTP status codes:

from impreza.exceptions import (
    AuthError,           # 401
    IPNotWhitelisted,    # 403 + code=IP_NOT_WHITELISTED
    Forbidden,           # 403 (other)
    NotFound,            # 404
    InvalidRequest,      # 400
    Conflict,            # 409
    InsufficientCredit,  # 402
    RateLimited,         # 429
    UpstreamError,       # 502/503/504 (after retry budget)
    ServerError,         # 5xx (other)
)

Go has the same hierarchy in client.APIError and friends; check via errors.As(err, &client.InsufficientCredit{}).

Rate limits. Per-key bucket, surfaces as 429 with a Retry-After header. The retry transport honors Retry-After (overriding its own backoff) before re-attempting.

What’s deliberately not here

A short honesty section:

  • No Terraform provider yet. Reserved as Phase 8 in our roadmap. The OpenAPI contract is stable enough to generate one — open an issue if you want to collaborate.
  • vps cloud rdns verbs are absent from both CLIs as of 0.3.2 / 0.1.1. The public-edge WAF on api.imprezahost.com rejects /vps/cloud/rdns/{ip} paths with dotted-IPv4 segments and returns its HTML maintenance page instead of JSON. Shipping CLI verbs that always fail in user-visible HTML felt worse than hiding them. The underlying SDK methods (vps.rdns.get / set / delete in Python, CloudRdnsGet/Set/Delete in Go) stay available for library users who proxy around the WAF. Verbs return to the CLI surface once the server-side rule is fixed.
  • 15 of 16 webhook events not yet fired server-side (see the AsyncAPI section above). Contract is stable; firing them is incremental server work.
  • vps-cloud-sizes / vps-cloud-locations catalog endpoints aren’t wrapped yet. The endpoints exist on the server; the SDK just doesn’t have helpers for them. Deferred three times across earlier phases — we’ll pick it up when we add cloud-specific resize workflows.
  • Load balancers, firewalls-as-a-service, VPC private networking, object storage, K8s, App Platform. Not in the Impreza product catalog. If we add them, the SDK gets them automatically; until then, they’re out of scope.

Start Here

If you already have an API key:

# Python
pip install impreza-cli
impreza context create personal --key imp_... --secret ...
impreza doctor
impreza vps list

# Or Go (Linux x86_64 shown; adjust for your platform):
curl -L https://github.com/imprezahost/impreza-devkit/releases/latest/download/impreza-cli-go_Linux_x86_64.tar.gz -o impreza.tar.gz
tar -xzf impreza.tar.gz
sudo mv impreza /usr/local/bin/
impreza --version

If you don’t have one yet: Client Area → API Management → Create key. Allowlist your IP (or CIDR), copy the secret immediately (we only show it once), and start using it.

Tab completion works on both CLIs:

impreza completion bash > /etc/bash_completion.d/impreza
impreza completion zsh > "${fpath[1]}/_impreza"
impreza completion fish > ~/.config/fish/completions/impreza.fish
impreza completion powershell | Out-String | Invoke-Expression

Reference docs:

We welcome issues, PRs, and feature requests — the repo is MIT-licensed, and we read everything that comes in.


Versions at publish time: impreza-sdk 0.3.2 · impreza-cli 0.3.2 · impreza-cli-go 0.1.1 · OpenAPI 3.1 + AsyncAPI 3.0 contracts shipped in-tree.

Get the API Key too!

You might also like

More Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.