Skip to main content

Python SDK for the HAX (Human Approval eXchange) API

Project description

HAX Python SDK

Python client for the HAX (Human Approval eXchange) API. Enables agents and automated systems to programmatically collect human input.

Installation

The Python SDK is not yet published to PyPI. Install from source:

pip install -e sdks/python  # from the repo root
# or
pip install -e .  # from sdks/python/

Quick Start

from hax import HaxClient

client = HaxClient(
    api_key="hax_live_...",
    base_url="http://localhost:3000/api/v1",
)

request = client.create_request(
    type="text-approval-v1",
    payload={"text": "Deploy main to prod?", "approveLabel": "Ship it", "denyLabel": "Hold"},
    webhook_url="https://myapp.com/webhook",
)

print("Share with approver:", request.url)

# Poll until completed/expired/cancelled
request = client.wait_for_response(request.id, timeout=300)
if request.is_completed:
    print("Decision:", request.response.get("decision"))

Features

  • Pydantic models: Typed request/response models with validation
  • FormBuilder: Fluent API for building typed forms with runtime type inference
  • E2E encryption: RSA-OAEP + AES-GCM hybrid encryption for sensitive responses
  • Webhook verification: HMAC-SHA256 signature verification
  • Delivery: Send requests via email or SMS
  • Polling: Built-in wait_for_response with configurable timeout
  • Error handling: Typed exception hierarchy

Request Methods

# Create a request
request = client.create_request(
    type="text-approval-v1",
    payload={"text": "Approve this action?"},
    title="Optional title",
    description="Optional description",
    webhook_url="https://myapp.com/webhook",
    expires_in_seconds=3600,
    metadata={"pr_number": 123},
)

# Send via email
request = client.request_via_email(
    type="confirm-action-v1",
    payload={"title": "Approve?", "confirmPhrase": "YES"},
    to_email="approver@example.com",
    subject="Approval Required",
)

# Send via SMS
request = client.request_via_sms(
    type="text-approval-v1",
    payload={"text": "Approve?"},
    to_phone="+15551234567",
)

# Get a request by ID
request = client.get_request("req_123")

# List recent requests
requests = client.list_requests()

# Cancel a pending request
cancelled = client.cancel_request("req_123")

# Submit a response (for testing)
completed = client.submit_response("req_123", {"decision": "approve"})

# Wait for completion with timeout
result = client.wait_for_response("req_123", poll_interval=2.0, timeout=60)

# List available template types
types = client.list_types()

Status Helpers

if request.is_pending:
    print("Waiting for response...")
if request.is_completed:
    print("Response:", request.response)
if request.is_expired:
    print("Request expired")
if request.is_cancelled:
    print("Request was cancelled")

FormBuilder

Build typed forms with a fluent API:

from hax import HaxClient, FormBuilder

client = HaxClient(api_key="hax_live_...")

form = (FormBuilder()
    .title("Event Registration")
    .input("name", label="Full Name", required=True)
    .input("email", label="Email", variant="email", required=True)
    .number("age", label="Age", min=0, max=120)
    .checkbox("newsletter", checkbox_label="Subscribe to newsletter"))

handle = client.create_form_request(form,
    webhook_url="https://myapp.com/webhook")

print(f"Form URL: {handle.url}")

# Wait for typed response
response = handle.wait_for_response(timeout=300)
print(response.values.name)       # str
print(response.values.email)      # str
print(response.values.age)        # float
print(response.values.newsletter) # bool

Available Field Types

Method Output Type Description
.input(id) str Text input (variants: text, email, url, tel)
.textarea(id) str Multi-line text input
.select(id, options=...) str Dropdown select
.radio_group(id, options=...) str Radio button group
.date(id) str Date picker (ISO format)
.number(id) float Numeric input
.slider(id, min=, max=) float Slider control
.checkbox(id) bool Single checkbox
.switch(id) bool Toggle switch
.checkbox_group(id, options=...) list[str] Multi-select checkboxes
.hidden(id, value) type(value) Hidden field

