Skip to main content

webhook-platform

Official Python SDK for Hookflow.

The PyPI distribution is webhook-platform; the module you import is hookflow. pip install webhook-platform, then from hookflow import ....

Scope. This SDK covers Events, Endpoints, Subscriptions, Deliveries, Incoming Sources, Incoming Events, and webhook signature verification — 7 of the platform's 35 API controllers. It does not wrap Transformations, Rules, Workflows, Schemas, DLQ, Analytics, Usage, Alerts, Incidents, PII rules, Audit Log, Tunnels, API keys, Members, or Projects — use the Generic Requests helpers for those until the SDK grows to cover them.

Installation

pip install webhook-platform

Quick Start

import os

from hookflow import Hookflow, Event

client = Hookflow(
    api_key=os.environ["HOOKFLOW_API_KEY"],  # e.g. "Kz1uAIM8VeJUQN7yGSYCst64WxNLabBHfOYbrPlJ1yk"
    base_url="http://localhost:8080",  # optional
)

# Send an event
event = client.events.send(
    Event(
        type="order.completed",
        data={
            "order_id": "ord_12345",
            "amount": 99.99,
            "currency": "USD",
        },
    )
)

print(f"Event created: {event.event_id}")
print(f"Deliveries created: {event.deliveries_created}")

API Reference

Events

from hookflow import Event

# Send event with idempotency key
event = client.events.send(
    Event(type="order.completed", data={"order_id": "123"}),
    idempotency_key="unique-key",
)

Endpoints

from hookflow import EndpointCreateParams, EndpointUpdateParams

# Create endpoint
endpoint = client.endpoints.create(
    project_id,
    EndpointCreateParams(
        url="https://api.example.com/webhooks",
        description="Production webhooks",
        enabled=True,
    ),
)

# List endpoints — the API paginates this one, so the endpoints are in .content
# (iterating the page yields them directly)
page = client.endpoints.list(project_id, page=0, size=20)
for endpoint in page:
    print(endpoint.url)

# Update endpoint
client.endpoints.update(
    project_id,
    endpoint_id,
    EndpointUpdateParams(enabled=False),
)

# Delete endpoint
client.endpoints.delete(project_id, endpoint_id)

# Rotate secret
updated = client.endpoints.rotate_secret(project_id, endpoint_id)
print(f"New secret: {updated.secret}")

# Test endpoint connectivity
result = client.endpoints.test(project_id, endpoint_id)
print(f"Test {'passed' if result.success else 'failed'}: {result.latency_ms}ms")
print(f"{result.http_status_code}{result.message}")

Subscriptions

from hookflow import SubscriptionCreateParams

# Subscribe endpoint to an event type
subscription = client.subscriptions.create(
    project_id,
    SubscriptionCreateParams(
        endpoint_id=endpoint.id,
        event_type="order.completed",
        enabled=True,
    ),
)

# List subscriptions — a bare list; unlike endpoints, this one is not paginated
subscriptions = client.subscriptions.list(project_id)

# Update subscription
client.subscriptions.update(
    project_id,
    subscription_id,
    event_type="order.shipped",
)

# Delete subscription
client.subscriptions.delete(project_id, subscription_id)

Deliveries

from hookflow import DeliveryListParams, DeliveryStatus

# List deliveries with filters
deliveries = client.deliveries.list(
    project_id,
    DeliveryListParams(status=DeliveryStatus.FAILED, page=0, size=20),
)

print(f"Total failed: {deliveries.total_elements}")

# Get delivery attempts
attempts = client.deliveries.get_attempts(delivery_id)
for attempt in attempts:
    print(f"Attempt {attempt.attempt_number}: {attempt.http_status_code} ({attempt.duration_ms}ms)")

# Replay failed delivery
client.deliveries.replay(delivery_id)

Incoming Webhooks

Receive, validate, and forward webhooks from third-party providers (Stripe, GitHub, Twilio, etc.).

Incoming Sources

from hookflow import IncomingSourceCreateParams, IncomingSourceUpdateParams

# Create an incoming source with HMAC verification
source = client.incoming_sources.create(
    project_id,
    IncomingSourceCreateParams(
        name="Stripe Webhooks",
        slug="stripe",
        provider_type="STRIPE",
        verification_mode="HMAC_GENERIC",
        hmac_secret="whsec_...",
        hmac_header_name="Stripe-Signature",
    ),
)

print(f"Ingress URL: {source.ingress_url}")

# List sources
sources = client.incoming_sources.list(project_id)

# Update source
client.incoming_sources.update(
    project_id,
    source_id,
    IncomingSourceUpdateParams(name="Stripe Production", rate_limit_per_second=100),
)

# Delete source
client.incoming_sources.delete(project_id, source_id)

Incoming Destinations

from hookflow import IncomingDestinationCreateParams

# Add a forwarding destination
dest = client.incoming_sources.create_destination(
    project_id,
    source_id,
    IncomingDestinationCreateParams(
        url="https://your-api.com/webhooks/stripe",
        enabled=True,
        max_attempts=5,
        timeout_seconds=30,
    ),
)

