Use case · Developer patterns

Batch Screening & Bulk Company Lookup API

You have a file — a customer book, a vendor master, a payout list — and every row needs to be resolved, screened and checked. Three POST endpoints do that in chunks of up to 100 (50 for sanctions), one result per row, in the order you sent them, instead of a loop of thousands of single calls.

Published 15 September 2026

The problem

A compliance engineer gets a CSV export from the core banking system or the CRM: thousands of counterparties needing a KYB refresh. The naive approach loops single calls — GET /companies/search, GET /sanctions/screen, GET /insolvency/search — once per row. At 20,000 rows that trips the per-minute limit constantly, retries are ad hoc, and nothing records what was checked, at what threshold, with what result.

The batch endpoints exist for this: send an array, get an array back in the same order, one status per row, one error shape throughout.

The pattern — three endpoints

Each is the batch form of a GET endpoint you may already call one row at a time: company lookup, sanctions screening and insolvency monitoring. All three take { "items": [...] }, return data[] in request order plus a meta block, and are refused up front — before any item runs — if the batch does not fit your quota.

Resolve — POST /companies/lookup

Up to 100 items. Give country plus an identifier (company_number, or the aliases nif, siren, vat), or country plus name for a fuzzy match.

curl -X POST 'https://api.prometiam.com/functions/v1/risk-api/companies/lookup' \
  -H 'Authorization: Bearer rk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "items": [
      { "country": "ES", "company_number": "A46103834" },
      { "country": "FR", "siren": "552032534" },
      { "country": "GB", "name": "Ocado Group" }
    ]
  }'

Screen — POST /sanctions/screen (beta)

Up to 50 items (or the names shorthand for names with no disambiguators). threshold, list, active_only, group and limit apply to every item; group: "entity" folds repeated listings for the same person or company into one row.

curl -X POST 'https://api.prometiam.com/functions/v1/risk-api/sanctions/screen' \
  -H 'Authorization: Bearer rk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "items": [
      { "name": "Vladimir Putin" },
      { "name": "Acme Trading Ltd", "entity_type": "company" }
    ],
    "threshold": 80,
    "group": "entity"
  }'

Check — POST /insolvency/check

Up to 100 items across sixteen corporate-insolvency markets. Give company_number (exact, alias siren) or name (contains match), plus country.

curl -X POST 'https://api.prometiam.com/functions/v1/risk-api/insolvency/check' \
  -H 'Authorization: Bearer rk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "items": [
      { "country": "FR", "company_number": "917499519" },
      { "country": "DE", "name": "Wirecard" }
    ]
  }'

Full parameter and field reference: API documentation.

Reading the quota error

Every item in a batch — found or not, matched or clear — counts as one request against your per-minute, daily and monthly limits. A batch that does not fit under the tightest of those three right now is refused before anything runs: a 429 whose body carries error.code: "batch_exceeds_quota" and a max_items_now field naming how many items would fit.

// 429 response shape (message text omitted — it is descriptive, not fixed)
{
  "error": { "code": "batch_exceeds_quota", "status": 429 },
  "max_items_now": 59
}

Read max_items_now, slice your batch to that size, send it, and send the remainder once your window resets. Because requests-per-minute is usually the tightest of the three limits, it is the one that decides whether a full-size batch clears in one call:

TierRequests/minute100-item lookup or insolvency batch50-item sanctions batch
Free10Needs several callsNot on this plan
Starter60Needs at least 2 callsNot on this plan
Professional120Clears in one callClears in one call
Scale300Clears in one callClears in one call

"Not on this plan" is a separate, scope-level limit: sanctions screening needs Professional regardless of batch size, and returns 403 upgrade_required instead of a 429. No batch endpoint draws on the trial allowance.

What comes back

