PEP Screening API for PEP and Sanctions Checks
One REST call screens PEP records, sanctions lists and criminal watchlists together. POST /api/v1/search with X-API-Key, poll, then read the pep field.
There is no PEP-only endpoint and no per-domain filter to fake one. POST /api/v1/search with search_type sanctions_check (€0.39) screens the name against PEP records, sanctions lists and criminal watchlists in a single pass and returns the three domains in separate fields of the same response, so one integration answers all three questions and you are billed once. Nothing here is synchronous. HTTP 202 hands back a search_id and you poll GET /api/v1/search/{search_id} until status is completed. Authenticate with an X-API-Key header carrying the search:write permission; the key itself is minted in the dashboard, or by POST /api/v1/keys against your account bearer token. Two behaviours to build around. pep is null when nothing matched, where the sanctions fields always carry a formatted block, so null is a result and not an empty response. And there is no score in the payload, no SDK and no webhook: a PEP finding arrives as markdown for a reviewer, because *Potential PEP match* is the beginning of a decision rather than the end of one. The register behind it is public sources, over 750,000 records from 134 of them, and the snapshot stored against your search_id holds the status as it stood that day, which is the thing you need when the person leaves office two years later.
What this workflow covers
SCOPE- The request body is entity_name (required, 1–500 characters), entity_type (person, company or any), search_type, and the optional country, identifier, website and language fields. Passing country sharpens identity resolution on common names.
- The 202 response carries search_id, status, message and estimated_time_seconds, which is 15 for sanctions_check and 30 for full_search. Both figures are constants in the code, not measured or guaranteed latency.
- No per-domain filter exists in the request model, so a PEP-only check cannot be bought. One call is a PEP and sanctions API in the literal sense: PEP, sanctions and criminal watchlists screened together and returned in separate fields.
- PEP findings arrive in the pep field of GET /api/v1/search/{search_id}, as markdown headed '## PEP Findings' with the status line *Potential PEP match* and, where the source record carries them, identification, datasets and dates lines plus a source label.
- When nothing matches, pep is null rather than a no-match block, unlike the sanctions fields. Read null as no PEP record found in the checked public sources, not as a negative confirmation.
- PEP attribution is normalised before it leaves the API: aggregator and vendor labels are stripped from the text, so the source line names the public register rather than a data reseller.
- GET /api/v1/coverage returns a pep domain with available_tables, the table names and latest_updated_at, which is how you evidence what the PEP register held on a given screening date.
- completed is the status you wait for. processing precedes it, queued precedes that for a deep_research_report, and failures surface as error_queue_create, error_credit_deduction, error_streaming, error_saving_results or error_no_final_payload.
- Each key carries its tier's limits: free 10/minute and 100/day, standard 60 and 1,000, premium 300 and 10,000, enterprise 1,000 and 100,000. Every response returns X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, and a 429 adds Retry-After.
- Every failure uses the same envelope of error, message and status_code. 401 for a missing or invalid X-API-Key, 403 for a key without search:write or search:read, 404 for an unknown search_id, 400 for an unsupported search_type, 429 for rate limits.
- There is no idempotency key, so a retried POST is a second billed search.
- Price per check: €0.39 for sanctions_check and €5.90 for full_search, which adds transliteration, alias expansion and an explainable review of each candidate with false-positive flags. Evidence PDFs are exported from screening history in the portal; /api/v1 has no PDF endpoint.
Code samples
APIcurl -X POST https://screenveritai.com/api/v1/search \
-H "X-API-Key: svai_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"entity_name": "Test Person",
"entity_type": "person",
"search_type": "sanctions_check",
"country": "PL"
}'
# HTTP/1.1 202 Accepted
# {
# "search_id": "6e2b8d05-9f13-4a77-b8c4-0d51e7a3f962",
# "status": "processing",
# "message": "Search queued successfully. Poll the GET endpoint for results.",
# "estimated_time_seconds": 15
# }
# Allowance and prepaid balance before a run of checks:
curl https://screenveritai.com/api/v1/account/credits \
-H "X-API-Key: svai_your_key_here"
# {
# "subscription_tier": "standard",
# "credits_used": 0.0,
# "credits_limit": null,
# "credits_remaining": null,
# "credit_pools": {
# "sanctions_checks": {"used": 12.0, "limit": 500, "remaining": 488.0},
# "full_search": {"used": 1.0, "limit": 25, "remaining": 24.0}
# },
# "prepaid_balance_cents": 4210,
# "prepaid_currency": "eur"
# }// No PEP record found — pep is null, not a "no match" block.
{
"id": "6e2b8d05-9f13-4a77-b8c4-0d51e7a3f962",
"entity_name": "Test Person",
"entity_type": "person",
"status": "completed",
"search_type": "sanctions_check",
"pep": null,
"criminal_watchlists": null,
"ofac_sanctions": "## OFAC Sanctions Findings\n\n**Status:** *No relevant listing found* ...",
"created_at": "2026-09-04T11:04:26.501000+00:00",
"completed_at": "2026-09-04T11:04:41.983000+00:00"
}
// Potential PEP match in the public source registry.
{
"id": "9a4c7f31-08e5-4bd2-9a60-33be1d6c8f57",
"entity_name": "Second Test Person",
"entity_type": "person",
"status": "completed",
"search_type": "sanctions_check",
"pep": "## PEP Findings\n\n**Status:** *Potential PEP match*\n\n**Details:**\n- **Matched entity:** SECOND TEST PERSON\n- **Identification:** 1968-02-23 | PL\n- **Dates:** first seen 2018-05-02 | last seen 2026-08-29\n\n**Source:** *Public PEP source registry*",
"criminal_watchlists": null,
"created_at": "2026-09-04T11:12:09.114000+00:00",
"completed_at": "2026-09-04T11:12:25.660000+00:00"
}import os
import time
import requests
BASE = "https://screenveritai.com/api/v1"
HEADERS = {"X-API-Key": os.environ["SCREENVERITAI_API_KEY"]}
SANCTIONS_FIELDS = (
"ofac_sanctions", "eu_sanctions", "fr_sanctions", "pl_sanctions",
"uk_sanctions", "un_sanctions", "canada_sanctions", "australia_sanctions",
"switzerland_sanctions", "south_africa_sanctions", "new_zealand_sanctions",
)
def screen(entity_name, country=""):
created = requests.post(
f"{BASE}/search",
headers=HEADERS,
json={
"entity_name": entity_name,
"entity_type": "person",
"search_type": "sanctions_check",
"country": country,
},
timeout=30,
)
created.raise_for_status()
search_id = created.json()["search_id"]
while True:
response = requests.get(f"{BASE}/search/{search_id}", headers=HEADERS, timeout=30)
response.raise_for_status()
result = response.json()
if result["status"] == "completed":
return result
if result["status"].startswith("error"):
raise RuntimeError(f"search {search_id} ended in {result['status']}")
time.sleep(3)
result = screen("Test Person", country="PL")
# One call, three domains. pep and criminal_watchlists are null when empty;
# the sanctions fields always carry a block, so check the status marker.
pep_hit = bool(result["pep"])
watchlist_hit = bool(result["criminal_watchlists"])
sanctions_hit = any(
"**Status:** *Sanctioned*" in (result.get(field) or "")
for field in SANCTIONS_FIELDS
)
print(result["id"], {"pep": pep_hit, "watchlist": watchlist_hit, "sanctions": sanctions_hit})
if pep_hit:
print(result["pep"]) # markdown block for the reviewer, not a scorecurl -X POST https://screenveritai.com/api/v1/keys \
-H "Authorization: Bearer <SUPABASE_JWT>" \
-H "Content-Type: application/json" \
-d '{
"name": "onboarding-service",
"permissions": ["search:read", "search:write"],
"expires_in_days": 90
}'
# HTTP/1.1 201 Created
# The api_key value is shown once in this response and never again.
# {
# "id": "1f0a7c62-4d38-4c11-9e77-2b5a90fd6c48",
# "name": "onboarding-service",
# "key_prefix": "svai_9c41ab",
# "permissions": ["search:read", "search:write"],
# "rate_limit_tier": "standard",
# "rate_limit_per_minute": 60,
# "rate_limit_per_day": 1000,
# "is_active": true,
# "expires_at": "2026-12-03T11:20:44.312000+00:00",
# "api_key": "svai_9c41ab..."
# }
#
# Permissions are search:read, search:write, batch:read, batch:write.
# expires_in_days accepts 1-365, or null for a key that does not expire.
# A maximum of 10 active keys per account.Key statistics
DATA- PEP register
- 750,000+ records from 134 public sources
- ScreenVeritAI coverage model
- Quick Check price per screened name
- €0.39 — PEP, sanctions and criminal watchlists in one call
- ScreenVeritAI pricing
- Rate limit, standard tier
- 60 requests/minute, 1,000/day per API key
- ScreenVeritAI API v1
Compliance glossary
TERMS- search_id
- The UUID the 202 hands back. It addresses the result at GET /api/v1/search/{search_id} and identifies the stored snapshot that records the person's PEP status as it stood on the screening date.
- Match
- A public-source PEP record that the name search returned. It appears in the pep field as a '## PEP Findings' section with the status line *Potential PEP match*, plus the identification, datasets and date lines the source record carries.
- Disposition
- The outcome a reviewer records against a candidate: confirmed as the same person, dismissed as a false positive, or escalated for enhanced due diligence. A PEP designation is a risk classification calling for review rather than a prohibition, so the disposition is where the actual decision lives.
- Point-in-time evidence
- A completed check, frozen as it ran and never recalculated when the PEP register changes. Because a person becomes and ceases to be a PEP with office rather than with a designation event, the snapshot is the only defensible record of what a check showed at the time.
Authoritative references
SOURCES- 01ScreenVeritAI API v1 — interactive documentation
ScreenVeritAI
- 02ScreenVeritAI API v1 — OpenAPI schema
ScreenVeritAI
- 03FATF Recommendations 12 and 22 — Politically Exposed Persons
Financial Action Task Force
- 04Directive (EU) 2015/849 — definition of a politically exposed person
Official Journal of the European Union
- 05Specially Designated Nationals and Blocked Persons List (SDN)
U.S. Department of the Treasury, OFAC
Frequently asked questions
Q&A- We are getting null in pep more often than expected. Error or no match?
- No match. The sanctions fields always return a formatted block, pep and criminal_watchlists return null, and that asymmetry catches most integrations exactly once. Write it into your UI as no PEP record found in the checked public sources, never as a negative confirmation. The register is drawn from public sources, and a person can hold office without appearing in one.
- Can I point staging at a free endpoint while I build?
- No. There is no sandbox, no free test key and no dry-run flag; every POST /api/v1/search bills at the published price. GET /api/v1/health is public for a reachability check, and GET /api/v1/account/credits shows the remaining allowance and prepaid balance. Capture one real completed response as a fixture on day one and run staging against that.
- What is a sane polling interval?
- Every 2–3 seconds. Faster than that just burns rate limit against a job that has not moved. The 202 returns estimated_time_seconds of 15 for sanctions_check and 30 for full_search, both fixed per-type constants in the code rather than measurements, so use them to size a deadline rather than to promise anyone a number. A check that has not reached completed goes to manual review; a timeout is not a pass.
- Can I split this into a PEP call and a sanctions call for two different teams?
- Not on our side, and there is no reason to pay twice for it. The request model has no per-domain or per-list filter, so a single €0.39 check returns pep, criminal_watchlists and the eleven per-jurisdiction sanctions fields in one payload. That is why one integration serves as both a PEP screening API and a sanctions API. Fan the payload out to your teams from your own store rather than issuing two searches for the same name.
- Two spellings of the same name, one hits and one does not. What am I doing wrong?
- Nothing, if you are on sanctions_check. It is the deterministic mode and matches the name as submitted. Transliteration, alias matching and query expansion run in Full Search (search_type: full_search, €5.90), which also writes readable reasoning for each candidate and flags likely false positives. If you stay on sanctions_check, at least pass country; it sharpens identity resolution on common names.
- Which language SDKs do you support?
- None, and we would rather say so than ship one we do not maintain. There are no SDKs and no client libraries. It is plain REST with JSON bodies and an X-API-Key header, with interactive documentation at /api/v1/docs, reference docs at /api/v1/redoc and a machine-readable schema at /api/v1/openapi.json that a generator will turn into a client.
- Is PEP data billed separately from sanctions?
- No. It is inside the per-check price: €0.39 for sanctions_check and €5.90 for full_search, each covering PEP, sanctions and criminal watchlists in one call, with no PEP add-on and no platform fee. Monthly plans include a check allowance that the API spends before the prepaid balance, so a plan changes what a call draws down, not what it returns.