# List destinations
dests = client.incoming_sources.list_destinations(project_id, source_id)

# Delete destination
client.incoming_sources.delete_destination(project_id, source_id, dest_id)

Incoming Events

from hookflow import IncomingEventListParams

# List incoming events (with optional source filter)
events = client.incoming_events.list(
    project_id,
    IncomingEventListParams(source_id=source.id, page=0, size=20),
)

# Get event details
event = client.incoming_events.get(project_id, event_id)

# Get forward attempts
attempts = client.incoming_events.get_attempts(project_id, event_id)

# Replay event to all destinations
result = client.incoming_events.replay(project_id, event_id)
print(f"Replayed to {result.destinations_count} destinations")

Webhook Signature Verification

Verify incoming webhooks in your endpoint:

from hookflow import verify_signature, construct_event, HookflowError

# Flask example
from flask import Flask, request

app = Flask(__name__)

@app.route("/webhooks", methods=["POST"])
def handle_webhook():
    payload = request.get_data(as_text=True)
    headers = dict(request.headers)
    secret = os.environ["WEBHOOK_SECRET"]

    try:
        # Option 1: Just verify
        verify_signature(payload, headers.get("X-Signature", ""), secret)

        # Option 2: Verify and parse
        event = construct_event(payload, headers, secret)

        # event.data is the parsed body; event.event_id / event.delivery_id /
        # event.timestamp come from the X-Event-Id / X-Delivery-Id /
        # X-Timestamp headers. See "What lands on your endpoint" below for
        # event.type.
        print(f"Delivery {event.delivery_id} of event {event.event_id}: {event.data}")
        handle_order_completed(event.data)

        return "OK", 200

    except HookflowError as e:
        print(f"Webhook verification failed: {e.message}")
        return "Invalid signature", 400

What lands on your endpoint

Hookflow PUTs the event's payload on the wire, not an envelope. This:

client.events.send(Event(type="order.completed", data={"order_id": "ord_1"}))

arrives at your endpoint as the data object alone —

POST /webhooks HTTP/1.1
Content-Type: application/json
X-Signature: t=1738000000000,v1=<hex hmac-sha256>
X-Timestamp: 1738000000000
X-Event-Id: 6f0e…
X-Delivery-Id: 91ab…
X-Sequence-Number: 0
Idempotency-Key: 6f0e…-<endpoint-id>

{"order_id":"ord_1"}

So construct_event fills event_id, delivery_id and timestamp from the headers and data from the body, but type is empty: the event type is not on the wire for a default subscription. Route on the payload, on the endpoint you registered, or set the subscription's payload_template to wrap the event so type becomes part of the body.

The signature is computed over f"{timestamp}.{raw_body}" with HMAC-SHA256 and the endpoint secret, and the server rejects timestamps more than 300 seconds old — verify against the raw body bytes, before any JSON parse and re-serialize.

FastAPI Example

from fastapi import FastAPI, Request, HTTPException
from hookflow import construct_event, HookflowError

app = FastAPI()

@app.post("/webhooks")
async def handle_webhook(request: Request):
    payload = await request.body()
    headers = dict(request.headers)

    try:
        event = construct_event(
            payload.decode("utf-8"),
            headers,
            os.environ["WEBHOOK_SECRET"],
        )

        # Process event...
        return {"status": "ok"}

    except HookflowError as e:
        raise HTTPException(status_code=400, detail=e.message)

Error Handling

from hookflow import (
    HookflowError,
    RateLimitError,
    AuthenticationError,
    ValidationError,
)

try:
    client.events.send(Event(type="test", data={}))
except RateLimitError as e:
    # retry_after_ms is milliseconds. e.rate_limit_info.reset is the raw
    # X-RateLimit-Reset header, which the API sends in Unix *seconds*.
    print(f"Rate limited. Retry after {e.retry_after_ms}ms")
    time.sleep(e.retry_after_ms / 1000)
except AuthenticationError:
    print("Invalid API key")
except ValidationError as e:
    print(f"Validation failed: {e.field_errors}")
except HookflowError as e:
    print(f"Error {e.status}: {e.message}")

Error Response Format

All API errors return a consistent JSON body:

{
  "error": "error_code",
  "message": "Human-readable description",
  "status": 400,
  "fieldErrors": { "field": "reason" }
}
  • error — machine-readable error code (snake_case), always present
  • message — human-readable description, always present
  • status — HTTP status code (integer), always present
  • fieldErrors — field-level validation details (only present for validation_error)

Error Codes Reference

HTTP Status error Code SDK Exception Description
400 validation_error ValidationError Invalid request parameters; see fieldErrors
400 invalid_request HookflowError Malformed or semantically invalid request
401 unauthorized AuthenticationError Missing or invalid API key / expired token
403 forbidden HookflowError Insufficient permissions for the action
404 not_found NotFoundError Requested resource does not exist
413 payload_too_large HookflowError Request body exceeds maximum allowed size
422 unprocessable_entity HookflowError Valid syntax but violates business rules
429 rate_limit_exceeded RateLimitError Too many requests; check X-RateLimit-* headers
500 internal_error HookflowError Unexpected server error

