A 400-row customer book, four uploads, and the twelve rows that are neither clean nor listed
Nobody publishes the boring part of batch screening: what the file has to look like, what comes back, and what you do with the twelve rows that are neither clean nor listed. So here is a 400-row book run end to end, with a sample CSV you can download, upload and break, and the arithmetic behind the review queue it produces. The mechanics are the same whether you are screening a customer book for AML or sweeping a vendor master for denied parties; the caveats at the end are not.
- Rows per upload
- 100
- A 400-row book is four uploads. Above the cap the API answers HTTP 400 "Row limit exceeded (100 max)".
- File
- CSV or XLSX, max 2 MB
- UTF-8 or CP1252 for CSV; the first worksheet for XLSX; anything else returns HTTP 415.
- Columns actually read
- name, country, search_type
- name is required; country and search_type are optional. Every other column in the file is ignored — it is not screened and not stored.
- Cost of a 400-row sanctions run
- €156.00
- 400 checks at €0.39. Batch bills at the same per-check rate as a single check, and credits are deducted at upload, not on completion.
- Duplicate rows
- Removed before billing
- Rows are deduplicated on (name, country), case-insensitively, before the job is created — so you are not charged twice for the same pair.
Short answer
BATCHExport your book to a CSV with a name column — optionally country and
search_type — and upload it, in the portal or with one POST /api/v1/batch.
A file holds at most 100 rows and 2 MB, so 400 customers is four uploads and
four job ids. Each job reports total_rows, processed_rows, success_rows and
error_rows while it runs; when status is completed you read the results
from /rows or download them from /export. Billing happens at upload —
400 rows at sanctions_check is 400 × €0.39 = €156.00 — and duplicate
(name, country) pairs are removed before you are charged. Then the actual work
starts: triaging the handful of rows that came back with a candidate.
Sample file to try it with: sample-batch-screening.csv — 36 rows of deliberately fictitious names, including a duplicate pair, an initial-only name and three transliterations of the same invented person.
What the parser reads
SCHEMAThe parser reads exactly three columns, matched case-insensitively on the header row and trimmed:
| Column | Required | Behaviour |
|---|---|---|
name | yes | The string that is screened. Rows with an empty name are skipped silently. |
country | no | Stored, returned, and part of the deduplication key. Does not narrow the match. |
search_type | no | sanctions_check (default), adverse_media or full_search. One value per file — a mixed file is rejected with HTTP 400. |
Everything else in the file is ignored. The sample CSV deliberately carries two
extra columns, customer_id and dob, because a real export always does:
name,country,search_type,customer_id,dob
Example Person 01,NL,sanctions_check,CUST-1001,1979-03-14
A. Example,PL,sanctions_check,CUST-1003,
Aleksandr Examplov,LV,sanctions_check,CUST-1004,1968-07-21
Example Trading Ltd,GB,sanctions_check,SUP-2002,
Example Trading Ltd,,sanctions_check,SUP-2003,
"Example Components Co., Ltd",CN,sanctions_check,SUP-2016,
Those two columns are not screened and not stored. Keep them anyway — in your
copy of the file. When a candidate comes back on row 3, customer_id is how you
find the customer, and dob is the first question you ask to close or confirm
the match. A date of birth in the file does not sharpen the search; it sharpens
the review, which is where the time actually goes.
Three parser behaviours to know before you send 400 rows up. Rows are collapsed
on (name, country), lower-cased, so in the sample Example Trading Ltd, GB
appears twice, 36 rows become 35, and you are billed for 35. But
Example Trading Ltd with no country survives as a separate subject: a blank
country is a distinct key, not a wildcard.
The ordinal is your join key. Each row keeps the position it held in your file,
header excluded, and after deduplication the numbering has gaps — the sample
produces ordinals 1–36 with 20 missing, and that missing number is the
duplicate. Do not renumber. Join your source file on ordinal.
Quoting is generous, encoding is not. "Example Components Co., Ltd" parses
correctly, but the file itself must be UTF-8 or CP1252, and a UTF-16 export from
a spreadsheet returns HTTP 400 "Unsupported file encoding".
Splitting 400 rows
LIMITSawk 'NR==1 {hdr=$0; next}
(NR-2)%100==0 {file=sprintf("upload-part-%02d.csv", int((NR-2)/100)+1); print hdr > file}
{print > file}' customers.csv
Four files, each carrying the header — a headerless chunk is rejected with "Missing required 'name' column". Upload them in order: work is claimed oldest job first and then in ordinal order, so file 1 drains before file 2 begins. That is a property to plan around, not to fight — put the highest-risk segment in file 1.
The upload
APIPortal: Batch screening → File upload, drop the CSV or XLSX, and the same limits apply. By API:
curl -X POST https://screenveritai.com/api/v1/batch \
-H "X-API-Key: svai_2f9c1b7d4a6e8035c1d29f4b7e60a83d" \
-F "file=@upload-part-01.csv"
{
"job_id": "b41d90c7-25ea-4f6b-9a0c-1e77d3f8b512",
"total_rows": 100,
"credits_charged": 100,
"search_type": "sanctions_check",
"status": "pending",
"message": "Batch job created successfully. Poll the GET endpoint for status."
}
total_rows is the count after deduplication — check it against your
expectation before you upload the next file. If it is lower than your row count,
your export contains repeats.
Watching the job
STATUScurl https://screenveritai.com/api/v1/batch/b41d90c7-25ea-4f6b-9a0c-1e77d3f8b512 \
-H "X-API-Key: svai_…"
{
"id": "b41d90c7-25ea-4f6b-9a0c-1e77d3f8b512",
"status": "running",
"total_rows": 100,
"processed_rows": 63,
"success_rows": 61,
"error_rows": 2,
"credits_charged": 100,
"source_filename": "upload-part-01.csv",
"search_type": "sanctions_check",
"created_at": "2026-09-04T09:31:02.774318+00:00",
"completed_at": null
}
A job goes pending → running → completed; it becomes completed when
processed_rows reaches total_rows, whatever mix of successes and errors that
includes. Individual rows go queued → processing → done or error.
error_rows are not screened rows — they are rows that failed, and they need
re-running, not filing. Pull them with /export?errors_only=true.
The results
ROWScurl "https://screenveritai.com/api/v1/batch/b41d90c7-…/rows?page=1&page_size=100" \
-H "X-API-Key: svai_…"
Each row carries its result_json — the same payload a single screening
produces:
{
"ordinal": 18,
"name": "Example Trading Ltd",
"country": "GB",
"status": "done",
"result_json": {
"entity_processed": "Example Trading Ltd",
"ofac_sanctions_details": "## OFAC Sanctions Findings\n\n**Status:** *Sanctioned*\n\n**Details:**\n- **Matched entity:** EXAMPLE TRADING LTD\n- **Identification:** 51234; EXAMPLE TRADING LIMITED\n- **Measures:** SDGT\n- **Dates:** 2026-08-26\n- **Authority:** Office of Foreign Assets Control\n- **Evidence URL:** https://sanctionssearch.ofac.treas.gov/Details.aspx?id=99999\n\n**Source:** *Office of Foreign Assets Control*",
"eu_sanctions_details": "## EU Sanctions Findings\n\n**Status:** *No relevant listing found*\n\n…",
"criminal_watchlists_details": "",
"pep_details": "",
"sanctions_sources": [
{ "source_id": "ofac_sanctions", "title": "OFAC Sanctions", "authority": "Office of Foreign Assets Control", "details": "…", "hit_count": 1 }
],
"screening_v2_sanctions_hit_count": 1,
"screening_v2_criminal_watchlists_hit_count": 0,
"screening_v2_pep_hit_count": 0
},
"error_message": null
}
Two fields do the triage work. sanctions_sources is an array with one
object per screened source and a hit_count on each — sum it and you have a
sortable number for the whole run. The three …_hit_count keys are the same
signal per domain. Everything else is text for the human who reviews the row:
the matched spelling, the register's own reference, the programme, the listing
date, the authority and its URL.
GET /{job_id}/export returns the whole job as a CSV —
ordinal, name, country, search_type, status, error_message, result_json — once
the job is completed; before that it answers HTTP 400. That file is the one you
attach to the run; it packs each row's payload into a single cell, so parse from
/rows and archive from /export.
Triage rubric
REVIEWWrite this down before the batch runs, because a rule invented after seeing the results is not a control.
| Result | Action | Owner | Clock |
|---|---|---|---|
Any sanctions hit_count > 0 | Stop the relationship or shipment, escalate | MLRO / trade-compliance lead | Same day |
Potential watchlist match | Senior review before release | Senior analyst | 1 business day |
Potential PEP match | Standard review; EDD and source-of-funds if confirmed | Analyst | 2 business days |
All sources No relevant listing found | Auto-clear: file the result as evidence | None | — |
Row status: error | Re-run the row; never file a failure as a pass | Ops | Same day |
Four rows from the sample file, worked:
Ordinal 18 — Example Trading Ltd, GB. hit_count: 1, status Sanctioned,
programme SDGT, listing date 2026-08-26, authority OFAC, evidence URL present.
This is not a review, it is an escalation: freeze, quote the register reference
51234 in the file, and route to counsel. Note also ordinal 19 — the same name
with a blank country, screened separately. Both rows need a disposition; only one
is your customer.
Ordinal 3 — A. Example, PL. No sanctions candidate; pep_details returns
Potential PEP match against a public officials register, identification
"Andrzej Example; 1971; pl". An initial plus a common surname is the single most
productive false-positive pattern there is. Ask three cheap questions — does the
customer's recorded date of birth contradict 1971, does the role fit, does the
register publish an identifier your file also holds — and close it as a false
positive with that sentence written down.
Ordinals 4, 5 and 6 — Aleksandr Examplov, Aleksander Eksamplow,
Alexander Ekzamplov. Three spellings of one invented person, in three
countries. They are three subjects to the deduplicator and may return three
different result sets. The lesson is operational: screen the spellings you
actually hold — the passport spelling, the contract spelling, the bank-transfer
spelling — rather than picking the one that looks canonical. The same applies to
ordinals 10 and 11, Mohammed Al-Example and Muhammad Al Example: two rows,
two charges, possibly one human.
Ordinal 15 — Example Person 15, FI. Every source returns No relevant
listing found. Auto-clear, and file the result. Record it as "no relevant
listing found in the checked sources on 4 September 2026", never as "clear" — the
run can only speak about the sources it screened on the day it ran.
How big the queue will be
EXPECTATIONSThere is no honest industry number for this, and any vendor quoting a false-positive rate for your book is quoting a different book. The shape, though, is knowable.
Sanctions candidates are rare on an ordinary EU retail or SME customer list. The consolidated registers hold tens of thousands of entries against a world of counterparties, so a first run that returns zero sanctions candidates is a normal result rather than a broken configuration. What fills the queue is PEP candidates: the public PEP universe is orders of magnitude larger than the sanctions universe, and family members and close associates widen it further. The rest is name shape — initial-only names, two-token common names, transliterated names, and generic corporate strings like "Example Trading Ltd", which exists in a dozen jurisdictions.
So do the staffing arithmetic on your own first run, not on a benchmark: if 3 percent of 400 rows raise a candidate, that is 12 reviews; at 15 percent it is 60. Measure it once, and you can budget every subsequent run.
What the auditor will ask for
EVIDENCENot the screenshot. Per run, keep five things:
- The input file, unmodified, with the columns you exported and its date.
- The job ids and the
total_rows/success_rows/error_rowscounts, which prove the population screened was the population intended. - The exported results, stored as the point-in-time record — never regenerated later, because a re-run answers today's question, not the one you asked at onboarding.
- A disposition per candidate: true match, false positive or escalate, with the analyst, the timestamp and the sentence that explains the decision.
- The rubric itself, dated, showing the rule existed before the results did.
EU record-keeping obligations under the AML framework — Regulation (EU) 2024/1624 and the directives before it — run to five years from the end of the business relationship as a baseline, longer in several member states, so whatever you keep has to survive a system migration and an analyst's departure. A folder of PDFs with an index beats a screenshot in a ticket.
Re-running after a list moves
CADENCERe-upload the same file and keep both jobs. The diff between the two runs — rows that raised a candidate this time and did not last time — is the exact effect the list movement had on your book, and it is a far smaller review queue than the first run produced. That diff, dated, is also the cleanest answer to "when did you learn this?".
Cadence differs by why you are here. An AML or KYC customer book is re-screened on list change plus a periodic sweep tied to risk rating. A trade-compliance denied-party sweep is re-run against the transaction, not the calendar: before order release, before shipment, and again if the consignee, freight forwarder or end user changes.
AML book or vendor master
SCOPEThe mechanics above are identical for both jobs; the file and the caveats are not.
| AML / KYC customer book | Trade-compliance denied-party sweep | |
|---|---|---|
| Source of the file | CRM or core banking export | ERP vendor and customer master, shipment parties |
Typical name | Natural persons | Legal entities, plus consignees and forwarders |
| Dominant candidate type | PEP | Sanctions and restricted-party registers |
| The thing name screening cannot see | Ownership: the 50 percent rule blocks subsidiaries that appear on no list | Vessels and aircraft identified by IMO or tail number, and end-use controls |
| Re-screen trigger | List change, risk-rating review | Order release, shipment, party change |
Both caveats are the same admission in different words: a name check answers a name question. Ownership structures, vessel identifiers and end-use controls sit outside what any batch of strings can resolve, and a screening product that implies otherwise is selling you a false negative. That is why the whole source register sits on ScreenVeritAI's coverage page — so you can see the edge of what was screened before you decide to lean on it.
A batch run is documentation of a check, not a decision about a counterparty. The disposition stays with your reviewers.
Frequently asked questions
Q&AHow many rows can one batch screening file contain?
One hundred, and the file must be under 2 MB. A 400-row customer book therefore goes up as four uploads of 100 rows, each returning its own job id. Above the cap the API answers HTTP 400 with "Row limit exceeded (100 max)" and nothing is charged.
What columns does the batch CSV need?
Three are read: name (required), country (optional) and search_type (optional, defaulting to sanctions_check). Header names are matched case-insensitively and trimmed. Every other column — customer id, date of birth, account number — is ignored by the parser: it is not screened and not stored, so keep it in your own copy of the file for reconciliation.
Does filling in the country improve the match?
Not the matching itself. The list screening runs on the name. What country does is participate in the deduplication key and travel with the row into the results and the export, so "Example Trading Ltd, GB" and "Example Trading Ltd" with no country are screened as two subjects rather than collapsed into one. Fill it in for reconciliation and for your own reviewers, not for recall.
How much does a 400-row batch screening run cost?
400 checks at €0.39 is €156.00 with search_type sanctions_check. Batch bills at the same per-check rate as single checks, credits are deducted when the job is created rather than when it finishes, and duplicate (name, country) pairs are removed before the count is taken. Choosing full_search instead changes the arithmetic by more than an order of magnitude, so set search_type deliberately.
How many hits should I expect from 400 rows?
Nobody can tell you honestly, and a vendor-quoted false-positive rate is not evidence about your book. The drivers are knowable: sanctions registers are small relative to a customer list, so genuine sanctions candidates on an ordinary EU retail book are rare; PEP sources are far larger, so most of your queue will be potential PEP matches; and short, common or initial-only names generate most of the noise. Measure your own first run and use that as the baseline for staffing.
How do I export batch results?
GET /api/v1/batch/{job_id}/export returns a CSV with the columns ordinal, name, country, search_type, status, error_message and result_json. It returns HTTP 400 while the job is still running, and the result_json column packs the whole payload of a row into a single cell — good as a human artefact, awkward to parse, so use the /rows endpoint when a machine is reading.
Do I have to re-run the whole batch after a list update?
Yes, and that is the point: results are point-in-time snapshots and are never recalculated when a list changes, so the only way to know today's answer is to ask today's question. Re-upload the same file, keep both jobs, and diff the candidate rows between them. The rows that appear in the new run and not the old one are exactly what the list movement did to your book.
Sources
SOURCES- 01ScreenVeritAI API v1 — interactive documentation
ScreenVeritAI · 2026-09-04
- 02Specially Designated Nationals and Blocked Persons List (SDN) — human-readable lists
Office of Foreign Assets Control · 2026-09-04
- 03OFAC Sanctions List Service
Office of Foreign Assets Control · 2026-09-04
- 04Overview of sanctions and related tools (EU consolidated financial sanctions)
European Commission, DG FISMA · 2026-09-04
- 05United Nations Security Council Consolidated List
United Nations Security Council · 2026-09-04
- 06The UK Sanctions List
Foreign, Commonwealth & Development Office · 2026-09-04
- 07
Informational analysis of published regulatory sources. Not legal advice. Verify the primary sources before acting.