Appearance
Entities
List entities
GET /api/v1/entitiesEntities in the key's account, newest first by id. Needs entity.view. Paginated.
| Parameter | Default | Notes |
|---|---|---|
page | 1 | A page past the end returns an empty list, not an error |
per_page | 50 | Capped 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/entitiesjson
{
"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 pageOr 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
endEither 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/:idThe 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
| Field | Type | Notes |
|---|---|---|
id | integer | |
registration_number | string | Saudi Arabia: the 7-prefixed unified number |
legal_name | string | |
country_id | integer | |
status | string | draft, ready_for_review, approved, rejected, onboarded, under_monitoring |
registration_status | string | unknown, valid_status, invalid_status, expired |
current_score | integer | Verification coverage, 0–100. Present only once scored. |
risk_level | string | null | low, medium, high — only when assessed |
risk_level_updated_at | string | ISO 8601. Present only alongside a risk_level. |
risk_level_assessed | boolean | Present and false when no assessment has run |
sanctions_status | string | not_run, clear, hit, needs_review |
website_url, social_url, business_activity_summary | string | null | Operator-maintained |
updated_at | string | ISO 8601 |
current_relationships_count | integer | Parties marked current |
reviews_count, documents_count, checks_count | integer | |
checks | array | See below |
reviews | array | See 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"
}| Field | Notes |
|---|---|
check_type | See the check catalogue |
status | pending, running, completed, failed |
subject_* | The party this check targeted, for party-scoped checks; null for entity-scoped |
result | Sanitised 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
| Parameter | Effect |
|---|---|
registration_number | Exact match. This is how you find one merchant — you know its registration number, not Verity's internal id |
country_code | ISO 3166-1 alpha-2, case-insensitive |
updated_since | An 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:00ZFilters 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:
| Situation | Message |
|---|---|
| No country sent | Country is required — send an ISO country code, for example "SA" |
| Not a country Verity knows | Country "ZZ" is not a country Verity knows |
| Known, not enabled for you | Country 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/entitiesNeeds 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}}'| Field | Required | Notes |
|---|---|---|
registration_number | yes | Must match the country's format rule |
country_id | yes | Must 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.