form4api 0.9.0__tar.gz → 0.11.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.
Files changed (29) hide show
  1. {form4api-0.9.0 → form4api-0.11.0}/PKG-INFO +378 -289
  2. {form4api-0.9.0 → form4api-0.11.0}/README.md +359 -270
  3. {form4api-0.9.0 → form4api-0.11.0}/form4api/__init__.py +52 -48
  4. {form4api-0.9.0 → form4api-0.11.0}/form4api/_client.py +339 -319
  5. {form4api-0.9.0 → form4api-0.11.0}/form4api/_errors.py +105 -105
  6. {form4api-0.9.0 → form4api-0.11.0}/form4api/_generated.py +2263 -2165
  7. {form4api-0.9.0 → form4api-0.11.0}/form4api/_types.py +264 -200
  8. {form4api-0.9.0 → form4api-0.11.0}/form4api/_webhook_utils.py +8 -4
  9. {form4api-0.9.0 → form4api-0.11.0}/form4api/resources/__init__.py +3 -3
  10. {form4api-0.9.0 → form4api-0.11.0}/form4api/resources/_companies.py +51 -51
  11. {form4api-0.9.0 → form4api-0.11.0}/form4api/resources/_insiders.py +90 -90
  12. {form4api-0.9.0 → form4api-0.11.0}/form4api/resources/_signals.py +159 -159
  13. {form4api-0.9.0 → form4api-0.11.0}/form4api/resources/_transactions.py +444 -299
  14. form4api-0.11.0/form4api/resources/_webhooks.py +153 -0
  15. {form4api-0.9.0 → form4api-0.11.0}/form4api.egg-info/PKG-INFO +378 -289
  16. {form4api-0.9.0 → form4api-0.11.0}/form4api.egg-info/SOURCES.txt +4 -1
  17. {form4api-0.9.0 → form4api-0.11.0}/pyproject.toml +28 -28
  18. {form4api-0.9.0 → form4api-0.11.0}/setup.cfg +4 -4
  19. {form4api-0.9.0 → form4api-0.11.0}/tests/test_client.py +677 -677
  20. {form4api-0.9.0 → form4api-0.11.0}/tests/test_generated.py +480 -480
  21. {form4api-0.9.0 → form4api-0.11.0}/tests/test_method_name.py +88 -88
  22. form4api-0.11.0/tests/test_release_0_10_0.py +222 -0
  23. form4api-0.11.0/tests/test_release_0_11_0.py +149 -0
  24. form4api-0.11.0/tests/test_unknown_fields.py +257 -0
  25. form4api-0.9.0/form4api/resources/_webhooks.py +0 -56
  26. {form4api-0.9.0 → form4api-0.11.0}/LICENSE +0 -0
  27. {form4api-0.9.0 → form4api-0.11.0}/form4api.egg-info/dependency_links.txt +0 -0
  28. {form4api-0.9.0 → form4api-0.11.0}/form4api.egg-info/requires.txt +0 -0
  29. {form4api-0.9.0 → form4api-0.11.0}/form4api.egg-info/top_level.txt +0 -0
