Skip to main content
Use the Admin API to keep a custom list in step with a system of record, such as a CRM, case management tool, or offboarding register, without anyone uploading files in the Minerva dashboard. Your integration adds, updates, and archives records, publishes a new version, switches the list on in each workspace that needs it, and waits until screening uses the new version. These endpoints need an admin application key. Workspace-scoped and tenant-scoped keys keep working for screening and profile management, but these endpoints refuse them for reads as well as changes.
Enterprise feature: Custom lists must be enabled by Minerva before your organization can use them. Contact your Minerva representative or support@gominerva.com to discuss enabling custom-list screening.
This guide covers the key and authentication, the object model, every endpoint, a complete CRM sync recipe, bulk file uploads, and error handling.

The admin application key

An admin application key is a tenant-wide key. In the Minerva dashboard it appears as the Admin (tenant-wide) key type.
  • Only an Owner or Admin can create one, and only in Live mode. See Create an admin key for the steps.
  • The key acts only while the person who created it is still an owner or admin of the organization. If that person is demoted, disabled, or removed, every request made with the key is refused with 403. Create a replacement key from an account that still holds the role.
  • Workspace-scoped and tenant-scoped keys are refused with 403 and the message “An admin application key is required.”
  • The key belongs to one organization. It is never accepted for another organization’s orgId.
  • Changes made with the key are recorded in the list’s audit trail under the application’s name, not under a person.
An admin key can change every custom list and every workspace in your organization. Create one only for the integration that needs it, keep it in a secrets manager, and deactivate it when that integration is retired.

Authentication and base URL

Send every request to the custom lists base URL, with your organization ID in place of {orgId}:
admin-api.gominerva.com is deprecated for API clients. It keeps working for now, but new and updated integrations should call api.gominerva.com. The paths change as well as the host:
  • admin-api.gominerva.com/v2/organizations/{orgId}/custom-lists/… becomes api.gominerva.com/custom-lists/v2/organizations/{orgId}/…
  • admin-api.gominerva.com/v2/organizations/{orgId}/workspaces/… becomes api.gominerva.com/workspaces/v2/organizations/{orgId}/…
  • admin-api.gominerva.com/v2/webhooks/screening/… becomes api.gominerva.com/webhooks/v2/screening/…
Authenticate with the admin key in the Authorization header. The legacy x-api-key header is also accepted.
orgId must be the organization the key was created in. Successful responses wrap the result in a data field.

Object model

Two rules shape every integration:
  • Publishing moves every workspace where the list is already active onto the new version. You only need to activate a workspace once.
  • A change reaches screening only when the deployment state is live. A 202 from publish means the deployment has started, not that it has finished.
A live list is searched only when the screening request includes the list’s feed category. For a list on the internal feed, that is the Internal feed (Internal Risk in the dashboard):
  • Onboarding screening: add Internal Risk to the workspace’s onboarding screening feeds on the Profile Onboarding configuration page.
  • Ongoing monitoring: give Internal a cadence other than never in the workspace’s monitoringFeedFrequencies. See Monitoring cadences and the Screening Frequencies Guide.
  • Direct API searches: include "Internal" in feeds. "all" includes Internal only when the workspace’s screening feed allowlist (allowedScreeningFeeds) lists Internal explicitly and Minerva has not limited your account to feeds that leave out Internal, so send ["all", "Internal"] or name the feeds you need. An explicit Internal returns 403 if the workspace allowlist is set and leaves it out. If the allowlist includes Internal and you still get 403, contact support@gominerva.com.
A list that is live but never requested is never matched, so check this before you treat a sync as complete.

Endpoints

The Custom Lists section of the Admin Actions - Endpoints reference in this tab lists every request and response field. The tables below summarize each family. Paths are relative to /custom-lists/v2/organizations/{orgId}.

Lists

POST requires name. default_feed_category is one of internal, sanctions, pep, news, legal, or criminal and defaults to internal. record_id_column names the column that holds the record UID in uploaded files, defaults to record_uid, and can only be set when the list is created.

