helprange 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- helprange-0.1.0/.gitignore +10 -0
- helprange-0.1.0/CHANGELOG.md +37 -0
- helprange-0.1.0/LICENSE +21 -0
- helprange-0.1.0/PKG-INFO +435 -0
- helprange-0.1.0/README.md +402 -0
- helprange-0.1.0/pyproject.toml +68 -0
- helprange-0.1.0/src/helprange/__init__.py +40 -0
- helprange-0.1.0/src/helprange/__main__.py +3 -0
- helprange-0.1.0/src/helprange/_client.py +455 -0
- helprange-0.1.0/src/helprange/_errors.py +153 -0
- helprange-0.1.0/src/helprange/_generated/__init__.py +2 -0
- helprange-0.1.0/src/helprange/_generated/models.py +2341 -0
- helprange-0.1.0/src/helprange/_generated/resources.py +3152 -0
- helprange-0.1.0/src/helprange/_types.py +53 -0
- helprange-0.1.0/src/helprange/_version.py +1 -0
- helprange-0.1.0/src/helprange/_webhooks.py +92 -0
- helprange-0.1.0/src/helprange/cli.py +458 -0
- helprange-0.1.0/src/helprange/py.typed +0 -0
- helprange-0.1.0/src/helprange/resources.py +3 -0
- helprange-0.1.0/src/helprange/types.py +3 -0
- helprange-0.1.0/tests/__init__.py +0 -0
- helprange-0.1.0/tests/conftest.py +71 -0
- helprange-0.1.0/tests/test_cli.py +180 -0
- helprange-0.1.0/tests/test_client.py +214 -0
- helprange-0.1.0/tests/test_idempotency.py +141 -0
- helprange-0.1.0/tests/test_resources.py +91 -0
- helprange-0.1.0/tests/test_webhooks.py +45 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this package are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the package uses [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.1.0] – unreleased
|
|
9
|
+
|
|
10
|
+
First release, generated from HelpRange API spec version 2026-11-15 (not published yet; this entry describes
|
|
11
|
+
everything that will ship in it).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Client with every REST API v2 endpoint, grouped by tag: account, folders, documents, links, triggers, data rooms,
|
|
16
|
+
NDAs, e-signature, offers, store, lead forms, activity, webhook endpoints.
|
|
17
|
+
- Data rooms end to end: `data_rooms.create` (with folders and NDA settings), `update`, `delete(confirm=True)`;
|
|
18
|
+
sub-resources `data_rooms.guests` (`list`, `invite`, `remove`), `data_rooms.documents` (`list`, `add`, `remove`,
|
|
19
|
+
`move`), `data_rooms.folders`, `data_rooms.questions`, `data_rooms.request_items`, `data_rooms.access_requests`,
|
|
20
|
+
`data_rooms.signature_requests`; `ndas.list`.
|
|
21
|
+
- `links.list` (every share link of the team, with filters and pagination) and `links.views` (the views of one link).
|
|
22
|
+
- `webhook_endpoints`: `list`, `create`, `get`, `update`, `delete`, `rotate_secret`, `send_test`, `deliveries`,
|
|
23
|
+
`redeliver`.
|
|
24
|
+
- Typed request and response models for every endpoint.
|
|
25
|
+
- `share()` – upload a file and create a share link in one call.
|
|
26
|
+
- Automatic `Idempotency-Key` (UUID v4) on every POST / PATCH, reused across retries; `idempotency_key=` on every
|
|
27
|
+
POST / PATCH method (or `request_options={"idempotency_key": …}`) to set your own.
|
|
28
|
+
- Retries with backoff on 429 (any method, honouring Retry-After), on 408/5xx/connection errors/timeouts (GET and
|
|
29
|
+
keyed POST / PATCH) and on 409 `idempotency_key_in_progress` (up to 5 extra attempts).
|
|
30
|
+
- Typed errors with status, code and body (incl. `IdempotencyKeyInProgressError`, `UnprocessableEntityError`,
|
|
31
|
+
`IdempotencyKeyReusedError`); test-mode (`hr_test_`) detection; rate-limit headers after each response.
|
|
32
|
+
- Webhook signature verification (`HelpRange-Signature`, v1 scheme, rotation-aware) and a signing helper for tests.
|
|
33
|
+
- Sync (`HelpRange`) and async (`AsyncHelpRange`) clients.
|
|
34
|
+
- `helprange` command-line tool: login, whoami, share, documents, `links` (all links, filters `--document`
|
|
35
|
+
`--folder` `--room` `--status` `--label`), `views --link LINK_ID` / `views DOCUMENT_ID`, `rooms create NAME
|
|
36
|
+
[--folder …] [--nda] [--invite EMAIL …]`, `rooms delete ID --yes`, rooms list/show/index/documents/guests/add/
|
|
37
|
+
invite/questions; `--json` output everywhere.
|
helprange-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HelpRange
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
helprange-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: helprange
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official HelpRange API client: upload documents, create tracked share links, run data rooms, e-signature, offers and analytics.
|
|
5
|
+
Project-URL: Homepage, https://www.helprange.com/developers
|
|
6
|
+
Project-URL: Documentation, https://www.helprange.com/developers/api
|
|
7
|
+
Project-URL: Repository, https://github.com/HelpRange/helprange-python
|
|
8
|
+
Project-URL: Issues, https://github.com/HelpRange/helprange-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/HelpRange/helprange-python/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: HelpRange <hello@helprange.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api,data room,document sharing,e-signature,helprange,sdk,virtual data room,webhooks
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Requires-Dist: httpx<1,>=0.23
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# HelpRange Python SDK
|
|
35
|
+
|
|
36
|
+
Official Python client and CLI for the [HelpRange API](https://www.helprange.com/developers): upload documents, create
|
|
37
|
+
tracked share links, see who viewed what (page by page, link by link), create and run data rooms (folders, NDA,
|
|
38
|
+
guests, Q&A, request lists), send documents for e-signature, create offers and manage your webhook endpoint.
|
|
39
|
+
|
|
40
|
+
- Sync (`HelpRange`) and asyncio (`AsyncHelpRange`) clients with the same methods
|
|
41
|
+
- Typed responses (`TypedDict`s generated from the [OpenAPI spec](https://www.helprange.com/developers/openapi.json) –
|
|
42
|
+
plain dicts at runtime, full completion in your editor and mypy/pyright checks)
|
|
43
|
+
- Automatic `Idempotency-Key` on every POST / PATCH, so retries on 429 / 5xx / network errors are safe for writes too
|
|
44
|
+
- Typed exceptions, test-mode detection
|
|
45
|
+
- Webhook signature verification
|
|
46
|
+
- A `helprange` command-line tool with `--json` output for scripts and agents
|
|
47
|
+
|
|
48
|
+
Python 3.9+. One dependency: [httpx](https://www.python-httpx.org/).
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
pip install helprange
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quickstart
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from helprange import HelpRange
|
|
60
|
+
|
|
61
|
+
hr = HelpRange() # reads HELPRANGE_API_KEY; or HelpRange("hr_live_…")
|
|
62
|
+
|
|
63
|
+
# Upload a PDF and create a tracked share link in one call
|
|
64
|
+
result = hr.share("proposal.pdf", label="ACME - Anna", expires_on="2026-12-31", allow_download=False, watermark=True)
|
|
65
|
+
print(result["link"]["url"]) # https://www.helprange.com/view/…/proposal.pdf
|
|
66
|
+
|
|
67
|
+
# Who opened it, when and for how long
|
|
68
|
+
stats = hr.documents.get(result["document"]["id"], recent_views=20)
|
|
69
|
+
for view in stats["recent_views"]:
|
|
70
|
+
print(view["viewer"], view["opened_at"], view["seconds_read"])
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Async:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import asyncio
|
|
77
|
+
from helprange import AsyncHelpRange
|
|
78
|
+
|
|
79
|
+
async def main():
|
|
80
|
+
async with AsyncHelpRange() as hr:
|
|
81
|
+
rooms = await hr.data_rooms.list()
|
|
82
|
+
print([r["name"] for r in rooms["data_rooms"]])
|
|
83
|
+
|
|
84
|
+
asyncio.run(main())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Authentication
|
|
88
|
+
|
|
89
|
+
Create a team API key in HelpRange → **Settings → API keys**. It acts as the administrator who created it, limited to
|
|
90
|
+
its **scopes** (`documents:read`, `documents:write`, `links:read`, `links:write`, `rooms:read`, `rooms:write`,
|
|
91
|
+
`analytics:read`, `team:read`). Every method's docstring names the scope it needs.
|
|
92
|
+
|
|
93
|
+
The key is taken from the `api_key` argument, else `HELPRANGE_API_KEY`. The SDK sends `Authorization: Bearer <key>`.
|
|
94
|
+
The REST API is part of the Max and Unlimited plans, the Deal Room Pass and the trial; other plans get
|
|
95
|
+
`PaymentRequiredError` (402).
|
|
96
|
+
|
|
97
|
+
## Sandbox (test mode)
|
|
98
|
+
|
|
99
|
+
Keys starting with `hr_test_` work on a separate sandbox of your team with sample data – never on live data:
|
|
100
|
+
|
|
101
|
+
- no e-mail is sent (only addresses at reserved test domains such as `example.com` are accepted; others raise
|
|
102
|
+
`BadRequestError` with `code == "sandbox_email_domain"`), nothing reaches Stripe, sandbox activity sends no webhooks;
|
|
103
|
+
- every JSON response carries `"livemode": False`;
|
|
104
|
+
- sandbox data is deleted 30 days after the sandbox was created or reset (Settings → API keys → Reset sandbox).
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
hr = HelpRange("hr_test_…")
|
|
108
|
+
hr.test_mode # True
|
|
109
|
+
hr.account.me()["livemode"] # False
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Resources
|
|
113
|
+
|
|
114
|
+
| Attribute | What |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `hr.account` | Who is connected, the plan and its limits |
|
|
117
|
+
| `hr.folders` | Folders and subfolders |
|
|
118
|
+
| `hr.documents` | Uploads, documents, statistics and share links |
|
|
119
|
+
| `hr.links` | Every share link of the team (filters, pagination) and the views of one link |
|
|
120
|
+
| `hr.triggers` | Newest views and uploads (polling) |
|
|
121
|
+
| `hr.data_rooms` | Create, change and delete data rooms; the index. Sub-resources: `.guests`, `.documents`, `.folders`, `.questions`, `.request_items`, `.access_requests`, `.signature_requests` |
|
|
122
|
+
| `hr.ndas` | The team's NDAs (for a data room's `nda_id`) and whether the plan has NDAs |
|
|
123
|
+
| `hr.signature_requests` | E-signature requests and signed PDFs |
|
|
124
|
+
| `hr.offers` | Offers with their own link |
|
|
125
|
+
| `hr.store` | Store sales and buyer access |
|
|
126
|
+
| `hr.lead_forms` | Lead-magnet / landing-page templates |
|
|
127
|
+
| `hr.activity` | The team activity feed |
|
|
128
|
+
| `hr.webhook_endpoints` | The team's webhook endpoint: create, change, delete, rotate the secret, test, delivery log, redeliver |
|
|
129
|
+
|
|
130
|
+
Path parameters are positional, everything else is a keyword argument named as in the API reference:
|
|
131
|
+
`hr.documents.create_link(250, label="ACME", passcode="oak-41")`. Arguments you leave out are not sent. For the few
|
|
132
|
+
fields where `null` means something (e.g. `subfolder_id=None` takes documents out of their subfolder), pass `None`
|
|
133
|
+
explicitly. Every method also takes `request_options={"timeout": …, "max_retries": …, "headers": {…}}`; every
|
|
134
|
+
POST / PATCH method also takes `idempotency_key=` (see [Retries](#retries-and-limits)).
|
|
135
|
+
|
|
136
|
+
Uploads accept a path, `bytes` or a binary file object: `hr.documents.upload(file="deck.pdf", folder_id=31)`, or a
|
|
137
|
+
public URL: `hr.documents.upload(file_url="https://example.com/deck.pdf")`.
|
|
138
|
+
|
|
139
|
+
Types live in `helprange.types` (`from helprange.types import ShareLink, GetDocumentResponse`). For an endpoint this
|
|
140
|
+
version does not wrap yet: `hr.request("GET", "/api/v2/…")`.
|
|
141
|
+
|
|
142
|
+
### All endpoints
|
|
143
|
+
|
|
144
|
+
<!-- BEGIN GENERATED ENDPOINTS -->
|
|
145
|
+
| Method | Endpoint | Scope |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| `hr.account.me()` | `GET /api/v2/me` | `team:read` |
|
|
148
|
+
| `hr.account.plan()` | `GET /api/v2/plan` | `team:read` |
|
|
149
|
+
| `hr.folders.list()` | `GET /api/v2/folders` | `documents:read` |
|
|
150
|
+
| `hr.folders.get(folder_id)` | `GET /api/v2/folders/{folder_id}` | `documents:read` |
|
|
151
|
+
| `hr.folders.set_subfolders_enabled(folder_id, ...)` | `POST /api/v2/folders/{folder_id}` | `documents:write` |
|
|
152
|
+
| `hr.folders.list_subfolders(folder_id)` | `GET /api/v2/folders/{folder_id}/subfolders` | `documents:read` |
|
|
153
|
+
| `hr.folders.create_subfolder(folder_id, ...)` | `POST /api/v2/folders/{folder_id}/subfolders` | `documents:write` |
|
|
154
|
+
| `hr.folders.move_documents(folder_id, ...)` | `POST /api/v2/folders/{folder_id}/subfolders/move` | `documents:write` |
|
|
155
|
+
| `hr.folders.rename_subfolder(subfolder_id, ...)` | `POST /api/v2/subfolders/{subfolder_id}` | `documents:write` |
|
|
156
|
+
| `hr.folders.delete_subfolder(subfolder_id)` | `POST /api/v2/subfolders/{subfolder_id}/delete` | `documents:write` |
|
|
157
|
+
| `hr.documents.list(...)` | `GET /api/v2/documents` | `documents:read` |
|
|
158
|
+
| `hr.documents.upload(...)` | `POST /api/v2/files` | `documents:write` |
|
|
159
|
+
| `hr.documents.create_link(file_id, ...)` | `POST /api/v2/files/{file_id}/links` | `links:write` |
|
|
160
|
+
| `hr.documents.get(file_id, ...)` | `GET /api/v2/documents/{file_id}` | `documents:read` |
|
|
161
|
+
| `hr.documents.list_links(file_id)` | `GET /api/v2/documents/{file_id}/links` | `links:read` |
|
|
162
|
+
| `hr.links.list(...)` | `GET /api/v2/links` | `links:read` |
|
|
163
|
+
| `hr.links.views(link_id, ...)` | `GET /api/v2/links/{link_id}/views` | `analytics:read` |
|
|
164
|
+
| `hr.triggers.recent_views()` | `GET /api/v2/views` | `analytics:read` |
|
|
165
|
+
| `hr.triggers.recent_uploads()` | `GET /api/v2/uploads` | `documents:read` |
|
|
166
|
+
| `hr.store.grant_buyer_access(folder_id, ...)` | `POST /api/v2/folders/{folder_id}/buyers` | `links:write` |
|
|
167
|
+
| `hr.store.revoke_buyer_access(folder_id, ...)` | `POST /api/v2/folders/{folder_id}/buyers/remove` | `links:write` |
|
|
168
|
+
| `hr.store.list_sales(...)` | `GET /api/v2/store/sales` | `analytics:read` |
|
|
169
|
+
| `hr.lead_forms.list_templates()` | `GET /api/v2/landing-templates` | `links:read` |
|
|
170
|
+
| `hr.activity.list(...)` | `GET /api/v2/activity` | `analytics:read` |
|
|
171
|
+
| `hr.signature_requests.list(...)` | `GET /api/v2/signature-requests` | `documents:read` |
|
|
172
|
+
| `hr.signature_requests.create(...)` | `POST /api/v2/signature-requests` | `documents:write` |
|
|
173
|
+
| `hr.signature_requests.get(envelope_id)` | `GET /api/v2/signature-requests/{envelope_id}` | `documents:read` |
|
|
174
|
+
| `hr.signature_requests.remind(envelope_id)` | `POST /api/v2/signature-requests/{envelope_id}/remind` | `documents:write` |
|
|
175
|
+
| `hr.signature_requests.void(envelope_id, ...)` | `POST /api/v2/signature-requests/{envelope_id}/void` | `documents:write` |
|
|
176
|
+
| `hr.signature_requests.get_signed_pdf(envelope_id)` | `GET /api/v2/signature-requests/{envelope_id}/signed-pdf` | `documents:read` |
|
|
177
|
+
| `hr.data_rooms.list()` | `GET /api/v2/data-rooms` | `rooms:read` |
|
|
178
|
+
| `hr.data_rooms.create(...)` | `POST /api/v2/data-rooms` | `rooms:write` |
|
|
179
|
+
| `hr.data_rooms.get(data_room_id)` | `GET /api/v2/data-rooms/{data_room_id}` | `rooms:read` |
|
|
180
|
+
| `hr.data_rooms.update(data_room_id, ...)` | `POST /api/v2/data-rooms/{data_room_id}` | `rooms:write` |
|
|
181
|
+
| `hr.data_rooms.delete(data_room_id, ...)` | `POST /api/v2/data-rooms/{data_room_id}/delete` | `rooms:write` |
|
|
182
|
+
| `hr.ndas.list()` | `GET /api/v2/ndas` | `rooms:read` |
|
|
183
|
+
| `hr.data_rooms.get_index(data_room_id)` | `GET /api/v2/data-rooms/{data_room_id}/index` | `rooms:read` |
|
|
184
|
+
| `hr.data_rooms.guests.list(data_room_id)` | `GET /api/v2/data-rooms/{data_room_id}/guests` | `rooms:read` |
|
|
185
|
+
| `hr.data_rooms.guests.invite(data_room_id, ...)` | `POST /api/v2/data-rooms/{data_room_id}/guests` | `rooms:write` |
|
|
186
|
+
| `hr.data_rooms.guests.remove(data_room_id, ...)` | `POST /api/v2/data-rooms/{data_room_id}/guests/remove` | `rooms:write` |
|
|
187
|
+
| `hr.data_rooms.access_requests.list(data_room_id, ...)` | `GET /api/v2/data-rooms/{data_room_id}/access-requests` | `rooms:read` |
|
|
188
|
+
| `hr.data_rooms.signature_requests.list(data_room_id)` | `GET /api/v2/data-rooms/{data_room_id}/signature-requests` | `rooms:read` |
|
|
189
|
+
| `hr.data_rooms.questions.list(data_room_id, ...)` | `GET /api/v2/data-rooms/{data_room_id}/questions` | `rooms:read` |
|
|
190
|
+
| `hr.data_rooms.questions.add(data_room_id, ...)` | `POST /api/v2/data-rooms/{data_room_id}/questions` | `rooms:write` |
|
|
191
|
+
| `hr.data_rooms.request_items.list(data_room_id, ...)` | `GET /api/v2/data-rooms/{data_room_id}/requests` | `rooms:read` |
|
|
192
|
+
| `hr.data_rooms.request_items.add(data_room_id, ...)` | `POST /api/v2/data-rooms/{data_room_id}/requests` | `rooms:write` |
|
|
193
|
+
| `hr.data_rooms.folders.list(data_room_id)` | `GET /api/v2/data-rooms/{data_room_id}/folders` | `rooms:read` |
|
|
194
|
+
| `hr.data_rooms.folders.create(data_room_id, ...)` | `POST /api/v2/data-rooms/{data_room_id}/folders` | `rooms:write` |
|
|
195
|
+
| `hr.data_rooms.documents.move(data_room_id, ...)` | `POST /api/v2/data-rooms/{data_room_id}/folders/move` | `rooms:write` |
|
|
196
|
+
| `hr.data_rooms.folders.rename(data_room_id, folder_id, ...)` | `POST /api/v2/data-rooms/{data_room_id}/folders/{folder_id}` | `rooms:write` |
|
|
197
|
+
| `hr.data_rooms.folders.delete(data_room_id, folder_id)` | `POST /api/v2/data-rooms/{data_room_id}/folders/{folder_id}/delete` | `rooms:write` |
|
|
198
|
+
| `hr.data_rooms.documents.list(data_room_id)` | `GET /api/v2/data-rooms/{data_room_id}/documents` | `rooms:read` |
|
|
199
|
+
| `hr.data_rooms.documents.add(data_room_id, ...)` | `POST /api/v2/data-rooms/{data_room_id}/documents` | `rooms:write` |
|
|
200
|
+
| `hr.data_rooms.documents.remove(data_room_id, ...)` | `POST /api/v2/data-rooms/{data_room_id}/documents/remove` | `rooms:write` |
|
|
201
|
+
| `hr.offers.list(...)` | `GET /api/v2/offers` | `links:read` |
|
|
202
|
+
| `hr.offers.create(...)` | `POST /api/v2/offers` | `links:write` |
|
|
203
|
+
| `hr.offers.get(offer_id)` | `GET /api/v2/offers/{offer_id}` | `links:read` |
|
|
204
|
+
| `hr.offers.update(offer_id, ...)` | `PATCH /api/v2/offers/{offer_id}` | `links:write` |
|
|
205
|
+
| `hr.offers.add_files(offer_id, ...)` | `POST /api/v2/offers/{offer_id}/files` | `links:write` |
|
|
206
|
+
| `hr.offers.remove_files(offer_id, ...)` | `POST /api/v2/offers/{offer_id}/files/remove` | `links:write` |
|
|
207
|
+
| `hr.offers.list_responses(offer_id, ...)` | `GET /api/v2/offers/{offer_id}/responses` | `analytics:read` |
|
|
208
|
+
| `hr.offers.list_all_responses(...)` | `GET /api/v2/offer-responses` | `analytics:read` |
|
|
209
|
+
| `hr.webhook_endpoints.list()` | `GET /api/v2/webhook-endpoints` | `webhooks:manage` |
|
|
210
|
+
| `hr.webhook_endpoints.create(...)` | `POST /api/v2/webhook-endpoints` | `webhooks:manage` |
|
|
211
|
+
| `hr.webhook_endpoints.get(endpoint_id)` | `GET /api/v2/webhook-endpoints/{endpoint_id}` | `webhooks:manage` |
|
|
212
|
+
| `hr.webhook_endpoints.update(endpoint_id, ...)` | `POST /api/v2/webhook-endpoints/{endpoint_id}` | `webhooks:manage` |
|
|
213
|
+
| `hr.webhook_endpoints.delete(endpoint_id)` | `POST /api/v2/webhook-endpoints/{endpoint_id}/delete` | `webhooks:manage` |
|
|
214
|
+
| `hr.webhook_endpoints.rotate_secret(endpoint_id)` | `POST /api/v2/webhook-endpoints/{endpoint_id}/rotate-secret` | `webhooks:manage` |
|
|
215
|
+
| `hr.webhook_endpoints.send_test(endpoint_id)` | `POST /api/v2/webhook-endpoints/{endpoint_id}/test` | `webhooks:manage` |
|
|
216
|
+
| `hr.webhook_endpoints.deliveries(endpoint_id, ...)` | `GET /api/v2/webhook-endpoints/{endpoint_id}/deliveries` | `webhooks:manage` |
|
|
217
|
+
| `hr.webhook_endpoints.redeliver(endpoint_id, delivery_id)` | `POST /api/v2/webhook-endpoints/{endpoint_id}/deliveries/{delivery_id}/redeliver` | `webhooks:manage` |
|
|
218
|
+
<!-- END GENERATED ENDPOINTS -->
|
|
219
|
+
|
|
220
|
+
## Data rooms
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
# NDAs need the Max plan (or the Deal Room Pass / trial)
|
|
224
|
+
nda_available = hr.ndas.list()["nda_available"]
|
|
225
|
+
|
|
226
|
+
room = hr.data_rooms.create(
|
|
227
|
+
name="Project Falcon",
|
|
228
|
+
folders=["1. Corporate", "2. Financials", "3. Legal"],
|
|
229
|
+
settings={"download": False, "watermark": True, "nda": nda_available, "nda_template": "investor", "nda_mode": "click"},
|
|
230
|
+
)
|
|
231
|
+
print(room["links"]["guest_link"]) # the room's shareable link
|
|
232
|
+
|
|
233
|
+
hr.data_rooms.documents.add(room["id"], document_ids=[250, 251], folder_id=room["folders"][1]["id"])
|
|
234
|
+
|
|
235
|
+
guest = hr.data_rooms.guests.invite(room["id"], email="jane@example.com")
|
|
236
|
+
print(guest["link"]) # Jane's own link – also e-mailed to her unless send_invite=False
|
|
237
|
+
|
|
238
|
+
hr.data_rooms.guests.list(room["id"])
|
|
239
|
+
hr.data_rooms.guests.remove(room["id"], email="jane@example.com")
|
|
240
|
+
hr.data_rooms.update(room["id"], settings={"expires_on": "2026-12-31"})
|
|
241
|
+
|
|
242
|
+
# Deleting a room (and its guest access) needs an explicit confirmation
|
|
243
|
+
hr.data_rooms.delete(room["id"], confirm=True)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Links and views
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
# Every share link of the team – filter by document_id, folder_id, room_id, label, status, created_after/_before
|
|
250
|
+
page = 1
|
|
251
|
+
while page:
|
|
252
|
+
res = hr.links.list(document_id=250, status="active", page=page, limit=200)
|
|
253
|
+
for link in res["links"]:
|
|
254
|
+
print(link["id"], link["label"], link["views"], link["url"])
|
|
255
|
+
page = res["next_page"]
|
|
256
|
+
|
|
257
|
+
# Who opened one link, when, for how long
|
|
258
|
+
for v in hr.links.views(264, limit=100)["views"]:
|
|
259
|
+
print(v["viewer"], v["opened_at"], v["seconds_read"], v["pages_read"])
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Errors
|
|
263
|
+
|
|
264
|
+
Every exception is a `helprange.HelpRangeError` with `status`, `code` (the API's machine-readable reason, when it
|
|
265
|
+
sends one), `message` and the parsed `body`:
|
|
266
|
+
|
|
267
|
+
| Exception | When |
|
|
268
|
+
| --- | --- |
|
|
269
|
+
| `BadRequestError` | 400 – e.g. a duplicate link label, `sandbox_email_domain` |
|
|
270
|
+
| `AuthenticationError` | 401 – `invalid_key`, `key_revoked`, `key_expired`, `key_owner_removed` |
|
|
271
|
+
| `PaymentRequiredError` | 402 – the plan does not include the API / feature |
|
|
272
|
+
| `PermissionDeniedError` | 403 – `insufficient_scope` (see `.required_scope`) |
|
|
273
|
+
| `NotFoundError` | 404 |
|
|
274
|
+
| `ConflictError` | 409 – e.g. a frozen room, the team already has a webhook endpoint |
|
|
275
|
+
| `IdempotencyKeyInProgressError` | 409 `idempotency_key_in_progress` (a `ConflictError`) – still running after the in-progress retries |
|
|
276
|
+
| `UnprocessableEntityError` | 422 |
|
|
277
|
+
| `IdempotencyKeyReusedError` | 422 `idempotency_key_reused` (an `UnprocessableEntityError`) – the key was used for a different request |
|
|
278
|
+
| `PayloadTooLargeError` | 413 – `FILE_TOO_LARGE`, `REQUEST_TOO_LARGE` |
|
|
279
|
+
| `LockedError` | 423 – e.g. `ROOM_FROZEN` |
|
|
280
|
+
| `RateLimitError` | 429 – `RATE_LIMITED`, `DAILY_LIMIT`, `PAUSED`, `UPLOAD_LIMIT` (see `.retry_after`) |
|
|
281
|
+
| `InternalServerError` | 5xx |
|
|
282
|
+
| `APIConnectionError` / `APITimeoutError` | no HTTP answer / timed out |
|
|
283
|
+
| `WebhookVerificationError` | `verify_webhook()` rejected a delivery |
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
from helprange import PermissionDeniedError, RateLimitError
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
hr.data_rooms.guests.invite(10, email="jane@example.com")
|
|
290
|
+
except PermissionDeniedError as e:
|
|
291
|
+
print("missing scope:", e.required_scope)
|
|
292
|
+
except RateLimitError as e:
|
|
293
|
+
print(f"over a limit ({e.code}); retry in {e.retry_after}s")
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Retries and limits
|
|
297
|
+
|
|
298
|
+
Every `POST` and `PATCH` carries an `Idempotency-Key` header – a new UUID v4 per call, **the same one for every retry
|
|
299
|
+
of that call**. The API runs a keyed request once and answers a repeat with the stored response (header
|
|
300
|
+
`Idempotent-Replayed: true`), so a retried write never creates a second data room, share link or invitation.
|
|
301
|
+
|
|
302
|
+
- **429** (any method) – the API did not carry out the request. The client waits `Retry-After` and retries, unless the
|
|
303
|
+
wait is longer than `max_retry_after` (default 60 s – e.g. a daily cap or a paused API); then it raises
|
|
304
|
+
`RateLimitError` at once.
|
|
305
|
+
- **408 / 5xx, connection errors and timeouts** – retried for `GET` and for keyed `POST` / `PATCH`, with exponential
|
|
306
|
+
backoff (0.5 s, 1 s, 2 s … up to 8 s, with jitter; a short `Retry-After` is respected).
|
|
307
|
+
- **409 `idempotency_key_in_progress`** – the first attempt is still running on the server (typically after a
|
|
308
|
+
timeout). The client waits with backoff (~1, 2, 4, 8, 8 s) and asks again, up to 5 times, without using up
|
|
309
|
+
`max_retries`; then it raises `IdempotencyKeyInProgressError`.
|
|
310
|
+
- **422 `idempotency_key_reused`** – never retried; raises `IdempotencyKeyReusedError`. It means the key was used in the
|
|
311
|
+
last 24 hours for a different method, path or body.
|
|
312
|
+
|
|
313
|
+
Pass your own key to make a write safe to repeat across processes or restarts (e.g. a job id):
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
hr.data_rooms.create(name="Project Falcon", idempotency_key=f"deal-{deal.id}-room")
|
|
317
|
+
# or, on any method: request_options={"idempotency_key": "…"}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Keys are kept for 24 hours, per API key. `hr.share(..., request_options={"idempotency_key": k})` uses `k` for the
|
|
321
|
+
upload and `k:link` for the share link.
|
|
322
|
+
|
|
323
|
+
`max_retries` (default 2) and `timeout` (default 60 s per attempt) can be set on the client or per request.
|
|
324
|
+
After each response `hr.rate_limit` holds `limit`, `remaining`, `reset` and `livemode` from the response headers.
|
|
325
|
+
Team limits: 3,600 requests an hour, 120 a minute, 300 writes an hour, 2,000 share links and 1,000 guests a day.
|
|
326
|
+
|
|
327
|
+
## Webhooks
|
|
328
|
+
|
|
329
|
+
HelpRange POSTs signed JSON events (`document.opened`, `file_request.uploaded`, `room.access_requested`,
|
|
330
|
+
`nda.accepted`, `document.acknowledged`, `offer.accepted`, `offer.declined`, `offer.contact_requested`) to the endpoint
|
|
331
|
+
you set in Settings → Integrations or through `hr.webhook_endpoints`. Verify each delivery with the **raw** body:
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
# Flask
|
|
335
|
+
import os
|
|
336
|
+
|
|
337
|
+
from flask import Flask, request
|
|
338
|
+
from helprange import verify_webhook, WebhookVerificationError
|
|
339
|
+
|
|
340
|
+
app = Flask(__name__)
|
|
341
|
+
|
|
342
|
+
@app.post("/webhooks/helprange")
|
|
343
|
+
def helprange_webhook():
|
|
344
|
+
try:
|
|
345
|
+
event = verify_webhook(request.get_data(), request.headers, os.environ["HELPRANGE_WEBHOOK_SECRET"])
|
|
346
|
+
except WebhookVerificationError:
|
|
347
|
+
return "", 400
|
|
348
|
+
# deduplicate on event["id"] – retries carry the same id
|
|
349
|
+
if event["event"] == "document.opened":
|
|
350
|
+
print(event["data"])
|
|
351
|
+
return "", 200 # answer 2xx within 10 seconds
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Django: `verify_webhook(request.body, request.headers, secret)`; FastAPI: `verify_webhook(await request.body(),
|
|
355
|
+
request.headers, secret)`.
|
|
356
|
+
|
|
357
|
+
The scheme: `HelpRange-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>`. `verify_webhook` accepts
|
|
358
|
+
a timestamp within 5 minutes (`tolerance=` to change it) and any `v1` that matches – during the 24 hours after a secret
|
|
359
|
+
rotation HelpRange sends two. Pass a list of secrets if you rotate on your side. `sign_webhook_payload()` builds a valid
|
|
360
|
+
header for your tests. See the [webhooks guide](https://www.helprange.com/developers/webhooks).
|
|
361
|
+
|
|
362
|
+
Manage the endpoint from code:
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
ep = hr.webhook_endpoints.create(url="https://example.com/webhooks/helprange")
|
|
366
|
+
print(ep["secret"]) # shown once – store it
|
|
367
|
+
hr.webhook_endpoints.send_test(ep["id"]) # a test event, delivered right away
|
|
368
|
+
failed = hr.webhook_endpoints.deliveries(ep["id"], status="failed")["deliveries"]
|
|
369
|
+
if failed:
|
|
370
|
+
hr.webhook_endpoints.redeliver(ep["id"], failed[0]["id"])
|
|
371
|
+
secret = hr.webhook_endpoints.rotate_secret(ep["id"])["secret"] # the old secret keeps signing for 24 hours
|
|
372
|
+
hr.webhook_endpoints.update(ep["id"], enabled=True) # turn it back on after 410 Gone / repeated failures
|
|
373
|
+
hr.webhook_endpoints.delete(ep["id"])
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Command line
|
|
377
|
+
|
|
378
|
+
```sh
|
|
379
|
+
helprange login # prompts for the key, checks it, saves ~/.config/helprange/config (0600)
|
|
380
|
+
helprange whoami
|
|
381
|
+
helprange share proposal.pdf --to anna@acme.com --to bob@acme.com --expires 2026-12-31 --no-download --watermark
|
|
382
|
+
helprange share https://example.com/deck.pdf # a public URL works too
|
|
383
|
+
helprange documents
|
|
384
|
+
helprange links # every active share link of the team
|
|
385
|
+
helprange links --document 250 --status all # filters: --document --folder --room --status --label (+ --page --limit)
|
|
386
|
+
helprange views --link 264 # who opened link 264, when, how long
|
|
387
|
+
helprange views 250 # a document's totals and latest views
|
|
388
|
+
helprange rooms create "Project Falcon" --folder Corporate --folder Financials --nda \
|
|
389
|
+
--invite jane@example.com --invite max@example.com # prints the guest link and each guest's own link
|
|
390
|
+
helprange rooms list
|
|
391
|
+
helprange rooms show 10 | index 10 | documents 10 | guests 10 | questions 10 --status open
|
|
392
|
+
helprange rooms add 10 250 251 # add documents 250 and 251 to room 10
|
|
393
|
+
helprange rooms invite 10 jane@example.com # HelpRange e-mails the invitation
|
|
394
|
+
helprange rooms delete 10 --yes # without --yes nothing is deleted
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
- `--json` on any command prints JSON (errors go to stderr as JSON too) – meant for scripts and AI agents.
|
|
398
|
+
- The key: `--api-key`, else `HELPRANGE_API_KEY`, else the saved one. `helprange logout` forgets it.
|
|
399
|
+
- `share --to` creates **one link per recipient**, labelled with their e-mail, so the statistics show who opened what.
|
|
400
|
+
HelpRange does not e-mail links created through the API – send them yourself.
|
|
401
|
+
- Exit codes: 0 ok, 1 API error, 2 usage / configuration error.
|
|
402
|
+
|
|
403
|
+
`python -m helprange …` works the same.
|
|
404
|
+
|
|
405
|
+
## Examples
|
|
406
|
+
|
|
407
|
+
[`examples/`](examples): [`share_pdf.py`](examples/share_pdf.py) (share a PDF and get a link),
|
|
408
|
+
[`who_viewed.py`](examples/who_viewed.py) (who viewed a document, through which link, time per page),
|
|
409
|
+
[`data_room.py`](examples/data_room.py) (async: create a room with folders and an NDA, add documents, invite guests
|
|
410
|
+
and print their links, index, Q&A),
|
|
411
|
+
[`webhook_endpoints.py`](examples/webhook_endpoints.py) (create / update the endpoint, test event, redeliver, rotate
|
|
412
|
+
the secret),
|
|
413
|
+
[`webhook_server.py`](examples/webhook_server.py) (stdlib webhook receiver).
|
|
414
|
+
|
|
415
|
+
## Why httpx
|
|
416
|
+
|
|
417
|
+
httpx is the one dependency: it gives the sync and the async client the same request model, real connect/read
|
|
418
|
+
timeouts, connection pooling, correct multipart uploads and `MockTransport` for tests, and is already installed in
|
|
419
|
+
most modern Python stacks (it is pure Python). The stdlib (`urllib`) has no async client, so an async SDK on top of it
|
|
420
|
+
would need threads, and it lacks pooling and multipart.
|
|
421
|
+
|
|
422
|
+
## Development
|
|
423
|
+
|
|
424
|
+
```sh
|
|
425
|
+
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
|
|
426
|
+
.venv/bin/pytest -q # fake transport, no network
|
|
427
|
+
.venv/bin/ruff check src tests examples && .venv/bin/mypy src examples
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
`src/helprange/_generated/` is generated from the OpenAPI spec – don't edit it by hand; see the generator in the
|
|
431
|
+
`helprange-sdks` workspace.
|
|
432
|
+
|
|
433
|
+
## License
|
|
434
|
+
|
|
435
|
+
MIT © HelpRange
|