API documentation

Policy365 Sanctions Screening API

Screens people and companies against the UK Sanctions List (FCDO), the EU's consolidated financial sanctions list, the UN Security Council Consolidated List and the Consolidated Canadian Autonomous Sanctions List, and looks UK companies up at Companies House to screen their officers and persons with significant control.

Getting started

  1. Start a free trial. A card is taken at sign-up; nothing is charged during the 14 days.
  2. Your API key is shown once, when the account is made. Copy it then: it can't be shown again, because only a hash of it is kept.
  3. More keys can be issued, and old ones revoked, from your account (sign in by a link sent to your email). Two keys can be live at once, so a key can be changed without a gap.

A key looks like ssk_ followed by 43 characters. Keep it out of source code and out of anything shared: the examples below read it from an environment variable, SANCTIONS_API_KEY.

The base URL is:

https://screening.policy365.cloud

Requests and answers are JSON (UTF-8), apart from the CSV endpoints. Field names are camelCase. Times are UTC, in ISO 8601.

Authentication

Every call to /api/… needs your key in the X-Api-Key header. Without a live key the answer is 401. A key stops working when it is revoked or expires, or when the account is closed; the change reaches the service within a minute. GET /health needs no key.

X-Api-Key: ssk_your_key_here

A first call

This screens one person. Replace the key, or set SANCTIONS_API_KEY first.

curl -sS https://screening.policy365.cloud/api/screen \
  -H "X-Api-Key: $SANCTIONS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reference": "TEST-1", "subjects": [ { "id": "p1", "name": "Oleg Deripaska", "kind": "person", "birthYear": 1968 } ] }'

The answer's status is match, and subjects[0].matches names the listed entries it matched. Each call like this is billed (10p), and counts toward the trial's allowance.

Statuses

Every screening answers with a status for each subject, and one for the whole request.