Records

  • {recordId} accepts the record’s record_uid, so you can address records by your own identifier. URL-encode it.
  • A record needs name and entity_type (individual, organization, or unknown). POST also needs record_uid. Optional fields are aliases, countries, date_of_birth, date_of_incorporation, reason, notes, and a custom_fields map of strings.
  • PATCH replaces the record’s content. Send the complete record: fields you leave out are cleared.
  • POST returns 409 when the record_uid already exists on the list, including on an archived record.
  • limit is 1 to 1000. The response’s data.total is the total number of matching records.

Publishing

publish and discard require expected_revision, the working_set_revision you read from pending changes. If the working set changed after you read it, the request returns 409 and nothing is published.

Activation and status

  • The activation body takes enabled (default true). When you switch a list on, feedCategory and versionId are required.
  • A list on the internal feed can be activated only in a workspace whose match scoring sets an Internal threshold for every workflow. Otherwise the request returns 400 and names the missing workflows.
  • If the workspace’s screening feed allowlist is set and leaves out the list’s feed, activation returns 400 with, for example, “Internal must be enabled in the workspace before a custom list can be activated for that feed.” A workspace with an empty allowlist accepts every feed.
  • The status response carries deployment_status (pending, live, or down), workspace_activation_status (not_activated, activated, or disabled), latest_version_id, active_version_id, and deployment_stage. When the state is down, deployment_failure_reason explains the failure when one is available.

Bulk uploads

Recipe: sync an offboarded-clients register from your CRM

This walkthrough keeps a list of offboarded clients in step with a CRM. Each run sends the clients added to or changed in the register since the last run, archives clients removed from it, publishes, makes sure the list is active in each workspace, and waits until screening uses the new version. Only then does it mark the CRM entries as synced. Set these variables first:

1. Create the list once

Create the list on the first run and store its ID in your integration’s configuration. Later runs reuse it.
cURL
The response returns 201 with the list in data. Keep data.id as LIST_ID. To find an existing list instead, call GET $BASE and match on name.

2. Add or update each record

Use the CRM’s client ID as record_uid. Update the record first, addressing it by record_uid. PATCH replaces the whole record rather than merging into it, so send the complete record from the CRM every time: name, entity_type, and every other field you want to keep. A field you leave out is cleared, and a body without name or entity_type returns 400.
cURL
  • 200 means the record existed and now holds this content. If data.status is archived, the client was removed earlier and has returned to the register. Restore it with PUT $BASE/$LIST_ID/records/CRM-10442/status and {"status": "active"}.
  • 404 with the JSON body {"message": "not found"} means the record is new. A 404 with any other body, such as “API-key authentication was denied.”, is an error, not a new record. Add a new record with POST, sending the same body plus "record_uid": "CRM-10442":
cURL

3. Archive records removed from the register

cURL
Archived records stay in the list’s history and leave screening when you publish. Archiving a record that is already archived succeeds.

4. Read the working-set revision

cURL
If has_changes is false, nothing needs publishing; skip to step 6 to confirm the list is live.

5. Publish

Send the revision you just read as expected_revision:
cURL
The response returns 202 with the new version in data. Keep data.id as VERSION_ID. Publishing starts the deployment in every workspace where the list is already active. If the response is 409, the working set changed after you read the revision, for example because another process or a dashboard user edited the list. Nothing was published. Read pending changes again and retry with the new revision.

6. Activate the list in each workspace, once

Check whether the list is already active in the workspace:
cURL
If data.workspace_activation_status is activated, the publish has already moved this workspace onto the new version; go to step 7. Otherwise, activate the version you published:
cURL
Repeat this step for every workspace that should screen against the list. If step 4 showed no changes and you skipped step 5, use data.active_version_id from the status response as VERSION_ID. If activation returns 400 because the workspace’s screening feed allowlist leaves out Internal, or because its match scoring has no Internal threshold, fix the workspace configuration first. Then make sure the workspace’s screening requests include the Internal feed, as described under Object model. Otherwise the list goes live but is never searched.

7. Wait until the deployment is live

