Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- `tango.webhooks` subpackage with HMAC-SHA256 signing helpers (`verify_signature`, `generate_signature`, `parse_signature_header`) that mirror the canonical Tango server scheme byte-for-byte. Importable from a default `pip install tango-python` (pure stdlib).
- `WebhookReceiver`: a stdlib-based local HTTP listener for development and integration tests. Verifies signatures, optionally forwards each delivery to a downstream URL, and records deliveries in memory for inspection. Usable as a context manager (`with WebhookReceiver(secret=...).run() as rx: ...`).
- `tango.webhooks.simulate.deliver(...)`: locally sign and POST a payload to any URL — no Tango involvement. Useful for offline iteration on receiver code.
- New `tango[webhooks]` extra (adds `click`) ships a `tango` console script covering the full webhook lifecycle for developer integrations:
- `listen` — local receiver
- `simulate` — sign a payload locally; with `--to`, also POST it
- `trigger` — ask Tango to send a real test delivery
- `fetch-sample` — print the canonical payload Tango emits for an event type
- `list-event-types` — discover what's subscribable
- `endpoints list|get|create|delete` — manage delivery endpoints
- `subscriptions list|get|create|delete` — manage what events you receive
Together these let a developer go from zero to receiving real Tango webhooks without leaving the shell or dropping into Python.

### Notes
- Console script name `tango` may be revisited before the next release if it conflicts with sibling tooling (`tango-scripts` reuses the bare name).

### Documentation
- New `docs/WEBHOOKS.md` — comprehensive guide covering install, concepts, a zero-to-receiving quickstart, full CLI reference, and programmatic patterns for `WebhookReceiver` / `simulate.sign` / `simulate.deliver` in pytest fixtures.
- `docs/API_REFERENCE.md`: filled in `get_webhook_subscription`, replaced the hand-rolled signature-verification snippet with a pointer to `tango.webhooks.verify_signature`, and added a new "Webhook tooling (`tango.webhooks`)" section that documents every importable from the new subpackage.
- `README.md`: new "Webhook Tooling" section under Advanced Features, plus the new guide is linked from the Documentation index.

## [0.5.0] - 2026-04-08