StatusMeansWhat to do
clearNothing on the lists resembles the name closely enough to look at.Go ahead.
reviewA listed name is like this one (a score of 0.7 or more), or something else needs a person's eye: an officer disqualified under sanctions regulations, or a person with significant control whose details Companies House withholds.Refer it. Someone looks at the matches before going ahead.
matchThe name is a near spelling of a listed name, every word paired (a score of 0.9 or more).Refer it. Do not go ahead until it has been looked at under your own sanctions procedure.
notScreenedOnly from a company check by name: the company couldn't be identified beyond doubt, so no one was screened.Refer it. Someone chooses the company and screens it by number.
  • The overall status is match if any subject matched, else review if any is to be reviewed (or anyone couldn't be screened), else clear.
  • Anything but a 200 answer is not screened. A 4xx or 5xx never means clear.
  • A direct sales portal should refer anything but clear. For a company check by name, refer anything but resolution matched with status clear.
  • Screening checks; it doesn't decide. A review or a match is for your own compliance judgement.

A birth year that doesn't fit a listed person's cuts the score to 60% of what it was, which clears most common names. Give the birth year and month wherever you have them.

Endpoints

CallDoesCallsBilled
POST/api/screenScreens 1 to 50 people and companiesSanctions lists Companies House*Yes
GET/api/screen/csvScreens one name; answers CSVSanctions listsYes
POST/api/companies/checkFinds a company by name and, if sure, screens it with its peopleCompanies House Sanctions listsWhen matched
GET/api/companies/check/csvThe same, as CSVCompanies House Sanctions listsWhen matched
GET/api/companies/searchSearches Companies HouseCompanies HouseNo
GET/api/companies/{number}A company's profile, officers and controllersCompanies HouseNo
POST/api/companies/{number}/screenScreens a company with its peopleCompanies House Sanctions listsYes
GET/api/companies/{number}/screen/csvThe same, as CSVCompanies House Sanctions listsYes
GET/api/screenings/{id}A recorded screeningOur recordsNo
GET/api/listsThe lists loadedOur recordsNo
GET/healthWhether the service may screen (no key)Our recordsNo

* Companies House only for a company given with its companyNumber.

Screen

POST/api/screen

Calls Sanctions lists Companies House Every name is checked against the sanctions lists. Companies House is called only for a company given with its companyNumber: to fetch its officers and controllers, and, with disqualifications, to check the disqualified directors register.

Screens the people and companies given, and records the screening. A company given with its companyNumber is looked up at Companies House, and its officers in office and persons with significant control are screened too.

Request

{
  "reference": "Q-10234",
  "subjects": [
    { "id": "firm", "name": "Highland Lamb Co Ltd", "kind": "company", "role": "proposer" },
    { "id": "p1", "name": "Oleg Deripaska", "kind": "person", "birthYear": 1968, "birthMonth": 1, "role": "principal" }
  ]
}
FieldRules
referenceOptional. Your own reference (a quote or proposal number), at most 200 characters. Kept with the record and returned in the answer.
subjects1 to 50.
subjects[].idRequired, unique within the request. The answer is given by it.
subjects[].nameRequired, at most 300 characters; may be left out for a company given by companyNumber, and the registered name is used.
subjects[].kindRequired: person or company.
subjects[].birthYearOptional, 1900 to this year. Give it wherever you have it.
subjects[].birthMonthOptional, 1 to 12, and only with a birthYear.
subjects[].roleOptional. Their part, such as proposer or principal; returned in the answer.
subjects[].companyNumberOptional, for a company only: its Companies House number as Companies House writes it, such as 00445790 or SC296753.
disqualificationsOptional, true or false (default). With a company given by number, also checks each officer against the disqualified directors register.
monitorOptional, for an account whose plan includes re-screening. true keeps watching every subject screened: each is checked again every time a sanctions list changes (Watching names); the answer's watchedUntil says until when. false doesn't. Not given: as your plan says (watched only if it watches everything). Without re-screening on your plan, true is refused with 400.
monitorMonthsOptional, 1 to 120; 12 if not given. How long to watch them. Watching someone already watched renews the watch.

With the people Companies House adds, a request may screen at most 300 subjects in all.

Response 200

{
  "screeningId": "fb44353e-6b0e-4c8e-9f0a-2d3b5c7e9a10",
  "screenedAt": "2026-10-14T17:42:01.123+00:00",
  "reference": "Q-10234",
  "status": "match",
  "lists": [
    { "source": "uk", "title": "…", "loaded": true, "current": true, "generated": "…", "downloadedAt": "…",
      "checkedAt": "…", "fingerprint": "067e24…", "parties": 6408 }
  ],
  "thresholds": { "match": 0.9, "review": 0.7 },
  "subjects": [
    { "subjectId": "firm", "name": "Highland Lamb Co Ltd", "kind": "company", "role": "proposer", "companyNumber": null,
      "status": "clear", "score": 0, "matchedName": null, "datasets": [], "matches": [], "disqualifications": null },
    { "subjectId": "p1", "name": "Oleg Deripaska", "kind": "person", "role": "principal", "companyNumber": null,
      "status": "match", "score": 1, "matchedName": "Oleg DERIPASKA", "datasets": ["uk", "ca"],
      "matches": [
        { "source": "uk", "listId": "RUS0269", "caption": "Oleg Vladimirovich DERIPASKA", "matchedName": "Oleg DERIPASKA",
          "score": 1, "nameScore": 1, "birthYearRuledOut": false,
          "regime": "The Russia (Sanctions) (EU Exit) Regulations 2019" }
      ],
      "disqualifications": null }
  ],
  "companies": [],
  "unscreened": [],
  "units": { "screenings": 2, "companyChecks": 0, "disqualificationChecks": 0, "billed": 2 },
  "verifyCode": "ZNWV-EVQA-BSTV",
  "verifyUrl": "https://screening.policy365.cloud/verify?code=ZNWV-EVQA-BSTV"
}
  • screeningId finds the record later, with GET /api/screenings/{id}.
  • units is what this call billed, at 10p a unit: screenings (every person and company screened, those Companies House added included) and disqualificationChecks (every officer checked against the disqualified register) make up billed; companyChecks, the companies looked up, are counted but not billed. The same as the call's line on your statement.
  • verifyCode is the record's verification code, and verifyUrl the link that checks it: give either to anyone who needs to know the record is genuine and unaltered. They aren't part of the signed answer, so they come after it; GET /api/screenings/{id} gives them again.
  • lists is the version of each list screened against, by its SHA-256 fingerprint.
  • subjects are in the order given, each company given by number followed by the subjects Companies House added for it.
  • matches are the best three listed entries at or above the review threshold, best first; empty when clear. score is after the birth year is taken into account, nameScore before; birthYearRuledOut says the birth year cut it.
  • datasets are the lists the matches came from: uk, eu, un, ca. An EU entry's listId is its EU reference, such as EU.3633.76.
  • companies is what Companies House said about each company given by number (the same shape as the company lookup).
  • unscreened lists anyone who couldn't be screened, and why. Anything in it makes the screening a review at least.

With a company number

  • Each officer in office and each person with significant control becomes a subject of its own, with ids such as firm.officer1 and firm.controller1, and the month and year of birth Companies House shows. Their companyNumber is the company's.
  • Each person is screened once, with every role, and checked against the disqualified register once (and billed once): someone Companies House lists as both secretary and director, under one officer id, is one subject, as is someone who is both an officer and a controller.
  • If the name you give differs from the registered name, the registered name is screened too, as firm.registered.
  • Corporate officers and controllers are screened as companies.
  • An officer disqualified under sanctions regulations makes their subject a review even when no list matches. With disqualifications, each officer's disqualifications lists their entries on the register (from, until, reason, act, underSanctions, companies); otherwise it is null.
  • A person with significant control whose details Companies House withholds can't be screened; they are listed in unscreened.
  • If Companies House has no such company the answer is 404, and if it can't be reached 502. Nothing is screened: the company's name alone is never screened and called clear.

Response with a company number 200: Tesco PLC as { "id": "company", "kind": "company", "companyNumber": "00445790" }, with disqualifications; its officers cut to two here.

{
  "screeningId": "5c209f4b-fafd-4efa-8762-c271f3ab9796",
  "screenedAt": "2026-10-14T23:26:45.456+00:00",
  "reference": "Q-1",
  "status": "clear",
  "lists": [
    { "source": "ca", "title": "Consolidated Canadian Autonomous Sanctions List", "loaded": true, "current": true,
      "generated": null, "downloadedAt": "2026-10-14T17:41:28.22+00:00", "checkedAt": "2026-10-14T17:41:28.22+00:00",
      "fingerprint": "cf413b2dee406c209a010d53faeeb9c0ae6c046670f628132c02d16718bf085b", "parties": 5707 },
    { "source": "uk", "title": "…", … },
    { "source": "un", "title": "…", … }
  ],
  "thresholds": { "match": 0.9, "review": 0.7 },
  "subjects": [
    { "subjectId": "company", "name": "TESCO PLC", "kind": "company", "role": "company", "companyNumber": null,
      "status": "clear", "score": 0, "matchedName": null, "datasets": [], "matches": [], "disqualifications": null },
    { "subjectId": "company.officer1", "name": "Christopher Jon TAYLOR", "kind": "person", "role": "secretary",
      "companyNumber": "00445790", "status": "clear", "score": 0, "matchedName": null, "datasets": [], "matches": [],
      "disqualifications": [] },
    { "subjectId": "company.officer2", "name": "Melissa BETHELL", "kind": "person", "role": "director",
      "companyNumber": "00445790", "status": "clear", "score": 0, "matchedName": null, "datasets": [], "matches": [],
      "disqualifications": [] }
  ],
  "companies": [    {
      "profile": { "companyNumber": "00445790", "name": "TESCO PLC", "status": "active", "type": "plc",
                   "created": "1947-11-27", "registeredOffice": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, AL7 1GA, United Kingdom",
                   "sicCodes": ["47110"] },
      "officers": [
        { "name": "Christopher Jon TAYLOR", "registeredName": "TAYLOR, Christopher Jon", "role": "secretary", "isCorporate": false,
          "appointedOn": "2025-04-14", "birthYear": null, "birthMonth": null, "nationality": null, "occupation": null,
          "personNumber": "334718260001", "disqualifications": [] },
        { "name": "Melissa BETHELL", "registeredName": "BETHELL, Melissa", "role": "director", "isCorporate": false,
          "appointedOn": "2018-09-24", "birthYear": 1974, "birthMonth": 9, "nationality": "British", "occupation": null,
          "personNumber": "248450070003", "disqualifications": [] }
      ],
      "controllers": [
        { "name": "…", "kind": "corporate", "companiesHouseKind": "corporate-entity-person-with-significant-control",
          "birthYear": null, "birthMonth": null, "notifiedOn": "2016-04-06", "naturesOfControl": ["ownership-of-shares-75-to-100-percent"] }
      ],
      "disqualificationsChecked": true,
      "lookedUpAt": "2026-10-14T23:26:45.123+00:00"
    }
  ],
  "unscreened": [],
  "units": { "screenings": 3, "companyChecks": 1, "disqualificationChecks": 2, "billed": 5 },
  "verifyCode": "K7QM-3XRA-PW9D",
  "verifyUrl": "https://screening.policy365.cloud/verify?code=K7QM-3XRA-PW9D"
}

Billed: 10p for each subject screened, those Companies House adds included, and 10p for each officer (a person, not a company) checked against the disqualified register.

Screen, as CSV

GET/api/screen/csv

Calls Sanctions lists One name, checked against the sanctions lists. Companies House isn't called.

Screens one name given in the query, and answers CSV. For MS Access, Excel and anything else that would rather read a line of text than JSON.

GET /api/screen/csv?type=person&name=Oleg%20Deripaska&dob=1968-01-02&reference=ACCESS-1
X-Api-Key: ssk_your_key_here
QueryRules
typeRequired: person or company.
nameRequired. URL-encode it as UTF-8.
dobOptional: yyyy, yyyy-mm or yyyy-mm-dd. Only the year and month are used.
referenceOptional, at most 200 characters. Kept with the record and returned last on the row.

Response 200 text/csv: a header row and one data row, each ending CRLF.

status,score,matchedName,caption,source,listId,regime,birthYearRuledOut,screeningId,screenedAt,reference
match,1,Oleg DERIPASKA,Oleg Vladimirovich DERIPASKA,uk,RUS0269,The Russia (Sanctions) (EU Exit) Regulations 2019,false,a63c1edb-…,2026-10-14T18:12:14Z,ACCESS-1
  • A clear row leaves the match columns empty: clear,0,,,,,,,{screeningId},{screenedAt},{reference}.
  • status is always the first field and never quoted, so it can be read without a CSV parser: everything before the first comma.
  • Other fields are quoted only where they need it (a comma, a quote or a line break), with quotes doubled. A Canadian list id such as "Russia 1, Part 2 item 821" has a comma in it, and so may a reference, so don't split the rest of the row on commas blindly.
  • The match columns are the best listed entry's. The full record, with the best three, is found by screeningId.
  • Failures: see Errors. Anything but a 200 is not screened.
The name and date of birth travel in the URL. The service doesn't log query strings, but a proxy of your own may: keep it from recording them, or use POST /api/screen.

Billed: 10p.

Company check by name

POST/api/companies/check

Calls Companies House Sanctions lists Companies House is searched for the name first. Only if one company is certain are its officers and controllers fetched from Companies House and everyone checked against the sanctions lists.

A company check from a name alone, for underwriting run in the background when all you have is the name the proposer typed.

{ "name": "Highland Lamb Ltd", "reference": "Q-1", "disqualifications": true }
FieldRules
nameRequired, 2 to 300 characters, and more than a legal form.
referenceOptional, at most 200 characters.
disqualificationsOptional, default false. Checks each officer against the disqualified directors register.
monitorOptional, as on POST /api/screen: when the company is matched, true keeps watching it with its officers and controllers (Watching names), and the answer's watchedUntil says until when. Ambiguous or not found, nothing is screened, so nothing is watched: pass monitor again to POST /api/companies/{number}/screen with the company chosen. Not given: as your plan says.
monitorMonthsOptional, 1 to 120; 12 if not given.

The service picks the company itself only when there is no doubt: exactly one active company whose name is the name given once case, punctuation, "The", "&"/"and" and the legal form (Ltd, Limited, PLC, LLP, CIC and their Welsh forms) are set aside, and Son/Sons, Brother/Brothers and Farm/Farms are taken as the same word ("J Smith & Son Ltd" is "J SMITH AND SONS LIMITED"), as are initials whether spaced or not ("AM & S A Howie Ltd" is "AM & SA HOWIE LTD"), and a name typed without spaces whose letters are exactly the same ("AABFARMSLTD" is "AAB FARMS LIMITED"). A company found this way is screened by its registered name. That is stricter than the screening's own name matching: "Highland Holdings Ltd" is not "Highland Ltd", and "Highland Lamb Co Ltd" is not "Highland Lamb Ltd".

Response 200, always with a resolution and a status:

resolutionstatusWhat else comes back
matchedThe screening's: clear, review or matchcompany, and screening: the full screening, as POST /api/screen answers it, recorded the same way.
ambiguousnotScreenedcandidates, best first, each with sameName and a score.
notFoundnotScreenedNothing: no company with a name at all like it.
{
  "resolution": "ambiguous",
  "status": "notScreened",
  "name": "Highland Lamb Ltd",
  "reference": "Q-1",
  "company": null,
  "candidates": [
    { "companyNumber": "SC123456", "name": "HIGHLAND LAMB LIMITED", "companyStatus": "dissolved", "type": "ltd",
      "created": "2009-03-02", "address": "…", "score": 1, "sameName": true },
    { "companyNumber": "SC654321", "name": "HIGHLAND LAMB CO LTD", "companyStatus": "active", "type": "ltd",
      "created": "2015-06-11", "address": "…", "score": 0.88, "sameName": false }
  ],
  "screening": null,
  "recordId": "3b0c…",
  "units": { "screenings": 0, "companyChecks": 0, "disqualificationChecks": 0, "billed": 0 },
  "verifyCode": "4TQX-9MEB-HW2R",
  "verifyUrl": "https://screening.policy365.cloud/verify?code=4TQX-9MEB-HW2R"
}

When matched: the screening is the same as POST /api/companies/{number}/screen gives, in full:

{
  "resolution": "matched",
  "status": "clear",
  "name": "Tesco PLC",
  "reference": "Q-1",
  "company": { "companyNumber": "00445790", "name": "TESCO PLC", "status": "active", "type": "plc", "created": "1947-11-27",
               "address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA" },
  "candidates": [],
  "screening":  {
    "screeningId": "5c209f4b-fafd-4efa-8762-c271f3ab9796",
    "screenedAt": "2026-10-14T23:26:45.456+00:00",
    "reference": "Q-1",
    "status": "clear",
    "lists": [
      { "source": "ca", "title": "Consolidated Canadian Autonomous Sanctions List", "loaded": true, "current": true,
        "generated": null, "downloadedAt": "2026-10-14T17:41:28.22+00:00", "checkedAt": "2026-10-14T17:41:28.22+00:00",
        "fingerprint": "cf413b2dee406c209a010d53faeeb9c0ae6c046670f628132c02d16718bf085b", "parties": 5707 },
      { "source": "uk", "title": "…", … },
      { "source": "un", "title": "…", … }
    ],
    "thresholds": { "match": 0.9, "review": 0.7 },
    "subjects": [
      { "subjectId": "company", "name": "TESCO PLC", "kind": "company", "role": "company", "companyNumber": null,
        "status": "clear", "score": 0, "matchedName": null, "datasets": [], "matches": [], "disqualifications": null },
      { "subjectId": "company.officer1", "name": "Christopher Jon TAYLOR", "kind": "person", "role": "secretary",
        "companyNumber": "00445790", "status": "clear", "score": 0, "matchedName": null, "datasets": [], "matches": [],
        "disqualifications": [] },
      { "subjectId": "company.officer2", "name": "Melissa BETHELL", "kind": "person", "role": "director",
        "companyNumber": "00445790", "status": "clear", "score": 0, "matchedName": null, "datasets": [], "matches": [],
        "disqualifications": [] }
    ],
    "companies": [    {
        "profile": { "companyNumber": "00445790", "name": "TESCO PLC", "status": "active", "type": "plc",
                     "created": "1947-11-27", "registeredOffice": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, AL7 1GA, United Kingdom",
                     "sicCodes": ["47110"] },
        "officers": [
          { "name": "Christopher Jon TAYLOR", "registeredName": "TAYLOR, Christopher Jon", "role": "secretary", "isCorporate": false,
            "appointedOn": "2025-04-14", "birthYear": null, "birthMonth": null, "nationality": null, "occupation": null,
            "personNumber": "334718260001", "disqualifications": [] },
          { "name": "Melissa BETHELL", "registeredName": "BETHELL, Melissa", "role": "director", "isCorporate": false,
            "appointedOn": "2018-09-24", "birthYear": 1974, "birthMonth": 9, "nationality": "British", "occupation": null,
            "personNumber": "248450070003", "disqualifications": [] }
        ],
        "controllers": [
          { "name": "…", "kind": "corporate", "companiesHouseKind": "corporate-entity-person-with-significant-control",
            "birthYear": null, "birthMonth": null, "notifiedOn": "2016-04-06", "naturesOfControl": ["ownership-of-shares-75-to-100-percent"] }
        ],
        "disqualificationsChecked": true,
        "lookedUpAt": "2026-10-14T23:26:45.123+00:00"
      }
    ],
    "unscreened": []
  },
  "recordId": "5c209f4b-fafd-4efa-8762-c271f3ab9796",
  "units": { "screenings": 3, "companyChecks": 1, "disqualificationChecks": 2, "billed": 5 },
  "verifyCode": "K7QM-3XRA-PW9D",
  "verifyUrl": "https://screening.policy365.cloud/verify?code=K7QM-3XRA-PW9D"
}
  • When matched, company holds companyNumber, name, status, type, created and address, and candidates is empty.
  • Ambiguous covers a near spelling, a former name, two that fit, and a company with the right name that isn't active (dissolved, in liquidation). Someone then picks a candidate and screens it with POST /api/companies/{number}/screen, using the same reference.
  • Companies House answers every search with its nearest companies, however unlike. A candidate is offered only if its name scores 0.6 or more against the name given, or is the very name. If none is left, the answer is notFound.
  • recordId finds the check's record with GET /api/screenings/{id}: the screening's when matched, otherwise the check's own. A referral is recorded too, and verifyCode and verifyUrl are that record's.
  • units is what the call billed: nothing unless matched. The screening inside is the answer as recorded, so the units and code are given once, at the top.
  • Reading only status never mistakes "we couldn't tell which company" for clear.
  • Failures are 400, 502 and 503, not an answer.

Billed: when matched, as a company screened by number (10p a subject, 10p an officer checked for disqualification). Ambiguous and not found are free.

Company check, as CSV

GET/api/companies/check/csv

Calls Companies House Sanctions lists As the company check by name: Companies House first, then the sanctions lists if the company is certain.

The same check, answered as CSV.

GET /api/companies/check/csv?name=Highland%20Lamb%20Ltd&reference=Q-1&disqualifications=true

monitor=true and monitorMonths may be added, as in the JSON.

Response 200 text/csv when matched: the company, then its people.

status,resolution,name,role,kind,score,matchedName,caption,source,listId,regime,birthYearRuledOut,disqualified,companyNumber,companyStatus,address,screeningId,screenedAt,reference
clear,matched,Tesco PLC,company,company,0,,,,,,,,00445790,active,"Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA",5c209f4b-…,2026-10-14T23:26:45Z,Q-1
clear,matched,Christopher Jon TAYLOR,secretary,person,0,,,,,,,,00445790,active,"Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA",5c209f4b-…,2026-10-14T23:26:45Z,Q-1

When ambiguous: a choose row for each candidate, nothing screened.

status,resolution,name,role,kind,score,matchedName,caption,source,listId,regime,birthYearRuledOut,disqualified,companyNumber,companyStatus,address,screeningId,screenedAt,reference
choose,ambiguous,HIGHLAND LAMB LIMITED,candidate,company,1,,,,,,,,SC296753,dissolved,"Baird Avenue, Dryburgh Industrial Estate, Dundee, DD2 3TN",,,Q-4

When not found: one row.

status,resolution,name,role,kind,score,matchedName,caption,source,listId,regime,birthYearRuledOut,disqualified,companyNumber,companyStatus,address,screeningId,screenedAt,reference
notScreened,notFound,Zzqx Widgets Ltd,,company,,,,,,,,,,,,,,Q-5
  • Matched: a row for each subject screened, the company first, then its officers and controllers; and a review row named (not screened) for anyone who couldn't be screened. The worst status on any row is the check's.
  • Ambiguous: a choose row for each candidate, with its companyNumber, companyStatus, address and score.
  • Not found: one notScreened row with the name given.
  • disqualified is true or false for an officer checked against the register, and empty otherwise.
  • status is first and never quoted.

Billed: as POST /api/companies/check.

GET/api/companies/search

Calls Companies House Companies House's search only. No one is checked against the sanctions lists.

Searches Companies House. Nothing is screened or recorded.

GET /api/companies/search?q=tesco

q is a name, part of one, or a company number: two characters at least.

[
  { "companyNumber": "00445790", "name": "TESCO PLC", "status": "active", "type": "plc",
    "created": "1947-11-27", "address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, AL7 1GA" }
]

Billed: no.

Company lookup

GET/api/companies/{number}

Calls Companies House Companies House only: the profile, officers and controllers, and, with disqualifications, the disqualified directors register. No one is checked against the sanctions lists.

A company's profile, its officers in office and its persons with significant control still notified, each with the month and year of birth Companies House shows. Former officers and ceased controllers are left out. Nothing is screened or recorded.

GET /api/companies/00445790?disqualifications=true

disqualifications=true adds each officer's entries on the disqualified directors register. It is optional because it costs Companies House a search and up to three fetches for each officer.

{
  "profile": { "companyNumber": "00445790", "name": "TESCO PLC", "status": "active", "type": "plc",
               "created": "1947-11-27", "registeredOffice": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, AL7 1GA, United Kingdom",
               "sicCodes": ["47110"] },
  "officers": [
    { "name": "Christopher Jon TAYLOR", "registeredName": "TAYLOR, Christopher Jon", "role": "secretary", "isCorporate": false,
      "appointedOn": "2025-04-14", "birthYear": null, "birthMonth": null, "nationality": null, "occupation": null,
      "personNumber": "334718260001", "disqualifications": [] },
    { "name": "Melissa BETHELL", "registeredName": "BETHELL, Melissa", "role": "director", "isCorporate": false,
      "appointedOn": "2018-09-24", "birthYear": 1974, "birthMonth": 9, "nationality": "British", "occupation": null,
      "personNumber": "248450070003", "disqualifications": [] }
  ],
  "controllers": [
    { "name": "…", "kind": "corporate", "companiesHouseKind": "corporate-entity-person-with-significant-control",
      "birthYear": null, "birthMonth": null, "notifiedOn": "2016-04-06", "naturesOfControl": ["ownership-of-shares-75-to-100-percent"] }
  ],
  "disqualificationsChecked": true,
  "lookedUpAt": "2026-10-14T23:26:45.123+00:00"
}
  • A controller's kind is individual, corporate, legalPerson or withheld. A withheld controller has no name.
  • An officer's disqualifications is null when not checked.
  • 404 if Companies House has no such company; 502 if it can't answer.

Billed: no, with or without disqualifications.

Company screen by number

POST/api/companies/{number}/screen

Calls Companies House Sanctions lists Companies House for the company's officers and controllers (and the disqualified directors register, if asked), then everyone against the sanctions lists.

Screens a company with its officers in office and persons with significant control, as one recorded screening. The same as POST /api/screen with one company given by companyNumber.

POST /api/companies/00445790/screen
Content-Type: application/json

{ "reference": "Q-1", "disqualifications": true, "monitor": true }

The body is optional, and so is every field: reference, disqualifications, and monitor and monitorMonths as on POST /api/screen, watching the company with its officers and controllers. The company is subject company and its people company.officer1, company.controller1 and so on.

Response 200, the same shape as POST /api/screen's (its officers cut to two here):

{
  "screeningId": "5c209f4b-fafd-4efa-8762-c271f3ab9796",
  "screenedAt": "2026-10-14T23:26:45.456+00:00",
  "reference": "Q-1",
  "status": "clear",
  "lists": [
    { "source": "ca", "title": "Consolidated Canadian Autonomous Sanctions List", "loaded": true, "current": true,
      "generated": null, "downloadedAt": "2026-10-14T17:41:28.22+00:00", "checkedAt": "2026-10-14T17:41:28.22+00:00",
      "fingerprint": "cf413b2dee406c209a010d53faeeb9c0ae6c046670f628132c02d16718bf085b", "parties": 5707 },
    { "source": "uk", "title": "…", … },
    { "source": "un", "title": "…", … }
  ],
  "thresholds": { "match": 0.9, "review": 0.7 },
  "subjects": [
    { "subjectId": "company", "name": "TESCO PLC", "kind": "company", "role": "company", "companyNumber": null,
      "status": "clear", "score": 0, "matchedName": null, "datasets": [], "matches": [], "disqualifications": null },
    { "subjectId": "company.officer1", "name": "Christopher Jon TAYLOR", "kind": "person", "role": "secretary",
      "companyNumber": "00445790", "status": "clear", "score": 0, "matchedName": null, "datasets": [], "matches": [],
      "disqualifications": [] },
    { "subjectId": "company.officer2", "name": "Melissa BETHELL", "kind": "person", "role": "director",
      "companyNumber": "00445790", "status": "clear", "score": 0, "matchedName": null, "datasets": [], "matches": [],
      "disqualifications": [] }
  ],
  "companies": [    {
      "profile": { "companyNumber": "00445790", "name": "TESCO PLC", "status": "active", "type": "plc",
                   "created": "1947-11-27", "registeredOffice": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, AL7 1GA, United Kingdom",
                   "sicCodes": ["47110"] },
      "officers": [
        { "name": "Christopher Jon TAYLOR", "registeredName": "TAYLOR, Christopher Jon", "role": "secretary", "isCorporate": false,
          "appointedOn": "2025-04-14", "birthYear": null, "birthMonth": null, "nationality": null, "occupation": null,
          "personNumber": "334718260001", "disqualifications": [] },
        { "name": "Melissa BETHELL", "registeredName": "BETHELL, Melissa", "role": "director", "isCorporate": false,
          "appointedOn": "2018-09-24", "birthYear": 1974, "birthMonth": 9, "nationality": "British", "occupation": null,
          "personNumber": "248450070003", "disqualifications": [] }
      ],
      "controllers": [
        { "name": "…", "kind": "corporate", "companiesHouseKind": "corporate-entity-person-with-significant-control",
          "birthYear": null, "birthMonth": null, "notifiedOn": "2016-04-06", "naturesOfControl": ["ownership-of-shares-75-to-100-percent"] }
      ],
      "disqualificationsChecked": true,
      "lookedUpAt": "2026-10-14T23:26:45.123+00:00"
    }
  ],
  "unscreened": [],
  "units": { "screenings": 3, "companyChecks": 1, "disqualificationChecks": 2, "billed": 5 },
  "verifyCode": "K7QM-3XRA-PW9D",
  "verifyUrl": "https://screening.policy365.cloud/verify?code=K7QM-3XRA-PW9D"
}

Billed: 10p for each subject screened, and 10p for each officer checked against the disqualified register.

Company screen, as CSV

GET/api/companies/{number}/screen/csv

Calls Companies House Sanctions lists As the company screen by number.

The same screening, answered as CSV: one row per subject, the company first.

GET /api/companies/00445790/screen/csv?reference=T-1&disqualifications=true

monitor=true and monitorMonths may be added, as in the JSON.

status,name,role,kind,score,matchedName,caption,source,listId,regime,birthYearRuledOut,disqualified,companyNumber,screeningId,screenedAt,reference
clear,TESCO PLC,company,company,0,,,,,,,,00445790,e40de257-…,2026-10-14T18:44:52Z,T-1
clear,Melissa BETHELL,director,person,0,,,,,,,false,00445790,e40de257-…,2026-10-14T18:44:52Z,T-1
  • Anyone who couldn't be screened gets a review row named (not screened).
  • The worst status on any row is the screening's.
  • disqualified is true or false for an officer checked against the register, and empty otherwise.
  • status is first and never quoted.

Billed: as POST /api/companies/{number}/screen.

Watching names

GET/api/monitor DELETE/api/monitor/{watchId}

Calls Our records Your account's watches. Neither Companies House nor the lists are called.

Re-screening is part of an account's plan: ask us to add it. With it, screen with "monitor": true and every subject screened is watched, 12 months unless you say otherwise: each time a sanctions list changes, everyone watched is checked against what changed. If one starts to resemble a listed entry, they're screened again in full as a screening of your own, recorded and signed as usual, with your decisions applied (a notThem holds while the entry it was about is unchanged). If it's still a review or match, it's under Needs a decision on the site, and your account's owner and reviewers are emailed: how many, and a link, never the names. These re-checks aren't billed by the name. With a webhook, your system is told too: monitor.matched.

  • GET /api/monitor: the watches running now, newest first, and how many (live); with ?all=true, ended and lapsed ones too. Each has its watchId, the name and birth as screened, your reference, fromScreeningId, endsAt, lastCheckedAt and lastHitScreeningId.
  • DELETE /api/monitor/{watchId}: stop watching. 204, or 404 if it isn't a running watch of yours. An ended watch is kept six years, like the records.
  • Screening the same person again with monitor renews their watch. A watch nobody renews lapses at endsAt.

Billed: no.

Webhooks

GET/api/webhooks POST/api/webhooks DELETE/api/webhooks/{id} POST/api/webhooks/{id}/test

Calls Our records Your systems, at the addresses you give.

Your systems told when something happens, rather than asking. Add up to five https addresses (on the site, under Account, or POST /api/webhooks with { "url": "https://…", "events": ["monitor.matched", "decision.recorded"] }); the answer, 201, gives the address's secret that once. Every event to it is signed with that secret.

EventWhendata
monitor.matchedA re-check, when a list changed, found someone you watch resembling a new or changed entry: still a review or match after your decisions.screeningId, watchId, status, reference, list (source, generated, fingerprint), verifyUrl
decision.recordedSomeone on your account, or your system, recorded a decision.decisionId, screeningId, subjectId, source, listId, verdict, decidedAt
webhook.testSent on demand, to see an address works.message
POST https://your-system.example.com/screening/events
webhook-id: evt_6f0b2c…
webhook-timestamp: 1792112400
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
Content-Type: application/json

{ "id": "evt_6f0b2c…", "type": "monitor.matched", "time": "2026-10-16T09:40:00Z", "source": "https://screening.policy365.cloud",
  "data": { "screeningId": "fb44353e-…", "watchId": 1042, "status": "review", "reference": "POL-1234",
            "list": { "source": "uk", "generated": "2026-10-16T08:00:00Z", "fingerprint": "067e24…" },
            "verifyUrl": "https://screening.policy365.cloud/verify?code=K7QM-3XRA-PW9D" } }
  • Thin: ids and status, never the names screened. Fetch the rest with your key, GET /api/screenings/{screeningId}.
  • Check the signature before trusting one: it's the Standard Webhooks scheme, so their library for your language does it in a line. Refuse one whose webhook-timestamp is more than five minutes off.
  • Answer 2xx within 15 seconds. Anything else, or no answer, is tried again after 1 minute, 5, 30, 2 hours, 6 and 12, then given up; redirects aren't followed. The same event keeps its webhook-id on every try, so ignore one you've had.
  • GET /api/webhooks/deliveries shows the last 50 deliveries and how each went, and POST /api/webhooks/deliveries/{id}/resend sends one again.
  • The OpenAPI document's webhooks section describes each event's body in full.

Checking a signature, with Standard Webhooks' libraries:

// C#: dotnet add package StandardWebhooks
var hook = new StandardWebhooks.StandardWebhook(secret);   // "whsec_…", as you were given it
hook.Verify(body, request.Headers);                        // throws if it isn't ours, or is stale

# Python: pip install standardwebhooks
from standardwebhooks.webhooks import Webhook
Webhook(secret).verify(body, headers)

// Node: npm install standardwebhooks
import { Webhook } from "standardwebhooks";
new Webhook(secret).verify(body, headers);

Billed: no.

Screening records

GET/api/screenings/{id}

Calls Our records A screening already made, read back from our records. Neither Companies House nor the sanctions lists are called.

A recorded screening, or a company check by name that screened no one: who asked, when, your reference, which list versions were used, the request and the answer. {id} is a screeningId or recordId.

request is the request as made, and result the answer as given, in full: here, the company screen by number above. units, verifyCode and verifyUrl are as the call gave them.

{
  "screeningId": "5c209f4b-fafd-4efa-8762-c271f3ab9796",
  "screenedAt": "2026-10-14T23:26:45.456+00:00",
  "caller": "your-client-name",
  "reference": "Q-1",
  "status": "clear",
  "subjectCount": 3,
  "lists": [ { "source": "ca", "title": "…", "fingerprint": "cf413b…", … }, … ],
  "request": {
    "reference": "Q-1",
    "subjects": [
      { "id": "company", "name": null, "kind": "company", "birthYear": null, "birthMonth": null, "role": "company",
        "companyNumber": "00445790" }
    ],
    "disqualifications": true
  },
  "result":  {
    "screeningId": "5c209f4b-fafd-4efa-8762-c271f3ab9796",
    "screenedAt": "2026-10-14T23:26:45.456+00:00",
    "reference": "Q-1",
    "status": "clear",
    "lists": [
      { "source": "ca", "title": "Consolidated Canadian Autonomous Sanctions List", "loaded": true, "current": true,
        "generated": null, "downloadedAt": "2026-10-14T17:41:28.22+00:00", "checkedAt": "2026-10-14T17:41:28.22+00:00",
        "fingerprint": "cf413b2dee406c209a010d53faeeb9c0ae6c046670f628132c02d16718bf085b", "parties": 5707 },
      { "source": "uk", "title": "…", … },
      { "source": "un", "title": "…", … }
    ],
    "thresholds": { "match": 0.9, "review": 0.7 },
    "subjects": [
      { "subjectId": "company", "name": "TESCO PLC", "kind": "company", "role": "company", "companyNumber": null,
        "status": "clear", "score": 0, "matchedName": null, "datasets": [], "matches": [], "disqualifications": null },
      { "subjectId": "company.officer1", "name": "Christopher Jon TAYLOR", "kind": "person", "role": "secretary",
        "companyNumber": "00445790", "status": "clear", "score": 0, "matchedName": null, "datasets": [], "matches": [],
        "disqualifications": [] },
      { "subjectId": "company.officer2", "name": "Melissa BETHELL", "kind": "person", "role": "director",
        "companyNumber": "00445790", "status": "clear", "score": 0, "matchedName": null, "datasets": [], "matches": [],
        "disqualifications": [] }
    ],
    "companies": [    {
        "profile": { "companyNumber": "00445790", "name": "TESCO PLC", "status": "active", "type": "plc",
                     "created": "1947-11-27", "registeredOffice": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, AL7 1GA, United Kingdom",
                     "sicCodes": ["47110"] },
        "officers": [
          { "name": "Christopher Jon TAYLOR", "registeredName": "TAYLOR, Christopher Jon", "role": "secretary", "isCorporate": false,
            "appointedOn": "2025-04-14", "birthYear": null, "birthMonth": null, "nationality": null, "occupation": null,
            "personNumber": "334718260001", "disqualifications": [] },
          { "name": "Melissa BETHELL", "registeredName": "BETHELL, Melissa", "role": "director", "isCorporate": false,
            "appointedOn": "2018-09-24", "birthYear": 1974, "birthMonth": 9, "nationality": "British", "occupation": null,
            "personNumber": "248450070003", "disqualifications": [] }
        ],
        "controllers": [
          { "name": "…", "kind": "corporate", "companiesHouseKind": "corporate-entity-person-with-significant-control",
            "birthYear": null, "birthMonth": null, "notifiedOn": "2016-04-06", "naturesOfControl": ["ownership-of-shares-75-to-100-percent"] }
        ],
        "disqualificationsChecked": true,
        "lookedUpAt": "2026-10-14T23:26:45.123+00:00"
      }
    ],
    "unscreened": []
  },
  "units": { "screenings": 3, "companyChecks": 1, "disqualificationChecks": 2, "billed": 5 },
  "verifyCode": "K7QM-3XRA-PW9D",
  "verifyUrl": "https://screening.policy365.cloud/verify?code=K7QM-3XRA-PW9D"
}

404 if there is no such record. Billed: no.

Decisions on reviews

POST/api/screenings/{id}/decisions GET/api/screenings/{id}/decisions

Calls Our records Recorded against your account. Neither Companies House nor the sanctions lists are called.

Records whether a subject of a screening is the listed entry it resembled. The decision is remembered for that person (their name, date of birth and company number, as screened) against that entry as it stood, and every later screening of them by your account takes it into account:

  • notThem: that entry no longer raises a review for them. A subject whose every match is decided notThem comes back clear. If the entry changes (a new alias or date of birth), it raises a review again.
  • isThem: that entry is a match for them whatever its score.
  • withdrawn: undoes the last decision about that entry. Decisions are never changed or deleted; a new one replaces the last.
POST /api/screenings/fb44353e-…/decisions
Content-Type: application/json

{ "subjectId": "p1", "source": "eu", "listId": "EU.1042.29", "verdict": "notThem",
  "reason": "Born 1985 per passport; the listed person is older", "decidedBy": "Jane Smith, underwriting" }
FieldRules
subjectIdRequired: the subject as the screening's answer named it (p1, firm.officer2).
source, listIdRequired: the entry, as one of that subject's matches gave it.
verdictRequired: notThem, isThem or withdrawn.
reasonRequired, up to 1,000 characters: shown whenever the decision is applied, and on the record.
decidedByRequired: the person who decided. A key isn't a person; it's recorded with the key it came through.

Response 201: the decision, with its decisionId and decidedAt. 400 says what's wrong; 404 if the screening isn't yours.

In every answer, a subject's decisions lists the earlier decisions its status took into account: each with its verdict, reason, decidedBy, decidedAt, and holds, false when the entry has changed since. Each match carries its entry's entryFingerprint; a subject carries the birthYear and birthMonth it was screened with.

Billed: no.

Lists and health

GET/api/lists and GET/health

Calls Our records The state of the sanctions lists this service holds. Nothing outside is called.

/api/lists says what's loaded and when each list was last fetched:

{
  "ready": true,
  "parties": 13125,
  "lists": [
    { "source": "uk", "title": "…", "loaded": true, "current": true, "generated": "…", "downloadedAt": "…",
      "checkedAt": "…", "fingerprint": "067e24…", "parties": 6408 },
    { "source": "un", … },
    { "source": "ca", … }
  ]
}

Each list is fetched daily. If any list hasn't been fetched successfully for 48 hours, ready is false and the service refuses to screen (503) rather than answer clear from old lists.

/health needs no key. It answers 200 { "ready": true } when the service may screen, and 503 { "ready": false, "stale": ["un"] } when it may not. It is for a monitor or a load balancer.

Billed: no.

Errors

Anything but a 200 is not screened, and is never billed. JSON endpoints answer errors as problem details (application/problem+json).

StatusMeansWhat to do
400A wrong request. Every problem is listed.Fix the request.
401No live key in X-Api-Key: missing, wrong, revoked or expired, or the account is closed.Check the key, or issue a new one from your account.
402The trial's allowance of 500 units is used. Screening starts again when the trial ends and billing begins. Searches, lookups and records still work.Screening resumes when the trial ends; the detail gives the date.
404Companies House has no company by that number, or there is no such screening record.Check the number or id.
429Past your rate limit: 120 calls a minute unless agreed otherwise.Wait the number of seconds in Retry-After, then try again.
502Companies House couldn't be reached, or refused.Try again later. Nothing was screened.
503Not screened: the sanctions lists aren't all loaded and current, the screening couldn't be recorded, or Companies House's limit for this service is reached for now. We pace our calls to Companies House, so a call waits up to 20 seconds for room before it's refused.Treat as not screened. With Retry-After (Companies House's limit), wait that many seconds, then try again; otherwise try again later.

A 400 from POST /api/screen:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "request": [ "subjects[0].name: needed", "subjects[1].birthMonth: only with a birthYear" ]
  }
}

