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.
- {form4api-0.8.0 → form4api-0.10.0}/PKG-INFO +359 -254
- {form4api-0.8.0 → form4api-0.10.0}/README.md +340 -235
- {form4api-0.8.0 → form4api-0.10.0}/form4api/__init__.py +52 -42
- {form4api-0.8.0 → form4api-0.10.0}/form4api/_client.py +331 -294
- {form4api-0.8.0 → form4api-0.10.0}/form4api/_errors.py +105 -105
- {form4api-0.8.0 → form4api-0.10.0}/form4api/_generated.py +2248 -2107
- {form4api-0.8.0 → form4api-0.10.0}/form4api/_types.py +258 -157
- {form4api-0.8.0 → form4api-0.10.0}/form4api/_webhook_utils.py +8 -4
- {form4api-0.8.0 → form4api-0.10.0}/form4api/resources/__init__.py +3 -3
- {form4api-0.8.0 → form4api-0.10.0}/form4api/resources/_companies.py +51 -51
- {form4api-0.8.0 → form4api-0.10.0}/form4api/resources/_insiders.py +90 -90
- {form4api-0.8.0 → form4api-0.10.0}/form4api/resources/_signals.py +159 -159
- {form4api-0.8.0 → form4api-0.10.0}/form4api/resources/_transactions.py +444 -299
- form4api-0.10.0/form4api/resources/_webhooks.py +135 -0
- {form4api-0.8.0 → form4api-0.10.0}/form4api.egg-info/PKG-INFO +359 -254
- {form4api-0.8.0 → form4api-0.10.0}/form4api.egg-info/SOURCES.txt +3 -1
- {form4api-0.8.0 → form4api-0.10.0}/pyproject.toml +28 -28
- {form4api-0.8.0 → form4api-0.10.0}/setup.cfg +4 -4
- {form4api-0.8.0 → form4api-0.10.0}/tests/test_client.py +677 -550
- {form4api-0.8.0 → form4api-0.10.0}/tests/test_generated.py +480 -433
- {form4api-0.8.0 → form4api-0.10.0}/tests/test_method_name.py +88 -88
- form4api-0.10.0/tests/test_release_0_10_0.py +222 -0
- form4api-0.10.0/tests/test_unknown_fields.py +257 -0
- form4api-0.8.0/form4api/resources/_webhooks.py +0 -56
- {form4api-0.8.0 → form4api-0.10.0}/LICENSE +0 -0
- {form4api-0.8.0 → form4api-0.10.0}/form4api.egg-info/dependency_links.txt +0 -0
- {form4api-0.8.0 → form4api-0.10.0}/form4api.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
-
[](https://pypi.org/project/form4api/)
|
|
25
|
-
[](https://pypi.org/project/form4api/)
|
|
26
|
-
[](https://pypi.org/project/form4api/)
|
|
27
|
-
[](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.
|
|
85
|
-
| `client.
|
|
86
|
-
| `client.
|
|
87
|
-
| `client.
|
|
88
|
-
| `client.
|
|
89
|
-
| `client.
|
|
90
|
-
| `client.
|
|
91
|
-
| `client.
|
|
92
|
-
| `client.
|
|
93
|
-
| `client.
|
|
94
|
-
| `client.
|
|
95
|
-
| `client.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
`
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
[](https://pypi.org/project/form4api/)
|
|
25
|
+
[](https://pypi.org/project/form4api/)
|
|
26
|
+
[](https://pypi.org/project/form4api/)
|
|
27
|
+
[](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
|