Every response is { data: [...], meta: {...} }, one row per input item, in request order. Every row also carries index (its position in the request array), input (the item as sent) and error (set on error or timeout).

  • Lookup: status — found / not_found / error / timeout; company — the same object GET /companies/search returns, or null; match_score — 0–100 for a name item, null for an exact identifier. meta.found and meta.not_found total the batch.
  • Screen: status — match / clear / error / timeout (clear means no hit at or above your threshold on the active lists, not a certification); hits[] — the same shape as GET /sanctions/screen, one row per entity when group: "entity" is set; hit_count, distinct_entities, best_match_score. meta totals matches and clear and echoes threshold and grouping.
  • Check: status — found / none / error / timeout (none means no corporate notice in the covered markets, not proof of solvency); notices[] — up to 5, newest first, the shape of GET /insolvency/search; latest_filing_date. meta totals found and none and lists the countries actually queried.

A hit's field names are entity_name, entity_type, match_score, source_list, matched_name, matched_on, listing_date and notice_url — not name/score/list/added_on.

Customer dashboard screenshot: a pasted list of companies looked up at onceCustomer dashboard screenshot: a pasted list of companies looked up at once
POST /companies/lookup without code: paste a list or upload a CSV, see which lines were found and download the table. Every line costs 1 credit, found or not. Open it in the dashboard

A real example: CSV in, one row out per row in

~45 lines: reads a CSV, chunks it, calls all three endpoints per chunk, writes one output row per input row.

#!/usr/bin/env python3
"""Resolve, screen and check a vendor list.
Reads vendors.csv (country, company_number, name); calls the three Prometiam
batch endpoints in chunks of 50; writes one output row per input row."""
import csv, os, sys, time, requests

API = "https://api.prometiam.com/functions/v1/risk-api"
HEADERS = {"Authorization": f"Bearer {os.environ['PROMETIAM_API_KEY']}"}
CHUNK = 50  # the smallest per-call cap of the three endpoints (sanctions)
INSOLVENCY_COUNTRIES = {"FR", "DE", "GB", "AT", "CH", "NO", "FI", "US", "NL", "DK", "HR", "SE", "ES", "LV", "EE"}

def post_batch(path, items, tries=4):
    """POST one chunk; back off on 429/5xx. A persistent 429 carries max_items_now
    naming how many items fit right now -- shrink the chunk and resend."""
    for attempt in range(tries):
        r = requests.post(f"{API}{path}", headers=HEADERS, json={"items": items}, timeout=30)
        if r.status_code < 300:
            return r.json()["data"]
        if r.status_code == 429:
            max_now = r.json().get("max_items_now")
            if max_now and max_now < len(items):
                items = items[:max_now]
        time.sleep(2 ** attempt)
    r.raise_for_status()

rows = list(csv.DictReader(open(sys.argv[1], encoding="utf-8")))
lookup, sanctions, insolvency = [], [], {}

for start in range(0, len(rows), CHUNK):
    chunk = list(enumerate(rows[start:start + CHUNK], start=start))

    lookup += post_batch("/companies/lookup", [
        {"country": r["country"], "company_number": r["company_number"] or None, "name": r["name"] or None}
        for _, r in chunk])
    sanctions += post_batch("/sanctions/screen", [{"name": r["name"]} for _, r in chunk])

    due = [(i, r) for i, r in chunk if r["country"] in INSOLVENCY_COUNTRIES]
    if due:
        results = post_batch("/insolvency/check", [{"country": r["country"], "name": r["name"]} for _, r in due])
        insolvency.update(dict(zip([i for i, _ in due], results)))

with open(sys.argv[2], "w", newline="", encoding="utf-8") as out:
    w = csv.writer(out)
    w.writerow(["name", "company_status", "match_score", "sanctions_status",
                "best_match_score", "insolvency_status", "latest_filing_date"])
    for i, row in enumerate(rows):
        company = lookup[i].get("company") or {}
        ins = insolvency.get(i, {})
        w.writerow([row["name"], company.get("status"), lookup[i].get("match_score"),
                    sanctions[i].get("status"), sanctions[i].get("best_match_score"),
                    ins.get("status", "not_checked"), ins.get("latest_filing_date")])

