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, the Consolidated Canadian Autonomous Sanctions List and the US Treasury's OFAC Specially Designated Nationals and Consolidated (non-SDN) lists, 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://policy365.ca

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://policy365.ca/api/screen \
  -H "X-Api-Key: $SANCTIONS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "groupReference": "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 (3p), 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
GET/api/lookupCompanies matching a name or number at Companies HouseCompanies HouseNo
GET/api/lookup/{number}A company, and everyone who holds or held a role in itCompanies HouseNo
GET/api/lookup/person/{personId}A person, and every role they hold or heldOur registerNo
POST/api/screenScreens 1 to 50 people and companiesSanctions lists Companies House*Yes
GET/api/screen/csvScreens one name; answers CSVSanctions listsYes
POST/api/screen/companyFinds a company by name and, if sure, screens it with its peopleCompanies House Sanctions listsWhen matched
GET/api/screen/company/csvThe same, as CSVCompanies House Sanctions listsWhen matched
POST/api/screen/company/{number}Screens a company with its peopleCompanies House Sanctions listsYes
GET/api/screen/company/{number}/csvThe same, as CSVCompanies House Sanctions listsYes
GET/api/screenings/{id}A recorded screeningOur recordsNo
GET/api/watchesThe names being watchedOur 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.

Lookup first. The lookups are free, and nothing is screened or recorded. Use them to find a client's company number, and keep the number on the client's record: it is what makes a screening complete. A company screened by its number is screened with its directors and persons with significant control, straight from Companies House and each with a month and year of birth; a company screened by name alone is one name against the lists.

GET/api/lookup

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

Companies matching a name or a number at Companies House. Nothing is screened or recorded.

GET /api/lookup?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/lookup/{number}

Calls Companies House Our register first; Companies House when our copy is more than a day old, or for disqualifications. No one is checked against the sanctions lists.

A company, and everyone who holds or held a role in it, each once: officers in office and former, persons with significant control notified and ceased, each with all their appointments in it. A director who is also an owner appears once, with both. Each has a personId in our register, the same in every company they're part of, which GET /api/lookup/person/{personId} takes. Nothing is screened or recorded.

GET /api/lookup/SC123456?current=true
  • current=true: only appointments still running (no until), for today's picture.
  • disqualifications=true: each officer's entries on the disqualified directors register, asked of Companies House each time (a search and up to three fetches an officer).
{
  "company": {
    "companyNumber": "SC123456", "name": "GLEN AFFRIC FARMS LTD", "status": "active", "type": "ltd",
    "created": "2010-01-01", "dissolved": null,
    "registeredOffice": { "premises": null, "line1": "1 High Street", "line2": null, "locality": "Inverness",
                          "region": null, "postalCode": "IV1 1AA", "country": "Scotland" },
    "sicCodes": ["01410"],
    "previousNames": [ { "name": "AFFRIC LAMB LTD", "from": "2010-01-01", "until": "2015-06-30" } ]
  },
  "people": [
    { "personId": 123, "officerId": "Xk8…", "kind": "person", "name": "Fiona MACLEOD", "birthYear": 1975, "birthMonth": 6,
      "nationality": "British", "countryOfResidence": "Scotland", "occupation": "Farmer", "companyNumber": null,
      "appointments": [
        { "type": "director", "from": "2015-01-01", "until": null },
        { "type": "personWithSignificantControl", "from": "2016-04-06", "until": null,
          "naturesOfControl": ["ownership-of-shares-75-to-100-percent"] }
      ],
      "disqualifications": null },
    { "personId": 124, "officerId": null, "kind": "company", "name": "AFFRIC HOLDINGS LTD", "companyNumber": "SC654321",
      "appointments": [ { "type": "corporateDirector", "from": "2012-03-01", "until": "2018-09-30" } ] }
  ],
  "withheld": 0,
  "asOf": "2026-10-16T22:40:00+00:00"
}
  • An appointment's type is Companies House's role in camel case (director, secretary, llpMember, corporateDirector…) or personWithSignificantControl; until is null while it runs.
  • A company holding a role has kind: "company" and, where Companies House gives it, its own companyNumber, to look up in turn.
  • withheld counts persons with significant control whose details Companies House withholds. asOf is when it was last confirmed with Companies House.
  • The register holds Companies House's data alone, never anything you send us, and never a screening's result.
  • 404 if Companies House has no such company; 502 if it can't answer and we've no copy.

Billed: no, with or without disqualifications.

Person lookup

GET/api/lookup/person/{personId}

Calls Our register Neither Companies House nor the lists.

Someone in our register, by the personId a company lookup or a screening gave: their details and every role they hold or held in a company we know of, each with the company's number, name and status. There's no search by name: a name doesn't pick out one person.

{ "personId": 123, "officerId": "Xk8…", "kind": "person", "name": "Fiona MACLEOD", "birthYear": 1975, "birthMonth": 6, …,
  "appointments": [
    { "type": "director", "from": "2015-01-01", "until": null, "companyNumber": "SC123456", "companyName": "GLEN AFFRIC FARMS LTD", "companyStatus": "active" },
    { "type": "director", "from": "2008-02-01", "until": "2014-12-31", "companyNumber": "SC100200", "companyName": "AFFRIC LAMB LTD", "companyStatus": "dissolved" }
  ] }

