Skip to content

Entities

List entities

GET /api/v1/entities

Entities in the key's account, newest first by id. Needs entity.view. Paginated.

ParameterDefaultNotes
page1A page past the end returns an empty list, not an error
per_page50Capped at 200. A nonsense value falls back to the default rather than failing.
bash
curl -H "Authorization: Bearer $VERITY_API_KEY" \
     https://your-host/api/v1/entities
json
{
  "entities": [
    {
      "id": 42,
      "registration_number": "7000000033",
      "legal_name": "Desert Holdings International",
      "country_id": 1,
      "status": "under_monitoring",
      "registration_status": "invalid_status",
      "current_score": 35,
      "risk_level": null,
      "risk_level_assessed": false,
      "sanctions_status": "hit",
      "website_url": null,
      "social_url": null,
      "business_activity_summary": "Cross-border commodity trading",
      "updated_at": "2026-06-28T08:30:00Z",
      "current_relationships_count": 1,
      "reviews_count": 1,
      "documents_count": 0,
      "checks_count": 1,
      "checks": [],
      "reviews": []
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 50,
    "total_count": 137,
    "total_pages": 3
  }
}

meta is present on every response, even a single page, so a caller that ignores pagination still sees that it received part of the data rather than quietly taking a prefix for the whole portfolio.

Pagination headers

http
X-Total-Count: 137
Link: <https://your-host/api/v1/entities?page=1&per_page=50>; rel="first",
      <https://your-host/api/v1/entities?page=2&per_page=50>; rel="next",
      <https://your-host/api/v1/entities?page=3&per_page=50>; rel="last"

Standard RFC 8288 link relations — first, prev, next and last, each present only when it applies. Following next until it is absent is the simplest correct way to walk the whole account.

Walking the whole account

Two equivalent approaches. Follow Link if you want to think about it as little as possible; count with meta if you would rather drive the loop yourself.

Follow rel="next" until it is absent.

python
import re, requests

def links(response):
    header = response.headers.get("Link", "")
    return {rel: url for url, rel in re.findall(r'<([^>]+)>;\s*rel="([^"]+)"', header)}

url = "https://your-host/api/v1/entities"
headers = {"Authorization": f"Bearer {token}"}
entities = []

while url:
    response = requests.get(url, headers=headers)
    response.raise_for_status()
    entities += response.json()["entities"]
    url = links(response).get("next")      # absent on the last page

Or count pages from meta.

ruby
page = 1
entities = []

loop do
  body = get("/api/v1/entities?page=#{page}")
  entities.concat(body["entities"])
  break if page >= body["meta"]["total_pages"]
  page += 1
end

Either way, stopping on an empty entities array is also safe — a page past the end returns one rather than an error.

Ordering and stability

Results are ordered by id descending — newest-created first.

That is deliberately the only sort key. id is unique and immutable, so the order is total and does not change under you: a merchant edited while you are paging stays exactly where it was. An updated_at ordering cannot promise that — an edit moves a row to the front, so a caller could see it twice on two different pages and never see another at all.

Newest-created, not newest-touched

This means the endpoint is not a change feed. Polling page 1 will not surface a merchant that was updated but not created recently. If you need recently-changed entities, that wants a filter rather than an ordering — say so and it can be added.

Only inserts shift the list, and they shift it at the front: an entity created mid-walk pushes everything down by one, which can show you a row twice. If an exact extract matters, de-duplicate by id and compare your count against meta.total_count.

If you integrated before pagination existed

This endpoint previously returned every entity in one response. A consumer that assumed that now receives the first 50 and must be updated.

Nothing about the truncation is hidden: meta.total_count and meta.total_pages are on every response, and X-Total-Count is on every response header. The cheapest correct fix is one of the loops above; the cheapest temporary fix is per_page=200, which merely raises the ceiling rather than removing it.

Get an entity

GET /api/v1/entities/:id

The same entity object, with checks and reviews populated. Checks are newest-first; reviews put active ones (in_progress, escalated, open) before finished ones.

Entity fields

FieldTypeNotes
idinteger
registration_numberstringSaudi Arabia: the 7-prefixed unified number
legal_namestring
country_idinteger
statusstringdraft, ready_for_review, approved, rejected, onboarded, under_monitoring
registration_statusstringunknown, valid_status, invalid_status, expired
current_scoreintegerVerification coverage, 0–100. Present only once scored.
risk_levelstring | nulllow, medium, highonly when assessed
risk_level_updated_atstringISO 8601. Present only alongside a risk_level.
risk_level_assessedbooleanPresent and false when no assessment has run
sanctions_statusstringnot_run, clear, hit, needs_review
website_url, social_url, business_activity_summarystring | nullOperator-maintained
updated_atstringISO 8601
current_relationships_countintegerParties marked current
reviews_count, documents_count, checks_countinteger
checksarraySee below
reviewsarraySee below

Check object

json
{
  "id": 91,
  "check_type": "organization_screening",
  "provider_key": "focal",
  "status": "completed",
  "provider_status": "succeeded",
  "triggering_reason": "entity_create",
  "subject_type": null,
  "subject_id": null,
  "subject_display_name": null,
  "result": { "status": "FINISHED", "matched": false, "matches_count": 0 },
  "checked_at": "2026-06-28T08:30:00Z",
  "created_at": "2026-06-28T08:30:00Z",
  "updated_at": "2026-06-28T08:30:00Z"
}
FieldNotes
check_typeSee the check catalogue
statuspending, running, completed, failed
subject_*The party this check targeted, for party-scoped checks; null for entity-scoped
resultSanitised provider result. Credentials and internal keys are stripped. Shape varies by check type.

Read result defensively

result mirrors what the provider returned for that check type, so its keys differ between types and can gain fields when a provider changes. Read the keys you need and tolerate the rest. In particular, a missing matches_count means the provider has not answered — it does not mean zero.

Review object

json
{
  "id": 7,
  "purpose": "initial",
  "status": "in_progress",
  "outcome": null,
  "outcome_reason": null,
  "assigned_user_id": 3,
  "assigned_user_name": "Nora Analyst",
  "started_at": "2026-06-28T08:00:00Z",
  "completed_at": null,
  "created_at": "2026-06-28T08:00:00Z",
  "updated_at": "2026-06-28T08:40:00Z",
  "api_path": "/api/v1/entity_reviews/7"
}

Filtering the list

ParameterEffect
registration_numberExact match. This is how you find one merchant — you know its registration number, not Verity's internal id
country_codeISO 3166-1 alpha-2, case-insensitive
updated_sinceAn ISO 8601 timestamp. Returns only entities changed at or after it — a change feed
GET /api/v1/entities?registration_number=7010466964
GET /api/v1/entities?updated_since=2026-08-12T09:00:00Z

Filters compose, and all of them respect pagination and the id ordering — so a caller walking pages of a filtered list still cannot see one entity twice while missing another.

An updated_since that does not parse is refused with 422, not ignored. A change feed that silently returned everything because the timestamp was malformed would leave you believing you had seen a day of changes when you had in fact seen the whole portfolio.

Create an entity

Name the country with country_code — an ISO 3166-1 alpha-2 code, which means the same thing in every environment. country_id is accepted for existing callers, but it is a database row number that differs between staging and production, so prefer the code.

json
{ "entity": { "registration_number": "7010466964", "country_code": "SA" } }

The code is case-insensitive. The country must be configured and active for your account — a country Verity knows but your account has not enabled is refused, and the error says which of the two is the problem:

SituationMessage
No country sentCountry is required — send an ISO country code, for example "SA"
Not a country Verity knowsCountry "ZZ" is not a country Verity knows
Known, not enabled for youCountry is not configured for this account

The first two are yours to fix; the last needs an administrator to enable the country under Settings → Country defaults.

POST /api/v1/entities

Needs entity.create. Creating an entity runs the intake checks configured for that country, so this is not a cheap call — it makes real provider requests.

bash
curl -X POST https://your-host/api/v1/entities \
     -H "Authorization: Bearer $VERITY_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{"entity": {"registration_number": "7000000033", "country_id": 1}}'
FieldRequiredNotes
registration_numberyesMust match the country's format rule
country_idyesMust have an active country configuration on your account

Returns 201 with the same body as GET /api/v1/entities/:id.

Validation failures

422 with per-field messages:

json
{ "errors": { "registration_number": ["must be a 10-digit Saudi unified number starting with 7"] } }

An unconfigured country comes back as {"errors": {"country": ["is not configured for this account"]}}.

Markdown

Both show endpoints serve text/markdown via Accept: text/markdown or a .md suffix — a human-readable summary including the score breakdown. It reports Risk level: Not assessed where no assessment has run rather than printing the default.