NARALIN/API Reference
Overview

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.

SANDBOX
$0/mo
500 lookups

Evaluate and prototype. Single entity lookup, full risk score. No credit card required.

GROWTH
$1,500/mo
25,000 lookups

Series A–B fintechs and compliance teams. Batch lookup, PSC ownership depth.

PROFESSIONAL
$5,000/mo
250,000 lookups

Institutional compliance. Relationship graph API, 99.95% SLA, dedicated support.

ENTERPRISE
Custom
Unlimited

Tier-1 banks & funds. Bulk feeds, raw export, dedicated infra, SOC 2 on request.

Quick Start
PYTHON
bash
pip install naralin
NODE / TYPESCRIPT
bash
npm install naralin

Set 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.

url
https://api.naralin.ai
All requests are served over HTTPS. HTTP requests are rejected.

Subscription 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.

SANDBOX$0/mo

Evaluate & Prototype

Solo developers and compliance evaluators. No credit card required.

POST/v1/entity/lookupSingle entity lookup
GET/healthAPI health check
GROWTH$1,500/mo

Scaling Compliance Teams

Series A–B fintechs, boutique funds, and growing compliance operations.

POST/v1/entity/lookupSingle entity lookup
POST/v1/entity/batch_lookupUp to 100 entities per request
GET/v1/entity/{id}Fetch by stable entity ID
PROFESSIONAL$5,000/mo

Institutional Compliance

Financial institutions, M&A due diligence teams, and regulated entities. 99.95% SLA.

POST/v1/entity/lookupSingle entity lookup
POST/v1/entity/batch_lookupUp to 100 entities per request
GET/v1/entity/{id}Fetch by stable entity ID
GET/v1/entity/{id}/relationshipsOfficer and ownership graph
ENTERPRISECustom

Tier-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.

All endpoints — unlimited
POST /v1/entity/bulk Bulk file upload + async processing
Raw data feed on request
Custom webhook alerts on match

Authentication

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.

KEY FORMAT
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.

bash
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

HeaderRequiredValue
x-api-keyrequiredYour API key. Format: nar_live_...
Content-TypePOST onlyapplication/json
x-request-idoptionalYour trace/correlation ID. Echoed back in response for debugging.
DUAL AUTH MODES
API Keyx-api-key: nar_live_... — Primary method for server-to-server integration. Preferred for production use.
Bearer JWTAuthorization: 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 queryFixture returnedRisk tier
"sanctioned" or "darkside" or "sanctions_primary"SANCTIONS_LIST_A + EU sanctions, offshore jurisdictionCRITICAL
"pep" or "politician" or "minister"Tier 1 PEP (Head of State / Senior Official)MEDIUM
"debarred" or "development_bank" or "mdb"Development bank cross-debarmentHIGH
"notfound" or "unknown"Entity not found (found: false)
anything elseClean entity, no flagsLOW
bash
"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.

TierMonthly LookupsBatchSLA
Sandbox500Best-effort
Growth25,000✓ up to 10099.5%
Professional250,000✓ up to 10099.95%
EnterpriseUnlimited✓ Bulk asyncCustom

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.

TierRequests / minuteWindow
Sandbox2060s sliding
Growth12060s sliding
Professional60060s sliding
EnterpriseCustomCustom
http
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.

HeaderExampleDescription
X-RateLimit-Limit10000Total monthly lookups for your plan
X-RateLimit-Remaining9857Lookups remaining this month
X-RateLimit-Used143Lookups consumed this month
X-RateLimit-Reset2026-04-01ISO 8601 date quota resets (1st of next month)
x-request-idreq_8f3a9c2d...Unique request ID — include in support tickets
Retry-After30Seconds until safe to retry (on 429 responses only)
http
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 COUNTING

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 BODY FORMAT
json
{
  "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"
  }
}
HTTPerror.codeCause & Resolution
200Request successful. Inspect found field on entity responses.
400invalid_requestMalformed request body or invalid parameters. Check error.param for the offending field.
401authentication_errorMissing or invalid API key. Verify your nar_live_... key is set correctly.
403permission_deniedAuthenticated but not authorized. Your plan does not include this endpoint.
404not_foundEntity ID not found. Use lookup or batch_lookup to discover entities first.
429quota_exceededMonthly lookup limit reached. See error.limit, error.used, error.reset_at. Upgrade your plan.
429rate_limit_exceededToo many requests in a short window. Respect Retry-After header.
500api_errorServer-side error. Retry with exponential backoff. Include request_id in support tickets.
503service_unavailableDatabase or upstream dependency temporarily unavailable. Retry after a few seconds.
QUOTA EXCEEDED — EXTRA FIELDS
json
{
  "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"
  }
}
Endpoint

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.