JSON that can't be read at all is reported against its path, such as $.subjects[0].birthYear.

A 503, 502 or 404 from a screening:

{
  "status": 503,
  "title": "Not screened",
  "detail": "The sanctions lists aren't all loaded and current",
  "lists": [ … ]
}

The CSV endpoints answer a 400, 404, 502 or 503 as one line of plain text saying why, such as Not screened: The sanctions lists aren't all loaded and current. A 401, 402 or 429 is refused before the endpoint is reached, and comes back as JSON problem details as above. Either way, check the HTTP status first: anything but 200 is not screened.

Pricing and billing

Pay as you go: no tie-in, no contract term, no minimum spend and no set-up fee. You pay for what you use, monthly, and can cancel any time from your account.

WhatPrice
Each name screened: a person or company you give, or one Companies House adds (officers, controllers, a registered name that differs)10p
Each officer checked against the disqualified directors register10p
Company check by name that is ambiguous or not foundFree
Company search and lookup; screening records; lists; healthFree
Any call that fails (400, 401, 402, 404, 429, 502, 503)Free
  • Prices exclude VAT.
  • Billed monthly in arrears through Stripe, for what was used in the month.
  • A 14-day free trial. A card is taken at sign-up and nothing is charged during the trial. The trial is capped at 500 units (names screened plus disqualification checks); past that, billed calls are refused with 402 until the trial ends.
  • Cancel at any time from the billing portal, reached from your account.
  • A company screened by number costs one unit for the company, one for each officer and controller, and, with disqualifications, one for each officer who is a person. A company with 6 directors and 2 controllers, one of them also a director, is 8 names: 80p, or £1.40 with the disqualification check.

