OpenSanctions
Document information
| Field | Value |
|---|---|
| Canonical URL | /docs/11_partner_services/30_opensanctions |
| Version (published date) | 2026-08-14 |
| Tags | partners, opensanctions, sanctions, pep, kyc, restapi-generic |
Summary
OpenSanctions is a hosted screening API over combined sanctions, PEP (politically exposed person), and related watchlist data. You describe a person or company; /match returns scored candidates. /search is full-text search for user interfaces. /entities/{id} returns the full nested record. Tealfabric has exercised these HTTP surfaces against restapi-generic-1.1.0.
Sign up with OpenSanctions for an API key. Non-commercial use can be free; commercial screening requires their data license. Tealfabric does not issue keys, meter quota, or resell the dataset.
Why use it with Tealfabric
Tealfabric stores your customers, entities, and processes. It does not maintain global sanctions or PEP lists. Use OpenSanctions when a workflow must check a counterparty against those lists:
- Screen a new customer, supplier, or beneficial owner at onboarding
- Re-screen existing
entitieswhen watchlists change - Combine a Finnish Business ID from PRH BIS with a sanctions match on the same legal name
- Let a WebApp search the watchlist for investigators (search is not a screening decision)
Typical pattern: keep the entity in Tealfabric; call /match from a process step; store id, score, match, and topics in DataPool or entity-information; route hits above your threshold to a human review step. Do not treat a search-box score as a compliance decision.
What the API provides
Hosted base URL:
https://api.opensanctions.org
Every hosted call needs an API key in Authorization (prefix ApiKey , not Bearer ). See Authentication.
| Endpoint | HTTP | Role |
|---|---|---|
POST /match/{dataset} | POST JSON body | Screening: query-by-example, scored candidates. Use dataset default unless you scope to one collection (sanctions, peps, or a source such as us_ofac_sdn). |
GET /search/{dataset} | GET query string | User-facing full-text search. Not for screening. Search scores are not a match confidence. |
GET /entities/{id} | GET | Full nested entity (sanctions, ownership, family). /match and /search return a shallow record; fetch this when you need relationships. Free to call. |
A /match body looks like:
{
"queries": {
"q": {
"schema": "Person",
"properties": {
"name": ["Example Name"],
"birthDate": ["1970"],
"nationality": ["fi"]
}
}
}
}
Use schema Person or Company (or parent types such as LegalEntity). Properties are always arrays. Send every identifier you have (name, birth date, nationality, tax or company number). The matcher ranks candidates; score is confidence, match is true when score meets the threshold (vendor default 0.7). properties.topics tells you why the hit exists (sanction, sanction.linked, role.pep, debarment, and others).
You can batch up to 100 queries in one POST (queries is a map of your own keys). Each map entry is billed separately.
Interactive docs: getting started, match request, search, data dictionary. OpenAPI JSON is linked from those pages.
Platform compatibility
Split read and match into two integrations. /match is POST; on restapi-generic-1.1.0 that is operation: send, not receive. A GET integration that you accidentally set to method: POST remaps receive to send and changes the envelope — keep the verbs explicit.
Lookup (search + entity GET)
| Integration setting | Value |
|---|---|
| Connector | restapi-generic-1.1.0 |
url | https://api.opensanctions.org |
path | leave empty (pass search/default or entities/{id} per call) |
method | GET |
auth_header_name | Authorization |
api_key | ApiKey <your OpenSanctions key> (include the ApiKey prefix; the connector sends the value unchanged) |
accept | application/json |
| Process step operation | receive |
Match (screening POST)
| Integration setting | Value |
|---|---|
| Connector | restapi-generic-1.1.0 |
url | https://api.opensanctions.org |
path | /match/default |
method | POST |
auth_header_name | Authorization |
api_key | ApiKey <your OpenSanctions key> |
content_type / accept | application/json |
| Process step operation | send |
Leave executable_by_ai_agents off on the match integration until you explicitly want Trace AI to spend quota. Point each integration description at https://www.opensanctions.org/docs/api/.
send wraps the vendor JSON in { message_count, response, http_status }. receive wraps a JSON object as a one-element data array. Unwrap accordingly.
Connector details: Generic REST API connector.
Connect from a process step
Replace the integration IDs. The first example screens a person from step input; the second looks up a full entity after a hit.
const matchIntegrationId = "YOUR_MATCH_INTEGRATION_ID";
const lookupIntegrationId = "YOUR_LOOKUP_INTEGRATION_ID";
const fullName = String(process_input.full_name ?? "").trim();
const birthDate = String(process_input.birth_date ?? "").trim();
const schema = process_input.schema === "Company" ? "Company" : "Person";
if (!fullName) {
throw new Error("full_name is required");
}
const properties: Record<string, string[]> = { name: [fullName] };
if (birthDate) {
properties.birthDate = [birthDate];
}
if (typeof process_input.nationality === "string" && process_input.nationality.trim()) {
properties.nationality = [process_input.nationality.trim().toLowerCase()];
}
const matchResult = (await integration.executeSync(matchIntegrationId, {
operation: "send",
body: {
queries: {
q: { schema, properties },
},
},
})) as Record<string, unknown>;
if (!matchResult?.success) {
const err = matchResult?.error;
const message =
typeof err === "string"
? err
: err && typeof err === "object"
? String((err as Record<string, unknown>).message ?? "OpenSanctions match failed")
: "OpenSanctions match failed";
throw new Error(message);
}
const envelope = (matchResult.data ?? {}) as Record<string, unknown>;
const apiRoot = (envelope.response ?? envelope) as Record<string, unknown>;
const responses = (apiRoot.responses ?? {}) as Record<string, unknown>;
const q = (responses.q ?? {}) as Record<string, unknown>;
const results = Array.isArray(q.results) ? q.results : [];
const hits = results.filter((row) => {
const rec = row && typeof row === "object" ? (row as Record<string, unknown>) : {};
return rec.match === true;
});
let nested: unknown = null;
const first = hits[0] as Record<string, unknown> | undefined;
if (first?.id) {
const entityResult = (await integration.executeSync(lookupIntegrationId, {
operation: "receive",
path: `entities/${String(first.id)}`,
})) as Record<string, unknown>;
if (entityResult?.success) {
const lookupEnvelope = (entityResult.data ?? {}) as Record<string, unknown>;
let root: unknown = lookupEnvelope.data ?? lookupEnvelope;
if (Array.isArray(root) && root.length === 1) {
root = root[0];
}
nested = root;
}
}
return {
success: true,
data: {
hit_count: hits.length,
hits,
nested_entity: nested,
},
};WebApp-style search (not for KYC decisions):
const result = await integration.executeSync(lookupIntegrationId, {
operation: "receive",
path: "search/default",
query: { q: String(process_input.q ?? ""), schema: "Person", limit: "10" },
});
Optional match query params (threshold, algorithm, topics) go in query on the send call, not in the JSON body. See the match request.
Example use cases with Tealfabric
Onboarding screen. After a WebApp collects name, date of birth, and country, a process step sends /match/default. If any result has match: true, pause the flow, write the hit into DataPool, and notify a reviewer. Clear names continue to entity create.
Finnish company + watchlist. Look up Y-tunnus with PRH BIS, then /match with schema: "Company", properties.name from the register, and any identifiers you have. One process, two partner integrations.
Periodic re-screen. A scheduled ProcessFlow pages tenant entities, screens each legal name, and compares id / last_change to the last stored result so newly listed counterparties surface after onboarding. Cache /entities/{id} responses; that call is not metered.
Investigator WebApp. Bind a search box to GET /search/default. Show captions and topics. Open /entities/{id} (or the OpenSanctions site URL) for the nested profile. Do not auto-block from search ranking.
PEP vs sanctioned routing. Branch on properties.topics: role.pep can mean enhanced due diligence; sanction can mean a hard stop. Keep the policy in your step code.
Agent lookup. Enable executable_by_ai_agents on the GET integration so Trace AI can fetch /entities/{id} without posting match batches. Keep match POST off for agents unless you accept quota spend.
Pricing and accounts
Create a key on the OpenSanctions API product page. A business email typically gets a time-limited trial. Journalism, civil-society, and academic work can get free keys. Business use of the dataset needs a commercial license (commercial FAQ).
Hosted API metering (orientation only; confirm on the vendor site):
| Call | Metering |
|---|---|
POST /match | Per logical query in the body (about €0.10 each; up to 100 per HTTP request) |
GET /search | Per HTTP request |
GET /entities | Free |
| Failed HTTP responses | Not billed |
Monthly key quotas apply; HTTP 429 until the next calendar month if you exceed them. Volume discounts are a vendor conversation (they mention a floor around 20,000 requests/month). On-premise yente is licensed as bulk data, not per query.
Quota, invoices, and CC BY-NC vs commercial terms stay with OpenSanctions. In Tealfabric you only store the ApiKey … value on the integration (or inject it from ProcessFlow Keystore).
Limits to keep in mind
- Use
/matchfor screening./searchis a UI search box. - POST
/match→send. GET/searchand/entities→receive. - Do not send the OpenSanctions key to the browser.
- Batching match queries does not reduce cost: 1,000 counterparties = 1,000 billable queries.
- Cache entity fetches. Re-screen on a schedule instead of calling
/matchon every page view. - Commercial tenants need an OpenSanctions license; a trial key is not a production compliance control.