### Added
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,46 @@ contracts = client.list_contracts(
# Returns: {"key": "...", "transactions.0.action_date": "...", "transactions.0.obligated": "..."}
```

### Webhook Tooling

The SDK ships first-class tooling for **building and testing webhook integrations against the Tango API** — including signing helpers, a local receiver, and a command-line tool covering the full lifecycle:

```bash
pip install 'tango-python[webhooks]'
```

This adds a `tango` console script with subcommands for the full webhook lifecycle:

```bash
# Discover what's available
tango webhooks list-event-types
tango webhooks fetch-sample --event-type entities.updated

# Local development
tango webhooks listen --port 8011 --secret $SECRET # receiver
tango webhooks simulate --secret $SECRET --event-type entities.updated # sign + print
tango webhooks simulate --secret $SECRET --event-type entities.updated \
--to http://127.0.0.1:8011/tango/webhooks # also POST

# Manage real subscriptions and endpoints
tango webhooks endpoints create|list|get|delete
tango webhooks subscriptions create|list|get|delete

# Force a real test delivery from Tango
tango webhooks trigger
```

The signing helpers (`verify_signature`, `generate_signature`) are pure stdlib and importable from the default install — your receiver code doesn't need the extra:

```python
from tango.webhooks import verify_signature

if not verify_signature(raw_body, secret, request.headers.get("X-Tango-Signature")):
return 401, "invalid signature"
```

For the full guide — workflow, CLI reference, and programmatic patterns for pytest fixtures — see [`docs/WEBHOOKS.md`](docs/WEBHOOKS.md).

### Type Hints with IDE Support

Import TypedDict types for IDE autocomplete:
Expand Down Expand Up @@ -476,6 +516,7 @@ tango-python/
- [Shape System Guide](docs/SHAPES.md) - Comprehensive guide to response shaping
- [API Reference](docs/API_REFERENCE.md) - Detailed API documentation
- [Developer Guide](docs/DEVELOPERS.md) - Technical documentation for developers
- [Webhooks Guide](docs/WEBHOOKS.md) - Workflow, CLI reference, and programmatic patterns for webhook integrations
- [Quick Start Notebook](docs/quick_start.ipynb) - Interactive Jupyter notebook with examples

## Requirements
Expand Down
96 changes: 86 additions & 10 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,8 @@ for code in naics.results:

Webhook APIs let **Large / Enterprise** users manage subscription filters for outbound Tango webhooks.

> **For testing, signing, and a CLI tool**, see [`docs/WEBHOOKS.md`](WEBHOOKS.md). This section covers SDK method signatures only.

### list_webhook_event_types()

Discover supported `event_type` values and subject types.
Expand All @@ -1246,6 +1248,12 @@ Notes:

- This endpoint uses `page` + `page_size` (tier-capped) rather than `limit`.

### get_webhook_subscription()

```python
sub = client.get_webhook_subscription("SUBSCRIPTION_UUID")
```

### create_webhook_subscription()

```python
Expand Down Expand Up @@ -1335,21 +1343,89 @@ Every delivery includes an HMAC signature header:

Compute the digest over the **raw request body bytes** using your shared secret.

The SDK ships a stdlib-only verifier that mirrors the Tango server's signing scheme byte-for-byte. Use it instead of hand-rolling — it's importable from a default install (no extras needed):

```python
import hashlib
import hmac
from tango.webhooks import verify_signature

if not verify_signature(raw_body, secret, request.headers.get("X-Tango-Signature")):
return 401
```

`verify_signature` returns `False` for missing/empty/malformed headers — it never raises. Comparison is constant-time.

def verify_tango_webhook_signature(secret: str, raw_body: bytes, signature_header: str | None) -> bool:
if not signature_header:
return False
sig = signature_header.strip()
if sig.startswith("sha256="):
sig = sig[len("sha256=") :]
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
---

## Webhook tooling (`tango.webhooks`)

The `tango.webhooks` subpackage adds testing and developer-tooling primitives on top of the API methods above. Signing helpers ship with the default install; the receiver and CLI ship with `pip install 'tango-python[webhooks]'`. See [`docs/WEBHOOKS.md`](WEBHOOKS.md) for usage guides; this section is the import-level reference.

### Signing (default install)

```python
from tango.webhooks import (
verify_signature, # (body: bytes, secret: str, header: str | None) -> bool
generate_signature, # (body: bytes, secret: str) -> str (lowercase hex)
parse_signature_header, # (header: str | None) -> str | None (strips "sha256=")
SIGNATURE_HEADER, # "X-Tango-Signature"
SIGNATURE_PREFIX, # "sha256="
)
```

### `WebhookReceiver` (with `[webhooks]` extra)

A stdlib-based local HTTP receiver, useful in tests and during local development.

```python
from tango.webhooks import WebhookReceiver, Delivery

with WebhookReceiver(secret="dev").run() as rx:
# ... cause something to POST to rx.url ...
deliveries: list[Delivery] = rx.deliveries
```

Constructor (all keyword arguments):

| Arg | Default | Meaning |
|---|---|---|
| `secret` | `""` | Shared secret. Empty means signatures are not verified. |
| `path` | `/tango/webhooks` | URL path to accept POSTs on. |
| `host` | `127.0.0.1` | Bind address. |
| `port` | `0` | TCP port. `0` = OS picks a free port. |
| `forward_to` | `None` | Optional URL to mirror each delivery to. |
| `max_history` | `256` | Cap on the in-memory `deliveries` deque. |
| `on_delivery` | `None` | Callback fired for every delivery (verified or not). |
| `require_signature` | `None` | Override default (require iff `secret` is set). |

Each `Delivery` is a dataclass: `received_at`, `path`, `signature_header`, `body_bytes`, `body_json`, `verified`, `remote_addr`, `forward_status`, `forward_error`.

### `simulate.sign` and `simulate.deliver`

```python
from tango.webhooks import sign, SignedRequest
from tango.webhooks import simulate

# Offline — produce the signed wire form without POSTing:
signed: SignedRequest = sign({"events": [{"event_type": "..."}]}, secret="s")
signed.body # bytes you would put on the wire
signed.signature # bare lowercase hex
signed.headers # {"Content-Type": ..., "X-Tango-Signature": "sha256=..."}

# With delivery — sign and POST to a target URL:
result = simulate.deliver(target_url="http://localhost:8011/tango/webhooks",
payload={...}, secret="s")
result.status_code # status from the receiver
result.signature # bare hex
result.sent_bytes # exact bytes that were POSTed
result.response_body # body the receiver returned
```

`simulate.deliver` and `simulate.sign` accept payloads as `dict`, `list`, `str`, or raw `bytes`. Dicts/lists are serialized via `json.dumps(..., sort_keys=True, separators=(",", ":"))` so signatures are reproducible across runs.

### CLI entry point

The `tango[webhooks]` extra also installs a `tango` console script. See [`docs/WEBHOOKS.md` § CLI reference](WEBHOOKS.md#cli-reference) for the full command list.

---

## Response Objects
Expand Down
Loading
Loading