form4api 0.8.0__tar.gz → 0.10.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 (28) hide show
  1. {form4api-0.8.0 → form4api-0.10.0}/PKG-INFO +359 -254
  2. {form4api-0.8.0 → form4api-0.10.0}/README.md +340 -235
  3. {form4api-0.8.0 → form4api-0.10.0}/form4api/__init__.py +52 -42
  4. {form4api-0.8.0 → form4api-0.10.0}/form4api/_client.py +331 -294
  5. {form4api-0.8.0 → form4api-0.10.0}/form4api/_errors.py +105 -105
  6. {form4api-0.8.0 → form4api-0.10.0}/form4api/_generated.py +2248 -2107
  7. {form4api-0.8.0 → form4api-0.10.0}/form4api/_types.py +258 -157
  8. {form4api-0.8.0 → form4api-0.10.0}/form4api/_webhook_utils.py +8 -4
  9. {form4api-0.8.0 → form4api-0.10.0}/form4api/resources/__init__.py +3 -3
  10. {form4api-0.8.0 → form4api-0.10.0}/form4api/resources/_companies.py +51 -51
  11. {form4api-0.8.0 → form4api-0.10.0}/form4api/resources/_insiders.py +90 -90
  12. {form4api-0.8.0 → form4api-0.10.0}/form4api/resources/_signals.py +159 -159
  13. {form4api-0.8.0 → form4api-0.10.0}/form4api/resources/_transactions.py +444 -299
  14. form4api-0.10.0/form4api/resources/_webhooks.py +135 -0
  15. {form4api-0.8.0 → form4api-0.10.0}/form4api.egg-info/PKG-INFO +359 -254
  16. {form4api-0.8.0 → form4api-0.10.0}/form4api.egg-info/SOURCES.txt +3 -1
  17. {form4api-0.8.0 → form4api-0.10.0}/pyproject.toml +28 -28
  18. {form4api-0.8.0 → form4api-0.10.0}/setup.cfg +4 -4
  19. {form4api-0.8.0 → form4api-0.10.0}/tests/test_client.py +677 -550
  20. {form4api-0.8.0 → form4api-0.10.0}/tests/test_generated.py +480 -433
  21. {form4api-0.8.0 → form4api-0.10.0}/tests/test_method_name.py +88 -88
  22. form4api-0.10.0/tests/test_release_0_10_0.py +222 -0
  23. form4api-0.10.0/tests/test_unknown_fields.py +257 -0
  24. form4api-0.8.0/form4api/resources/_webhooks.py +0 -56
  25. {form4api-0.8.0 → form4api-0.10.0}/LICENSE +0 -0
  26. {form4api-0.8.0 → form4api-0.10.0}/form4api.egg-info/dependency_links.txt +0 -0
  27. {form4api-0.8.0 → form4api-0.10.0}/form4api.egg-info/requires.txt +0 -0
  28. {form4api-0.8.0 → form4api-0.10.0}/form4api.egg-info/top_level.txt +0 -0