Webhooks

Verify and parse webhook events:

from hax import verify_signature, parse_event

# In your webhook handler
def handle_webhook(request):
    # Verify signature
    is_valid = verify_signature(
        payload=request.body,
        signature=request.headers["X-Hax-Signature"],
        secret="whsec_...",
    )
    if not is_valid:
        return 400, "Invalid signature"

    # Parse the event
    event = parse_event(request.body)

    if event.event_type == "completed":
        print(f"Request {event.request_id} completed!")
        print(f"Response: {event.response}")
    elif event.event_type == "expired":
        print(f"Request {event.request_id} expired")

    return 200, "OK"

Event Types

  • request.sent — Notification was delivered (email/SMS)
  • request.opened — Human opened the request link
  • request.completed — Human submitted a response
  • request.expired — Request expired without action

Encryption

For sensitive response data, use end-to-end encryption:

from hax import HaxClient

# Passphrase-based (automatic encrypt/decrypt)
client = HaxClient(
    api_key="hax_live_...",
    encryption_key="my-secret-passphrase",
)

# Public key is automatically sent with requests
request = client.create_request(
    type="text-approval-v1",
    payload={"text": "Approve this sensitive action?"},
)

# Response is automatically decrypted when retrieved
completed = client.get_request(request.id)
print(completed.response)  # Decrypted plaintext

Manual Decryption

from hax import generate_key_pair, decrypt_response, is_encrypted_response

public_key, private_key = generate_key_pair("my-secret")

# Use public_key when creating the client
client = HaxClient(api_key="...", public_key=public_key)

# Later, manually decrypt
request = client.get_request("req_123")
if is_encrypted_response(request.response):
    decrypted = decrypt_response(request.response["_encrypted"], private_key)

DID Identity (signature-only "knock")

Instead of a workspace API key, an agent can authenticate with a DID identity — an Ed25519 did:key it owns. Each request is signed with a detached JWS over the exact body bytes, sent in the X-HAX-DID and X-HAX-Signature headers. This lets an agent "knock" on a workspace (by public handle or sender invite) and be reviewed by the workspace owner before it can send requests.

from hax import HaxClient

# No api_key → a DID identity is resolved (and minted on first use,
# persisted to ~/.hax/identity.json).
client = HaxClient(base_url="https://your-hax.example.com/api/v1")

print(client.get_identity())  # {"did": "did:key:z6Mk..."}

# Knock a workspace by its public handle (or pass sender_invite=...).
client.create_request(
    type="text-approval-v1",
    payload={"text": "May I join this workspace?"},
    workspace="acme",
)

# Poll until the owner accepts (or blocks) this DID.
status = client.wait_for_acceptance(workspace="acme", timeout=300.0, poll=3.0)
# -> "active" | "blocked" | "timeout"

# Or check status directly.
client.get_knock_status(workspace="acme")
# -> {"did": ..., "workspace": "acme", "status": "active", "heldAskCount": 0}

Both API key and DID can be used together — when api_key is set, requests carry both the Authorization header and the DID signature.

Managing identities

from hax import generate_identity, save_identity, load_identity, did_fingerprint

identity = generate_identity()             # fresh Ed25519 did:key
print(identity.did)                        # did:key:z6Mk...
print(did_fingerprint(identity.private_key_jwk))  # ab:12:... (display fingerprint)

save_identity(identity)                     # writes ~/.hax/identity.json (0600)

# Resolution order: explicit did+jwk > identity_file > $HAX_IDENTITY_FILE >
# in-process AgentField agent > ~/.hax/identity.json > mint (when allowed).
resolved = load_identity(allow_mint=True)

You can also pass an identity to the client explicitly:

client = HaxClient(
    base_url="...",
    did=identity.did,
    private_key_jwk=identity.private_key_jwk,
)
# or: HaxClient(base_url="...", identity_file="/path/to/identity.json")