Subscribe instead of poll

A custom_list_deployment webhook tells your integration when a deployment goes live or down, so it does not have to poll. For workspace scope, create the webhook once in each workspace where you activate the list, with the same admin key. Alternatively, register one tenant-scoped destination to cover all current and future workspaces. Send x-workspace-id: a workspace-scoped webhook created without it lands in the organization’s default workspace, and it never fires for a list that is not active there.
cURL
Webhooks live under /webhooks/v2/screening on api.gominerva.com. The response carries the webhook key; store it in your secrets manager and check it against the X-Webhook-Key header on every delivery. You can also create or edit the webhook on the Developers page in the dashboard; edits there keep the subscriptions you created through the API. When a delivery arrives:
  • Dedupe on the X-Webhook-Delivery-ID header. A retried delivery repeats the same ID.
  • When meta.status is live and meta.versionId equals VERSION_ID, screening in the workspace named by meta.workspaceId uses the new version. Once every activated workspace reports this, go to step 8.
  • Treat custom_list.deployment.down as a failure: either this deployment failed, or the list was live and stopped being available for screening. Leave the CRM entries unsynced and alert your team. meta.failure.message describes the failure; contact support@gominerva.com if it persists.
  • Ignore a pending delivery whose meta.occurredAt is older than the last delivery you processed for that list and workspace. Deliveries can arrive out of order, and publishing a different list can briefly send pending and then live for this list.
If a delivery has not arrived when you expect one, for example because your endpoint was unavailable after the retries ran out, make one call to the status endpoint below to reconcile.

Poll as a fallback

Without a webhook, poll the status endpoint with backoff, for example every 10 seconds at first and doubling up to every 2 minutes:
cURL
  • pending: the version is still deploying. Keep polling.
  • live with latest_version_id equal to VERSION_ID: screening in that workspace uses the new version.
  • down: the deployment failed. deployment_stage names the step that failed, and deployment_failure_reason explains it when available. Leave the CRM entries unsynced, alert your team, and contact support@gominerva.com if it persists.
Check each activated workspace the same way.

8. Mark the CRM entries as synced

Mark the CRM entries from this run as synced only when every workspace reports live on VERSION_ID and requests the Internal feed in its onboarding screening, monitoring, or API searches. If the run stops earlier, the next run sends the same changes again; updates by record_uid are safe to repeat.

Bulk alternative: upload a file

For a full export rather than a stream of changes, upload a CSV or XLSX file. full_replace archives every record that is missing from the file, which makes the file the complete source of truth; upsert_only keeps them.
  1. Download the template with GET $BASE/$LIST_ID/uploads/template (add format=xlsx for Excel) and match its column headers. Each row needs the record UID column, name, and entity_type.
  2. Upload the file:
    cURL
    The response returns 202 with the upload in data. Keep data.id as UPLOAD_ID and data.job_id as JOB_ID. record_id_column must match the list’s record_id_column.
  3. Poll GET $BASE/jobs/$JOB_ID until data.status is completed or failed, then read GET $BASE/$LIST_ID/uploads/$UPLOAD_ID. When its status is ready_for_review, check adds, updates, unchanged, archives, and validation_errors. When it is failed, fix the rows listed in validation_errors and upload again.
  4. Publish the upload:
    cURL
    The response returns 202 with a new data.job_id. Poll the job until it is completed; the upload’s status is then published.
  5. Read the new version ID from GET $BASE/$LIST_ID/versions, which lists the newest version first, then continue with steps 6 to 8 of the recipe.
A file upload publishes on its own; you do not call /publish with expected_revision. If the list changes between validation and publish, the upload publish returns 409. Upload the file again.

Errors

Error bodies come in two forms. Authentication errors and some 400 validation errors are plain text. All other errors are JSON with a message field, and most custom-list errors also carry an error code. Parse the body as JSON only when Content-Type is application/json. Retry 408, 429, and 503 with exponential backoff, for example starting at 2 seconds and doubling up to 2 minutes, with random jitter so parallel workers do not retry together. Retry a 409 only after reading the current state again.