@@ -1,254 +1,359 @@
1
- Metadata-Version: 2.4
2
- Name: form4api
3
- Version: 0.8.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.transactions` | `.list(**params)`, `.paginate(**params)` |
85
- | `client.insiders` | `.search(name, **params)`, `.get(cik)`, `.list(**params)`, `.directory(**params)`, `.transactions(cik, **params)`, `.summary(cik)` *(Pro)*, `.scorecard(cik)` *(Pro)*, `.leaderboard(**params)` *(Business)* |
86
- | `client.companies` | `.get(ticker)`, `.insiders(ticker)`, `.list(**params)` |
87
- | `client.signals` | `.list(**params)`, `.paginate(**params)`, `.explain(ticker)`, `.sentiment(ticker, **params)` — Business; `.convergence(**params)` — Pro |
88
- | `client.congress` | `.trades(**params)`, `.politicians(**params)` *(Pro)*, `.politician(id_or_slug)` *(Pro)*, `.ticker(ticker)` *(Pro)* |
89
- | `client.form144` | `.list(**params)` — Business plan |
90
- | `client.holdings` | `.list(**params)`, `.managers(**params)` — Business plan |
91
- | `client.filings` | `.list(**params)`, `.recent(**params)`, `.get(accession_number)` |
92
- | `client.stats` | `.get()` — public, no key required |
93
- | `client.data_quality` | `.get()` — public, no key required |
94
- | `client.status` | `.history(**params)` |
95
- | `client.webhooks` | `.create(url, event_types)`, `.list()`, `.delete(id)`, `.events(**params)` |
96
-
97
- Every plan-gated endpoint the API exposes has a typed method here. Calling one
98
- your key isn't entitled to raises `PlanError` (HTTP 402) carrying
99
- `required_plan`, `current_plan`, and `upgrade_url` rather than failing opaquely.
100
-
101
- An `AsyncForm4ApiClient` mirrors the whole surface with the same resources and
102
- method names — `await client.insiders.leaderboard()`.
103
-
104
- For the full parameter reference see the [REST docs](https://form4api.com/docs).
105
- For LLM workflows, `form4api-mcp` exposes the same endpoints as tools.
106
-
107
- ### Transaction filters
108
-
109
- ```python
110
- client.transactions.list(
111
- ticker="AAPL", # filter by ticker — accepts up to 25 comma-separated symbols
112
- cik="0000320193", # or by company CIK
113
- insider_cik="...", # filter by insider CIK
114
- code="P", # transaction code: P=purchase, S=sale, A=grant, etc.
115
- from_date="2026-01-01",
116
- to_date="2026-12-31",
117
- filed_from="2026-01-01", # filters on Filing.FiledAt (when it hit EDGAR), not the trade date
118
- filed_to="2026-12-31",
119
- exclude_10b5=True, # omit trades filed under a Rule 10b5-1 plan
120
- per_page=100,
121
- page=1,
122
- )
123
-
124
- # Multi-ticker lookup in one call
125
- client.transactions.list(ticker="AAPL,MSFT,NVDA")
126
- ```
127
-
128
- ### Granular filtering (v0.4.0+)
129
-
130
- ```python
131
- # The "just show me real buys & sells" preset: open-market only,
132
- # no 10b5-1 plan trades, no derivatives.
133
- client.transactions.list(ticker="AAPL", significant=True)
134
-
135
- # Multi-code include / exclude (comma-separated SEC codes)
136
- client.transactions.list(codes="P,S")
137
- client.transactions.list(exclude_codes="A,M,F,G")
138
-
139
- # Whole-category filters: open_market | grants | derivatives | gifts | other
140
- client.transactions.list(category="open_market")
141
- client.transactions.list(exclude_category="derivatives")
142
- client.transactions.list(exclude_derivative=True)
143
-
144
- # Trade-size screening (Pro plan or higher)
145
- client.transactions.list(min_value=1_000_000) # USD, shares x price
146
- client.transactions.list(min_shares=10_000, max_shares=100_000)
147
- ```
148
-
149
- ### Transaction fields
150
-
151
- | Field | Type | Description |
152
- |-------|------|-------------|
153
- | `ticker` | `str` | Stock ticker |
154
- | `company_name` | `str` | Company name |
155
- | `insider_name` | `str` | Insider full name |
156
- | `insider_cik` | `str` | Insider CIK |
157
- | `insider_title` | `str \| None` | Officer title as reported on the Form 4 |
158
- | `is_director` | `bool` | Director flag |
159
- | `is_officer` | `bool` | Officer flag |
160
- | `is10_pct_owner` | `bool` | 10% owner flag |
161
- | `accession_number` | `str` | SEC accession number |
162
- | `security_title` | `str` | Security type |
163
- | `transaction_code` | `str` | Transaction code |
164
- | `is_open_market` | `bool` | `True` when code is P or S (not grants/awards) |
165
- | `is10b5_plan` | `bool` | Filed under a Rule 10b5-1 pre-scheduled trading plan |
166
- | `shares_amount` | `float` | Shares transacted |
167
- | `price_per_share` | `float \| None` | Price per share |
168
- | `total_value` | `float \| None` | `shares_amount × price_per_share` in USD |
169
- | `shares_owned_after` | `float \| None` | Holdings after transaction |
170
- | `direct_indirect` | `str \| None` | "D" (direct) or "I" (indirect) |
171
- | `is_derivative` | `bool` | Derivative security flag |
172
- | `transaction_date` | `str` | ISO datetime |
173
- | `period_of_report` | `str` | ISO datetime |
174
- | `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 |
175
- | `value_quality` | `str \| None` | Confidence flag on `total_value`'s pricing source |
176
- | `accepted_at` | `str \| None` | Precise UTC SEC-acceptance instant; `None` if not captured |
177
- | `document_url` | `str \| None` | Public SEC document URL |
178
- | `institutional_ownership` | `InstitutionalOwnership \| None` | Latest 13F institutional ownership snapshot for the ticker, when available |
179
-
180
- ### Company fields
181
-
182
- | Field | Type | Description |
183
- |-------|------|-------------|
184
- | `cik` | `str` | SEC CIK |
185
- | `name` | `str` | Company name |
186
- | `ticker` | `str \| None` | Stock ticker |
187
- | `exchange` | `str \| None` | Exchange |
188
- | `total_filings` | `int` | Total Form 4 filings |
189
- | `active_insiders` | `int` | Distinct insiders who have filed |
190
- | `sic_description` | `str \| None` | SEC SIC industry description |
191
- | `state_of_incorporation` | `str \| None` | Two-letter state code |
192
- | `website` | `str \| None` | Company website as filed with SEC |
193
-
194
- ### Pagination
195
-
196
- `paginate()` pages through the data until it runs out (a short or empty page)
197
- or, since the backend's 2026-08-01 plan-gated pagination depth (Free: 20
198
- pages, Starter: 100, Pro+: unlimited), the next page is rejected with a 402.
199
- That 402 is not swallowed — it raises `PaginationLimitError` mid-iteration,
200
- after every page already yielded has been delivered to your loop. Pages you
201
- already received are real and complete; the error only means iteration
202
- stopped early. Pass `max_pages` to stop deliberately before that ever
203
- happens, or catch `PaginationLimitError` to know when a Free/Starter key ran
204
- out of depth on a bulk pull:
205
-
206
- ```python
207
- from form4api import PaginationLimitError
208
-
209
- # transactions.paginate() — yields one list per page automatically
210
- all_txns = []
211
- try:
212
- for batch in client.transactions.paginate(ticker="NVDA", exclude_10b5=True, per_page=100):
213
- all_txns.extend(batch)
214
- except PaginationLimitError as e:
215
- print(f"Stopped after {e.pages_yielded} pages — {e}")
216
- # all_txns still holds every page yielded before the limit hit
217
-
218
- # signals.paginate()
219
- all_signals = []
220
- for batch in client.signals.paginate(cluster_buy=True, per_page=100, max_pages=10):
221
- all_signals.extend(batch)
222
- ```
223
-
224
- ## Error handling
225
-
226
- ```python
227
- from form4api import (
228
- Form4ApiClient,
229
- AuthError,
230
- PlanError,
231
- PaginationLimitError,
232
- RateLimitError,
233
- NotFoundError,
234
- )
235
-
236
- client = Form4ApiClient("YOUR_API_KEY")
237
-
238
- try:
239
- signals = client.signals.list()
240
- except PaginationLimitError as e:
241
- print(f"Paginate stopped after {e.pages_yielded} pages — upgrade to go deeper")
242
- except PlanError as e:
243
- print(f"Upgrade required")
244
- except RateLimitError as e:
245
- print(f"Retry after {e.retry_after}s")
246
- except AuthError:
247
- print("Invalid API key")
248
- except NotFoundError:
249
- print("Resource not found")
250
- ```
251
-
252
- ## License
253
-
254
- MIT
1
+ Metadata-Version: 2.4
2
+ Name: form4api
3
+ Version: 0.10.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()`, `.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.test(sub.subscription_id) # WebhookTestResult(delivered, status_code, duration_ms, error)
238
+ client.webhooks.enable(sub.subscription_id) # undo an auto-disable (disabled_reason == "delivery_failures")
239
+ client.webhooks.set_auth_header(sub.subscription_id, "Authorization", "Bearer new")
240
+ client.webhooks.delete_auth_header(sub.subscription_id) # returns None (204, no body)
241
+ ```
242
+
243
+ **Event types** (exact names):
244
+
245
+ | Event | Plan |
246
+ |---|---|
247
+ | `TransactionFiled` | Free |
248
+ | `ClusterBuy` | Free |
249
+ | `ClusterSell` | Free |
250
+ | `CongressTradeFiled` | Starter+ |
251
+ | `ConvergenceSignal` | Pro+ |
252
+
253
+ Payloads are compact PascalCase references with no envelope (the body is the
254
+ bare object), for example `{"FilingId": ..., "AccessionNumber": ...}` for
255
+ `TransactionFiled`. The event name is in the `X-Event-Type` header, not the
256
+ body. `CongressTradeFiled` and `ConvergenceSignal` carry the full event body.
257
+
258
+ `webhooks.list()` returns delivery health on each `WebhookSubscription`:
259
+ `is_active`, `disabled_at`, `disabled_reason`, `last_attempt_at`,
260
+ `last_success_at`, `last_status_code`, `last_error_class`, `is_read_only` and
261
+ `auth_header_name`, all `None` when not applicable. The auth header value is
262
+ write-only and never returned. `webhooks.test()` reports a failed send as
263
+ `delivered=False` with an `error`, it does not raise.
264
+
265
+ **Verifying deliveries.** Each delivery carries
266
+ `X-Insider-Signature: sha256=<hex HMAC-SHA256(secret, raw body)>`. Verify it
267
+ against the raw request body, before parsing JSON:
268
+
269
+ ```python
270
+ from form4api import verify_webhook
271
+
272
+ body = request.get_data() # raw bytes, e.g. Flask
273
+ sig = request.headers["X-Insider-Signature"]
274
+ if not verify_webhook(body, sig, WEBHOOK_SECRET): # str or bytes; constant-time compare
275
+ abort(401)
276
+ ```
277
+
278
+ ### Search
279
+
280
+ `client.search(q, limit=None)` looks up companies and insiders by name or
281
+ ticker in a single call — useful for a combined search box where you don't
282
+ know in advance whether the user typed a ticker or a person's name. `q` must
283
+ be 2-64 characters; outside that range the API returns HTTP 400 and the SDK
284
+ raises `Form4ApiError` with `error_code` `"QUERY_TOO_SHORT"` or
285
+ `"QUERY_TOO_LONG"`. Insider matching is whitespace-tokenised, so `"tim cook"`
286
+ matches an insider named `"Cook Timothy D"`. `limit` caps the number of
287
+ results per array (1-20, API default 8).
288
+
289
+ ```python
290
+ result = client.search("tim cook")
291
+ for c in result.companies:
292
+ print(c.ticker, c.name, c.cik)
293
+ for i in result.insiders:
294
+ # i.ticker is a ticker associated with the insider and may be None —
295
+ # its exact meaning is still settling on the backend.
296
+ print(i.cik, i.name, i.title, i.ticker)
297
+ ```
298
+
299
+ ### Pagination
300
+
301
+ `paginate()` pages through the data until it runs out (a short or empty page)
302
+ or, since the backend's 2026-08-01 plan-gated pagination depth (Free: 20
303
+ pages, Starter: 100, Pro+: unlimited), the next page is rejected with a 402.
304
+ That 402 is not swallowed — it raises `PaginationLimitError` mid-iteration,
305
+ after every page already yielded has been delivered to your loop. Pages you
306
+ already received are real and complete; the error only means iteration
307
+ stopped early. Pass `max_pages` to stop deliberately before that ever
308
+ happens, or catch `PaginationLimitError` to know when a Free/Starter key ran
309
+ out of depth on a bulk pull:
310
+
311
+ ```python
312
+ from form4api import PaginationLimitError
313
+
314
+ # transactions.paginate() — yields one list per page automatically
315
+ all_txns = []
316
+ try:
317
+ for batch in client.transactions.paginate(ticker="NVDA", exclude_10b5=True, per_page=100):
318
+ all_txns.extend(batch)
319
+ except PaginationLimitError as e:
320
+ print(f"Stopped after {e.pages_yielded} pages — {e}")
321
+ # all_txns still holds every page yielded before the limit hit
322
+
323
+ # signals.paginate()
324
+ all_signals = []
325
+ for batch in client.signals.paginate(cluster_buy=True, per_page=100, max_pages=10):
326
+ all_signals.extend(batch)
327
+ ```
328
+
329
+ ## Error handling
330
+
331
+ ```python
332
+ from form4api import (
333
+ Form4ApiClient,
334
+ AuthError,
335
+ PlanError,
336
+ PaginationLimitError,
337
+ RateLimitError,
338
+ NotFoundError,
339
+ )
340
+
341
+ client = Form4ApiClient("YOUR_API_KEY")
342
+
343
+ try:
344
+ signals = client.signals.list()
345
+ except PaginationLimitError as e:
346
+ print(f"Paginate stopped after {e.pages_yielded} pages — upgrade to go deeper")
347
+ except PlanError as e:
348
+ print(f"Upgrade required")
349
+ except RateLimitError as e:
350
+ print(f"Retry after {e.retry_after}s")
351
+ except AuthError:
352
+ print("Invalid API key")
353
+ except NotFoundError:
354
+ print("Resource not found")
355
+ ```
356
+
357
+ ## License
358
+
359
+ MIT