Examples

Each example is complete and runs as written. Set the environment variable SANCTIONS_API_KEY to your key first (in PowerShell, $env:SANCTIONS_API_KEY = "ssk_…"; in bash, export SANCTIONS_API_KEY=ssk_…), so the key stays out of your source. The VBA examples hold it in a constant, ssk_your_key_here, to be replaced; each VBA example is a whole standard module, to be pasted into a module of its own. Choosing a language shows it in every example.

Screen two people

Two people screened in one request with POST /api/screen, with the overall status and each person's.

// Program.cs in a console app (dotnet new console), .NET 8 or later.
// The key is read from the SANCTIONS_API_KEY environment variable, not written in the code.
using System.Net.Http.Json;

var key = Environment.GetEnvironmentVariable("SANCTIONS_API_KEY")
    ?? throw new InvalidOperationException("Set SANCTIONS_API_KEY to your key");

using var http = new HttpClient { BaseAddress = new Uri("https://screening.policy365.cloud/") };
http.DefaultRequestHeaders.Add("X-Api-Key", key);

var request = new
{
    reference = "Q-10234",
    subjects = new object[]
    {
        new { id = "p1", name = "Oleg Deripaska", kind = "person", birthYear = 1968, birthMonth = 1, role = "principal" },
        new { id = "p2", name = "Jane Smith", kind = "person", birthYear = 1980, role = "director" },
    },
};