IE, PL and BE rows are skipped for the insolvency call since those three are not in the sixteen covered markets (FR, GB, NO, ES and the rest are, and stay in). not_checked is a marker this script writes, not an API value.

Chunking, retries and idempotency

Chunking. Split your file before you start: at most 100 per lookup or insolvency call, 50 per sanctions call. On Free or Starter, chunk smaller than that so a call is never refused outright — see the table above.

Retries. Back off on 429 and 5xx. A plain rate-limit 429 is worth a short sleep and retry; a batch_exceeds_quota 429 is worth resizing to max_items_now instead. Items that do not finish inside the server's processing budget come back as status: "timeout" rather than failing the whole call — resend just those.

Idempotency. None of the three item shapes — CompanyLookupItem, SanctionsScreenItem, InsolvencyCheckItem — has a label or reference field. Track rows by position: every result carries index and the original input, and results return in request order, so a local array in file order maps straight onto the response.

Audit trail. Store what you sent, the raw result, the threshold used, and match_score — that is what a check found on the day you ran it, not what changes afterward. For that, poll GET /sanctions/changes?since= with your last run's date: it returns listings, delistings and updates (change_type: listed / delisted / updated, filterable by list), so you know which rows to re-screen instead of re-running the whole file.

Pricing fit

ScenarioCreditsTier
20,000 customer names screened once60,000 (20,000 × 3)Scale — 50,000/month, so two months
5,000-row vendor master: lookup + screen + insolvency check30,000 (5,000 × 6)Professional's 10,000/month is too small; Scale fits

Sanctions screening needs Professional (€29.99/month, 10,000 credits/month, 120 requests/minute) regardless of volume, since it is a Professional-tier scope on its own terms. Lookup and insolvency check alone can fit Starter (€9.99/month, 1,000 credits/month) at lower volume, but a 100-item batch still will not clear in a single call on Starter's 60 requests/minute — see the quota table above.

Scope limits, stated plainly

  • Sanctions screening — single-name or batch — is beta and a Professional-tier scope. A clear result means no hit at or above your threshold on the active lists; it is not a certification.
  • Corporate insolvency — single or batch — covers sixteen markets: FR, DE, GB, AT, CH, NO, FI, US, NL, DK, HR, SE, ES, LV, EE, SG. It is corporate only; personal or consumer insolvency is excluded. A none result is not proof of solvency.
  • Company lookup covers every registry the API covers. Officers are held for ES, FR, GB, NO and PL only; the other countries are company records only.
  • match_score is returned only for name-based items (lookup and sanctions); exact-identifier lookups return match_score: null.
  • None of the three batch endpoints draws on the trial allowance, and an over-quota batch is refused before anything runs.

FAQ

How many items can I send in one batch call?
Up to 100 for POST /companies/lookup and POST /insolvency/check, and up to 50 for POST /sanctions/screen. Every item counts as one request against your plan's per-minute, daily and monthly limits, the same as calling the single-item endpoint that many times.

What happens if my batch is bigger than my quota allows?
Nothing runs. The call is refused up front with 429 batch_exceeds_quota, and the body names max_items_now — how many items fit right now under whichever of your per-minute, daily or monthly limits is tightest. Send a batch that size, then send the rest once the window resets.

Can I attach my own reference or label to each row?
No — none of the three batch item shapes has a label or reference field. Each result carries index (its position in the request array) and input (the item you sent, echoed back), and results return in request order, so track rows by position.

Does a batch call use my trial allowance?
No. All three batch endpoints are excluded from the free trial allowance — you need the scope on your plan outright. Sanctions screening is a Professional-tier scope, batch or single-item.

Which countries does the batch insolvency check cover?
Corporate insolvency notices only, across sixteen markets: FR, DE, GB, AT, CH, NO, FI, US, NL, DK, HR, SE, ES, LV, EE and SG. A none status means no corporate notice in those markets, which is not proof of solvency.

Related