@@ -1,289 +1,378 @@
1
- Metadata-Version: 2.4
2
- Name: form4api
3
- Version: 0.9.0
4
- Summary: Python client for the Form4API — real-time SEC Form 4 insider trading, Form 144 intent-to-sell, institutional 13F-HR, and congressional STOCK Act trading data, including the insider/Congress convergence signal
5
- License-Expression: MIT
6
- Project-URL: Homepage, https://www.form4api.com
7
- Project-URL: Documentation, https://www.form4api.com/docs
8
- Project-URL: Repository, https://github.com/theodor90/form4api-py.git
9
- Keywords: insider-trading,sec,sec-edgar,edgar,form-4,form4,form-144,13f,13f-hr,congress,stock-act,financial-data,stock-market,stocks,fintech,api,sdk,webhooks,form4api
10
- Requires-Python: >=3.11
11
- Description-Content-Type: text/markdown
12
- License-File: LICENSE
13
- Requires-Dist: httpx>=0.27
14
- Provides-Extra: dev
15
- Requires-Dist: pytest>=8; extra == "dev"
16
- Requires-Dist: pytest-httpx>=0.30; extra == "dev"
17
- Requires-Dist: respx>=0.21; extra == "dev"
18
- Dynamic: license-file
19
-
20
- # form4api
21
-
22
- Python client for [Form4API](https://www.form4api.com) — real-time SEC Form 4 insider trading, Form 144 intent-to-sell, institutional 13F-HR, and congressional STOCK Act trading data, including the insider/Congress convergence signal.
23
-
24
- [![PyPI version](https://img.shields.io/pypi/v/form4api.svg)](https://pypi.org/project/form4api/)
25
- [![PyPI downloads](https://img.shields.io/pypi/dm/form4api.svg)](https://pypi.org/project/form4api/)
26
- [![Python versions](https://img.shields.io/pypi/pyversions/form4api.svg)](https://pypi.org/project/form4api/)
27
- [![license](https://img.shields.io/pypi/l/form4api.svg)](https://github.com/theodor90/form4api-py/blob/main/LICENSE)
28
-
29
- Supports Python 3.11+. Uses `httpx` for both sync and async HTTP.
30
-
31
- ## Installation
32
-
33
- ```bash
34
- pip install form4api
35
- ```
36
-
37
- ## Sync quickstart
38
-
39
- ```python
40
- from form4api import Form4ApiClient
41
-
42
- client = Form4ApiClient("YOUR_API_KEY")
43
-
44
- # Recent open-market purchases at Apple (excluding 10b5-1 plan trades)
45
- txns = client.transactions.list(ticker="AAPL", code="P", exclude_10b5=True, per_page=5)
46
- for t in txns:
47
- print(t.insider_name, t.insider_title, t.shares_amount, "@", t.price_per_share)
48
- print(f" open market: {t.is_open_market}, 10b5 plan: {t.is10b5_plan}, value: ${t.total_value:,.0f}")
49
-
50
- # Company overview (includes SIC, state, website)
51
- company = client.companies.get("MSFT")
52
- print(company.name, company.active_insiders, "active insiders")
53
- print(company.sic_description, company.state_of_incorporation)
54
-
55
- # Insider detail
56
- insider = client.insiders.get("0001234567")
57
- print(insider.name, insider.officer_title)
58
-
59
- # Cluster-buy signals (Business plan)
60
- signals = client.signals.list(cluster_buy=True)
61
- for sig in signals:
62
- print(sig.company_name, sig.insider_count, "buyers on", sig.signal_date)
63
- ```
64
-
65
- ## Async quickstart
66
-
67
- ```python
68
- import asyncio
69
- from form4api import AsyncForm4ApiClient
70
-
71
- async def main():
72
- async with AsyncForm4ApiClient("YOUR_API_KEY") as client:
73
- txns = await client.transactions.list(ticker="AAPL", per_page=5)
74
- for t in txns:
75
- print(t.insider_name, t.shares_amount, "@", t.price_per_share)
76
-
77
- asyncio.run(main())
78
- ```
79
-
80
- ## Resources
81
-
82
- | Resource | Methods |
83
- |---|---|
84
- | `client.search(q, limit=None)` | Combined company + insider lookup — not namespaced under a resource |
85
- | `client.transactions` | `.list(**params)`, `.paginate(**params)` |
86
- | `client.insiders` | `.search(name, **params)`, `.get(cik)`, `.list(**params)`, `.directory(**params)`, `.transactions(cik, **params)`, `.summary(cik)` *(Pro)*, `.scorecard(cik)` *(Pro)*, `.leaderboard(**params)` *(Business)* |
87
- | `client.companies` | `.get(ticker)`, `.insiders(ticker)`, `.list(**params)` |
88
- | `client.signals` | `.list(**params)`, `.paginate(**params)`, `.explain(ticker)`, `.sentiment(ticker, **params)` — Business; `.convergence(**params)` — Pro |
89
- | `client.congress` | `.trades(**params)`, `.politicians(**params)` *(Pro)*, `.politician(id_or_slug)` *(Pro)*, `.ticker(ticker)` *(Pro)* |
90
- | `client.form144` | `.list(**params)` — Business plan |
91
- | `client.holdings` | `.list(**params)`, `.managers(**params)` — Business plan |
92
- | `client.filings` | `.list(**params)`, `.recent(**params)`, `.get(accession_number)` |
93
- | `client.stats` | `.get()` — public, no key required |
94
- | `client.data_quality` | `.get()` — public, no key required |
95
- | `client.status` | `.history(**params)` |
96
- | `client.webhooks` | `.create(url, event_types)`, `.list()`, `.delete(id)`, `.events(**params)` |
97
-
98
- **Congress date quality.** Each congress trade (and convergence leg) has
99
- `disclosure_lag_days: int | None` and `date_quality: str | None`. When the
100
- filing's own dates are impossible or implausible, the lag is `None` and
101
- `date_quality` is one of `"transaction_after_disclosure"`,
102
- `"future_transaction_date"` or `"implausible_lag"`; the raw dates are still
103
- returned. A lag over 45 days is not a legal finding.
104
-
105
- ```python
106
- for t in client.congress.trades(ticker="NVDA"):
107
- lag = f"dates unreliable ({t.date_quality})" if t.disclosure_lag_days is None else f"{t.disclosure_lag_days}d"
108
- print(t.politician.full_name, t.transaction_date, lag)
109
- ```
110
-
111
- Every plan-gated endpoint the API exposes has a typed method here. Calling one
112
- your key isn't entitled to raises `PlanError` (HTTP 402) carrying
113
- `required_plan`, `current_plan`, and `upgrade_url` rather than failing opaquely.
114
-
115
- An `AsyncForm4ApiClient` mirrors the whole surface with the same resources and
116
- method names — `await client.insiders.leaderboard()`.
117
-
118
- For the full parameter reference see the [REST docs](https://form4api.com/docs).
119
- For LLM workflows, `form4api-mcp` exposes the same endpoints as tools.
120
-
121
- ### Transaction filters
122
-
123
- ```python
124
- client.transactions.list(
125
- ticker="AAPL", # filter by ticker — accepts up to 25 comma-separated symbols
126
- cik="0000320193", # or by company CIK
127
- insider_cik="...", # filter by insider CIK
128
- code="P", # transaction code: P=purchase, S=sale, A=grant, etc.
129
- from_date="2026-01-01",
130
- to_date="2026-12-31",
131
- filed_from="2026-01-01", # filters on Filing.FiledAt (when it hit EDGAR), not the trade date
132
- filed_to="2026-12-31",
133
- exclude_10b5=True, # omit trades filed under a Rule 10b5-1 plan
134
- per_page=100,
135
- page=1,
136
- )
137
-
138
- # Multi-ticker lookup in one call
139
- client.transactions.list(ticker="AAPL,MSFT,NVDA")
140
- ```
141
-
142
- ### Granular filtering (v0.4.0+)
143
-
144
- ```python
145
- # The "just show me real buys & sells" preset: open-market only,
146
- # no 10b5-1 plan trades, no derivatives.
147
- client.transactions.list(ticker="AAPL", significant=True)
148
-
149
- # Multi-code include / exclude (comma-separated SEC codes)
150
- client.transactions.list(codes="P,S")
151
- client.transactions.list(exclude_codes="A,M,F,G")
152
-
153
- # Whole-category filters: open_market | grants | derivatives | gifts | other
154
- client.transactions.list(category="open_market")
155
- client.transactions.list(exclude_category="derivatives")
156
- client.transactions.list(exclude_derivative=True)
157
-
158
- # Trade-size screening (Pro plan or higher)
159
- client.transactions.list(min_value=1_000_000) # USD, shares x price
160
- client.transactions.list(min_shares=10_000, max_shares=100_000)
161
- ```
162
-
163
- ### Transaction fields
164
-
165
- | Field | Type | Description |
166
- |-------|------|-------------|
167
- | `ticker` | `str` | Stock ticker |
168
- | `company_name` | `str` | Company name |
169
- | `insider_name` | `str` | Insider full name |
170
- | `insider_cik` | `str` | Insider CIK |
171
- | `insider_title` | `str \| None` | Officer title as reported on the Form 4 |
172
- | `is_director` | `bool` | Director flag |
173
- | `is_officer` | `bool` | Officer flag |
174
- | `is10_pct_owner` | `bool` | 10% owner flag |
175
- | `accession_number` | `str` | SEC accession number |
176
- | `security_title` | `str` | Security type |
177
- | `transaction_code` | `str` | Transaction code |
178
- | `is_open_market` | `bool` | `True` when code is P or S (not grants/awards) |
179
- | `is10b5_plan` | `bool` | Filed under a Rule 10b5-1 pre-scheduled trading plan |
180
- | `shares_amount` | `float` | Shares transacted |
181
- | `price_per_share` | `float \| None` | Price per share |
182
- | `total_value` | `float \| None` | `shares_amount × price_per_share` in USD |
183
- | `shares_owned_after` | `float \| None` | Holdings after transaction |
184
- | `direct_indirect` | `str \| None` | "D" (direct) or "I" (indirect) |
185
- | `is_derivative` | `bool` | Derivative security flag |
186
- | `transaction_date` | `str` | ISO datetime |
187
- | `period_of_report` | `str` | ISO datetime |
188
- | `return1d`/`return1w`/`return1m`/`return3m`/`return6m` | `float \| None` | Forward return since the transaction date, as a fraction (0.109 = +10.9%); `None` until enough time/price data has elapsed |
189
- | `value_quality` | `str \| None` | Confidence flag on `total_value`'s pricing source |
190
- | `accepted_at` | `str \| None` | Precise UTC SEC-acceptance instant; `None` if not captured |
191
- | `document_url` | `str \| None` | Public SEC document URL |
192
- | `institutional_ownership` | `InstitutionalOwnership \| None` | Latest 13F institutional ownership snapshot for the ticker, when available |
193
-
194
- ### Company fields
195
-
196
- | Field | Type | Description |
197
- |-------|------|-------------|
198
- | `cik` | `str` | SEC CIK |
199
- | `name` | `str` | Company name |
200
- | `ticker` | `str \| None` | Stock ticker |
201
- | `exchange` | `str \| None` | Exchange |
202
- | `total_filings` | `int` | Total Form 4 filings |
203
- | `active_insiders` | `int` | Distinct insiders who have filed |
204
- | `sic_description` | `str \| None` | SEC SIC industry description |
205
- | `state_of_incorporation` | `str \| None` | Two-letter state code |
206
- | `website` | `str \| None` | Company website as filed with SEC |
207
-
208
- ### Search
209
-
210
- `client.search(q, limit=None)` looks up companies and insiders by name or
211
- ticker in a single call — useful for a combined search box where you don't
212
- know in advance whether the user typed a ticker or a person's name. `q` must
213
- be 2-64 characters; outside that range the API returns HTTP 400 and the SDK
214
- raises `Form4ApiError` with `error_code` `"QUERY_TOO_SHORT"` or
215
- `"QUERY_TOO_LONG"`. Insider matching is whitespace-tokenised, so `"tim cook"`
216
- matches an insider named `"Cook Timothy D"`. `limit` caps the number of
217
- results per array (1-20, API default 8).
218
-
219
- ```python
220
- result = client.search("tim cook")
221
- for c in result.companies:
222
- print(c.ticker, c.name, c.cik)
223
- for i in result.insiders:
224
- # i.ticker is a ticker associated with the insider and may be None —
225
- # its exact meaning is still settling on the backend.
226
- print(i.cik, i.name, i.title, i.ticker)
227
- ```
228
-
229
- ### Pagination
230
-
231
- `paginate()` pages through the data until it runs out (a short or empty page)
232
- or, since the backend's 2026-08-01 plan-gated pagination depth (Free: 20
233
- pages, Starter: 100, Pro+: unlimited), the next page is rejected with a 402.
234
- That 402 is not swallowed — it raises `PaginationLimitError` mid-iteration,
235
- after every page already yielded has been delivered to your loop. Pages you
236
- already received are real and complete; the error only means iteration
237
- stopped early. Pass `max_pages` to stop deliberately before that ever
238
- happens, or catch `PaginationLimitError` to know when a Free/Starter key ran
239
- out of depth on a bulk pull:
240
-
241
- ```python
242
- from form4api import PaginationLimitError
243
-
244
- # transactions.paginate() — yields one list per page automatically
245
- all_txns = []
246
- try:
247
- for batch in client.transactions.paginate(ticker="NVDA", exclude_10b5=True, per_page=100):
248
- all_txns.extend(batch)
249
- except PaginationLimitError as e:
250
- print(f"Stopped after {e.pages_yielded} pages — {e}")
251
- # all_txns still holds every page yielded before the limit hit
252
-
253
- # signals.paginate()
254
- all_signals = []
255
- for batch in client.signals.paginate(cluster_buy=True, per_page=100, max_pages=10):
256
- all_signals.extend(batch)
257
- ```
258
-
259
- ## Error handling
260
-
261
- ```python
262
- from form4api import (
263
- Form4ApiClient,
264
- AuthError,
265
- PlanError,
266
- PaginationLimitError,
267
- RateLimitError,
268
- NotFoundError,
269
- )
270
-
271
- client = Form4ApiClient("YOUR_API_KEY")
272
-
273
- try:
274
- signals = client.signals.list()
275
- except PaginationLimitError as e:
276
- print(f"Paginate stopped after {e.pages_yielded} pages — upgrade to go deeper")
277
- except PlanError as e:
278
- print(f"Upgrade required")
279
- except RateLimitError as e:
280
- print(f"Retry after {e.retry_after}s")
281
- except AuthError:
282
- print("Invalid API key")
283
- except NotFoundError:
284
- print("Resource not found")
285
- ```
286
-
287
- ## License
288
-
289
- MIT
1
+ Metadata-Version: 2.4
2
+ Name: form4api
3
+ Version: 0.11.0
4
+ Summary: Python client for the Form4API — real-time SEC Form 4 insider trading, Form 144 intent-to-sell, institutional 13F-HR, and congressional STOCK Act trading data, including the insider/Congress convergence signal
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://www.form4api.com
7
+ Project-URL: Documentation, https://www.form4api.com/docs
8
+ Project-URL: Repository, https://github.com/theodor90/form4api-py.git
9
+ Keywords: insider-trading,sec,sec-edgar,edgar,form-4,form4,form-144,13f,13f-hr,congress,stock-act,financial-data,stock-market,stocks,fintech,api,sdk,webhooks,form4api
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: httpx>=0.27
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8; extra == "dev"
16
+ Requires-Dist: pytest-httpx>=0.30; extra == "dev"
17
+ Requires-Dist: respx>=0.21; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # form4api
21
+
22
+ Python client for [Form4API](https://www.form4api.com) — real-time SEC Form 4 insider trading, Form 144 intent-to-sell, institutional 13F-HR, and congressional STOCK Act trading data, including the insider/Congress convergence signal.
23
+
24
+ [![PyPI version](https://img.shields.io/pypi/v/form4api.svg)](https://pypi.org/project/form4api/)
25
+ [![PyPI downloads](https://img.shields.io/pypi/dm/form4api.svg)](https://pypi.org/project/form4api/)
26
+ [![Python versions](https://img.shields.io/pypi/pyversions/form4api.svg)](https://pypi.org/project/form4api/)
27
+ [![license](https://img.shields.io/pypi/l/form4api.svg)](https://github.com/theodor90/form4api-py/blob/main/LICENSE)
28
+
29
+ Supports Python 3.11+. Uses `httpx` for both sync and async HTTP.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install form4api
35
+ ```
36
+
37
+ ## Sync quickstart
38
+
39
+ ```python
40
+ from form4api import Form4ApiClient
41
+
42
+ client = Form4ApiClient("YOUR_API_KEY")
43
+
44
+ # Recent open-market purchases at Apple (excluding 10b5-1 plan trades)
45
+ txns = client.transactions.list(ticker="AAPL", code="P", exclude_10b5=True, per_page=5)
46
+ for t in txns:
47
+ print(t.insider_name, t.insider_title, t.shares_amount, "@", t.price_per_share)
48
+ print(f" open market: {t.is_open_market}, 10b5 plan: {t.is10b5_plan}, value: ${t.total_value:,.0f}")
49
+
50
+ # Company overview (includes SIC, state, website)
51
+ company = client.companies.get("MSFT")
52
+ print(company.name, company.active_insiders, "active insiders")
53
+ print(company.sic_description, company.state_of_incorporation)
54
+
55
+ # Insider detail
56
+ insider = client.insiders.get("0001234567")
57
+ print(insider.name, insider.officer_title)
58
+
59
+ # Cluster-buy signals (Business plan)
60
+ signals = client.signals.list(cluster_buy=True)
61
+ for sig in signals:
62
+ print(sig.company_name, sig.insider_count, "buyers on", sig.signal_date)
63
+ ```
64
+
65
+ ## Async quickstart
66
+
67
+ ```python
68
+ import asyncio
69
+ from form4api import AsyncForm4ApiClient
70
+
71
+ async def main():
72
+ async with AsyncForm4ApiClient("YOUR_API_KEY") as client:
73
+ txns = await client.transactions.list(ticker="AAPL", per_page=5)
74
+ for t in txns:
75
+ print(t.insider_name, t.shares_amount, "@", t.price_per_share)
76
+
77
+ asyncio.run(main())
78
+ ```
79
+
80
+ ## Resources
81
+
82
+ | Resource | Methods |
83
+ |---|---|
84
+ | `client.search(q, limit=None)` | Combined company + insider lookup — not namespaced under a resource |
85
+ | `client.transactions` | `.list(**params)`, `.paginate(**params)` |
86
+ | `client.insiders` | `.search(name, **params)`, `.get(cik)`, `.list(**params)`, `.directory(**params)`, `.transactions(cik, **params)`, `.summary(cik)` *(Pro)*, `.scorecard(cik)` *(Pro)*, `.leaderboard(**params)` *(Business)* |
87
+ | `client.companies` | `.get(ticker)`, `.insiders(ticker)`, `.list(**params)` |
88
+ | `client.signals` | `.list(**params)`, `.paginate(**params)`, `.explain(ticker)`, `.sentiment(ticker, **params)` — Business; `.convergence(**params)` — Pro |
89
+ | `client.congress` | `.trades(**params)`, `.politicians(**params)` *(Pro)*, `.politician(id_or_slug)` *(Pro)*, `.ticker(ticker)` *(Pro)* |
90
+ | `client.form144` | `.list(**params)` — Business plan |
91
+ | `client.holdings` | `.list(**params)`, `.managers(**params)` — Business plan |
92
+ | `client.ownership` | `.schedule_13dg(**params)` — Business plan (Schedule 13D/13G >5% ownership crossings; filters `ticker`, `form_type`, `is_amendment`, `page`, `per_page`) |
93
+ | `client.filings` | `.list(**params)`, `.recent(**params)`, `.get(accession_number)` |
94
+ | `client.stats` | `.get()` — public, no key required |
95
+ | `client.data_quality` | `.get()` — public, no key required |
96
+ | `client.status` | `.history()` — takes no parameters |
97
+ | `client.webhooks` | `.create(url, event_types, auth_header=None)`, `.list()`, `.update(id, url)`, `.delete(id)`, `.events(**params)`, `.enable(id)`, `.test(id)`, `.set_auth_header(id, name, value)`, `.delete_auth_header(id)` |
98
+ | `verify_webhook(body, signature, secret)` | Top-level function (not on the client) that checks the `X-Insider-Signature` header on a delivery |
99
+
100
+ **Congress date quality.** Each congress trade (and convergence leg) has
101
+ `disclosure_lag_days: int | None` and `date_quality: str | None`. When the
102
+ filing's own dates are impossible or implausible, the lag is `None` and
103
+ `date_quality` is one of `"transaction_after_disclosure"`,
104
+ `"future_transaction_date"` or `"implausible_lag"`; the raw dates are still
105
+ returned. A lag over 45 days is not a legal finding.
106
+
107
+ ```python
108
+ for t in client.congress.trades(ticker="NVDA"):
109
+ lag = f"dates unreliable ({t.date_quality})" if t.disclosure_lag_days is None else f"{t.disclosure_lag_days}d"
110
+ print(t.politician.full_name, t.transaction_date, lag)
111
+ ```
112
+
113
+ Every endpoint that returns typed JSON has a method here, including every
114
+ plan-gated one. The exceptions are the CSV exports, the account/billing
115
+ bootstrap calls and the usage endpoints, which have no typed response and are
116
+ not wrapped. Calling one
117
+ your key isn't entitled to raises `PlanError` (HTTP 402) carrying
118
+ `required_plan`, `current_plan`, and `upgrade_url` rather than failing opaquely.
119
+
120
+ An `AsyncForm4ApiClient` mirrors the whole surface with the same resources and
121
+ method names — `await client.insiders.leaderboard()`.
122
+
123
+ For the full parameter reference see the [REST docs](https://form4api.com/docs).
124
+ For LLM workflows, `form4api-mcp` exposes the same endpoints as tools.
125
+
126
+ ### Transaction filters
127
+
128
+ ```python
129
+ client.transactions.list(
130
+ ticker="AAPL", # filter by ticker — accepts up to 25 comma-separated symbols
131
+ cik="0000320193", # or by company CIK
132
+ insider_cik="...", # filter by insider CIK
133
+ code="P", # transaction code: P=purchase, S=sale, A=grant, etc.
134
+ from_date="2026-01-01",
135
+ to_date="2026-12-31",
136
+ filed_from="2026-01-01", # filters on Filing.FiledAt (when it hit EDGAR), not the trade date
137
+ filed_to="2026-12-31",
138
+ exclude_10b5=True, # omit trades filed under a Rule 10b5-1 plan
139
+ per_page=100,
140
+ page=1,
141
+ )
142
+
143
+ # Multi-ticker lookup in one call
144
+ client.transactions.list(ticker="AAPL,MSFT,NVDA")
145
+ ```
146
+
147
+ ### Granular filtering (v0.4.0+)
148
+
149
+ ```python
150
+ # The "just show me real buys & sells" preset: open-market only,
151
+ # no 10b5-1 plan trades, no derivatives.
152
+ client.transactions.list(ticker="AAPL", significant=True)
153
+
154
+ # Multi-code include / exclude (comma-separated SEC codes)
155
+ client.transactions.list(codes="P,S")
156
+ client.transactions.list(exclude_codes="A,M,F,G")
157
+
158
+ # Whole-category filters: open_market | grants | derivatives | gifts | other
159
+ client.transactions.list(category="open_market")
160
+ client.transactions.list(exclude_category="derivatives")
161
+ client.transactions.list(exclude_derivative=True)
162
+
163
+ # Trade-size screening
164
+ client.transactions.list(min_value=1_000_000) # USD, shares x price — every plan
165
+ client.transactions.list(max_value=5_000_000) # Pro+
166
+ client.transactions.list(min_shares=10_000, max_shares=100_000) # Pro+
167
+
168
+ # Post-trade return screening (Pro+). Returns are FRACTIONS: 0.2 = +20%.
169
+ # Horizons: 1d, 1w, 1m, 3m, 6m. Rows with no computed return are excluded.
170
+ client.transactions.list(min_return_3m=0.10, max_return_3m=0.50)
171
+ client.transactions.list(has_returns=True) # only rows with all five horizons computed
172
+
173
+ # Other filters and ordering
174
+ client.transactions.list(inst_ownership_trend="increasing") # increasing | decreasing | stable (13F)
175
+ client.transactions.list(role="officer") # officer | director | ten_percent_owner
176
+ client.transactions.list(sort="value") # "date" (default) or "value" (largest first)
177
+ ```
178
+
179
+ `paginate()` accepts the same filters.
180
+
181
+ ### Transaction fields
182
+
183
+ | Field | Type | Description |
184
+ |-------|------|-------------|
185
+ | `ticker` | `str` | Stock ticker |
186
+ | `company_name` | `str` | Company name |
187
+ | `insider_name` | `str` | Insider full name |
188
+ | `insider_cik` | `str` | Insider CIK |
189
+ | `insider_title` | `str \| None` | Officer title as reported on the Form 4 |
190
+ | `is_director` | `bool` | Director flag |
191
+ | `is_officer` | `bool` | Officer flag |
192
+ | `is10_pct_owner` | `bool` | 10% owner flag |
193
+ | `accession_number` | `str` | SEC accession number |
194
+ | `security_title` | `str` | Security type |
195
+ | `transaction_code` | `str` | Transaction code |
196
+ | `is_open_market` | `bool` | `True` when code is P or S (not grants/awards) |
197
+ | `is10b5_plan` | `bool` | Filed under a Rule 10b5-1 pre-scheduled trading plan |
198
+ | `shares_amount` | `float` | Shares transacted |
199
+ | `price_per_share` | `float \| None` | Price per share |
200
+ | `total_value` | `float \| None` | `shares_amount × price_per_share` in USD |
201
+ | `shares_owned_after` | `float \| None` | Holdings after transaction |
202
+ | `direct_indirect` | `str \| None` | "D" (direct) or "I" (indirect) |
203
+ | `is_derivative` | `bool` | Derivative security flag |
204
+ | `transaction_date` | `str` | ISO datetime |
205
+ | `period_of_report` | `str` | ISO datetime |
206
+ | `return1d`/`return1w`/`return1m`/`return3m`/`return6m` | `float \| None` | Forward return since the transaction date, as a fraction (0.109 = +10.9%); `None` until enough time/price data has elapsed |
207
+ | `value_quality` | `str \| None` | Confidence flag on `total_value`'s pricing source |
208
+ | `filed_at` | `str \| None` | When the filing became public on EDGAR |
209
+ | `accepted_at` | `str \| None` | Precise UTC SEC-acceptance instant; `None` if not captured |
210
+ | `document_url` | `str \| None` | Public SEC document URL |
211
+ | `institutional_ownership` | `InstitutionalOwnership \| None` | Latest 13F institutional ownership snapshot for the ticker, when available |
212
+
213
+ ### Company fields
214
+
215
+ | Field | Type | Description |
216
+ |-------|------|-------------|
217
+ | `cik` | `str` | SEC CIK |
218
+ | `name` | `str` | Company name |
219
+ | `ticker` | `str \| None` | Stock ticker |
220
+ | `exchange` | `str \| None` | Exchange |
221
+ | `total_filings` | `int` | Total Form 4 filings |
222
+ | `active_insiders` | `int` | Distinct insiders who have filed |
223
+ | `sic_description` | `str \| None` | SEC SIC industry description |
224
+ | `state_of_incorporation` | `str \| None` | Two-letter state code |
225
+ | `website` | `str \| None` | Company website as filed with SEC |
226
+
227
+ ### Webhooks
228
+
229
+ ```python
230
+ sub = client.webhooks.create(
231
+ "https://example.com/hook",
232
+ ["TransactionFiled", "ClusterBuy"],
233
+ auth_header={"name": "Authorization", "value": "Bearer ..."}, # optional, sent with every delivery
234
+ )
235
+ print(sub.secret) # shown once; store it
236
+
237
+ client.webhooks.update(sub.subscription_id, "https://example.com/new-hook") # change the URL; returns WebhookSubscription incl. url_warning
238
+ client.webhooks.test(sub.subscription_id) # WebhookTestResult(delivered, status_code, duration_ms, error)
239
+ client.webhooks.enable(sub.subscription_id) # undo an auto-disable (disabled_reason == "delivery_failures")
240
+ client.webhooks.set_auth_header(sub.subscription_id, "Authorization", "Bearer new")
241
+ client.webhooks.delete_auth_header(sub.subscription_id) # returns None (204, no body)
242
+ ```
243
+
244
+ **Event types** (exact names):
245
+
246
+ | Event | Plan |
247
+ |---|---|
248
+ | `TransactionFiled` | Free |
249
+ | `ClusterBuy` | Free |
250
+ | `ClusterSell` | Free |
251
+ | `CongressTradeFiled` | Starter+ |
252
+ | `ConvergenceSignal` | Pro+ |
253
+
254
+ Payloads are compact PascalCase references with no envelope (the body is the
255
+ bare object), for example `{"FilingId": ..., "AccessionNumber": ...}` for
256
+ `TransactionFiled`. The event name is in the `X-Event-Type` header, not the
257
+ body. `CongressTradeFiled` and `ConvergenceSignal` carry the full event body.
258
+
259
+ `webhooks.list()` returns delivery health on each `WebhookSubscription`:
260
+ `is_active`, `disabled_at`, `disabled_reason`, `last_attempt_at`,
261
+ `last_success_at`, `last_status_code`, `last_error_class`, `is_read_only` and
262
+ `auth_header_name`, all `None` when not applicable. The auth header value is
263
+ write-only and never returned. `webhooks.test()` reports a failed send as
264
+ `delivered=False` with an `error`, it does not raise.
265
+
266
+ **Moving a webhook to a new URL.** `update(id, url)` changes the delivery URL
267
+ and keeps the id, secret, event types and auth header. It does not re-enable a
268
+ subscription the system disabled, so call `enable(id)` afterwards if needed.
269
+ The `last_*` health fields come back `None` because they described the old URL.
270
+
271
+ ```python
272
+ # Started on a tunnel, now moving to a stable endpoint
273
+ moved = client.webhooks.update(sub.subscription_id, "https://example.com/hooks/form4api")
274
+ if not moved.is_active:
275
+ client.webhooks.enable(moved.subscription_id)
276
+ ```
277
+
278
+ **`url_warning`.** `create()`, `update()` and each `list()` item carry
279
+ `url_warning`: a string when the URL's host is a temporary tunnel or test host
280
+ (trycloudflare, ngrok, webhook.site, loca.lt, serveo), otherwise `None`. It is a
281
+ warning only; the subscription is saved either way, and deliveries stop when the
282
+ tunnel closes. URLs over 500 characters are rejected with 400 `INVALID_URL`.
283
+
284
+ **Verifying deliveries.** Each delivery carries
285
+ `X-Insider-Signature: sha256=<hex HMAC-SHA256(secret, raw body)>`. Verify it
286
+ against the raw request body, before parsing JSON:
287
+
288
+ ```python
289
+ from form4api import verify_webhook
290
+
291
+ body = request.get_data() # raw bytes, e.g. Flask
292
+ sig = request.headers["X-Insider-Signature"]
293
+ if not verify_webhook(body, sig, WEBHOOK_SECRET): # str or bytes; constant-time compare
294
+ abort(401)
295
+ ```
296
+
297
+ ### Search
298
+
299
+ `client.search(q, limit=None)` looks up companies and insiders by name or
300
+ ticker in a single call — useful for a combined search box where you don't
301
+ know in advance whether the user typed a ticker or a person's name. `q` must
302
+ be 2-64 characters; outside that range the API returns HTTP 400 and the SDK
303
+ raises `Form4ApiError` with `error_code` `"QUERY_TOO_SHORT"` or
304
+ `"QUERY_TOO_LONG"`. Insider matching is whitespace-tokenised, so `"tim cook"`
305
+ matches an insider named `"Cook Timothy D"`. `limit` caps the number of
306
+ results per array (1-20, API default 8).
307
+
308
+ ```python
309
+ result = client.search("tim cook")
310
+ for c in result.companies:
311
+ print(c.ticker, c.name, c.cik)
312
+ for i in result.insiders:
313
+ # i.ticker is a ticker associated with the insider and may be None —
314
+ # its exact meaning is still settling on the backend.
315
+ print(i.cik, i.name, i.title, i.ticker)
316
+ ```
317
+
318
+ ### Pagination
319
+
320
+ `paginate()` pages through the data until it runs out (a short or empty page)
321
+ or, since the backend's 2026-08-01 plan-gated pagination depth (Free: 20
322
+ pages, Starter: 100, Pro+: unlimited), the next page is rejected with a 402.
323
+ That 402 is not swallowed — it raises `PaginationLimitError` mid-iteration,
324
+ after every page already yielded has been delivered to your loop. Pages you
325
+ already received are real and complete; the error only means iteration
326
+ stopped early. Pass `max_pages` to stop deliberately before that ever
327
+ happens, or catch `PaginationLimitError` to know when a Free/Starter key ran
328
+ out of depth on a bulk pull:
329
+
330
+ ```python
331
+ from form4api import PaginationLimitError
332
+
333
+ # transactions.paginate() — yields one list per page automatically
334
+ all_txns = []
335
+ try:
336
+ for batch in client.transactions.paginate(ticker="NVDA", exclude_10b5=True, per_page=100):
337
+ all_txns.extend(batch)
338
+ except PaginationLimitError as e:
339
+ print(f"Stopped after {e.pages_yielded} pages — {e}")
340
+ # all_txns still holds every page yielded before the limit hit
341
+
342
+ # signals.paginate()
343
+ all_signals = []
344
+ for batch in client.signals.paginate(cluster_buy=True, per_page=100, max_pages=10):
345
+ all_signals.extend(batch)
346
+ ```
347
+
348
+ ## Error handling
349
+
350
+ ```python
351
+ from form4api import (
352
+ Form4ApiClient,
353
+ AuthError,
354
+ PlanError,
355
+ PaginationLimitError,
356
+ RateLimitError,
357
+ NotFoundError,
358
+ )
359
+
360
+ client = Form4ApiClient("YOUR_API_KEY")
361
+
362
+ try:
363
+ signals = client.signals.list()
364
+ except PaginationLimitError as e:
365
+ print(f"Paginate stopped after {e.pages_yielded} pages — upgrade to go deeper")
366
+ except PlanError as e:
367
+ print(f"Upgrade required")
368
+ except RateLimitError as e:
369
+ print(f"Retry after {e.retry_after}s")
370
+ except AuthError:
371
+ print("Invalid API key")
372
+ except NotFoundError:
373
+ print("Resource not found")
374
+ ```
375
+
376
+ ## License
377
+
378
+ MIT