Generic Requests

As the API expands, you can call any endpoint directly without waiting for SDK updates:

# GET
schemas = client.get("/api/v1/projects/proj_123/schemas")

# GET with query params
items = client.get("/api/v1/projects/proj_123/items", params={"status": "active"})

# POST with body and idempotency key
result = client.post("/api/v1/some/new/endpoint", body={"key": "value"}, idempotency_key="unique-key")

# PUT
client.put("/api/v1/projects/proj_123/settings", body={"timezone": "UTC"})

# PATCH
client.patch("/api/v1/projects/proj_123/settings", body={"timezone": "UTC"})

# DELETE
client.delete("/api/v1/projects/proj_123/tags/old-tag")

All generic methods use the same authentication, error handling, and rate-limit logic as the built-in methods.

Configuration

client = Hookflow(
    api_key=os.environ["HOOKFLOW_API_KEY"],  # Required: Your project API key
    base_url="https://api.example.com",  # Optional (default: http://localhost:8080)
    timeout=30,                     # Optional: Request timeout in seconds (default: 30)
)

Timeouts and retries

timeout is passed straight to requests; hitting it raises HookflowError with code="timeout" and status=0. A connection-level failure raises the same class with code="network_error".

The client does not retry. One SDK call is exactly one HTTP request — no backoff, no idempotent replay, and no urllib3 Retry adapter is installed. That is deliberate: events.send accepts an idempotency_key, so a retry policy belongs to the caller who knows whether reissuing the request is safe. What is retried is the delivery itself, by the platform, on the subscription's retry_delays ladder.

Authentication

Every request the client makes carries X-API-Key: <your key> — the project API key, created in the dashboard or via POST /api/v1/projects/{project_id}/api-keys. The SDK never sends a bearer token and has no login surface: JWT-authenticated endpoints (auth, projects, organizations, members, API keys) are not part of it. Bootstrapping a project and a key is a one-time step you do with the dashboard, the CLI, or plain HTTP.

Type Hints

This SDK includes full type hints for better IDE support:

from hookflow import (
    Event,
    EventResponse,
    Endpoint,
    Delivery,
    DeliveryStatus,
)

Development

Running Tests

Local (requires Python 3.8+):

pip install -e ".[dev]"
pytest

Docker:

docker run --rm -v $(pwd):/app -w /app python:3.11-slim sh -c "pip install -e '.[dev]' && pytest"

Live-API smoke check

pytest stubs the transport, so it cannot see a renamed field. To drive the SDK against a real instance:

make up                          # from the repo root
python scripts/live_api_smoke.py # SMOKE_API_BASE_URL overrides the target

It registers a throwaway org, walks endpoint → subscription → event → deliveries → attempts → incoming, checks each error envelope, and verifies a signature the running server itself produced. It is not collected by pytest (testpaths = tests, python_files = test_*.py), so the unit suite still passes with no backend.

License

MIT

Download files

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

Source Distribution

webhook_platform-2.9.1.tar.gz (37.5 kB view details)

Uploaded Source

Built Distribution

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

webhook_platform-2.9.1-py3-none-any.whl (37.8 kB view details)

Uploaded Python 3

File details

Details for the file webhook_platform-2.9.1.tar.gz.

File metadata

  • Download URL: webhook_platform-2.9.1.tar.gz
  • Upload date:
  • Size: 37.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.16

File hashes

Hashes for webhook_platform-2.9.1.tar.gz
Algorithm Hash digest
SHA256 02ec1298eb76bdfed7e7b43db1e24c9fb069ec1e9088e456d38b3cce1866ec6e
MD5 273983d4f5b981c8cf954674fadff3b7
BLAKE2b-256 dada38063079138325ba642e2e2ce4d3486c11ce27495f364066a90b5bb20234

See more details on using hashes here.

File details

Details for the file webhook_platform-2.9.1-py3-none-any.whl.

File metadata

File hashes

Hashes for webhook_platform-2.9.1-py3-none-any.whl
Algorithm Hash digest
SHA256 62c67443085266d6d433fcf70b08d0225ef6badb7756741ca91ef9fd572c0e98
MD5 e8dbf20fd26cb0f0b13c2a2958a2c56c
BLAKE2b-256 1757ce0cb5d0d5d342e92b2c4a9dd87b7f47ac750e384ccd6a7b97329027a8be

See more details on using hashes here.

Release history Release notifications | RSS feed

2.11.0

2 files

2.10.0

2 files

This release

2.9.1 This release

2 files

2.9.0

2 files

2.8.0

2 files

2.7.0

2 files

2.6.1

2 files

2.6.0

2 files

2.5.0

2 files

2.2.1

2 files

2.1.0

2 files

2.0.0

2 files

1.1.0

2 files

1.0.3

2 files

1.0.2

2 files

1.0.1

2 files

1.0.0

2 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