404 if there's no such person. Billed: no.

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

{
  "groupReference": "Q-10234",
  "subjects": [
    { "id": "firm", "reference": "CL-77", "name": "Highland Lamb Co Ltd", "kind": "company", "role": "proposer" },
    { "id": "p1", "reference": "C-5521", "name": "Oleg Deripaska", "kind": "person", "birthYear": 1968, "birthMonth": 1, "role": "principal" }
  ]
}
FieldRules
groupReferenceOptional, at most 200 characters. What this screening belongs to: your policy, quote or case number. Use the same one every time, and a case's screenings group together: found together on the site, and each record shows the others under it. Kept with the record and returned in the answer. (reference here is its old name, still taken for now; it will go.)
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[].referenceOptional, at most 200 characters. Your own reference for this person or company: a client or contact ID. Kept, searchable on the site, and returned beside the subject, so every check of them can be tied back to your records, across policies. The people Companies House adds for a company carry the company's.
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.
watchOptional, 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.
watchMonthsOptional, 1 to 120; 12 if not given. How long to watch them. Watching someone already watched renews the watch. (monitor and monitorMonths are the old names, still taken for now.)
listsOptional: which lists to check, by source: uk, eu, un, ca. The UK list is always checked, so include "uk". Not given: the lists your account checks, which its owner sets on the account page (every list unless chosen). The answer's lists names exactly those checked. The company calls always use your account's lists.

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",
  "groupReference": "Q-10234",
  "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",
  "notice": "An automated comparison with the sanctions lists as last fetched. A review or a match means a name resembles a listed entry, for a person to look at: it is not a finding that anyone is sanctioned, and not legal advice."
}
  • screeningId finds the record later, with GET /api/screenings/{id}.
  • Each subject carries back its reference. One Companies House added has parentId, the subject it came with, and register: its identity on Companies House's register, { "companyNumber": "…" } for a company, { "officerId": "…" } for an officer (the same across all their appointments). A person with significant control who isn't also an officer has none.
  • units is what this call billed, at 3p 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.
  • notice says, in one sentence, what a result is and isn't. It's the same on every answer: show it beside a result wherever a person sees one.
  • 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",
  "notice": "An automated comparison with the sanctions lists as last fetched. A review or a match means a name resembles a listed entry, for a person to look at: it is not a finding that anyone is sanctioned, and not legal advice."
}

Billed: 3p for each subject screened, those Companies House adds included, and 3p 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&groupReference=POL-1&reference=C-5521
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.
groupReferenceOptional, at most 200 characters: the policy, quote or case. Kept with the record and returned last on the row.
referenceOptional, at most 200 characters. With a groupReference, it's the person's or company's own reference; without one, it's taken as the group's, as before.

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: 3p.

Company check by name

POST/api/screen/company

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", "groupReference": "Q-1", "reference": "CL-77", "disqualifications": true }
FieldRules
nameRequired, 2 to 300 characters, and more than a legal form.
groupReferenceOptional, at most 200 characters: the policy, quote or case.
referenceOptional, at most 200 characters. With a groupReference, the company's own reference (your client ID for it); without one, taken as the group's, as before.
disqualificationsOptional, default false. Checks each officer against the disqualified directors register.
watchOptional, 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 watch again to POST /api/screen/company/{number} with the company chosen. Not given: as your plan says.
watchMonthsOptional, 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",
  "notice": "An automated comparison with the sanctions lists as last fetched. A review or a match means a name resembles a listed entry, for a person to look at: it is not a finding that anyone is sanctioned, and not legal advice."
}

When matched: the screening is the same as POST /api/screen/company/{number} 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/screen/company/{number}, 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 (3p a subject, 3p an officer checked for disqualification). Ambiguous and not found are free.

Company check, as CSV

GET/api/screen/company/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/screen/company/csv?name=Highland%20Lamb%20Ltd&reference=Q-1&disqualifications=true

watch=true and watchMonths 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/screen/company.

Company screen by number

POST/api/screen/company/{number}

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/screen/company/00445790
Content-Type: application/json

{ "groupReference": "Q-1", "reference": "CL-77", "disqualifications": true, "watch": true }

The body is optional, and so is every field: groupReference, the policy, quote or case; reference, the company's own (taken as the group's when no groupReference is given); disqualifications, and watch and watchMonths 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",
  "notice": "An automated comparison with the sanctions lists as last fetched. A review or a match means a name resembles a listed entry, for a person to look at: it is not a finding that anyone is sanctioned, and not legal advice."
}

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

Company screen, as CSV

GET/api/screen/company/{number}/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/screen/company/00445790/csv?reference=T-1&disqualifications=true

watch=true and watchMonths 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/screen/company/{number}.

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.

Watching names