POST/v1/entity/lookup

Request Body

FieldTypeDescription
namerequired
stringEntity name to search. Min 2 chars, max 500 chars.
jurisdiction
stringISO-2 country code to narrow search. Boosts matching candidates from this jurisdiction.
current_only
booleanIf true (default), only return currently active officers and sanctions. Set to false to include historical records.

Example Request

bash
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

200OKContent-Type: application/json
json
{
  "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

500+ Sources100M+ Indexed EntitiesFuzzy Match + ScoreJSON <200ms
Endpoint — Growth+

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.

POST/v1/entity/batch_lookup

Request Body

FieldTypeDescription
entitiesrequired
object[]Array of entity objects. Max 100 per request.
entities[].namerequired
stringEntity name to search. Min 2 chars, max 500 chars.
entities[].jurisdiction
stringISO-2 country code to narrow search (optional)

Example Request

bash
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

json
{
  "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
}
FieldTypeDescription
resultsrequired
LookupResponse[]Array of full entity records, one per input entity. Same schema as single lookup.
processed_countrequired
integerTotal items attempted (always == len(entities) in request).
found_countrequired
integerNumber of items where found=true (matched an entity).
not_found_countrequired
integerItems where found=false and no exception occurred.
error_countrequired
integerItems that raised an exception during resolution (still returned as found=false).
batch_msrequired
integerTotal server-side processing time for the entire batch in milliseconds.
Endpoint — Growth+

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.

GET/v1/entity/{id}

Path Parameters

FieldTypeDescription
idrequired
stringStable entity ID returned from lookup (e.g. ent_7f3a9c2d)

Example Request

bash
curl -X GET "https://api.naralin.ai/v1/entity/ent_7f3a9c2d?current_only=true" \
  -H "x-api-key: nar_live_a8b3c2d1_bQ7zR4pL9wE3nH..."
Endpoint — All Tiers

Health Check

Returns 200 OK when all API systems are operational. No authentication required. Use for monitoring and dependency health checks.

GET/health
bash
curl -X GET "https://api.naralin.ai/health"

Example Response

json
{ "ok": true, "service": "naralin-api", "version": "1.4.0" }
Endpoint — All Tiers

Readiness Probe

Returns 200 OK when all backend dependencies are connected. Returns 503 if any dependency is unreachable. No authentication required.

GET/ready
bash
curl -X GET "https://api.naralin.ai/ready"

Example Response

json
{
  "ok": true,
  "service": "naralin-api",
  "version": "1.4.0",
  "databases": {
    "data_store": true,
    "metadata_store": true
  },
  "auth0_config_present": true
}
CONTAINER ORCHESTRATION

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.

FULL RESPONSE EXAMPLE
json
{
  "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
}
IDENTITY
FieldTypeDescription
foundbooleanAlways present. True if an entity matched. Check this before accessing any other field.
entity_idstring | nullStable UUID. Format: "ent_" + 16 hex chars. Safe to store and use with GET /v1/entity/{id}.
canonical_namestring | nullNormalized authoritative name from the highest-confidence source.
confidencefloatMatch confidence 0.0–1.0. Above 0.90 = exact or alias match.
match_typestring | null"exact", "alias", or "fuzzy".
PROFILE
FieldTypeDescription
entity_typestring | null"person", "company", "organization", "vessel", "aircraft", or "facility".
statusstring | null"active", "dissolved", "inactive", or null if unknown.
jurisdictionsstring[]ISO-3166-1 alpha-2 codes for all associated jurisdictions.
identifiersobjectExternal IDs keyed by source: corporate_registry, lei, cik, vat, etc.
descriptionstring | nullShort description when available from the source.
SANCTIONS
FieldTypeDescription
has_active_sanctionsbooleanTrue if any sanctions list entry is currently active (end_date is null).
active_sanctions_countintegerCount of distinct active sanction entries across all lists.
has_pep_exposurebooleanEntity is or is directly connected to a politically exposed person.
sanctions[].list_namestringCanonical list name: SANCTIONS_LIST_A, SANCTIONS_LIST_B, SANCTIONS_LIST_C, SANCTIONS_LIST_D, etc.
sanctions[].start_datestring | nullISO 8601 date designation became effective.
sanctions[].end_datestring | nullISO 8601 date designation was lifted. Null = currently active.
sanctions[].reasonstringBasis for designation as stated in the source list.
DEBARMENTS
FieldTypeDescription
has_active_debarmentbooleanFormal exclusion from public procurement or MDB financing is currently active.
debarments[].programstringCanonical key: "gov_procurement", "mdb_primary", "mdb_africa", "mdb_cross", etc.
debarments[].program_namestringHuman-readable program name.
debarments[].activebooleanTrue if end_date is in the future or null.
debarments[].is_cross_debarmentbooleanEligible for enforcement across multiple MDB signatories.
OFFICERS & OWNERSHIP
FieldTypeDescription
officers[].namestringOfficer full name.
officers[].rolestringDirector, Secretary, CEO, CFO, PSC, Shareholder, etc.
officers[].ownership_pctfloat | nullStated 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_pctfloat | nullProduct of all ownership percentages along the path from queried entity to this node.
ubo_chain[].is_individualbooleanTrue if this is a natural person — the terminal UBO in the chain.
ubo_chain_depthintegerMaximum depth reached in the UBO traversal.
ubo_resolvedbooleanTrue if at least one natural-person UBO was found.
RISK SIGNALS
FieldTypeDescription
shell_indicators[]string[]"offshore_jurisdiction", "nominee_director", "single_purpose_structure", "circular_ownership", "bearer_shares".
jurisdiction_risk[].jurisdiction_risk_statusstring | null"black" = regulatory blacklist, "grey" = greylist, null = no designation.
jurisdiction_risk[].offshore_tierinteger | null1 = extreme risk (BVI, Cayman), 2 = moderate risk (Cyprus, Seychelles).
jurisdiction_risk[].is_eu_high_riskbooleanOn EU AML high-risk third countries list.
jurisdiction_risk[].risk_contributionintegerPoints contributed to the overall risk_score from this jurisdiction.
hops_to_high_riskinteger | nullOwnership hops to the nearest HIGH/CRITICAL ancestor. Null if none found.
CORPORATE GRAPH
FieldTypeDescription
related_entities[].relationship_typestring"parent", "subsidiary", "shareholder", "intermediary", "officer_of".
related_entities[].directionstring"parent" = owns queried entity; "child" = owned by queried entity.
related_entities[].risk_levelstringRisk level of the related entity — surface network risk.
related_entities[].weightfloat | nullRelationship strength 0.0–1.0. Higher = more significant connection.
RISK SCORE
FieldTypeDescription
risk_scoreintegerComposite 0–100. 0–19: NONE, 20–39: LOW, 40–59: MEDIUM, 60–79: HIGH, 80–100: CRITICAL.
risk_levelstring"NONE", "LOW", "MEDIUM", "HIGH", or "CRITICAL".
risk_factors[]string[]Plain-English explanations of each factor contributing to the score.
PROVENANCE
FieldTypeDescription
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_msintegerServer-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.

ERROR CLASS HIERARCHY
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
PYTHON
python
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}")
TYPESCRIPT
typescript
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}`);
  }
}
AUTO-RETRY BEHAVIOR

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.

POST/v1/admin/api_keys

Create a new API key. The full secret is returned exactly once — store it immediately.

bash
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"}'
json
{
  "key_id": "key_4f2a9e3b",
  "label": "production-server",
  "api_key": "nar_live_4f2a9e3b_xK9mR2pL7wE4nH8vQ1tJ5sU6...",
  "created_at": "2026-03-21T14: 30: 00Z",
  "last_used_at": null
}
GET/v1/admin/api_keys

List all keys for your organization. Key secrets are never returned after initial creation.

bash
curl "https://api.naralin.ai/v1/admin/api_keys" \
  -H "x-api-key: nar_live_a8b3c2d1_..."
POST/v1/admin/api_keys/{key_id}/revoke

Revoke a key immediately. Irreversible. Requests using the revoked key return 401 within seconds.

FieldTypeDescription
key_idrequired
UUIDAPI key ID to revoke (from key creation or list response).
bash
curl -X POST "https://api.naralin.ai/v1/admin/api_keys/key_4f2a9e3b/revoke" \
  -H "x-api-key: nar_live_a8b3c2d1_..."

Response

json
{ "revoked": true, "api_key_id": "key_4f2a9e3b" }
BEST PRACTICES
Rotate production keys every 90 days. Use label to track purpose (production-server, ci-pipeline, staging).
Create separate keys per environment. Revoke CI keys after each pipeline run if possible.
Store keys in a secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler) — never in source control.
If a key is compromised, revoke it immediately. There is no rate-limit penalty for frequent rotations.
SDK

Python SDK

Official Python client with typed models, auto-retry, and the full error class hierarchy. Requires Python 3.9+ and httpx.

bash
pip install naralin
export NARALIN_API_KEY=nar_live_...

Client Initialization

python
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

python
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

python
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

PropertyTypeDescription
is_cleanboolNo sanctions, no debarment, no PEP exposure, risk NONE or LOW
is_high_riskboolrisk_level HIGH or CRITICAL, or any active sanctions or debarments
requires_reviewboolMEDIUM risk or PEP exposure — not clean, not high-risk
sanctioned_listslist[str]Names of all currently active sanctions lists
SDK

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.

bash
npm install naralin
"color:#565f89"># yarn add naralin  /  pnpm add naralin
export NARALIN_API_KEY=nar_live_...

Client Initialization

typescript
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

typescript
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

typescript
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

GetterTypeDescription
isCleanbooleanNo sanctions, no debarment, no PEP exposure, risk NONE or LOW
isHighRiskbooleanrisk_level HIGH/CRITICAL, or active sanctions or debarments
requiresReviewbooleanMEDIUM risk or PEP exposure
sanctionedListsstring[]Names of all currently active sanctions lists
Guides

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.

python
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.

python
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.

typescript
'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;
  }
}
Deep Dive

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.

RISK LEVEL THRESHOLDS
Score RangeLevelRecommended Action
0–29LOWAuto-approve eligible. No significant risk signals.
30–59MEDIUMEnhanced due diligence recommended. PEP exposure or offshore signals.
60–79HIGHManual review required. Active debarments, offshore leak listings, or opaque structures.
80–100CRITICALReject or escalate. Active sanctions, multi-list exposure, or combination of severe factors.
SCORING DIMENSIONS
1. SANCTIONS
+80Active sanctions match (any program)
+10 eaEach additional active sanctions list (capped at +20 bonus)
+18Historical sanctions, cleared < 2 years ago
+10Historical sanctions, cleared 2–5 years ago
+5Historical sanctions, cleared > 5 years ago
2. POLITICALLY EXPOSED PERSONS
+50PEP Tier 1 — Head of State / Government / Minister-level
+35PEP Tier 2 — Legislator / Senior Military / Senior Judiciary
+20PEP Tier 3 — Regional Official / Ambassador / State Enterprise Board
+15PEP (tier unclassified)
+12Relative or Close Associate (RCA) of a PEP
+10Former PEP (cleared / left office)
3. DEBARMENTS
+35Multilateral development bank active debarment
+30US Federal SAM/EPLS active exclusion
+25MDB active debarment (IADB / AfDB / ADB / EBRD)
+20EU / UK procurement debarment
+5Cross-debarment corroboration bonus (≥2 MDB programs)
~30%Weight reduction for historical (expired) debarments
4. OFFSHORE LEAK DATABASES
+20Panama Papers / Pandora Papers appearance
+18Suspicious activity report appearance
+15Paradise Papers / Offshore Leaks appearance
+10Bahamas Leaks / other datasets
5. JURISDICTION RISK
+30Blacklisted jurisdiction
+15Greylisted jurisdiction
+15Offshore secrecy Tier 1 (BVI, Cayman, Marshall Islands)
+8Offshore secrecy Tier 2 (Luxembourg, Singapore, Cyprus)
+10EU high-risk third country list
+5Corroboration bonus (grey + offshore overlap)
cap +40Maximum total contribution from jurisdiction
6. NETWORK / OWNERSHIP CHAIN
+25CRITICAL parent at 100% ownership (proportionally scaled)
+15HIGH parent at 100% ownership (proportionally scaled)
+8MEDIUM parent at 100% ownership (proportionally scaled)
cap +30Maximum total contribution from network risk
7. SHELL INDICATORS
+10Nominee structure detected
+8Ownership chain ≥ 3 layers deep
+5UBO not resolved to a natural person
+5SPV / holding company language in description
cap +20Maximum total contribution from shell indicators
8. ENTITY STATUS
+5Entity is dissolved, inactive, liquidated, expired, or annulled
Deep Dive

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.

RESOLUTION TIERS (Highest to Lowest Priority)
TierMatch TypeConfidenceDescription
1Exact canonical1.0Normalized canonical_name matches the search term exactly
2Exact alias1.0Normalized alias matches the search term exactly
3Token-anchorScoredSearch term appears as a full token in the candidate + high fuzzy score
4FuzzyScoredFuzzy match with token-overlap validation (min 50% token overlap)
Rejected0.0No meaningful overlap — returns found: false
QUALITY GATES
Minimum Score 85.0Candidates scoring below this threshold are rejected outright
Ambiguity Gap 5.0If the top two non-sibling candidates are within 5 points, the match is rejected to prevent false positives
Token Overlap 50%For short queries (≤2 tokens), at least 50% of search tokens must appear as full tokens in the candidate
Jurisdiction Boost +8.0Candidates matching the requested jurisdiction receive a scoring bonus
Token Anchor Bonus +10.0Candidates where the search term appears as a complete token receive a precision bonus
NORMALIZATION

Before 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.

Deep Dive

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.

SANCTIONS LISTS
SourceDescriptionRefresh
U.S. SDN ListsSpecially designated nationals and blocked personsDaily
EU Consolidated ListsPersons and entities subject to financial sanctionsDaily
UK Financial SanctionsOffice of Financial Sanctions ImplementationDaily
UN Security CouncilUN consolidated sanctions listDaily
Aggregated SanctionsMulti-jurisdictional aggregation of 30+ global sanctions listsDaily
Export Control ListsRestricted parties and export control designationsWeekly
PEP DATABASES
SourceDescriptionRefresh
Political Exposure DBPolitically exposed persons extracted from structured public databasesWeekly
EveryPoliticianHistorical and current politician dataMonthly
CORPORATE REGISTRIES
SourceDescriptionRefresh
UK Corporate RegistryFull register including officers, PSCs, and filing historyDaily
Legal Entity IdentifiersGlobal legal entity identification and corporate hierarchyDaily
Multi-Jurisdiction RegistriesAggregated corporate registry data from 140+ jurisdictionsWeekly
Securities FilingsSecurities and exchange commission filingsDaily
OFFSHORE LEAK DATABASES
SourceDescriptionRefresh
Panama Leaks11.5M documents from offshore service providers (2016)Static
Pandora Leaks11.9M documents from 14 offshore service providers (2021)Static
Paradise Leaks13.4M documents from offshore trust providers (2017)Static
Offshore Entity DatabaseOriginal offshore entity databaseStatic
Caribbean LeaksCaribbean corporate registry leak (2016)Static
SAR FilingsSuspicious activity reportsStatic
DEBARMENT LISTS
SourceDescriptionRefresh
Multilateral Development BanksDebarred and cross-debarmed firmsWeekly
U.S. Government ProcurementFederal excluded parties listDaily
African Development BankAfDB sanctions listWeekly
Asian Development BankADB integrity sanctions listWeekly
Inter-American Development BankRegional development bank sanctions listWeekly
European Development BankReconstruction and development bank sanctionsMonthly
JURISDICTION INTELLIGENCE
SourceDescriptionRefresh
AML Framework AssessmentsGrey / black list status for every jurisdictionQuarterly
High-Risk Third CountriesRegulatory high-risk third country designationsQuarterly
Financial Secrecy IndexOffshore tier classification and secrecy scoringAnnual