~/.hax/identity.json format

This file is shared with the hax CLI, so its keys are camelCase:

{
  "did": "did:key:z6Mk...",
  "privateKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "...", "d": "..." },
  "createdAt": "2026-06-13T12:00:00.000Z"
}

The file is written with 0600 permissions (owner read/write only) because it contains the private signing key. When running inside an AgentField agent, the client automatically picks up the agent's DID — no identity file is needed.

Error Handling

from hax import (
    HaxError,             # Base error
    AuthenticationError,  # Invalid API key (401)
    ValidationError,      # Invalid request data (400/422)
    NotFoundError,        # Resource not found (404)
    RateLimitError,       # Too many requests (429)
    ServerError,          # Server error (500+)
    DecryptionError,      # Decryption failure
)

try:
    request = client.create_request(...)
except AuthenticationError:
    print("Check your API key")
except ValidationError as e:
    print(f"Invalid request: {e}")
except RateLimitError:
    print("Rate limited, try again later")
except HaxError as e:
    print(f"API error: {e}")

Template Types

Template Description
text-approval-v1 Show text and collect an approve/deny decision
confirm-action-v1 Require typing a specific phrase to confirm a destructive action
collect-email-v1 Prompt the user for an email address
form-builder Advanced forms with field types, layouts, validation, and conditional logic
multi-choice-selection-v1 Single or multiple selection from customizable option cards
code-changes-v1 GitHub-style diff view with inline line comments
rich-text-editor-v1 Markdown-formatted text editing for documents and reports
file-upload-v1 Collect files (documents, images, CSVs) from users
signature-capture-v1 Capture e-signatures with optional signer name and legal text
data-table-review-v1 Review, select, or edit tabular data
scheduling-picker-v1 Date/time slot selection with optional recurring schedules
multi-step-wizard-v1 Sequential steps with navigation and progress indicator
side-by-side-comparison-v1 Compare two versions with diff highlighting
terminal-output-v1 Display command output/logs with approve-to-continue

Notes

  • Auth is an API key (HaxClient(api_key=...)) or a DID identity (signature-only "knock"; see above) — or both. Clerk/session auth is not required for API access.
  • API responses wrap resources (e.g., {"request": {...}}); the SDK unwraps this automatically.
  • Template payloads and responses are flexible; consult the template configs for the fields each template expects/returns.
  • The client supports context manager usage: with HaxClient(...) as client:

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hax_sdk-0.2.7rc1.tar.gz (46.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hax_sdk-0.2.7rc1-py3-none-any.whl (33.7 kB view details)

Uploaded Python 3

File details

Details for the file hax_sdk-0.2.7rc1.tar.gz.

File metadata

  • Download URL: hax_sdk-0.2.7rc1.tar.gz
  • Upload date:
  • Size: 46.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for hax_sdk-0.2.7rc1.tar.gz
Algorithm Hash digest
SHA256 4ce0dc5d8a5e28654ede5f3fb5f6b9496e4c4622098c21ea081a2e49d2a6407f
MD5 589f5ff8bcccf8f726ffa629521532a4
BLAKE2b-256 56dd2fe58c9025edd9b6a8e02be9d78e0d5f7f8e3a7a5d9123a034688a83ffa9

See more details on using hashes here.

File details

Details for the file hax_sdk-0.2.7rc1-py3-none-any.whl.

File metadata

  • Download URL: hax_sdk-0.2.7rc1-py3-none-any.whl
  • Upload date:
  • Size: 33.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for hax_sdk-0.2.7rc1-py3-none-any.whl
Algorithm Hash digest
SHA256 efc4d3770bde9bb8ffccf387462aa6b8f037d236209872f17d6aec6ca7d27f8b
MD5 0b15f8dba1f9f1a39823382da181c063
BLAKE2b-256 ee9eb045cbad278609e604ddd18058b4179bd2fa0af28425f382229901cdaaad

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page