GET/api/watches DELETE/api/watches/{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: £199 a month for up to 25,000 names watched, or £399 for up to 100,000; more than that, ask us. At the plan's limit a screening still runs but isn't watched, and the answer has no watchedUntil. With it, screen with "watch": 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/watches: 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 groupReference and the subject's own reference, fromScreeningId, endsAt, lastCheckedAt and lastHitScreeningId.
  • DELETE /api/watches/{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 watch 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", "groupReference": "POL-1234", "reference": "C-5521",
            "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.

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.

AI assistants

Claude, and other AI assistants that take a remote MCP connector, can look up companies and run checks for your people. The connector's address is:

https://policy365.ca/mcp
  • Connecting. Step by step for each app, with things to ask and what it covers, on Check who you're dealing with. In short: in Claude, add a custom connector (Settings, then Connectors) with that address. In Claude Code: claude mcp add --transport http policy365 https://policy365.ca/mcp. The assistant opens this site's sign-in; you sign in as yourself with the emailed code, see what it may do, and approve. No API key is pasted anywhere: the assistant holds a token for an hour at a time, renewed for up to 30 days, and you or your account's owner can end it from the account page.
  • Turning it on. It's off until the account's owner turns on "Let our people run checks from AI assistants" on the account page.
  • What it records. A check through an assistant is the account's own: the account's lists, recorded and signed, billed at the usual price, and shown in Recent as made by that person "through Claude". The assistant can't record decisions on reviews: those stay with a person, on this site.
  • Names pass through the assistant before they reach us, under your own terms with its provider (privacy notice).
ToolWhat it doesPrice
listsThe sanctions lists: each one's publish date, when it was fetched, and whether screening is available, as Lists and health. Read-only.Free
lookup_companyA UK company by its number: its status, registered office, and everyone who holds or held a role in it, as Company lookup. Screens no one. Read-only.Free
get_screeningA recorded screening by its id, or the latest screenings filed under a groupReference (up to 10), as Screening records. Only the account's own. A light account has no records, so it answers that nothing is recorded. Read-only.Free
check_companyA company by name: screened with its directors and owners when exactly one active company has that name, as Company check by name; otherwise the candidates to choose from.3p a name; free when ambiguous or not found
screenPeople (name, year and month of birth, role, your reference) and companies (by number, with their officers and controllers, or by name), as Screen. Its disqualifications option is optional and nothing is assumed: when it isn't given and a company is screened, the answer says the directors weren't checked against the disqualified register.3p a name
watch_recordWatches every name on a recorded screening from now, for 12 months, without screening them again, as Watching names. Needs the account's re-screening plan; not for light accounts.With a re-screening plan

Each answer is a short account in words for the assistant to pass on, the full answer as data (the API's own shape), and the record's verification link. With a re-screening plan, screen and check_company watch what they screen as the plan says, or as asked (watch); any record's names can be watched later with watch_record, or from its page with Watch these names, without screening them again. A check given no groupReference is filed under one made for it, such as CLD000012, for the assistant to give again on the case's next check. When an assistant connects, the server sends it instructions describing the tools and the choices to confirm with you before a company is screened: whether to check its directors against the disqualified register, whether to watch the names, and the case reference.

Light accounts and certificates

  • A light account is made when someone with no account connects an assistant: free, for 25 checks and 100 company look-ups a day. Its checks are screened in full but not recorded: we keep no names and no results, so a light check isn't evidence. It can't screen through this website or the API. Signing up with the same email makes it a full account, keeping the assistant connected.
  • A certificate turns a light check into evidence, for £2. Each light answer that screened someone gives a link to buy one, held for 24 hours. Paid (by card, Apple Pay or Google Pay, businesses only), the check is screened again against the lists as they then stand, recorded and signed in the buyer's business's name, and emailed with its verification code, which anyone can check at /verify for six years.

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 1,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)3p
Each officer checked against the disqualified directors register3p
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
A light account's checks through an AI assistant (25 a day; not recorded)Free
A certificate of a light account's check: screened again, recorded and signed£2 each
  • No VAT is charged: Trailhead Design Limited is a Canadian company. UK business customers account for any VAT due under the reverse charge.
  • 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 1,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: 24p, or 42p 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://policy365.ca/") };
http.DefaultRequestHeaders.Add("X-Api-Key", key);

var request = new
{
    groupReference = "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/screen/company 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://policy365.ca/") };
http.DefaultRequestHeaders.Add("X-Api-Key", key);

using var response = await http.PostAsJsonAsync("api/screen/company",
    new { name = "Highland Lamb Ltd", groupReference = "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/screen/company/{number}:");
        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://policy365.ca/") };
http.DefaultRequestHeaders.Add("X-Api-Key", key);

var url = "api/screen/csv?type=person"
    + "&name=" + Uri.EscapeDataString("Oleg Deripaska")
    + "&dob=1968-01"
    + "&groupReference=" + 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://policy365.ca/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. The operations are grouped under Lookup, Screening, Records, Watches, Webhooks and Service, and each has an id (lookupCompany, screen, recordDecision…), so a generated client's methods carry sensible names.