using var response = await http.PostAsJsonAsync("api/screen", request);
if (!response.IsSuccessStatusCode)
{
    // anything but 200 is not screened: refer the case, or try again later
    Console.WriteLine($"Not screened: {(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
    return;
}

var answer = await response.Content.ReadFromJsonAsync<ScreenAnswer>();
Console.WriteLine($"{answer!.Status} (screening {answer.ScreeningId})");
foreach (var s in answer.Subjects)
    Console.WriteLine($"  {s.SubjectId}: {s.Status} {s.Score} {s.MatchedName}");

record ScreenAnswer(Guid ScreeningId, string Status, List<SubjectAnswer> Subjects);
record SubjectAnswer(string SubjectId, string Status, double Score, string? MatchedName);

Check a company by name

POST /api/companies/check with the name the proposer typed, handling each resolution: go ahead only on matched with clear; refer everything else.

// Program.cs in a console app (dotnet new console), .NET 8 or later.
// The key is read from the SANCTIONS_API_KEY environment variable, not written in the code.
using System.Net.Http.Json;

var key = Environment.GetEnvironmentVariable("SANCTIONS_API_KEY")
    ?? throw new InvalidOperationException("Set SANCTIONS_API_KEY to your key");

using var http = new HttpClient { BaseAddress = new Uri("https://screening.policy365.cloud/") };
http.DefaultRequestHeaders.Add("X-Api-Key", key);

using var response = await http.PostAsJsonAsync("api/companies/check",
    new { name = "Highland Lamb Ltd", reference = "Q-1", disqualifications = true });
if (!response.IsSuccessStatusCode)
{
    // 400, 502 or 503: not screened
    Console.WriteLine($"Not screened: {(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
    return;
}

var check = await response.Content.ReadFromJsonAsync<CheckAnswer>();
switch (check!.Resolution)
{
    case "matched":
        Console.WriteLine($"{check.Company!.Name} ({check.Company.CompanyNumber}): {check.Status}");
        if (check.Status != "clear")
            Console.WriteLine("Refer: someone must look at this before going ahead.");
        break;
    case "ambiguous":
        Console.WriteLine("Not screened. Choose the company, then screen it with POST /api/companies/{number}/screen:");
        foreach (var c in check.Candidates)
            Console.WriteLine($"  {c.CompanyNumber} {c.Name} ({c.CompanyStatus}) score {c.Score}{(c.SameName ? ", same name" : "")}");
        break;
    default: // notFound
        Console.WriteLine("Not screened: Companies House has no company with a name like this. Refer.");
        break;
}

record CheckAnswer(string Resolution, string Status, Company? Company, List<Candidate> Candidates, Guid RecordId);
record Company(string CompanyNumber, string Name, string? Status);
record Candidate(string CompanyNumber, string Name, string? CompanyStatus, double Score, bool SameName);

Screen one name, as CSV

GET /api/screen/csv for one name, reading status as the first field.

// Program.cs in a console app (dotnet new console), .NET 8 or later.
// The key is read from the SANCTIONS_API_KEY environment variable, not written in the code.
var key = Environment.GetEnvironmentVariable("SANCTIONS_API_KEY")
    ?? throw new InvalidOperationException("Set SANCTIONS_API_KEY to your key");

using var http = new HttpClient { BaseAddress = new Uri("https://screening.policy365.cloud/") };
http.DefaultRequestHeaders.Add("X-Api-Key", key);

var url = "api/screen/csv?type=person"
    + "&name=" + Uri.EscapeDataString("Oleg Deripaska")
    + "&dob=1968-01"
    + "&reference=" + Uri.EscapeDataString("ACCESS-1");

using var response = await http.GetAsync(url);
var text = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
    // anything but 200 is not screened
    Console.WriteLine($"Not screened: {(int)response.StatusCode} {text}");
    return;
}

var row = text.Split("\r\n")[1];          // line 0 is the header
var status = row[..row.IndexOf(',')];     // status is the first field, never quoted
Console.WriteLine(status);                // clear, review or match

OpenAPI

The OpenAPI document is at https://screening.policy365.cloud/openapi/v1.json. Import it into Postman or Insomnia, or generate a client from it (NSwag, Kiota, openapi-generator). Send your key in the X-Api-Key header.