prometiam-risk-api 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prometiam_risk_api-0.1.0/LICENSE +21 -0
- prometiam_risk_api-0.1.0/PKG-INFO +126 -0
- prometiam_risk_api-0.1.0/README.md +107 -0
- prometiam_risk_api-0.1.0/prometiam_risk_api/__init__.py +23 -0
- prometiam_risk_api-0.1.0/prometiam_risk_api/client.py +245 -0
- prometiam_risk_api-0.1.0/prometiam_risk_api/errors.py +82 -0
- prometiam_risk_api-0.1.0/prometiam_risk_api.egg-info/PKG-INFO +126 -0
- prometiam_risk_api-0.1.0/prometiam_risk_api.egg-info/SOURCES.txt +12 -0
- prometiam_risk_api-0.1.0/prometiam_risk_api.egg-info/dependency_links.txt +1 -0
- prometiam_risk_api-0.1.0/prometiam_risk_api.egg-info/requires.txt +1 -0
- prometiam_risk_api-0.1.0/prometiam_risk_api.egg-info/top_level.txt +1 -0
- prometiam_risk_api-0.1.0/pyproject.toml +27 -0
- prometiam_risk_api-0.1.0/setup.cfg +4 -0
- prometiam_risk_api-0.1.0/tests/test_client.py +122 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Prometiam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prometiam-risk-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the Prometiam Risk Intelligence API: EU company registry data (ES, FR, GB, IE, PL, NO, FI, SE, HR, BE, DK), corporate insolvency across twelve markets, sanctions screening, VAT and LEI lookups.
|
|
5
|
+
Author-email: Prometiam <support@prometiam.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://www.prometiam.com/risk-api/docs
|
|
8
|
+
Project-URL: Documentation, https://www.prometiam.com/risk-api/docs
|
|
9
|
+
Keywords: prometiam,kyb,company-registry,sanctions-screening,insolvency,borme,bodacc,companies-house
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: httpx>=0.25
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# prometiam-risk-api (Python)
|
|
21
|
+
|
|
22
|
+
Official Python client for the [Prometiam Risk Intelligence API](https://www.prometiam.com/risk-api/docs): company registry data for Spain, France, the UK, Ireland, Poland, Norway, Finland, Sweden, Croatia, Belgium and Denmark, corporate insolvency notices across twelve markets, sanctions screening, VAT and LEI lookups.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install prometiam-risk-api
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import os
|
|
32
|
+
from prometiam_risk_api import Client
|
|
33
|
+
|
|
34
|
+
api = Client(api_key=os.environ["PROMETIAM_API_KEY"])
|
|
35
|
+
|
|
36
|
+
page = api.companies_search(name="Inditex", country="ES") # fuzzy, best match first (match_score 0-100)
|
|
37
|
+
first = page["data"][0]
|
|
38
|
+
print(first["company_name"], first["company_number"])
|
|
39
|
+
|
|
40
|
+
detail = api.company(first["id"], include="risk_flags,insolvency")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every method returns the API envelope `{"data": ..., "pagination": ..., "meta": ...}` exactly as documented.
|
|
44
|
+
|
|
45
|
+
## Batch calls
|
|
46
|
+
|
|
47
|
+
One HTTP call for a whole list. Every item counts as one request against your plan limits; a batch that does not fit raises `RateLimitError` whose `max_items_now` says how many items would fit.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
lookup = api.companies_lookup([
|
|
51
|
+
{"country": "ES", "company_number": "A46103834"},
|
|
52
|
+
{"country": "FR", "siren": "552032534"},
|
|
53
|
+
{"country": "ES", "name": "Inditex"}, # best fuzzy match, with match_score
|
|
54
|
+
])
|
|
55
|
+
for r in lookup["data"]:
|
|
56
|
+
print(r["status"], (r["company"] or {}).get("company_name"))
|
|
57
|
+
|
|
58
|
+
screen = api.sanctions_screen_batch(["Vladimir Putin", "Acme Trading Ltd"], group="entity")
|
|
59
|
+
flagged = [r for r in screen["data"] if r["status"] == "match"]
|
|
60
|
+
|
|
61
|
+
distress = api.insolvency_check([
|
|
62
|
+
{"country": "FR", "company_number": "917499519"},
|
|
63
|
+
{"country": "DE", "name": "Wirecard"},
|
|
64
|
+
])
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Pagination
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
for page in api.paginate("/people/search", {"name": "garcia", "country": "ES", "limit": 50}):
|
|
71
|
+
for person in page:
|
|
72
|
+
print(person["full_name"])
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Errors
|
|
76
|
+
|
|
77
|
+
| Error | When |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `AuthError` | 401, or 403 for a country the key cannot access |
|
|
80
|
+
| `UpgradeRequiredError` | 403 `upgrade_required`: scope not in your plan or the free trial allowance is used up. Carries `included_in`, `price`, `upgrade_url`, `trial_allowance` |
|
|
81
|
+
| `RateLimitError` | 429, retried automatically up to `max_retries` (default 2) honouring `Retry-After`; carries `retry_after` and `max_items_now` |
|
|
82
|
+
| `ValidationError` | 400 |
|
|
83
|
+
| `NotFoundError` | 404 |
|
|
84
|
+
| `PrometiamError` | anything else; `status`, `code`, `body` on every error |
|
|
85
|
+
|
|
86
|
+
## Options
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
Client(api_key, base_url="https://api.prometiam.com/functions/v1/risk-api", timeout=30.0, max_retries=2, transport=None)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use `with Client(...) as api:` to close the connection pool, and `api.request("GET", "/coverage")` for anything not wrapped.
|
|
93
|
+
|
|
94
|
+
## Rate limits
|
|
95
|
+
|
|
96
|
+
Per-key limits by plan (requests/minute, requests/day, requests/month):
|
|
97
|
+
|
|
98
|
+
| Plan | Price | RPM | Daily | Monthly |
|
|
99
|
+
|---|---|---|---|---|
|
|
100
|
+
| Free (14-day trial) | €0 | 10 | 200 | 1,000 |
|
|
101
|
+
| Starter | €9.99/mo | 60 | 2,000 | 10,000 |
|
|
102
|
+
| Professional | €29.99/mo | 300 | 20,000 | 100,000 |
|
|
103
|
+
| Scale | €99.99/mo | 600 | 200,000 | 1,000,000 |
|
|
104
|
+
| Enterprise | Custom | Custom | Custom | Custom |
|
|
105
|
+
|
|
106
|
+
`api.account()` returns your key's current limits and usage (`rate_limit.rpm_remaining`, `daily_remaining`, `monthly_remaining`). A 429 raises `RateLimitError`, retried automatically up to `max_retries` honouring `Retry-After`. Batch calls (`companies_lookup`, `sanctions_screen_batch`, `insolvency_check`) count every item in the list as one request against these limits; a batch that would not fit in the remaining quota raises `RateLimitError` up front (`code == "batch_exceeds_quota"`, `max_items_now` set).
|
|
107
|
+
|
|
108
|
+
## Scope limits
|
|
109
|
+
|
|
110
|
+
The API's coverage has real edges — every method above reflects them, not a marketing claim:
|
|
111
|
+
|
|
112
|
+
- **Company registry (`companies_search`, `company`, `people_search`, `events_*`, `directors_network`):** Spain, France, the UK, Ireland, Poland, Norway, Finland, Sweden, Croatia, Belgium, Denmark. Ireland and Poland are company-level today (no officers yet). Finland, Sweden, Croatia, Belgium and Denmark are company records only, with register-change events (name, status, legal form and registered address; share capital for Croatia; dated when the change first appears in the register data, not gazette notices, none before 2026-09-30) and monitoring, but no officers and no registry-compliance signal. Corporate insolvency notices are linked by business ID (Finland), organisationsnummer (Sweden), MBS (Croatia) and CVR number (Denmark); Belgium has none (its bankruptcy shows in the company status and in its register-change events). Sole traders (enskild näringsverksamhet, trgovac pojedinac), enterprises of natural persons (eenmanszaak / entreprise individuelle), sole proprietorships (enkeltmandsvirksomhed) and estates are never served. Denmark has no share capital, and its status includes the register's bankruptcy state.
|
|
113
|
+
- **Insolvency (`insolvency_search`, `insolvency_check`):** corporate only, twelve markets (FR, DE, GB, AT, CH, NO, FI, US, NL, DK, HR, SE). Personal/consumer insolvency is never returned. Spain's concursal data is on `records_search` (`GET /search`), not `insolvency_search`.
|
|
114
|
+
- **Monitoring (`monitors`, `monitor`, `monitor_create`, `monitor_delete`):** ES, IE, PL, FI, SE, BE, HR, DK; alerts on status changes, dissolutions, sanctions matches and corporate events (ES and FI/SE/BE/HR/DK).
|
|
115
|
+
- **Sanctions screening (`sanctions_screen*`):** beta.
|
|
116
|
+
- **PEP hits (`sanctions_screen(name, include_pep=True)`):** beta, Spain only.
|
|
117
|
+
- **Norway:** no corporate-event stream (`events_*` returns empty for NO, and for IE and PL).
|
|
118
|
+
- **Finland:** company records only (`people_search` returns empty for FI; `events_*` returns register-change events, dated when first seen in the register data and none before 2026-09-30; `monitor_create` accepts it). Its insolvency notices are linked by business ID.
|
|
119
|
+
- **Sweden:** company records only (`people_search` returns empty for SE; `events_*` returns register-change events; `monitor_create` accepts it). Corporate insolvency notices (konkurs, företagsrekonstruktion, ackordsförhandling; updated weekly, no court, case number or source link, proceedings that ended before collection not included) are on `insolvency`, linked by organisationsnummer.
|
|
120
|
+
- **Croatia:** company records only (`people_search` returns empty for HR; `events_*` returns register-change events, including share capital; `monitor_create` accepts it). Corporate insolvency notices (court decisions on bankruptcy, shortened bankruptcy and pre-bankruptcy proceedings, with court and case number where the register states them; only companies still on the register) are linked by MBS. A Croatian company is found by MBS (`company_number`), OIB (`oib`) or the VAT alias (`vat=HR` plus the OIB).
|
|
121
|
+
- **Belgium:** company records only (`people_search` returns empty for BE; `events_*` returns register-change events; `monitor_create` accepts it). Belgium is not one of the twelve insolvency markets: its bankruptcy shows in the company status and in its register-change events. A Belgian company is found by enterprise number (`company_number`, the dots and spaces optional) or the VAT alias (`vat=BE` plus the ten digits).
|
|
122
|
+
- **Denmark:** company records only (`people_search` returns empty for DK; `events_*` returns register-change events; `monitor_create` accepts it). Corporate insolvency notices (konkurs and tvangsakkord decisions from CVR credit information, dated by the decision, no court or case number) are linked by CVR number. Contains CVR data (Erhvervsstyrelsen), CC BY 4.0. A Danish company is found by CVR number (`company_number`, spaces and hyphens optional; the mod-11 check is advisory) or the VAT alias (`vat=DK` plus the eight digits).
|
|
123
|
+
|
|
124
|
+
## Get a key
|
|
125
|
+
|
|
126
|
+
Free at https://www.prometiam.com/signup, no card required. Docs: https://www.prometiam.com/risk-api/docs. Support: support@prometiam.com.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# prometiam-risk-api (Python)
|
|
2
|
+
|
|
3
|
+
Official Python client for the [Prometiam Risk Intelligence API](https://www.prometiam.com/risk-api/docs): company registry data for Spain, France, the UK, Ireland, Poland, Norway, Finland, Sweden, Croatia, Belgium and Denmark, corporate insolvency notices across twelve markets, sanctions screening, VAT and LEI lookups.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install prometiam-risk-api
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import os
|
|
13
|
+
from prometiam_risk_api import Client
|
|
14
|
+
|
|
15
|
+
api = Client(api_key=os.environ["PROMETIAM_API_KEY"])
|
|
16
|
+
|
|
17
|
+
page = api.companies_search(name="Inditex", country="ES") # fuzzy, best match first (match_score 0-100)
|
|
18
|
+
first = page["data"][0]
|
|
19
|
+
print(first["company_name"], first["company_number"])
|
|
20
|
+
|
|
21
|
+
detail = api.company(first["id"], include="risk_flags,insolvency")
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Every method returns the API envelope `{"data": ..., "pagination": ..., "meta": ...}` exactly as documented.
|
|
25
|
+
|
|
26
|
+
## Batch calls
|
|
27
|
+
|
|
28
|
+
One HTTP call for a whole list. Every item counts as one request against your plan limits; a batch that does not fit raises `RateLimitError` whose `max_items_now` says how many items would fit.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
lookup = api.companies_lookup([
|
|
32
|
+
{"country": "ES", "company_number": "A46103834"},
|
|
33
|
+
{"country": "FR", "siren": "552032534"},
|
|
34
|
+
{"country": "ES", "name": "Inditex"}, # best fuzzy match, with match_score
|
|
35
|
+
])
|
|
36
|
+
for r in lookup["data"]:
|
|
37
|
+
print(r["status"], (r["company"] or {}).get("company_name"))
|
|
38
|
+
|
|
39
|
+
screen = api.sanctions_screen_batch(["Vladimir Putin", "Acme Trading Ltd"], group="entity")
|
|
40
|
+
flagged = [r for r in screen["data"] if r["status"] == "match"]
|
|
41
|
+
|
|
42
|
+
distress = api.insolvency_check([
|
|
43
|
+
{"country": "FR", "company_number": "917499519"},
|
|
44
|
+
{"country": "DE", "name": "Wirecard"},
|
|
45
|
+
])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Pagination
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
for page in api.paginate("/people/search", {"name": "garcia", "country": "ES", "limit": 50}):
|
|
52
|
+
for person in page:
|
|
53
|
+
print(person["full_name"])
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Errors
|
|
57
|
+
|
|
58
|
+
| Error | When |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `AuthError` | 401, or 403 for a country the key cannot access |
|
|
61
|
+
| `UpgradeRequiredError` | 403 `upgrade_required`: scope not in your plan or the free trial allowance is used up. Carries `included_in`, `price`, `upgrade_url`, `trial_allowance` |
|
|
62
|
+
| `RateLimitError` | 429, retried automatically up to `max_retries` (default 2) honouring `Retry-After`; carries `retry_after` and `max_items_now` |
|
|
63
|
+
| `ValidationError` | 400 |
|
|
64
|
+
| `NotFoundError` | 404 |
|
|
65
|
+
| `PrometiamError` | anything else; `status`, `code`, `body` on every error |
|
|
66
|
+
|
|
67
|
+
## Options
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
Client(api_key, base_url="https://api.prometiam.com/functions/v1/risk-api", timeout=30.0, max_retries=2, transport=None)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Use `with Client(...) as api:` to close the connection pool, and `api.request("GET", "/coverage")` for anything not wrapped.
|
|
74
|
+
|
|
75
|
+
## Rate limits
|
|
76
|
+
|
|
77
|
+
Per-key limits by plan (requests/minute, requests/day, requests/month):
|
|
78
|
+
|
|
79
|
+
| Plan | Price | RPM | Daily | Monthly |
|
|
80
|
+
|---|---|---|---|---|
|
|
81
|
+
| Free (14-day trial) | €0 | 10 | 200 | 1,000 |
|
|
82
|
+
| Starter | €9.99/mo | 60 | 2,000 | 10,000 |
|
|
83
|
+
| Professional | €29.99/mo | 300 | 20,000 | 100,000 |
|
|
84
|
+
| Scale | €99.99/mo | 600 | 200,000 | 1,000,000 |
|
|
85
|
+
| Enterprise | Custom | Custom | Custom | Custom |
|
|
86
|
+
|
|
87
|
+
`api.account()` returns your key's current limits and usage (`rate_limit.rpm_remaining`, `daily_remaining`, `monthly_remaining`). A 429 raises `RateLimitError`, retried automatically up to `max_retries` honouring `Retry-After`. Batch calls (`companies_lookup`, `sanctions_screen_batch`, `insolvency_check`) count every item in the list as one request against these limits; a batch that would not fit in the remaining quota raises `RateLimitError` up front (`code == "batch_exceeds_quota"`, `max_items_now` set).
|
|
88
|
+
|
|
89
|
+
## Scope limits
|
|
90
|
+
|
|
91
|
+
The API's coverage has real edges — every method above reflects them, not a marketing claim:
|
|
92
|
+
|
|
93
|
+
- **Company registry (`companies_search`, `company`, `people_search`, `events_*`, `directors_network`):** Spain, France, the UK, Ireland, Poland, Norway, Finland, Sweden, Croatia, Belgium, Denmark. Ireland and Poland are company-level today (no officers yet). Finland, Sweden, Croatia, Belgium and Denmark are company records only, with register-change events (name, status, legal form and registered address; share capital for Croatia; dated when the change first appears in the register data, not gazette notices, none before 2026-09-30) and monitoring, but no officers and no registry-compliance signal. Corporate insolvency notices are linked by business ID (Finland), organisationsnummer (Sweden), MBS (Croatia) and CVR number (Denmark); Belgium has none (its bankruptcy shows in the company status and in its register-change events). Sole traders (enskild näringsverksamhet, trgovac pojedinac), enterprises of natural persons (eenmanszaak / entreprise individuelle), sole proprietorships (enkeltmandsvirksomhed) and estates are never served. Denmark has no share capital, and its status includes the register's bankruptcy state.
|
|
94
|
+
- **Insolvency (`insolvency_search`, `insolvency_check`):** corporate only, twelve markets (FR, DE, GB, AT, CH, NO, FI, US, NL, DK, HR, SE). Personal/consumer insolvency is never returned. Spain's concursal data is on `records_search` (`GET /search`), not `insolvency_search`.
|
|
95
|
+
- **Monitoring (`monitors`, `monitor`, `monitor_create`, `monitor_delete`):** ES, IE, PL, FI, SE, BE, HR, DK; alerts on status changes, dissolutions, sanctions matches and corporate events (ES and FI/SE/BE/HR/DK).
|
|
96
|
+
- **Sanctions screening (`sanctions_screen*`):** beta.
|
|
97
|
+
- **PEP hits (`sanctions_screen(name, include_pep=True)`):** beta, Spain only.
|
|
98
|
+
- **Norway:** no corporate-event stream (`events_*` returns empty for NO, and for IE and PL).
|
|
99
|
+
- **Finland:** company records only (`people_search` returns empty for FI; `events_*` returns register-change events, dated when first seen in the register data and none before 2026-09-30; `monitor_create` accepts it). Its insolvency notices are linked by business ID.
|
|
100
|
+
- **Sweden:** company records only (`people_search` returns empty for SE; `events_*` returns register-change events; `monitor_create` accepts it). Corporate insolvency notices (konkurs, företagsrekonstruktion, ackordsförhandling; updated weekly, no court, case number or source link, proceedings that ended before collection not included) are on `insolvency`, linked by organisationsnummer.
|
|
101
|
+
- **Croatia:** company records only (`people_search` returns empty for HR; `events_*` returns register-change events, including share capital; `monitor_create` accepts it). Corporate insolvency notices (court decisions on bankruptcy, shortened bankruptcy and pre-bankruptcy proceedings, with court and case number where the register states them; only companies still on the register) are linked by MBS. A Croatian company is found by MBS (`company_number`), OIB (`oib`) or the VAT alias (`vat=HR` plus the OIB).
|
|
102
|
+
- **Belgium:** company records only (`people_search` returns empty for BE; `events_*` returns register-change events; `monitor_create` accepts it). Belgium is not one of the twelve insolvency markets: its bankruptcy shows in the company status and in its register-change events. A Belgian company is found by enterprise number (`company_number`, the dots and spaces optional) or the VAT alias (`vat=BE` plus the ten digits).
|
|
103
|
+
- **Denmark:** company records only (`people_search` returns empty for DK; `events_*` returns register-change events; `monitor_create` accepts it). Corporate insolvency notices (konkurs and tvangsakkord decisions from CVR credit information, dated by the decision, no court or case number) are linked by CVR number. Contains CVR data (Erhvervsstyrelsen), CC BY 4.0. A Danish company is found by CVR number (`company_number`, spaces and hyphens optional; the mod-11 check is advisory) or the VAT alias (`vat=DK` plus the eight digits).
|
|
104
|
+
|
|
105
|
+
## Get a key
|
|
106
|
+
|
|
107
|
+
Free at https://www.prometiam.com/signup, no card required. Docs: https://www.prometiam.com/risk-api/docs. Support: support@prometiam.com.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Official Python client for the Prometiam Risk Intelligence API."""
|
|
2
|
+
from .client import DEFAULT_BASE_URL, SDK_VERSION, Client
|
|
3
|
+
from .errors import (
|
|
4
|
+
AuthError,
|
|
5
|
+
NotFoundError,
|
|
6
|
+
PrometiamError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
UpgradeRequiredError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Client",
|
|
14
|
+
"DEFAULT_BASE_URL",
|
|
15
|
+
"SDK_VERSION",
|
|
16
|
+
"PrometiamError",
|
|
17
|
+
"AuthError",
|
|
18
|
+
"ValidationError",
|
|
19
|
+
"NotFoundError",
|
|
20
|
+
"RateLimitError",
|
|
21
|
+
"UpgradeRequiredError",
|
|
22
|
+
]
|
|
23
|
+
__version__ = SDK_VERSION
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Prometiam Risk Intelligence API client.
|
|
2
|
+
|
|
3
|
+
from prometiam_risk_api import Client
|
|
4
|
+
api = Client(api_key=os.environ["PROMETIAM_API_KEY"])
|
|
5
|
+
page = api.companies_search(name="Inditex", country="ES")
|
|
6
|
+
print(page["data"][0]["company_name"])
|
|
7
|
+
|
|
8
|
+
Every method returns the API envelope ``{"data": ..., "pagination": ..., "meta": ...}`` untouched, so the
|
|
9
|
+
fields documented at https://www.prometiam.com/risk-api/docs are exactly what you get. Failures raise typed
|
|
10
|
+
errors (see errors.py); 429 and 5xx responses are retried automatically, honouring Retry-After.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import time
|
|
16
|
+
from typing import Any, Dict, Iterable, Iterator, List, Optional, Union
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
from .errors import RateLimitError, error_from_response
|
|
21
|
+
|
|
22
|
+
DEFAULT_BASE_URL = "https://api.prometiam.com/functions/v1/risk-api"
|
|
23
|
+
SDK_VERSION = "0.1.0"
|
|
24
|
+
|
|
25
|
+
Params = Dict[str, Any]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Client:
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
api_key: str,
|
|
32
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
33
|
+
timeout: float = 30.0,
|
|
34
|
+
max_retries: int = 2,
|
|
35
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
36
|
+
):
|
|
37
|
+
if not api_key:
|
|
38
|
+
raise ValueError("api_key is required (rk_live_...)")
|
|
39
|
+
self.base_url = base_url.rstrip("/")
|
|
40
|
+
self.max_retries = max_retries
|
|
41
|
+
self._http = httpx.Client(
|
|
42
|
+
base_url=self.base_url,
|
|
43
|
+
timeout=timeout,
|
|
44
|
+
transport=transport,
|
|
45
|
+
headers={
|
|
46
|
+
"Authorization": f"Bearer {api_key}",
|
|
47
|
+
"Accept": "application/json",
|
|
48
|
+
"User-Agent": f"prometiam-risk-api-python/{SDK_VERSION}",
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# ── plumbing ─────────────────────────────────────────────────────────
|
|
53
|
+
def close(self) -> None:
|
|
54
|
+
self._http.close()
|
|
55
|
+
|
|
56
|
+
def __enter__(self) -> "Client":
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def __exit__(self, *exc: Any) -> None:
|
|
60
|
+
self.close()
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _clean(params: Optional[Params]) -> Params:
|
|
64
|
+
return {k: v for k, v in (params or {}).items() if v is not None and v != ""}
|
|
65
|
+
|
|
66
|
+
def request(
|
|
67
|
+
self,
|
|
68
|
+
method: str,
|
|
69
|
+
path: str,
|
|
70
|
+
params: Optional[Params] = None,
|
|
71
|
+
json_body: Any = None,
|
|
72
|
+
idempotency_key: Optional[str] = None,
|
|
73
|
+
) -> Dict[str, Any]:
|
|
74
|
+
"""Low-level call. Sends the key, retries 429/5xx, raises typed errors.
|
|
75
|
+
|
|
76
|
+
``idempotency_key`` (batch POST endpoints only) is sent as the ``Idempotency-Key`` header:
|
|
77
|
+
a replay of the same key with the same request within 24h returns the stored response with
|
|
78
|
+
``Idempotent-Replayed: true`` and costs no quota. See docs #idempotency-keys.
|
|
79
|
+
"""
|
|
80
|
+
attempt = 0
|
|
81
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
82
|
+
while True:
|
|
83
|
+
res = self._http.request(method, path, params=self._clean(params), json=json_body, headers=headers)
|
|
84
|
+
text = res.text
|
|
85
|
+
try:
|
|
86
|
+
body = json.loads(text) if text else None
|
|
87
|
+
except ValueError:
|
|
88
|
+
body = None
|
|
89
|
+
if res.is_success:
|
|
90
|
+
return body if isinstance(body, dict) else {"data": body}
|
|
91
|
+
err = error_from_response(res.status_code, res.headers.get("retry-after"), body, text)
|
|
92
|
+
retryable = res.status_code == 429 or res.status_code >= 500
|
|
93
|
+
if retryable and attempt < self.max_retries:
|
|
94
|
+
attempt += 1
|
|
95
|
+
wait = err.retry_after if isinstance(err, RateLimitError) else 0.5 * (2 ** attempt)
|
|
96
|
+
time.sleep(min(wait, 60.0))
|
|
97
|
+
continue
|
|
98
|
+
raise err
|
|
99
|
+
|
|
100
|
+
def paginate(self, path: str, params: Optional[Params] = None, max_pages: Optional[int] = None) -> Iterator[List[Any]]:
|
|
101
|
+
"""Yield each page's ``data`` of a cursor-paginated GET endpoint."""
|
|
102
|
+
params = dict(params or {})
|
|
103
|
+
cursor = params.pop("cursor", None)
|
|
104
|
+
pages = 0
|
|
105
|
+
while True:
|
|
106
|
+
page = self.request("GET", path, params={**params, "cursor": cursor})
|
|
107
|
+
yield page.get("data") or []
|
|
108
|
+
pages += 1
|
|
109
|
+
pag = page.get("pagination") or {}
|
|
110
|
+
cursor = pag.get("next_cursor") if pag.get("has_more") else None
|
|
111
|
+
if not cursor or (max_pages is not None and pages >= max_pages):
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# ── companies ────────────────────────────────────────────────────────
|
|
115
|
+
def companies_search(self, **params: Any) -> Dict[str, Any]:
|
|
116
|
+
"""GET /companies/search: name (fuzzy, match_score), company_number/nif/siren/vat, founded_after, cnae, objeto..."""
|
|
117
|
+
return self.request("GET", "/companies/search", params)
|
|
118
|
+
|
|
119
|
+
def company(self, company_id: Union[int, str], **params: Any) -> Dict[str, Any]:
|
|
120
|
+
"""GET /companies/{id}: full detail; include=risk_flags,insolvency,procurement,lei or all."""
|
|
121
|
+
return self.request("GET", f"/companies/{company_id}", params)
|
|
122
|
+
|
|
123
|
+
def companies_lookup(self, items: Iterable[Dict[str, Any]], idempotency_key: Optional[str] = None) -> Dict[str, Any]:
|
|
124
|
+
"""POST /companies/lookup: up to 100 identifiers or names in one call; every item counts as one request.
|
|
125
|
+
``idempotency_key`` makes a retry of the exact same batch free and safe to repeat (docs #idempotency-keys)."""
|
|
126
|
+
return self.request("POST", "/companies/lookup", json_body={"items": list(items)}, idempotency_key=idempotency_key)
|
|
127
|
+
|
|
128
|
+
# ── people / events ──────────────────────────────────────────────────
|
|
129
|
+
def people_search(self, **params: Any) -> Dict[str, Any]:
|
|
130
|
+
return self.request("GET", "/people/search", params)
|
|
131
|
+
|
|
132
|
+
def person(self, person_id: Union[int, str], **params: Any) -> Dict[str, Any]:
|
|
133
|
+
return self.request("GET", f"/people/{person_id}", params)
|
|
134
|
+
|
|
135
|
+
def events_search(self, **params: Any) -> Dict[str, Any]:
|
|
136
|
+
return self.request("GET", "/company-events/search", params)
|
|
137
|
+
|
|
138
|
+
def events_timeline(self, **params: Any) -> Dict[str, Any]:
|
|
139
|
+
return self.request("GET", "/company-events/timeline", params)
|
|
140
|
+
|
|
141
|
+
def event(self, event_id: Union[int, str]) -> Dict[str, Any]:
|
|
142
|
+
return self.request("GET", f"/company-events/records/{event_id}")
|
|
143
|
+
|
|
144
|
+
# ── sanctions ────────────────────────────────────────────────────────
|
|
145
|
+
def sanctions_screen(self, name: str, **params: Any) -> Dict[str, Any]:
|
|
146
|
+
"""GET /sanctions/screen: fuzzy screen of one name; group='entity' folds hits per listed person or company."""
|
|
147
|
+
return self.request("GET", "/sanctions/screen", {"name": name, **params})
|
|
148
|
+
|
|
149
|
+
def sanctions_screen_batch(
|
|
150
|
+
self,
|
|
151
|
+
items: Iterable[Union[str, Dict[str, Any]]],
|
|
152
|
+
idempotency_key: Optional[str] = None,
|
|
153
|
+
**options: Any,
|
|
154
|
+
) -> Dict[str, Any]:
|
|
155
|
+
"""POST /sanctions/screen: up to 50 names in one call (sanctions scope; no trial allowance).
|
|
156
|
+
Options: threshold, list, active_only, group='entity', limit (hits per name).
|
|
157
|
+
``idempotency_key`` makes a retry of the exact same batch free and safe to repeat (docs #idempotency-keys)."""
|
|
158
|
+
norm = [{"name": i} if isinstance(i, str) else i for i in items]
|
|
159
|
+
return self.request("POST", "/sanctions/screen", json_body={**options, "items": norm}, idempotency_key=idempotency_key)
|
|
160
|
+
|
|
161
|
+
def sanctions_entity(self, entity_id: Union[int, str]) -> Dict[str, Any]:
|
|
162
|
+
return self.request("GET", f"/sanctions/entity/{entity_id}")
|
|
163
|
+
|
|
164
|
+
def sanctions_changes(self, **params: Any) -> Dict[str, Any]:
|
|
165
|
+
return self.request("GET", "/sanctions/changes", params)
|
|
166
|
+
|
|
167
|
+
def watchlist(self) -> Dict[str, Any]:
|
|
168
|
+
return self.request("GET", "/sanctions/watchlist")
|
|
169
|
+
|
|
170
|
+
def watchlist_add(self, **body: Any) -> Dict[str, Any]:
|
|
171
|
+
return self.request("POST", "/sanctions/watchlist", json_body=body)
|
|
172
|
+
|
|
173
|
+
def watchlist_remove(self, entry_id: Union[int, str]) -> Dict[str, Any]:
|
|
174
|
+
return self.request("DELETE", f"/sanctions/watchlist/{entry_id}")
|
|
175
|
+
|
|
176
|
+
# ── insolvency / notices ─────────────────────────────────────────────
|
|
177
|
+
def procurement_awards(self, **params: Any) -> Dict[str, Any]:
|
|
178
|
+
"""GET /procurement/awards: public-contract awards (ES, FR, GB, IE, PL, NO) by supplier, buyer, nif, cpv or date; one row per award to one supplier."""
|
|
179
|
+
return self.request("GET", "/procurement/awards", params)
|
|
180
|
+
|
|
181
|
+
def procurement_buyer(self, buyer_id: str, **params: Any) -> Dict[str, Any]:
|
|
182
|
+
"""GET /procurement/buyer/{id}: public-buyer risk profile (beta, ES + FR); scores about the contracting body."""
|
|
183
|
+
return self.request("GET", f"/procurement/buyer/{buyer_id}", params)
|
|
184
|
+
|
|
185
|
+
def procurement_buyers(self, **params: Any) -> Dict[str, Any]:
|
|
186
|
+
"""GET /procurement/buyers: scored public buyers ordered by score (beta, ES + FR)."""
|
|
187
|
+
return self.request("GET", "/procurement/buyers", params)
|
|
188
|
+
|
|
189
|
+
def procurement_relationship(self, **params: Any) -> Dict[str, Any]:
|
|
190
|
+
"""GET /procurement/relationship: a buyer and one of its five largest suppliers (beta, ES + FR)."""
|
|
191
|
+
return self.request("GET", "/procurement/relationship", params)
|
|
192
|
+
|
|
193
|
+
def insolvency_search(self, **params: Any) -> Dict[str, Any]:
|
|
194
|
+
"""GET /insolvency/search: corporate notices across FR, DE, GB, AT, CH, NO, FI, US, NL, DK, HR, SE."""
|
|
195
|
+
return self.request("GET", "/insolvency/search", params)
|
|
196
|
+
|
|
197
|
+
def insolvency_check(self, items: Iterable[Dict[str, Any]], idempotency_key: Optional[str] = None) -> Dict[str, Any]:
|
|
198
|
+
"""POST /insolvency/check: up to 100 counterparties in one call.
|
|
199
|
+
``idempotency_key`` makes a retry of the exact same batch free and safe to repeat (docs #idempotency-keys)."""
|
|
200
|
+
return self.request("POST", "/insolvency/check", json_body={"items": list(items)}, idempotency_key=idempotency_key)
|
|
201
|
+
|
|
202
|
+
def records_search(self, **params: Any) -> Dict[str, Any]:
|
|
203
|
+
"""GET /search: Spanish risk and concursal notices."""
|
|
204
|
+
return self.request("GET", "/search", params)
|
|
205
|
+
|
|
206
|
+
def record(self, record_id: Union[int, str]) -> Dict[str, Any]:
|
|
207
|
+
return self.request("GET", f"/records/{record_id}")
|
|
208
|
+
|
|
209
|
+
def notice(self, notice_id: Union[int, str], **params: Any) -> Dict[str, Any]:
|
|
210
|
+
return self.request("GET", f"/notices/{notice_id}", params)
|
|
211
|
+
|
|
212
|
+
# ── graph / registries ───────────────────────────────────────────────
|
|
213
|
+
def directors_network(self, **params: Any) -> Dict[str, Any]:
|
|
214
|
+
return self.request("GET", "/directors/network", params)
|
|
215
|
+
|
|
216
|
+
def vat(self, vat_number: str) -> Dict[str, Any]:
|
|
217
|
+
return self.request("GET", f"/vat/{vat_number}")
|
|
218
|
+
|
|
219
|
+
def lei(self, lei: str) -> Dict[str, Any]:
|
|
220
|
+
return self.request("GET", f"/lei/{lei}")
|
|
221
|
+
|
|
222
|
+
def lei_relationships(self, lei: str) -> Dict[str, Any]:
|
|
223
|
+
return self.request("GET", f"/lei/{lei}/relationships")
|
|
224
|
+
|
|
225
|
+
def lei_search(self, **params: Any) -> Dict[str, Any]:
|
|
226
|
+
return self.request("GET", "/lei/search", params)
|
|
227
|
+
|
|
228
|
+
# ── monitoring / account ─────────────────────────────────────────────
|
|
229
|
+
def monitors(self) -> Dict[str, Any]:
|
|
230
|
+
return self.request("GET", "/monitor")
|
|
231
|
+
|
|
232
|
+
def monitor(self, monitor_id: Union[int, str]) -> Dict[str, Any]:
|
|
233
|
+
return self.request("GET", f"/monitor/{monitor_id}")
|
|
234
|
+
|
|
235
|
+
def monitor_create(self, **body: Any) -> Dict[str, Any]:
|
|
236
|
+
return self.request("POST", "/monitor", json_body=body)
|
|
237
|
+
|
|
238
|
+
def monitor_delete(self, monitor_id: Union[int, str]) -> Dict[str, Any]:
|
|
239
|
+
return self.request("DELETE", f"/monitor/{monitor_id}")
|
|
240
|
+
|
|
241
|
+
def coverage(self) -> Dict[str, Any]:
|
|
242
|
+
return self.request("GET", "/coverage")
|
|
243
|
+
|
|
244
|
+
def account(self) -> Dict[str, Any]:
|
|
245
|
+
return self.request("GET", "/account")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Errors raised by the Prometiam client. Every one carries the HTTP status, the API's error code and the raw body."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PrometiamError(Exception):
|
|
8
|
+
def __init__(self, message: str, status: int, code: str, body: Any):
|
|
9
|
+
super().__init__(message)
|
|
10
|
+
self.message = message
|
|
11
|
+
self.status = status
|
|
12
|
+
self.code = code
|
|
13
|
+
self.body = body
|
|
14
|
+
|
|
15
|
+
def __repr__(self) -> str: # pragma: no cover - cosmetic
|
|
16
|
+
return f"{self.__class__.__name__}(status={self.status}, code={self.code!r}, message={self.message!r})"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AuthError(PrometiamError):
|
|
20
|
+
"""401, or 403 for a country the key cannot access."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ValidationError(PrometiamError):
|
|
24
|
+
"""400 / 422."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NotFoundError(PrometiamError):
|
|
28
|
+
"""404."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RateLimitError(PrometiamError):
|
|
32
|
+
"""429: per-minute, daily or monthly limit, or a batch larger than the remaining quota."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, message: str, status: int, code: str, body: Any, retry_after: float):
|
|
35
|
+
super().__init__(message, status, code, body)
|
|
36
|
+
self.retry_after = retry_after
|
|
37
|
+
b = body if isinstance(body, dict) else {}
|
|
38
|
+
self.max_items_now: Optional[int] = b.get("max_items_now") if isinstance(b.get("max_items_now"), int) else None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UpgradeRequiredError(PrometiamError):
|
|
42
|
+
"""403 upgrade_required: the scope is not in the plan, or the free trial allowance for it is used up."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, message: str, status: int, code: str, body: Any):
|
|
45
|
+
super().__init__(message, status, code, body)
|
|
46
|
+
b = body if isinstance(body, dict) else {}
|
|
47
|
+
self.required_scope: Optional[str] = b.get("required_scope")
|
|
48
|
+
self.included_in: Optional[str] = b.get("included_in")
|
|
49
|
+
self.price: Optional[str] = b.get("price")
|
|
50
|
+
self.upgrade_url: Optional[str] = b.get("upgrade_url")
|
|
51
|
+
self.trial_allowance: Optional[dict] = b.get("trial_allowance")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def error_from_response(status: int, retry_after_header: Optional[str], body: Any, raw_text: str) -> PrometiamError:
|
|
55
|
+
b = body if isinstance(body, dict) else {}
|
|
56
|
+
err = b.get("error") if isinstance(b.get("error"), dict) else {}
|
|
57
|
+
code = str(err.get("code") or b.get("code") or ("rate_limit_exceeded" if status == 429 else "server_error" if status >= 500 else "error"))
|
|
58
|
+
message = str(err.get("message") or b.get("message") or (raw_text[:200] if raw_text else f"HTTP {status}"))
|
|
59
|
+
if status == 401:
|
|
60
|
+
return AuthError(message, status, code, body)
|
|
61
|
+
if status == 403 and code == "upgrade_required":
|
|
62
|
+
return UpgradeRequiredError(message, status, code, body)
|
|
63
|
+
if status == 403:
|
|
64
|
+
return AuthError(message, status, code, body)
|
|
65
|
+
if status == 404:
|
|
66
|
+
return NotFoundError(message, status, code, body)
|
|
67
|
+
if status in (400, 422):
|
|
68
|
+
return ValidationError(message, status, code, body)
|
|
69
|
+
if status == 429:
|
|
70
|
+
retry_after: Optional[float] = None
|
|
71
|
+
for candidate in (retry_after_header, b.get("retry_after_seconds")):
|
|
72
|
+
if candidate is None or candidate == "":
|
|
73
|
+
continue
|
|
74
|
+
try:
|
|
75
|
+
v = float(candidate)
|
|
76
|
+
except (TypeError, ValueError):
|
|
77
|
+
continue
|
|
78
|
+
if v >= 0:
|
|
79
|
+
retry_after = v
|
|
80
|
+
break
|
|
81
|
+
return RateLimitError(message, status, code, body, 60.0 if retry_after is None else retry_after)
|
|
82
|
+
return PrometiamError(message, status, code, body)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prometiam-risk-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the Prometiam Risk Intelligence API: EU company registry data (ES, FR, GB, IE, PL, NO, FI, SE, HR, BE, DK), corporate insolvency across twelve markets, sanctions screening, VAT and LEI lookups.
|
|
5
|
+
Author-email: Prometiam <support@prometiam.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://www.prometiam.com/risk-api/docs
|
|
8
|
+
Project-URL: Documentation, https://www.prometiam.com/risk-api/docs
|
|
9
|
+
Keywords: prometiam,kyb,company-registry,sanctions-screening,insolvency,borme,bodacc,companies-house
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: httpx>=0.25
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# prometiam-risk-api (Python)
|
|
21
|
+
|
|
22
|
+
Official Python client for the [Prometiam Risk Intelligence API](https://www.prometiam.com/risk-api/docs): company registry data for Spain, France, the UK, Ireland, Poland, Norway, Finland, Sweden, Croatia, Belgium and Denmark, corporate insolvency notices across twelve markets, sanctions screening, VAT and LEI lookups.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install prometiam-risk-api
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import os
|
|
32
|
+
from prometiam_risk_api import Client
|
|
33
|
+
|
|
34
|
+
api = Client(api_key=os.environ["PROMETIAM_API_KEY"])
|
|
35
|
+
|
|
36
|
+
page = api.companies_search(name="Inditex", country="ES") # fuzzy, best match first (match_score 0-100)
|
|
37
|
+
first = page["data"][0]
|
|
38
|
+
print(first["company_name"], first["company_number"])
|
|
39
|
+
|
|
40
|
+
detail = api.company(first["id"], include="risk_flags,insolvency")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every method returns the API envelope `{"data": ..., "pagination": ..., "meta": ...}` exactly as documented.
|
|
44
|
+
|
|
45
|
+
## Batch calls
|
|
46
|
+
|
|
47
|
+
One HTTP call for a whole list. Every item counts as one request against your plan limits; a batch that does not fit raises `RateLimitError` whose `max_items_now` says how many items would fit.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
lookup = api.companies_lookup([
|
|
51
|
+
{"country": "ES", "company_number": "A46103834"},
|
|
52
|
+
{"country": "FR", "siren": "552032534"},
|
|
53
|
+
{"country": "ES", "name": "Inditex"}, # best fuzzy match, with match_score
|
|
54
|
+
])
|
|
55
|
+
for r in lookup["data"]:
|
|
56
|
+
print(r["status"], (r["company"] or {}).get("company_name"))
|
|
57
|
+
|
|
58
|
+
screen = api.sanctions_screen_batch(["Vladimir Putin", "Acme Trading Ltd"], group="entity")
|
|
59
|
+
flagged = [r for r in screen["data"] if r["status"] == "match"]
|
|
60
|
+
|
|
61
|
+
distress = api.insolvency_check([
|
|
62
|
+
{"country": "FR", "company_number": "917499519"},
|
|
63
|
+
{"country": "DE", "name": "Wirecard"},
|
|
64
|
+
])
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Pagination
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
for page in api.paginate("/people/search", {"name": "garcia", "country": "ES", "limit": 50}):
|
|
71
|
+
for person in page:
|
|
72
|
+
print(person["full_name"])
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Errors
|
|
76
|
+
|
|
77
|
+
| Error | When |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `AuthError` | 401, or 403 for a country the key cannot access |
|
|
80
|
+
| `UpgradeRequiredError` | 403 `upgrade_required`: scope not in your plan or the free trial allowance is used up. Carries `included_in`, `price`, `upgrade_url`, `trial_allowance` |
|
|
81
|
+
| `RateLimitError` | 429, retried automatically up to `max_retries` (default 2) honouring `Retry-After`; carries `retry_after` and `max_items_now` |
|
|
82
|
+
| `ValidationError` | 400 |
|
|
83
|
+
| `NotFoundError` | 404 |
|
|
84
|
+
| `PrometiamError` | anything else; `status`, `code`, `body` on every error |
|
|
85
|
+
|
|
86
|
+
## Options
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
Client(api_key, base_url="https://api.prometiam.com/functions/v1/risk-api", timeout=30.0, max_retries=2, transport=None)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use `with Client(...) as api:` to close the connection pool, and `api.request("GET", "/coverage")` for anything not wrapped.
|
|
93
|
+
|
|
94
|
+
## Rate limits
|
|
95
|
+
|
|
96
|
+
Per-key limits by plan (requests/minute, requests/day, requests/month):
|
|
97
|
+
|
|
98
|
+
| Plan | Price | RPM | Daily | Monthly |
|
|
99
|
+
|---|---|---|---|---|
|
|
100
|
+
| Free (14-day trial) | €0 | 10 | 200 | 1,000 |
|
|
101
|
+
| Starter | €9.99/mo | 60 | 2,000 | 10,000 |
|
|
102
|
+
| Professional | €29.99/mo | 300 | 20,000 | 100,000 |
|
|
103
|
+
| Scale | €99.99/mo | 600 | 200,000 | 1,000,000 |
|
|
104
|
+
| Enterprise | Custom | Custom | Custom | Custom |
|
|
105
|
+
|
|
106
|
+
`api.account()` returns your key's current limits and usage (`rate_limit.rpm_remaining`, `daily_remaining`, `monthly_remaining`). A 429 raises `RateLimitError`, retried automatically up to `max_retries` honouring `Retry-After`. Batch calls (`companies_lookup`, `sanctions_screen_batch`, `insolvency_check`) count every item in the list as one request against these limits; a batch that would not fit in the remaining quota raises `RateLimitError` up front (`code == "batch_exceeds_quota"`, `max_items_now` set).
|
|
107
|
+
|
|
108
|
+
## Scope limits
|
|
109
|
+
|
|
110
|
+
The API's coverage has real edges — every method above reflects them, not a marketing claim:
|
|
111
|
+
|
|
112
|
+
- **Company registry (`companies_search`, `company`, `people_search`, `events_*`, `directors_network`):** Spain, France, the UK, Ireland, Poland, Norway, Finland, Sweden, Croatia, Belgium, Denmark. Ireland and Poland are company-level today (no officers yet). Finland, Sweden, Croatia, Belgium and Denmark are company records only, with register-change events (name, status, legal form and registered address; share capital for Croatia; dated when the change first appears in the register data, not gazette notices, none before 2026-09-30) and monitoring, but no officers and no registry-compliance signal. Corporate insolvency notices are linked by business ID (Finland), organisationsnummer (Sweden), MBS (Croatia) and CVR number (Denmark); Belgium has none (its bankruptcy shows in the company status and in its register-change events). Sole traders (enskild näringsverksamhet, trgovac pojedinac), enterprises of natural persons (eenmanszaak / entreprise individuelle), sole proprietorships (enkeltmandsvirksomhed) and estates are never served. Denmark has no share capital, and its status includes the register's bankruptcy state.
|
|
113
|
+
- **Insolvency (`insolvency_search`, `insolvency_check`):** corporate only, twelve markets (FR, DE, GB, AT, CH, NO, FI, US, NL, DK, HR, SE). Personal/consumer insolvency is never returned. Spain's concursal data is on `records_search` (`GET /search`), not `insolvency_search`.
|
|
114
|
+
- **Monitoring (`monitors`, `monitor`, `monitor_create`, `monitor_delete`):** ES, IE, PL, FI, SE, BE, HR, DK; alerts on status changes, dissolutions, sanctions matches and corporate events (ES and FI/SE/BE/HR/DK).
|
|
115
|
+
- **Sanctions screening (`sanctions_screen*`):** beta.
|
|
116
|
+
- **PEP hits (`sanctions_screen(name, include_pep=True)`):** beta, Spain only.
|
|
117
|
+
- **Norway:** no corporate-event stream (`events_*` returns empty for NO, and for IE and PL).
|
|
118
|
+
- **Finland:** company records only (`people_search` returns empty for FI; `events_*` returns register-change events, dated when first seen in the register data and none before 2026-09-30; `monitor_create` accepts it). Its insolvency notices are linked by business ID.
|
|
119
|
+
- **Sweden:** company records only (`people_search` returns empty for SE; `events_*` returns register-change events; `monitor_create` accepts it). Corporate insolvency notices (konkurs, företagsrekonstruktion, ackordsförhandling; updated weekly, no court, case number or source link, proceedings that ended before collection not included) are on `insolvency`, linked by organisationsnummer.
|
|
120
|
+
- **Croatia:** company records only (`people_search` returns empty for HR; `events_*` returns register-change events, including share capital; `monitor_create` accepts it). Corporate insolvency notices (court decisions on bankruptcy, shortened bankruptcy and pre-bankruptcy proceedings, with court and case number where the register states them; only companies still on the register) are linked by MBS. A Croatian company is found by MBS (`company_number`), OIB (`oib`) or the VAT alias (`vat=HR` plus the OIB).
|
|
121
|
+
- **Belgium:** company records only (`people_search` returns empty for BE; `events_*` returns register-change events; `monitor_create` accepts it). Belgium is not one of the twelve insolvency markets: its bankruptcy shows in the company status and in its register-change events. A Belgian company is found by enterprise number (`company_number`, the dots and spaces optional) or the VAT alias (`vat=BE` plus the ten digits).
|
|
122
|
+
- **Denmark:** company records only (`people_search` returns empty for DK; `events_*` returns register-change events; `monitor_create` accepts it). Corporate insolvency notices (konkurs and tvangsakkord decisions from CVR credit information, dated by the decision, no court or case number) are linked by CVR number. Contains CVR data (Erhvervsstyrelsen), CC BY 4.0. A Danish company is found by CVR number (`company_number`, spaces and hyphens optional; the mod-11 check is advisory) or the VAT alias (`vat=DK` plus the eight digits).
|
|
123
|
+
|
|
124
|
+
## Get a key
|
|
125
|
+
|
|
126
|
+
Free at https://www.prometiam.com/signup, no card required. Docs: https://www.prometiam.com/risk-api/docs. Support: support@prometiam.com.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
prometiam_risk_api/__init__.py
|
|
5
|
+
prometiam_risk_api/client.py
|
|
6
|
+
prometiam_risk_api/errors.py
|
|
7
|
+
prometiam_risk_api.egg-info/PKG-INFO
|
|
8
|
+
prometiam_risk_api.egg-info/SOURCES.txt
|
|
9
|
+
prometiam_risk_api.egg-info/dependency_links.txt
|
|
10
|
+
prometiam_risk_api.egg-info/requires.txt
|
|
11
|
+
prometiam_risk_api.egg-info/top_level.txt
|
|
12
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
httpx>=0.25
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
prometiam_risk_api
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "prometiam-risk-api"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python client for the Prometiam Risk Intelligence API: EU company registry data (ES, FR, GB, IE, PL, NO, FI, SE, HR, BE, DK), corporate insolvency across twelve markets, sanctions screening, VAT and LEI lookups."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Prometiam", email = "support@prometiam.com" }]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
dependencies = ["httpx>=0.25"]
|
|
14
|
+
keywords = ["prometiam", "kyb", "company-registry", "sanctions-screening", "insolvency", "borme", "bodacc", "companies-house"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Topic :: Office/Business :: Financial",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://www.prometiam.com/risk-api/docs"
|
|
24
|
+
Documentation = "https://www.prometiam.com/risk-api/docs"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
packages = ["prometiam_risk_api"]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
9
|
+
|
|
10
|
+
from prometiam_risk_api import AuthError, Client, RateLimitError, UpgradeRequiredError # noqa: E402
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def make_client(handler, **kw):
|
|
14
|
+
calls = []
|
|
15
|
+
|
|
16
|
+
def _handler(request: httpx.Request) -> httpx.Response:
|
|
17
|
+
calls.append(request)
|
|
18
|
+
return handler(request, len(calls))
|
|
19
|
+
|
|
20
|
+
return Client(api_key="rk_live_test", transport=httpx.MockTransport(_handler), **kw), calls
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ClientTests(unittest.TestCase):
|
|
24
|
+
def test_sends_key_and_query(self):
|
|
25
|
+
api, calls = make_client(lambda r, n: httpx.Response(200, json={"data": [{"company_name": "INDITEX SA"}], "meta": {"query_ms": 5}}))
|
|
26
|
+
res = api.companies_search(name="Inditex", country="ES", limit=1, empty=None)
|
|
27
|
+
self.assertEqual(res["data"][0]["company_name"], "INDITEX SA")
|
|
28
|
+
self.assertEqual(calls[0].headers["authorization"], "Bearer rk_live_test")
|
|
29
|
+
self.assertEqual(calls[0].url.path, "/functions/v1/risk-api/companies/search")
|
|
30
|
+
self.assertEqual(calls[0].url.params["name"], "Inditex")
|
|
31
|
+
self.assertNotIn("empty", calls[0].url.params)
|
|
32
|
+
|
|
33
|
+
def test_batch_lookup_posts_items(self):
|
|
34
|
+
def h(r, n):
|
|
35
|
+
body = json.loads(r.content)
|
|
36
|
+
return httpx.Response(200, json={"data": [{"index": i, "status": "found"} for i, _ in enumerate(body["items"])], "meta": {"found": len(body["items"])}})
|
|
37
|
+
api, calls = make_client(h)
|
|
38
|
+
res = api.companies_lookup([{"country": "ES", "company_number": "A46103834"}, {"country": "FR", "siren": "552032534"}])
|
|
39
|
+
self.assertEqual(calls[0].method, "POST")
|
|
40
|
+
self.assertEqual(res["meta"]["found"], 2)
|
|
41
|
+
|
|
42
|
+
def test_sanctions_batch_normalises_strings(self):
|
|
43
|
+
api, calls = make_client(lambda r, n: httpx.Response(200, json={"data": []}))
|
|
44
|
+
api.sanctions_screen_batch(["Vladimir Putin", {"name": "Acme", "entity_type": "company"}], group="entity", threshold=85)
|
|
45
|
+
body = json.loads(calls[0].content)
|
|
46
|
+
self.assertEqual(body["items"], [{"name": "Vladimir Putin"}, {"name": "Acme", "entity_type": "company"}])
|
|
47
|
+
self.assertEqual(body["group"], "entity")
|
|
48
|
+
|
|
49
|
+
def test_idempotency_key_sent_as_header_on_batch_methods(self):
|
|
50
|
+
api, calls = make_client(lambda r, n: httpx.Response(200, json={"data": [], "meta": {}}))
|
|
51
|
+
api.companies_lookup([{"country": "ES", "company_number": "A46103834"}], idempotency_key="key-1")
|
|
52
|
+
self.assertEqual(calls[0].headers["idempotency-key"], "key-1")
|
|
53
|
+
|
|
54
|
+
api.sanctions_screen_batch(["Vladimir Putin"], idempotency_key="key-2")
|
|
55
|
+
self.assertEqual(calls[1].headers["idempotency-key"], "key-2")
|
|
56
|
+
|
|
57
|
+
api.insolvency_check([{"country": "FR", "siren": "552032534"}], idempotency_key="key-3")
|
|
58
|
+
self.assertEqual(calls[2].headers["idempotency-key"], "key-3")
|
|
59
|
+
|
|
60
|
+
api.companies_lookup([{"country": "ES", "company_number": "A46103834"}])
|
|
61
|
+
self.assertNotIn("idempotency-key", calls[3].headers)
|
|
62
|
+
|
|
63
|
+
def test_idempotency_key_reuse_and_in_progress_raise_typed_errors(self):
|
|
64
|
+
api422, _ = make_client(lambda r, n: httpx.Response(
|
|
65
|
+
422, json={"error": {"code": "idempotency_key_reuse", "message": "reused with a different body", "status": 422}}))
|
|
66
|
+
with self.assertRaises(Exception) as ctx:
|
|
67
|
+
api422.companies_lookup([{"country": "ES", "company_number": "A46103834"}], idempotency_key="dup")
|
|
68
|
+
self.assertEqual(ctx.exception.code, "idempotency_key_reuse")
|
|
69
|
+
self.assertEqual(ctx.exception.status, 422)
|
|
70
|
+
|
|
71
|
+
api409, _ = make_client(
|
|
72
|
+
lambda r, n: httpx.Response(409, headers={"Retry-After": "2"}, json={"error": {"code": "idempotency_key_in_progress", "message": "still running", "status": 409}}),
|
|
73
|
+
max_retries=0,
|
|
74
|
+
)
|
|
75
|
+
with self.assertRaises(Exception) as ctx:
|
|
76
|
+
api409.companies_lookup([{"country": "ES", "company_number": "A46103834"}], idempotency_key="inflight")
|
|
77
|
+
self.assertEqual(ctx.exception.code, "idempotency_key_in_progress")
|
|
78
|
+
self.assertEqual(ctx.exception.status, 409)
|
|
79
|
+
|
|
80
|
+
def test_retries_429_then_succeeds(self):
|
|
81
|
+
def h(r, n):
|
|
82
|
+
if n == 1:
|
|
83
|
+
return httpx.Response(429, headers={"Retry-After": "0"}, json={"error": {"code": "rate_limit_exceeded", "message": "slow", "status": 429}})
|
|
84
|
+
return httpx.Response(200, json={"data": {"ok": True}})
|
|
85
|
+
api, calls = make_client(h, max_retries=2)
|
|
86
|
+
self.assertTrue(api.coverage()["data"]["ok"])
|
|
87
|
+
self.assertEqual(len(calls), 2)
|
|
88
|
+
|
|
89
|
+
def test_rate_limit_error_after_retries(self):
|
|
90
|
+
api, _ = make_client(lambda r, n: httpx.Response(429, headers={"Retry-After": "0"}, json={"error": {"code": "batch_exceeds_quota", "message": "too big", "status": 429}, "max_items_now": 8}), max_retries=1)
|
|
91
|
+
with self.assertRaises(RateLimitError) as cm:
|
|
92
|
+
api.coverage()
|
|
93
|
+
self.assertEqual(cm.exception.max_items_now, 8)
|
|
94
|
+
|
|
95
|
+
def test_upgrade_required(self):
|
|
96
|
+
api, _ = make_client(lambda r, n: httpx.Response(403, json={"error": {"code": "upgrade_required", "message": "no", "status": 403}, "required_scope": "sanctions", "included_in": "professional", "price": "EUR 29.99/month", "trial_allowance": {"scope": "sanctions", "used": 25, "limit": 25, "resets_on": "2026-10-01"}}))
|
|
97
|
+
with self.assertRaises(UpgradeRequiredError) as cm:
|
|
98
|
+
api.sanctions_screen("x")
|
|
99
|
+
self.assertEqual(cm.exception.included_in, "professional")
|
|
100
|
+
self.assertEqual(cm.exception.trial_allowance["limit"], 25)
|
|
101
|
+
|
|
102
|
+
def test_auth_error(self):
|
|
103
|
+
api, _ = make_client(lambda r, n: httpx.Response(401, json={"error": {"code": "unauthorized", "message": "bad key", "status": 401}}))
|
|
104
|
+
with self.assertRaises(AuthError):
|
|
105
|
+
api.account()
|
|
106
|
+
|
|
107
|
+
def test_paginate_follows_cursor(self):
|
|
108
|
+
def h(r, n):
|
|
109
|
+
c = r.url.params.get("cursor")
|
|
110
|
+
if not c:
|
|
111
|
+
return httpx.Response(200, json={"data": [1, 2], "pagination": {"count": 2, "has_more": True, "next_cursor": "o:2"}})
|
|
112
|
+
if c == "o:2":
|
|
113
|
+
return httpx.Response(200, json={"data": [3], "pagination": {"count": 1, "has_more": False, "next_cursor": None}})
|
|
114
|
+
raise AssertionError("unexpected cursor")
|
|
115
|
+
api, calls = make_client(h)
|
|
116
|
+
pages = list(api.paginate("/people/search", {"name": "garcia", "limit": 2}))
|
|
117
|
+
self.assertEqual(pages, [[1, 2], [3]])
|
|
118
|
+
self.assertEqual(len(calls), 2)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
unittest.main()
|