API Reference
The Naralin Entity Intelligence API provides programmatic access to 100M+ entities across 500+ authoritative sources — sanctions lists, PEP databases, offshore leak datasets, beneficial ownership registries, and LEI data. Screen any entity in under 200ms. All responses are application/json.
Evaluate and prototype. Single entity lookup, full risk score. No credit card required.
Series A–B fintechs and compliance teams. Batch lookup, PSC ownership depth.
Institutional compliance. Relationship graph API, 99.95% SLA, dedicated support.
Tier-1 banks & funds. Bulk feeds, raw export, dedicated infra, SOC 2 on request.
pip install naralinnpm install naralinSet your API key: export NARALIN_API_KEY=nar_live_... — then you're ready to screen entities.
Base URL
All API requests should be made to the following base URL. The API is served from a global edge network.
https://api.naralin.aiSubscription Tiers
Naralin is structured around monthly lookup quotas. All tiers include full offshore leak data access, sanctions, and PEP data. Higher tiers unlock batch endpoints, ownership depth, and relationship graph queries.
Evaluate & Prototype
Solo developers and compliance evaluators. No credit card required.
/v1/entity/lookupSingle entity lookup/healthAPI health checkScaling Compliance Teams
Series A–B fintechs, boutique funds, and growing compliance operations.
/v1/entity/lookupSingle entity lookup/v1/entity/batch_lookupUp to 100 entities per request/v1/entity/{id}Fetch by stable entity IDInstitutional Compliance
Financial institutions, M&A due diligence teams, and regulated entities. 99.95% SLA.
/v1/entity/lookupSingle entity lookup/v1/entity/batch_lookupUp to 100 entities per request/v1/entity/{id}Fetch by stable entity ID/v1/entity/{id}/relationshipsOfficer and ownership graphTier-1 Banks, Funds & Large-Scale Compliance
Unlimited lookups. Dedicated infrastructure. Raw data export and bulk feeds. SOC 2 documentation on request. Dedicated account manager. Custom SLA and compliance guarantees.
POST /v1/entity/bulk Bulk file upload + async processingAuthentication
All API requests must include a valid API key in the x-api-key header. Keys are scoped to an organization and carry tier-level entitlements. Live keys begin with nar_live_; test keys with nar_test_.
Security
Never expose your API key in client-side code or source control. Use environment variables or a secrets manager. If a key is compromised, revoke it immediately via the dashboard or admin API.
nar_live_{8-char-prefix}_{32-char-secret}The prefix is safe to log. Only the full key authenticates. Store the full key once — it is never shown again.
curl -X POST "https://api.naralin.ai/v1/entity/lookup" \
-H "x-api-key: nar_live_a8b3c2d1_bQ7zR4pL9wE3nH..." \
-H "Content-Type: application/json" \
-d '{"name": "Huawei Technologies", "jurisdiction": "cn"}'Request Headers
| Header | Required | Value |
|---|---|---|
x-api-key | required | Your API key. Format: nar_live_... |
Content-Type | POST only | application/json |
x-request-id | optional | Your trace/correlation ID. Echoed back in response for debugging. |
x-api-key: nar_live_... — Primary method for server-to-server integration. Preferred for production use.Authorization: Bearer <token> — Auth0 JWTs for dashboard and browser-based flows. Scoped per user session.Sandbox Keys
Use a nar_test_ key to integrate and test without a live account. Sandbox keys bypass the database entirely and return deterministic fixture responses based on the entity name you pass. No signup, no quota consumed, no data stored.
Zero setup required
Pass x-api-key: nar_test_demo_00000000_aaaabbbbccccdddd0000000000000001 to any entity endpoint right now. The official SDKs ship with this key pre-configured for sandbox use.
| Name keyword in query | Fixture returned | Risk tier |
|---|---|---|
"sanctioned" or "darkside" or "sanctions_primary" | SANCTIONS_LIST_A + EU sanctions, offshore jurisdiction | CRITICAL |
"pep" or "politician" or "minister" | Tier 1 PEP (Head of State / Senior Official) | MEDIUM |
"debarred" or "development_bank" or "mdb" | Development bank cross-debarment | HIGH |
"notfound" or "unknown" | Entity not found (found: false) | — |
anything else | Clean entity, no flags | LOW |
"color:#565f89"># Sandbox: returns a CRITICAL-risk entity with active sanctions
curl -X POST "https://api.naralin.ai/v1/entity/lookup" \
-H "x-api-key: nar_test_demo_00000000_aaaabbbbccccdddd0000000000000001" \
-H "Content-Type: application/json" \
-d '{"name": "sanctioned corp"}'Rate Limits
The API enforces monthly lookup quotas per organization. Limits are tied to your subscription tier. Batch lookups count by entity, not by request. Exceeding your quota returns a structured 429. The official SDKs automatically retry with exponential backoff.
| Tier | Monthly Lookups | Batch | SLA |
|---|---|---|---|
| Sandbox | 500 | — | Best-effort |
| Growth | 25,000 | ✓ up to 100 | 99.5% |
| Professional | 250,000 | ✓ up to 100 | 99.95% |
| Enterprise | Unlimited | ✓ Bulk async | Custom |
Per-Minute Limit
In addition to monthly quotas, every /v1/entity/* endpoint enforces a per-minute request ceiling per API key. Exceeding it returns a 429 rate_limit_exceeded with a Retry-After: 60 header. The official SDKs handle this automatically via exponential backoff.
| Tier | Requests / minute | Window |
|---|---|---|
| Sandbox | 20 | 60s sliding |
| Growth | 120 | 60s sliding |
| Professional | 600 | 60s sliding |
| Enterprise | Custom | Custom |
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
{
"error": {
"code": "rate_limit_exceeded",
"message": "Too many requests. Retry after 60 seconds.",
"retry_after": 60
}
}Rate Limit Response Headers
Every response from a quota-checked endpoint includes the following headers so your application can monitor usage without polling.
| Header | Example | Description |
|---|---|---|
X-RateLimit-Limit | 10000 | Total monthly lookups for your plan |
X-RateLimit-Remaining | 9857 | Lookups remaining this month |
X-RateLimit-Used | 143 | Lookups consumed this month |
X-RateLimit-Reset | 2026-04-01 | ISO 8601 date quota resets (1st of next month) |
x-request-id | req_8f3a9c2d... | Unique request ID — include in support tickets |
Retry-After | 30 | Seconds until safe to retry (on 429 responses only) |
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 0
X-RateLimit-Used: 500
X-RateLimit-Reset: 2026-04-01
x-request-id: req_8f3a9c2d4e5b6c7a
Retry-After: 864000
{
"error": {
"code": "quota_exceeded",
"message": "Monthly lookup limit of 500 reached. Resets on 2026-04-01.",
"limit": 500,
"used": 500,
"reset_at": "2026-04-01",
"request_id": "req_8f3a9c2d4e5b6c7a",
"doc_url": "https://docs.naralin.ai/errors/quota_exceeded"
}
}Batch lookups are charged by entity count, not request count. A single POST /v1/entity/batch_lookup with 50 entities consumes 50 lookups from your monthly quota.
Error Codes
All errors return a consistent structured body. Every error includes a machine-readable code, a human-readable message, a request_id for support, and a doc_url linking to this reference.
{
"error": {
"code": "authentication_error",
"message": "Invalid or missing API key. Provide a valid nar_live_... key.",
"request_id": "req_8f3a9c2d4e5b6c7a",
"doc_url": "https://docs.naralin.ai/errors/authentication_error"
}
}| HTTP | error.code | Cause & Resolution |
|---|---|---|
| 200 | — | Request successful. Inspect found field on entity responses. |
| 400 | invalid_request | Malformed request body or invalid parameters. Check error.param for the offending field. |
| 401 | authentication_error | Missing or invalid API key. Verify your nar_live_... key is set correctly. |
| 403 | permission_denied | Authenticated but not authorized. Your plan does not include this endpoint. |
| 404 | not_found | Entity ID not found. Use lookup or batch_lookup to discover entities first. |
| 429 | quota_exceeded | Monthly lookup limit reached. See error.limit, error.used, error.reset_at. Upgrade your plan. |
| 429 | rate_limit_exceeded | Too many requests in a short window. Respect Retry-After header. |
| 500 | api_error | Server-side error. Retry with exponential backoff. Include request_id in support tickets. |
| 503 | service_unavailable | Database or upstream dependency temporarily unavailable. Retry after a few seconds. |
{
"error": {
"code": "quota_exceeded",
"message": "Monthly lookup limit of 500 reached. Resets on 2026-04-01.",
"limit": 500,
"used": 500,
"reset_at": "2026-04-01",
"request_id": "req_8f3a9c2d4e5b6c7a",
"doc_url": "https://docs.naralin.ai/errors/quota_exceeded"
}
}Entity Lookup
Fuzzy-match a single entity by name against 100M+ records. Returns a composite risk score, full signal breakdown, offshore leak appearances, and sanctions history. Available on all tiers.
/v1/entity/lookupRequest Body
| Field | Type | Description |
|---|---|---|
namerequired | string | Entity name to search. Min 2 chars, max 500 chars. |
jurisdiction | string | ISO-2 country code to narrow search. Boosts matching candidates from this jurisdiction. |
current_only | boolean | If true (default), only return currently active officers and sanctions. Set to false to include historical records. |
Example Request
curl -X POST "https://api.naralin.ai/v1/entity/lookup" \
-H "x-api-key: nar_live_a8b3c2d1_bQ7zR4pL9wE3nH..." \
-H "Content-Type: application/json" \
-d '{"name": "Mossack Fonseca", "jurisdiction": "PA"}'Response
{
"entity_id": "ent_7f3a9c2d",
"canonical_name": "Mossack Fonseca & Co.",
"entity_type": "company",
"risk_score": 94,
"risk_level": "CRITICAL",
"has_active_sanctions": false,
"is_pep": false,
"jurisdictions": ["PA"],
"offshore_leak_appearances": [
{
"dataset": "panama_papers",
"role": "intermediary",
"entity_count": 11516
}
],
"sanctions": [],
"signals": [
{
"type": "OFFSHORE_LEAK",
"severity": "HIGH",
"label": "Intermediary in Panama Papers (11,516 clients)",
"source": "LEAK_DB"
}
],
"officers": [],
"identifiers": []
}Data Pipeline
Batch Lookup
Screen up to 100 entities in a single request. Each query is processed independently and returns a full entity record. Ideal for bulk onboarding KYC, M&A target screening, and batch watchlist checks.
/v1/entity/batch_lookupRequest Body
| Field | Type | Description |
|---|---|---|
entitiesrequired | object[] | Array of entity objects. Max 100 per request. |
entities[].namerequired | string | Entity name to search. Min 2 chars, max 500 chars. |
entities[].jurisdiction | string | ISO-2 country code to narrow search (optional) |
Example Request
curl -X POST "https://api.naralin.ai/v1/entity/batch_lookup" \
-H "x-api-key: nar_live_a8b3c2d1_bQ7zR4pL9wE3nH..." \
-H "Content-Type: application/json" \
-d '{"entities": [{"name": "Mossack Fonseca"}, {"name": "Viktor Bout"}, {"name": "Huawei Technologies"}]}'Example Response
{
"results": [
{ "found": true, "entity_id": "ent_7f3a9c2d", "canonical_name": "Mossack Fonseca & Co.", "risk_score": 94, "risk_level": "CRITICAL", "has_active_sanctions": false, ... },
{ "found": true, "entity_id": "ent_2a8f1e9b", "canonical_name": "Viktor Bout", "risk_score": 100, "risk_level": "CRITICAL", "has_active_sanctions": true, ... },
{ "found": true, "entity_id": "ent_5c3d7f2a", "canonical_name": "Huawei Technologies Co., Ltd.", "risk_score": 72, "risk_level": "HIGH", "has_active_sanctions": true, ... }
],
"processed_count": 3,
"found_count": 3,
"not_found_count": 0,
"error_count": 0,
"batch_ms": 127
}| Field | Type | Description |
|---|---|---|
resultsrequired | LookupResponse[] | Array of full entity records, one per input entity. Same schema as single lookup. |
processed_countrequired | integer | Total items attempted (always == len(entities) in request). |
found_countrequired | integer | Number of items where found=true (matched an entity). |
not_found_countrequired | integer | Items where found=false and no exception occurred. |
error_countrequired | integer | Items that raised an exception during resolution (still returned as found=false). |
batch_msrequired | integer | Total server-side processing time for the entire batch in milliseconds. |
Get Entity
Fetch a full entity record by stable entity_id. Does not count against your monthly lookup quota. Use this to retrieve cached results after an initial lookup.
/v1/entity/{id}Path Parameters
| Field | Type | Description |
|---|---|---|
idrequired | string | Stable entity ID returned from lookup (e.g. ent_7f3a9c2d) |
Example Request
curl -X GET "https://api.naralin.ai/v1/entity/ent_7f3a9c2d?current_only=true" \
-H "x-api-key: nar_live_a8b3c2d1_bQ7zR4pL9wE3nH..."Health Check
Returns 200 OK when all API systems are operational. No authentication required. Use for monitoring and dependency health checks.
/healthcurl -X GET "https://api.naralin.ai/health"Example Response
{ "ok": true, "service": "naralin-api", "version": "1.4.0" }Readiness Probe
Returns 200 OK when all backend dependencies are connected. Returns 503 if any dependency is unreachable. No authentication required.
/readycurl -X GET "https://api.naralin.ai/ready"Example Response
{
"ok": true,
"service": "naralin-api",
"version": "1.4.0",
"databases": {
"data_store": true,
"metadata_store": true
},
"auth0_config_present": true
}Use /health for liveness probes (cheap, no DB calls). Use /ready for readiness probes (verifies all dependencies). Both are unauthenticated.
Response Schema
All entity lookup endpoints return the same LookupResponse schema. Fields may be null or absent when data is unavailable — always check found before accessing entity fields.
{
"found": true,
"entity_id": "ent_7f3a9c2d4e5b6c7a",
"canonical_name": "Darkside Capital Holdings Ltd",
"confidence": 0.97,
"match_type": "exact",
"entity_type": "company",
"status": "active",
"jurisdictions": ["CY", "RU"],
"identifiers": { "corporate_registry": "12345678", "lei": "2138004JGFQ0IM5HKA92" },
"has_active_sanctions": true,
"active_sanctions_count": 3,
"has_pep_exposure": false,
"sanctions": [
{ "list_name": "SANCTIONS_LIST_A", "start_date": "2019-04-15", "end_date": null, "reason": "WMD proliferation" },
{ "list_name": "SANCTIONS_LIST_B", "start_date": "2020-01-10", "end_date": null, "reason": "Terrorism financing" }
],
"has_active_debarment": false,
"debarments": [],
"officers": [
{ "name": "Alexei Volkov", "role": "Director", "source": "corporate_registry",
"start_date": "2019-01-15T00: 00: 00Z", "end_date": null, "ownership_pct": 47.3 }
],
"beneficial_owners": [],
"ubo_chain": [
{ "entity_id": "ent_cc98d7e2", "name": "Cyprus Holdings Ltd", "entity_type": "company",
"role": "Shareholder", "depth": 1, "direct_ownership_pct": 33.1,
"effective_ownership_pct": 33.1, "is_individual": false,
"is_pep": false, "has_active_sanctions": false,
"ownership_path": ["ent_7f3a9c2d4e5b6c7a", "ent_cc98d7e2"], "source": "corporate_registry" }
],
"ubo_chain_depth": 1,
"ubo_resolved": false,
"shell_indicators": ["offshore_jurisdiction", "nominee_director"],
"jurisdiction_risk": [
{ "code": "CY", "name": "Cyprus", "jurisdiction_risk_status": null, "is_offshore": true,
"offshore_tier": 2, "is_eu_high_risk": false, "risk_contribution": 12,
"risk_labels": ["offshore_jurisdiction"] }
],
"related_entities": [
{ "entity_id": "ent_aa11bb22", "canonical_name": "Darkside Capital Group",
"entity_type": "company", "risk_score": 88, "risk_level": "HIGH",
"relationship_type": "parent", "direction": "parent", "weight": 1.0, "source_name": "legal_entity_ids" }
],
"hops_to_high_risk": 0,
"risk_score": 94,
"risk_level": "CRITICAL",
"risk_factors": [
"Active sanctions: SANCTIONS_LIST_A, SANCTIONS_LIST_B",
"Offshore jurisdiction: Cyprus (tier 2)",
"Shell structure indicators: nominee director"
],
"primary_source_types": ["sanctions", "corporate_registry"],
"sources": ["sanctions_primary", "sanctions_regional", "corporate_registry"],
"lookup_ms": 47
}| Field | Type | Description |
|---|---|---|
found | boolean | Always present. True if an entity matched. Check this before accessing any other field. |
entity_id | string | null | Stable UUID. Format: "ent_" + 16 hex chars. Safe to store and use with GET /v1/entity/{id}. |
canonical_name | string | null | Normalized authoritative name from the highest-confidence source. |
confidence | float | Match confidence 0.0–1.0. Above 0.90 = exact or alias match. |
match_type | string | null | "exact", "alias", or "fuzzy". |
| Field | Type | Description |
|---|---|---|
entity_type | string | null | "person", "company", "organization", "vessel", "aircraft", or "facility". |
status | string | null | "active", "dissolved", "inactive", or null if unknown. |
jurisdictions | string[] | ISO-3166-1 alpha-2 codes for all associated jurisdictions. |
identifiers | object | External IDs keyed by source: corporate_registry, lei, cik, vat, etc. |
description | string | null | Short description when available from the source. |
| Field | Type | Description |
|---|---|---|
has_active_sanctions | boolean | True if any sanctions list entry is currently active (end_date is null). |
active_sanctions_count | integer | Count of distinct active sanction entries across all lists. |
has_pep_exposure | boolean | Entity is or is directly connected to a politically exposed person. |
sanctions[].list_name | string | Canonical list name: SANCTIONS_LIST_A, SANCTIONS_LIST_B, SANCTIONS_LIST_C, SANCTIONS_LIST_D, etc. |
sanctions[].start_date | string | null | ISO 8601 date designation became effective. |
sanctions[].end_date | string | null | ISO 8601 date designation was lifted. Null = currently active. |
sanctions[].reason | string | Basis for designation as stated in the source list. |
| Field | Type | Description |
|---|---|---|
has_active_debarment | boolean | Formal exclusion from public procurement or MDB financing is currently active. |
debarments[].program | string | Canonical key: "gov_procurement", "mdb_primary", "mdb_africa", "mdb_cross", etc. |
debarments[].program_name | string | Human-readable program name. |
debarments[].active | boolean | True if end_date is in the future or null. |
debarments[].is_cross_debarment | boolean | Eligible for enforcement across multiple MDB signatories. |
| Field | Type | Description |
|---|---|---|
officers[].name | string | Officer full name. |
officers[].role | string | Director, Secretary, CEO, CFO, PSC, Shareholder, etc. |
officers[].ownership_pct | float | null | Stated ownership percentage. Null for non-ownership roles. |
beneficial_owners[] | Officer[] | Subset of officers identified as beneficial owners. |
ubo_chain[] | UboChainEntry[] | Multi-hop traversal of the ownership graph. depth=1 = direct owner; deeper = intermediary. |
ubo_chain[].effective_ownership_pct | float | null | Product of all ownership percentages along the path from queried entity to this node. |
ubo_chain[].is_individual | boolean | True if this is a natural person — the terminal UBO in the chain. |
ubo_chain_depth | integer | Maximum depth reached in the UBO traversal. |
ubo_resolved | boolean | True if at least one natural-person UBO was found. |
| Field | Type | Description |
|---|---|---|
shell_indicators[] | string[] | "offshore_jurisdiction", "nominee_director", "single_purpose_structure", "circular_ownership", "bearer_shares". |
jurisdiction_risk[].jurisdiction_risk_status | string | null | "black" = regulatory blacklist, "grey" = greylist, null = no designation. |
jurisdiction_risk[].offshore_tier | integer | null | 1 = extreme risk (BVI, Cayman), 2 = moderate risk (Cyprus, Seychelles). |
jurisdiction_risk[].is_eu_high_risk | boolean | On EU AML high-risk third countries list. |
jurisdiction_risk[].risk_contribution | integer | Points contributed to the overall risk_score from this jurisdiction. |
hops_to_high_risk | integer | null | Ownership hops to the nearest HIGH/CRITICAL ancestor. Null if none found. |
| Field | Type | Description |
|---|---|---|
related_entities[].relationship_type | string | "parent", "subsidiary", "shareholder", "intermediary", "officer_of". |
related_entities[].direction | string | "parent" = owns queried entity; "child" = owned by queried entity. |
related_entities[].risk_level | string | Risk level of the related entity — surface network risk. |
related_entities[].weight | float | null | Relationship strength 0.0–1.0. Higher = more significant connection. |
| Field | Type | Description |
|---|---|---|
risk_score | integer | Composite 0–100. 0–19: NONE, 20–39: LOW, 40–59: MEDIUM, 60–79: HIGH, 80–100: CRITICAL. |
risk_level | string | "NONE", "LOW", "MEDIUM", "HIGH", or "CRITICAL". |
risk_factors[] | string[] | Plain-English explanations of each factor contributing to the score. |
| Field | Type | Description |
|---|---|---|
primary_source_types[] | string[] | "sanctions", "corporate_registry", "offshore_leaks", "pep", "debarment". |
sources[] | string[] | Specific source IDs: sanctions_primary, legal_entity_ids, offshore_leaks, corporate_registry, sanctions_aggregated, etc. |
lookup_ms | integer | Server-side resolution time in milliseconds. |
Error Handling
Both SDKs expose a typed error class hierarchy. Catch specific errors to handle quota limits, auth failures, and transient errors with precise recovery logic.
NaralinError (base) ├── AuthenticationError # 401 — invalid or missing API key ├── PermissionDeniedError # 403 — endpoint not on your plan ├── InvalidRequestError # 400 — malformed body or param │ └── .param # name of the offending field ├── NotFoundError # 404 — entity_id does not exist ├── QuotaExceededError # 429 — monthly limit reached │ ├── .limit # total monthly allowance │ ├── .used # lookups consumed this month │ └── .reset_at # ISO 8601 date quota resets ├── RateLimitError # 429 — per-minute limit exceeded │ └── .retry_after # seconds until safe to retry ├── APIError # 500 — server-side error ├── ServiceUnavailableError # 503 — dependency temporarily down ├── ConnectionError # network failure before response └── TimeoutError # request exceeded timeout
import naralin
client = naralin.Client() "color:#565f89"># reads NARALIN_API_KEY
try:
result = client.entity.lookup("Acme Corp", jurisdiction="gb")
print(f"Risk: {result.risk_level} ({result.risk_score}/100)")
except naralin.AuthenticationError as e:
print(f"Auth failed: {e.message}")
except naralin.QuotaExceededError as e:
"color:#565f89"># Monthly limit exhausted — do not retry
print(f"Quota: {e.used}/{e.limit} — resets {e.reset_at}")
except naralin.RateLimitError as e:
"color:#565f89"># SDK retries automatically; only raised after max retries
import time; time.sleep(e.retry_after)
except naralin.NaralinError as e:
print(f"API error [{e.error_code}]: {e.message}")import Naralin, {
AuthenticationError, QuotaExceededError, RateLimitError, NaralinError,
} from 'naralin';
const client = new Naralin();
try {
const result = await client.entity.lookup({ name: 'Acme Corp', jurisdiction: 'gb' });
console.log(`Risk: ${result.risk_level} (${result.risk_score}/100)`);
} catch (err) {
if (err instanceof AuthenticationError) {
console.error('Auth failed:', err.message);
} else if (err instanceof QuotaExceededError) {
console.error(`Quota: ${err.used}/${err.limit} — resets ${err.resetAt}`);
} else if (err instanceof RateLimitError) {
console.error(`Rate limited, retry after ${err.retryAfter}s`);
} else if (err instanceof NaralinError) {
console.error(`API error [${err.code}]: ${err.message}`);
}
}Both SDKs automatically retry 500, 502, 503, and 429 rate_limit_exceeded with exponential backoff (base 0.5s, max 10s, 20% jitter) up to 2 retries. QuotaExceededError is never retried — it requires upgrading your plan or waiting for the monthly reset.
API Key Management
Create, list, and revoke API keys programmatically via the admin endpoint. Requires a key with owner or admin role.
/v1/admin/api_keysCreate a new API key. The full secret is returned exactly once — store it immediately.
curl -X POST "https://api.naralin.ai/v1/admin/api_keys" \
-H "x-api-key: nar_live_a8b3c2d1_..." \
-H "Content-Type: application/json" \
-d '{"label": "production-server"}'{
"key_id": "key_4f2a9e3b",
"label": "production-server",
"api_key": "nar_live_4f2a9e3b_xK9mR2pL7wE4nH8vQ1tJ5sU6...",
"created_at": "2026-03-21T14: 30: 00Z",
"last_used_at": null
}/v1/admin/api_keysList all keys for your organization. Key secrets are never returned after initial creation.
curl "https://api.naralin.ai/v1/admin/api_keys" \
-H "x-api-key: nar_live_a8b3c2d1_..."/v1/admin/api_keys/{key_id}/revokeRevoke a key immediately. Irreversible. Requests using the revoked key return 401 within seconds.
| Field | Type | Description |
|---|---|---|
key_idrequired | UUID | API key ID to revoke (from key creation or list response). |
curl -X POST "https://api.naralin.ai/v1/admin/api_keys/key_4f2a9e3b/revoke" \
-H "x-api-key: nar_live_a8b3c2d1_..."Response
{ "revoked": true, "api_key_id": "key_4f2a9e3b" }Python SDK
Official Python client with typed models, auto-retry, and the full error class hierarchy. Requires Python 3.9+ and httpx.
pip install naralin
export NARALIN_API_KEY=nar_live_...Client Initialization
import naralin
"color:#565f89"># Reads NARALIN_API_KEY from environment automatically
client = naralin.Client()
"color:#565f89"># Explicit configuration
client = naralin.Client(
api_key="nar_live_...",
timeout=30.0, "color:#565f89"># seconds (default: 30)
max_retries=2, "color:#565f89"># auto-retry count (default: 2)
base_url="https://api.naralin.ai",
)Single Entity Lookup
result = client.entity.lookup("Revolut Ltd", jurisdiction="gb")
if result.found:
print(f"Name: {result.canonical_name}")
print(f"Risk: {result.risk_level} ({result.risk_score}/100)")
print(f"Sanctioned: {result.has_active_sanctions}")
print(f"PEP: {result.has_pep_exposure}")
print(f"Confidence: {result.confidence:.0%} ({result.match_type})")
"color:#565f89"># Convenience properties
if result.is_clean:
print("Status: CLEAN — auto-approve eligible")
elif result.is_high_risk:
print("Status: HIGH RISK — reject or escalate")
elif result.requires_review:
print("Status: REVIEW — enhanced due diligence")
"color:#565f89"># Sanctions detail
for sanction in result.sanctions:
print(f" {sanction.list_name}: {sanction.reason}")
else:
print("Not found — manual review recommended")Batch Lookup
batch = client.entity.batch_lookup([
{"name": "Darkside Capital Holdings Ltd", "jurisdiction": "cy"},
{"name": "Viktor Petrov"},
{"name": "Nordic Clean Energy AS", "jurisdiction": "no"},
"Elena Kasparova", "color:#565f89"># plain string shorthand
])
print(f"Screened: {batch.processed_count} Found: {batch.found_count}")
for result in batch:
if not result.found:
continue
flag = "🚨" if result.is_high_risk else ("⚠️ " if result.requires_review else "✅")
print(f" {flag} {result.canonical_name} — {result.risk_level}")
if result.sanctioned_lists:
print(f" Sanctions: {', '.join(result.sanctioned_lists)}")
"color:#565f89"># Access flagged entities directly
print(f"\nFlagged ({len(batch.flagged)}):")
for result in batch.flagged:
print(f" {result.canonical_name}: {', '.join(result.risk_factors)}")EntityResult Properties
| Property | Type | Description |
|---|---|---|
is_clean | bool | No sanctions, no debarment, no PEP exposure, risk NONE or LOW |
is_high_risk | bool | risk_level HIGH or CRITICAL, or any active sanctions or debarments |
requires_review | bool | MEDIUM risk or PEP exposure — not clean, not high-risk |
sanctioned_lists | list[str] | Names of all currently active sanctions lists |
TypeScript SDK
Official TypeScript/JavaScript SDK. Full type definitions, Promise-based, zero external dependencies (native fetch). Compatible with Node.js 18+, edge runtimes, Deno, and Bun.
npm install naralin
"color:#565f89"># yarn add naralin / pnpm add naralin
export NARALIN_API_KEY=nar_live_...Client Initialization
import Naralin from 'naralin';
"color:#565f89">// Reads process.env.NARALIN_API_KEY automatically
const client = new Naralin();
"color:#565f89">// Explicit configuration
const client = new Naralin({
apiKey: 'nar_live_...',
timeout: 30_000, "color:#565f89">// ms (default: 30_000)
maxRetries: 2, "color:#565f89">// auto-retry count (default: 2)
});Single Entity Lookup
const result = await client.entity.lookup({ name: 'Revolut Ltd', jurisdiction: 'gb' });
if (result.found) {
console.log(`Name: ${result.canonical_name}`);
console.log(`Risk: ${result.risk_level} (${result.risk_score}/100)`);
console.log(`Sanctioned: ${result.has_active_sanctions}`);
console.log(`PEP: ${result.has_pep_exposure}`);
console.log(`Confidence: ${(result.confidence * 100).toFixed(0)}% (${result.match_type})`);
"color:#565f89">// Convenience getters
if (result.isClean) {
console.log('Status: ✅ CLEAN');
} else if (result.isHighRisk) {
console.log('Status: 🚨 HIGH RISK');
console.log('Sanctions:', result.sanctionedLists.join(', '));
} else if (result.requiresReview) {
console.log('Status: ⚠️ REVIEW');
}
} else {
console.log('Not found — manual review recommended');
}Batch Lookup
const batch = await client.entity.batchLookup({
entities: [
{ name: 'Darkside Capital Holdings Ltd', jurisdiction: 'cy' },
{ name: 'Viktor Petrov' },
{ name: 'Nordic Clean Energy AS', jurisdiction: 'no' },
{ name: 'Elena Kasparova', jurisdiction: 'de' },
],
});
console.log(`Screened: ${batch.processed_count} Found: ${batch.found_count}`);
for (const r of batch) {
if (!r.found) continue;
const flag = r.isHighRisk ? '🚨' : r.requiresReview ? '⚠️ ' : '✅';
console.log(` ${flag} ${r.canonical_name} — ${r.risk_level}`);
if (r.sanctionedLists.length) {
console.log(` Sanctions: ${r.sanctionedLists.join(', ')}`);
}
}
"color:#565f89">// Access flagged/clean directly
for (const r of batch.flagged) {
console.log(`FLAGGED: ${r.canonical_name} — ${r.risk_factors.join(', ')}`);
}EntityResult Getters
| Getter | Type | Description |
|---|---|---|
isClean | boolean | No sanctions, no debarment, no PEP exposure, risk NONE or LOW |
isHighRisk | boolean | risk_level HIGH/CRITICAL, or active sanctions or debarments |
requiresReview | boolean | MEDIUM risk or PEP exposure |
sanctionedLists | string[] | Names of all currently active sanctions lists |
Integration Patterns
Real-world patterns for common compliance and onboarding workflows.
KYC Onboarding Gate
Screen a customer at onboarding. Auto-approve low-risk entities, flag MEDIUM risk for enhanced due diligence, reject sanctioned and high-risk entities.
from dataclasses import dataclass
from typing import Optional
import naralin
client = naralin.Client()
@dataclass
class OnboardingDecision:
approved: bool
reason: str
entity_id: Optional[str]
risk_level: str
flags: list[str]
def screen_for_onboarding(name: str, jurisdiction: str) -> OnboardingDecision:
try:
result = client.entity.lookup(name, jurisdiction=jurisdiction)
except naralin.QuotaExceededError as e:
return OnboardingDecision(
approved=False, reason=f"Screening unavailable (resets {e.reset_at})",
entity_id=None, risk_level="UNKNOWN", flags=["screening_unavailable"],
)
if not result.found:
return OnboardingDecision(
approved=False, reason="Entity not found — manual review required",
entity_id=None, risk_level="UNKNOWN", flags=["not_found"],
)
if result.has_active_sanctions:
return OnboardingDecision(
approved=False,
reason=f"Active sanctions: {', '.join(result.sanctioned_lists)}",
entity_id=result.entity_id, risk_level=result.risk_level,
flags=result.risk_factors,
)
if result.has_active_debarment:
return OnboardingDecision(
approved=False, reason="Active debarment from procurement programs",
entity_id=result.entity_id, risk_level=result.risk_level,
flags=result.risk_factors,
)
if result.is_high_risk:
return OnboardingDecision(
approved=False, reason=f"Risk level {result.risk_level} exceeds threshold",
entity_id=result.entity_id, risk_level=result.risk_level,
flags=result.risk_factors,
)
if result.requires_review:
return OnboardingDecision(
approved=False, reason="Enhanced due diligence required",
entity_id=result.entity_id, risk_level=result.risk_level,
flags=result.risk_factors,
)
return OnboardingDecision(
approved=True, reason="Passed automated screening",
entity_id=result.entity_id, risk_level=result.risk_level, flags=[],
)Python Framework Dependency
Inject entity screening as a framework dependency. Any route that accepts a counterparty name gets automatic screening.
from framework import App, Depends, HTTPException
from pydantic import BaseModel
import naralin
app = App()
naralin_client = naralin.Client()
class CounterpartyRequest(BaseModel):
company_name: str
jurisdiction: str | None = None
async def require_clean_entity(req: CounterpartyRequest):
"color:#9ece6a">"""Dependency: screen the counterparty and reject high-risk entities."""
try:
result = naralin_client.entity.lookup(
req.company_name, jurisdiction=req.jurisdiction
)
except naralin.QuotaExceededError:
raise HTTPException(503, detail="Screening temporarily unavailable")
if not result.found:
raise HTTPException(422, detail="Entity could not be verified")
if result.has_active_sanctions or result.is_high_risk:
raise HTTPException(403, detail=f"Counterparty rejected: {result.risk_level} risk")
return result
@app.post("/onboard")
async def onboard_customer(
body: CounterpartyRequest,
entity = Depends(require_clean_entity),
):
return {"status": "approved", "entity_id": entity.entity_id, "risk_level": entity.risk_level}Next.js Server Action
Screen entities in a Next.js App Router server action. Runs server-side — your API key is never exposed to the browser.
'use server';
import Naralin, { QuotaExceededError, NaralinError } from 'naralin';
const client = new Naralin(); "color:#565f89">// reads process.env.NARALIN_API_KEY
export async function screenEntity(formData: FormData) {
const name = formData.get('company_name') as string;
const jurisdiction = formData.get('jurisdiction') as string | undefined;
try {
const result = await client.entity.lookup({ name, jurisdiction });
if (!result.found) {
return { status: 'not_found' as const };
}
if (result.isHighRisk) {
return {
status: 'rejected' as const,
riskLevel: result.risk_level,
factors: result.risk_factors,
sanctions: result.sanctionedLists,
};
}
if (result.requiresReview) {
return { status: 'review' as const, riskLevel: result.risk_level, entityId: result.entity_id };
}
return { status: 'approved' as const, entityId: result.entity_id, riskLevel: result.risk_level };
} catch (err) {
if (err instanceof QuotaExceededError) {
return { status: 'error' as const, message: `Screening unavailable — resets ${err.resetAt}` };
}
if (err instanceof NaralinError) {
return { status: 'error' as const, message: err.message };
}
throw err;
}
}Risk Scoring Methodology
Every entity receives a composite risk_score (0–100) computed from 8 independent dimensions. Each contribution is traceable: the risk_factors array on the response contains a human-readable label for every factor that contributed to the score. The total is capped at 100.
Design Philosophy
Every risk contribution is deterministic and auditable. Ownership-weighted propagation ensures a 5% shareholder contributes proportionally less risk than a 100% parent. Temporal awareness means historical sanctions decay over time. The engine never uses black-box ML — every point is explainable.
| Score Range | Level | Recommended Action |
|---|---|---|
| 0–29 | LOW | Auto-approve eligible. No significant risk signals. |
| 30–59 | MEDIUM | Enhanced due diligence recommended. PEP exposure or offshore signals. |
| 60–79 | HIGH | Manual review required. Active debarments, offshore leak listings, or opaque structures. |
| 80–100 | CRITICAL | Reject or escalate. Active sanctions, multi-list exposure, or combination of severe factors. |
+80 | Active sanctions match (any program) |
+10 ea | Each additional active sanctions list (capped at +20 bonus) |
+18 | Historical sanctions, cleared < 2 years ago |
+10 | Historical sanctions, cleared 2–5 years ago |
+5 | Historical sanctions, cleared > 5 years ago |
+50 | PEP Tier 1 — Head of State / Government / Minister-level |
+35 | PEP Tier 2 — Legislator / Senior Military / Senior Judiciary |
+20 | PEP Tier 3 — Regional Official / Ambassador / State Enterprise Board |
+15 | PEP (tier unclassified) |
+12 | Relative or Close Associate (RCA) of a PEP |
+10 | Former PEP (cleared / left office) |
+35 | Multilateral development bank active debarment |
+30 | US Federal SAM/EPLS active exclusion |
+25 | MDB active debarment (IADB / AfDB / ADB / EBRD) |
+20 | EU / UK procurement debarment |
+5 | Cross-debarment corroboration bonus (≥2 MDB programs) |
~30% | Weight reduction for historical (expired) debarments |
+20 | Panama Papers / Pandora Papers appearance |
+18 | Suspicious activity report appearance |
+15 | Paradise Papers / Offshore Leaks appearance |
+10 | Bahamas Leaks / other datasets |
+30 | Blacklisted jurisdiction |
+15 | Greylisted jurisdiction |
+15 | Offshore secrecy Tier 1 (BVI, Cayman, Marshall Islands) |
+8 | Offshore secrecy Tier 2 (Luxembourg, Singapore, Cyprus) |
+10 | EU high-risk third country list |
+5 | Corroboration bonus (grey + offshore overlap) |
cap +40 | Maximum total contribution from jurisdiction |
+25 | CRITICAL parent at 100% ownership (proportionally scaled) |
+15 | HIGH parent at 100% ownership (proportionally scaled) |
+8 | MEDIUM parent at 100% ownership (proportionally scaled) |
cap +30 | Maximum total contribution from network risk |
+10 | Nominee structure detected |
+8 | Ownership chain ≥ 3 layers deep |
+5 | UBO not resolved to a natural person |
+5 | SPV / holding company language in description |
cap +20 | Maximum total contribution from shell indicators |
+5 | Entity is dissolved, inactive, liquidated, expired, or annulled |
Entity Resolution
The resolution engine uses a multi-tier deterministic algorithm to match search queries to entities. It's designed for precision over recall — it will return found: false rather than risk returning the wrong entity. A false negative is a minor inconvenience. A false positive in compliance screening is a catastrophe.
Determinism Guarantee
Same input always produces same output. All candidate ranking uses explicit multi-key sorting (score → tier → name length → UUID). Tie-breaks are deterministic. Unstable iteration order is never relied upon.
| Tier | Match Type | Confidence | Description |
|---|---|---|---|
1 | Exact canonical | 1.0 | Normalized canonical_name matches the search term exactly |
2 | Exact alias | 1.0 | Normalized alias matches the search term exactly |
3 | Token-anchor | Scored | Search term appears as a full token in the candidate + high fuzzy score |
4 | Fuzzy | Scored | Fuzzy match with token-overlap validation (min 50% token overlap) |
— | Rejected | 0.0 | No meaningful overlap — returns found: false |
85.0 — Candidates scoring below this threshold are rejected outright5.0 — If the top two non-sibling candidates are within 5 points, the match is rejected to prevent false positives50% — For short queries (≤2 tokens), at least 50% of search tokens must appear as full tokens in the candidate+8.0 — Candidates matching the requested jurisdiction receive a scoring bonus+10.0 — Candidates where the search term appears as a complete token receive a precision bonusBefore scoring, all names undergo: Unicode NFKD normalization → lowercase → punctuation removal → whitespace collapse. Company suffixes (Ltd, Inc, GmbH, Pty, etc.) are optionally stripped for comparison. Person names are never suffix-stripped.
The engine uses rapidfuzz.fuzz.WRatio for fuzzy scoring — a weighted ratio that handles token transposition and partial matches effectively.
Data Sources
Naralin ingests from 500+ authoritative data sources. All sources are refreshed on deterministic schedules and versioned. The sources[] field on every entity response tells you exactly which sources contributed to that record.
| Source | Description | Refresh |
|---|---|---|
U.S. SDN Lists | Specially designated nationals and blocked persons | Daily |
EU Consolidated Lists | Persons and entities subject to financial sanctions | Daily |
UK Financial Sanctions | Office of Financial Sanctions Implementation | Daily |
UN Security Council | UN consolidated sanctions list | Daily |
Aggregated Sanctions | Multi-jurisdictional aggregation of 30+ global sanctions lists | Daily |
Export Control Lists | Restricted parties and export control designations | Weekly |
| Source | Description | Refresh |
|---|---|---|
Political Exposure DB | Politically exposed persons extracted from structured public databases | Weekly |
EveryPolitician | Historical and current politician data | Monthly |
| Source | Description | Refresh |
|---|---|---|
UK Corporate Registry | Full register including officers, PSCs, and filing history | Daily |
Legal Entity Identifiers | Global legal entity identification and corporate hierarchy | Daily |
Multi-Jurisdiction Registries | Aggregated corporate registry data from 140+ jurisdictions | Weekly |
Securities Filings | Securities and exchange commission filings | Daily |
| Source | Description | Refresh |
|---|---|---|
Panama Leaks | 11.5M documents from offshore service providers (2016) | Static |
Pandora Leaks | 11.9M documents from 14 offshore service providers (2021) | Static |
Paradise Leaks | 13.4M documents from offshore trust providers (2017) | Static |
Offshore Entity Database | Original offshore entity database | Static |
Caribbean Leaks | Caribbean corporate registry leak (2016) | Static |
SAR Filings | Suspicious activity reports | Static |
| Source | Description | Refresh |
|---|---|---|
Multilateral Development Banks | Debarred and cross-debarmed firms | Weekly |
U.S. Government Procurement | Federal excluded parties list | Daily |
African Development Bank | AfDB sanctions list | Weekly |
Asian Development Bank | ADB integrity sanctions list | Weekly |
Inter-American Development Bank | Regional development bank sanctions list | Weekly |
European Development Bank | Reconstruction and development bank sanctions | Monthly |
| Source | Description | Refresh |
|---|---|---|
AML Framework Assessments | Grey / black list status for every jurisdiction | Quarterly |
High-Risk Third Countries | Regulatory high-risk third country designations | Quarterly |
Financial Secrecy Index | Offshore tier classification and secrecy scoring | Annual |