Skip to main content

Apruvly Python Client

Official stdlib-only Python client for the Apruvly approval workflow API.

Source: github.com/apruvly/apruvly-python.

  • Zero runtime dependencies — uses only the Python standard library (urllib, json, dataclasses, hmac, …) for a smaller and more auditable supply chain.
  • Resource-oriented API — client.workflows, client.decisions, client.billing, client.integrations, client.directory.
  • Typed models — dataclasses aligned with OpenAPI 1.1.0 (including mixed camelCase / snake_case payloads).
  • Webhook helpers — verify notify_url signatures with constant-time HMAC comparison.

Requires Python 3.10+.

Install

pip install apruvly

From a local checkout:

pip install -e .

Quickstart

Create an API key in the Apruvly dashboard under Settings → API Keys. Keys look like wf- followed by 64 alphanumeric characters.

from apruvly import Client
from apruvly.models import WorkflowConfig, WorkflowObject, email_step

client = Client(api_key="wf-...")

# Point at another environment (local / staging):
client = Client(api_key="wf-...", base_url="http://localhost:1509")

# Or configure via environment variables (also used when args are omitted):
#   export APRUVLY_API_KEY=wf-...
#   export APRUVLY_BASE_URL=http://localhost:1509
client = Client()

# Other constructor knobs: timeout=30, accept_language="en"

# Conservative throttle + retry are on by default (see below).

created = client.workflows.create(
    WorkflowConfig(
        object=WorkflowObject(title="Q1 Marketing Budget"),
        expires="24h",
        start="manager",
        workflow={
            "manager": email_step(
                to=["manager@example.com"],
                subject="Please approve",
                body="Approve: ${approve_link}\nReject: ${reject_link}",
            )
        },
    )
)

status = client.workflows.get(created.id)
print(status.status, status.currentStep)

You can also pass a plain dict for workflow payloads (useful when reading JSON from disk or building channel-specific steps dynamically).

Authentication & scopes

Every request (except that health needs any valid key) requires a Bearer token:

Authorization: Bearer wf-...
Scope Operations
(none beyond auth) GET /api/v1/health
api:workflow:write create, validate, approve, reject
api:workflow:read get by id / external id
api:workflow:list search, recent
api:workflow:cancel cancel
api:settings:read billing, list/get integrations
api:settings:write upsert/delete integrations
api:directory:list list areas / people
api:directory:read get area / person
api:directory:write create / update
api:directory:bulk bulk create / update
api:directory:delete delete

Missing scope → ForbiddenError (HTTP 403).

Client surface

client.health()

client.workflows.create(config)
client.workflows.validate(config)
client.workflows.get(workflow_id)
client.workflows.get_by_external_id(external_id)
client.workflows.cancel(workflow_id)
client.workflows.cancel_by_external_id(external_id)
client.workflows.search(query=..., status=..., limit=...)
client.workflows.recent()

client.decisions.approve(workflow_id, challenge, comment="LGTM")
client.decisions.reject(workflow_id, challenge, comment="Needs changes")

client.billing.get()

client.integrations.list()
client.integrations.get("slack")
client.integrations.upsert("slack", {"botToken": "...", "signingSecret": "..."})
client.integrations.delete("slack")

client.directory.areas.list() / .create() / .get() / .update() / .delete()
client.directory.areas.bulk_create(...) / .bulk_update(...)
client.directory.people.list(page=1, q="Ada") / .create() / ...

Successful calls return the unwrapped data field from the API envelope { success, message, data }. HTTP 204 endpoints return None.

Throttle, retry, and backoff

By default the client is conservative:

Setting Default Meaning
min_interval 0.2 s Minimum spacing between request starts (client-side throttle)
max_retries 2 Extra attempts after the first (up to 3 total)
backoff_factor 1.0 s Exponential base (1s, 2s, 4s, …)
backoff_max 30 s Cap for backoff / Retry-After
jitter true Full jitter on sleep delays

Retries apply to HTTP 429 / 502 / 503 / 504. Transport errors (connection drops) are retried only for safer methods (GET, PUT, DELETE, …) — not POST by default, to avoid duplicate workflow creates. On 429, a Retry-After header (seconds) is honored when present.

from apruvly import Client, RetryConfig

# Explicit policy
client = Client(
    api_key="wf-...",
    retry=RetryConfig(min_interval=0.5, max_retries=3, backoff_factor=1.0),
)

# Or individual overrides
client = Client(api_key="wf-...", min_interval=0.5, max_retries=1)

# Disable (e.g. tight unit tests)
client = Client(api_key="wf-...", retry=RetryConfig.disabled())

Environment overlays (used when retry= is omitted):

export APRUVLY_MIN_INTERVAL=0.2
export APRUVLY_MAX_RETRIES=2
export APRUVLY_BACKOFF_FACTOR=1.0
export APRUVLY_BACKOFF_MAX=30
export APRUVLY_RETRY_JITTER=true

Errors

from apruvly import (
    AuthError,
    ForbiddenError,
    NotFoundError,
    PaymentRequiredError,
    QuotaExceededError,
    ValidationError,
)

try:
    client.workflows.create(config)
except QuotaExceededError as exc:
    print(exc.quota_type, exc.limit)
except PaymentRequiredError:
    print("Top up credits or upgrade plan")
except ValidationError as exc:
    print(exc.field, exc.message)
Exception Typical HTTP
AuthError 401
ForbiddenError 403
NotFoundError 404
PaymentRequiredError 402
ValidationError 400, 422
ConflictError 409
NotAcceptableError 406
QuotaExceededError 429
APIError other / transport

All inherit from ApruvlyError and expose status_code, message, error_code, and data.

Webhook signature verification

When an API key has a notify_url, Apruvly POSTs a signed payload on terminal workflow events:

Apruvly-Signature: t=<unix>,v1=<hmac_sha256(secret, "<unix>.<raw_body>")>
from apruvly import verify_notify_signature, SignatureVerificationError

try:
    verify_notify_signature(
        request_body_bytes,
        request.headers.get("Apruvly-Signature"),
        webhook_secret,
        tolerance=300,  # seconds
    )
except SignatureVerificationError:
    # reject the request
    ...

Examples

Script Description
examples/create_workflow.py Validate + create an email approval
examples/search.py List recent workflows and billing balance
examples/verify_webhook.py Sign and verify a sample notify payload
export APRUVLY_API_KEY=wf-...
python examples/create_workflow.py

Design notes

  • No third-party HTTP stack — easier to vendor, audit, and deploy in locked-down environments.
  • Sync-only — matches urllib and keeps the client small.
  • Forward-compatible workflow payloads — prefer typed builders (email_step, webhook_action, …) when convenient; fall back to dicts for new channel fields.
  • Contract snapshot: docs/openapi.json (Apruvly API v1.1.0).

Development

pip install -e ".[dev]"
ruff check src tests examples
mypy src
pytest

Security: see SECURITY.md.

License

MIT — see LICENSE.

Release files for apruvly 0.3.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 apruvly 0.3.0
File Size Uploaded
apruvly-0.3.0.tar.gz 40.0 kB Details

Built distribution (wheel)

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

Total release size: 71.2 kB

Release files / apruvly-0.3.0.tar.gz

Download URL apruvly-0.3.0.tar.gz
Size 40.0 kB
Tags Source
SHA-256 checksum
How to use checksums
c0c506e8ac0b15eae68990b10424dd3c94095deebfb3d177ac2086f439a61b75
BLAKE2b-256 checksum
How to use checksums
cb31d8e348d058c813192655e50d239284be043b4073581fb66cd78437ed03fa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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 Jul 12, 2026.

Transparency log

Release files / apruvly-0.3.0-py3-none-any.whl

Download URL apruvly-0.3.0-py3-none-any.whl
Size 31.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3211b360d92a71c93610898f972b89aeac9d638032e3a4828afd7d335f2c1ea9
BLAKE2b-256 checksum
How to use checksums
cc778565b80eed6407dd8e8fbdea3ad17f6d08ec4ae956dc8c5f5de91831b302
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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 Jul 12, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.1.0

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