> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gominerva.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Automate custom lists via API

> Create custom lists, sync records from your systems, publish versions, activate them per workspace, and confirm they are live, with an admin application key.

Use the Admin API to keep a [custom list](https://docs.gominerva.com/custom-lists-guide.md) 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.

> [!WARNING]
>
> **Enterprise feature:** Custom lists must be enabled by Minerva before your
> organization can use them. Contact your Minerva representative or
> [support@gominerva.com](mailto: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](https://docs.gominerva.com/api-reference/workspace-management-api.md#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.

> [!WARNING]
>
> 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}`:

```http theme={null}
https://api.gominerva.com/custom-lists/v2/organizations/{orgId}
```

> [!NOTE]
>
> `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.

```http theme={null}
Authorization: Api-Key YOUR_ADMIN_API_KEY
```

```http theme={null}
x-api-key: YOUR_ADMIN_API_KEY
```

`orgId` must be the organization the key was created in. Successful responses wrap the result in a `data` field.

## Object model

| Object | What it is |
| - | - |
| List | A tenant-owned collection of records with a name, description, default feed category, and `risk_score_impact` (0 to 100). Lists are shared by every workspace in the organization. |
| Record | One individual or organization on the list. Each record has a `record_uid`, a stable identifier from your source system that is unique within the list, plus `name`, `entity_type`, and optional fields. |
| Working set | The editable records and list settings. Every change you make lands here first and does not affect screening until you publish. `working_set_revision` increases by one with each change. |
| Version | An immutable snapshot of the working set, created by publishing. Only published versions can be activated. |
| Activation | A per-workspace setting that chooses which published version screens in that workspace, and on which feed category. A list can be active in one workspace and inactive in another. |
| Deployment state | Whether the activated version is serving screening: `pending` while it is being indexed, `live` once screening uses it, or `down` if the deployment failed. |

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.

> [!WARNING]
>
> 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](https://docs.gominerva.com/api-reference/workspace-management-api.md#monitoring-cadences)
>   and the [Screening Frequencies Guide](https://docs.gominerva.com/screening-frequencies-guide.md).
> * **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](mailto: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

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/` | List the organization's lists. Add `workspace_id` to include that workspace's activation. |
| `POST` | `/` | Create a list. |
| `GET` | `/{listId}` | Read a list. |
| `PATCH` | `/{listId}` | Update a list's name, description, default feed category, or risk rating impact. |
| `DELETE` | `/{listId}` | Archive a list. This switches it off in every workspace. |
| `GET` | `/entitlements` | Read your custom-list limits, such as records per list and upload size. |
| `GET` | `/workspaces/{workspaceId}/manifest` | Read which lists are active in a workspace. |

`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

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/{listId}/records` | List working-set records. Filter with `status` and `q`; page with `limit` and `offset`. |
| `POST` | `/{listId}/records` | Add a record. |
| `PATCH` | `/{listId}/records/{recordId}` | Replace a record's content. |
| `DELETE` | `/{listId}/records/{recordId}` | Archive a record. |
| `PUT` | `/{listId}/records/{recordId}/status` | Archive or restore a record with `{"status": "archived"}` or `{"status": "active"}`. |

* `{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

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/{listId}/pending-changes` | Summarize unpublished changes and read the current `working_set_revision`. |
| `POST` | `/{listId}/publish` | Publish the working set as a new version. |
| `POST` | `/{listId}/discard` | Discard unpublished changes and return to the last published version. |
| `GET` | `/{listId}/versions` | List the list's versions. |

`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

| Method | Path | Purpose |
| - | - | - |
| `PUT` | `/workspaces/{workspaceId}/lists/{listId}/activation` | Switch a list on or off in a workspace. |
| `GET` | `/{listId}/status` | Read deployment state. Add `workspace_id` to scope it to a workspace. |

* 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

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/{listId}/uploads/template` | Download an empty template. Add `format=xlsx` for Excel; CSV is the default. |
| `POST` | `/{listId}/uploads/file` | Upload a CSV or XLSX file for validation. |
| `POST` | `/{listId}/uploads/preview/file` | Validate a file and preview rows without creating a publishable upload. |
| `GET` | `/{listId}/uploads` | List recent uploads. |
| `GET` | `/{listId}/uploads/{uploadId}` | Read an upload with its plan, preview rows, and validation errors. |
| `GET` | `/{listId}/uploads/{uploadId}/status` | Read an upload's status. |
| `POST` | `/{listId}/uploads/{uploadId}/publish` | Publish a validated upload as a new version. |
| `GET` | `/jobs/{jobId}` | Read a background job started by an upload or upload publish. |

## 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:

```bash theme={null}
export MINERVA_ADMIN_KEY="YOUR_ADMIN_API_KEY"
export ORG_ID="YOUR_ORG_ID"
export WORKSPACE_ID="YOUR_WORKSPACE_ID"
export BASE="https://api.gominerva.com/custom-lists/v2/organizations/$ORG_ID"
```

### 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.

```bash cURL theme={null}
curl -X POST "$BASE" \
  -H "Authorization: Api-Key $MINERVA_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "name": "Offboarded clients",
    "description": "Clients exited by the business. Synced from the CRM.",
    "default_feed_category": "internal",
    "risk_score_impact": 80
  }'
```

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`.

```bash cURL theme={null}
curl -X PATCH "$BASE/$LIST_ID/records/CRM-10442" \
  -H "Authorization: Api-Key $MINERVA_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "name": "Jane Example",
    "entity_type": "individual",
    "aliases": ["Jane A. Example"],
    "countries": ["GB"],
    "date_of_birth": "1980-04-12",
    "reason": "Offboarded: relationship exited",
    "custom_fields": { "offboarded_on": "2026-10-28" }
  }'
```

* `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"`:

```bash cURL theme={null}
curl -X POST "$BASE/$LIST_ID/records" \
  -H "Authorization: Api-Key $MINERVA_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "record_uid": "CRM-10442",
    "name": "Jane Example",
    "entity_type": "individual",
    "aliases": ["Jane A. Example"],
    "countries": ["GB"],
    "date_of_birth": "1980-04-12",
    "reason": "Offboarded: relationship exited",
    "custom_fields": { "offboarded_on": "2026-10-28" }
  }'
```

### 3. Archive records removed from the register

```bash cURL theme={null}
curl -X DELETE "$BASE/$LIST_ID/records/CRM-09817" \
  -H "Authorization: Api-Key $MINERVA_ADMIN_KEY"
```

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

```bash cURL theme={null}
curl "$BASE/$LIST_ID/pending-changes" \
  -H "Authorization: Api-Key $MINERVA_ADMIN_KEY"
```

```json theme={null}
{
  "data": {
    "working_set_revision": 42,
    "added": 3,
    "updated": 1,
    "removed": 1,
    "total_after": 1288,
    "has_changes": true,
    "sample": [
      { "record_uid": "CRM-10442", "name": "Jane Example", "change": "added" }
    ],
    "config_changes": []
  }
}
```

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`:

```bash cURL theme={null}
curl -X POST "$BASE/$LIST_ID/publish" \
  -H "Authorization: Api-Key $MINERVA_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  --data '{"expected_revision": 42}'
```

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:

```bash cURL theme={null}
curl "$BASE/$LIST_ID/status?workspace_id=$WORKSPACE_ID" \
  -H "Authorization: Api-Key $MINERVA_ADMIN_KEY"
```

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:

```bash cURL theme={null}
curl -X PUT "$BASE/workspaces/$WORKSPACE_ID/lists/$LIST_ID/activation" \
  -H "Authorization: Api-Key $MINERVA_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  --data "{\"enabled\": true, \"feedCategory\": \"internal\", \"versionId\": \"$VERSION_ID\"}"
```

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](#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](https://docs.gominerva.com/api-reference/webhooks.md#custom-list-deployments-custom_list_deployment) 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](https://docs.gominerva.com/api-reference/webhooks.md#one-receiver-for-all-client-workspaces) 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.

```bash cURL theme={null}
curl -X POST "https://api.gominerva.com/webhooks/v2/screening" \
  -H "Authorization: Api-Key $MINERVA_ADMIN_KEY" \
  -H "x-workspace-id: $WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  --data '{
    "webhookName": "Custom list deployments",
    "url": "https://example.com/minerva/custom-lists",
    "events": [
      {
        "kind": "custom_list_deployment",
        "values": [
          "custom_list.deployment.pending",
          "custom_list.deployment.live",
          "custom_list.deployment.down"
        ]
      }
    ]
  }'
```

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](mailto: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:

```bash cURL theme={null}
curl "$BASE/$LIST_ID/status?workspace_id=$WORKSPACE_ID" \
  -H "Authorization: Api-Key $MINERVA_ADMIN_KEY"
```

```json theme={null}
{
  "data": {
    "list_id": "LIST_ID",
    "latest_version_id": "VERSION_ID",
    "deployment_status": "live",
    "deployment_stage": "live",
    "workspace_activation_status": "activated",
    "feed_category": "internal",
    "updated_at": "2026-11-03T09:14:27Z"
  }
}
```

* `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](mailto: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:

   ```bash cURL theme={null}
   curl -X POST "$BASE/$LIST_ID/uploads/file" \
     -H "Authorization: Api-Key $MINERVA_ADMIN_KEY" \
     -F "merge_mode=full_replace" \
     -F "record_id_column=record_uid" \
     -F "file=@offboarded-clients.csv"
   ```

   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:

   ```bash cURL theme={null}
   curl -X POST "$BASE/$LIST_ID/uploads/$UPLOAD_ID/publish" \
     -H "Authorization: Api-Key $MINERVA_ADMIN_KEY"
   ```

   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`.

| Status | When | What to do |
| - | - | - |
| `400` | The request is invalid, for example a missing `name` or `entity_type`, an unknown `feedCategory`, or a `limit` out of range. Activation also returns `400` when the workspace has no Internal match-scoring thresholds, or when its screening feed allowlist leaves out the list's feed ("Internal must be enabled in the workspace before a custom list can be activated for that feed."). `error` is `validation_failed` where it applies. | Fix the request. Do not retry unchanged. |
| `403` | The request has no API key: "Missing Authorization: Api-Key or x-api-key header." | Send the key. |
| `400`, `401`, `403`, or `404` | The key is unknown, has been revoked or has expired, or its application has been deactivated: "API-key authentication was denied." The status varies; the message is the same. | Use an active key. Do not treat this `404` as a new record. |
| `403` | The key is a workspace-scoped or tenant-scoped key, not an admin key: "An admin application key is required." | Use an admin key. |
| `403` | `orgId` is not the key's organization: "The API key does not belong to this organization." | Use your own organization ID. |
| `403` | The key's creator is no longer an owner or admin: "The admin key's creator no longer has owner or admin access." | Create a replacement key. |
| `403` | Custom lists are not enabled for your organization: "Custom lists are not enabled for this tenant." (`error` is `not_enabled`). | Contact [support@gominerva.com](mailto:support@gominerva.com). |
| `404` | "Custom list management is not enabled." Custom-list management through the API has not been turned on for this Minerva environment yet. This is separate from your organization's entitlement (the `403` above). | Contact [support@gominerva.com](mailto:support@gominerva.com). |
| `404` | The list, record, upload, or workspace does not exist in your organization. The body is JSON: `{"message": "not found"}`. | Check the ID. This `404` from a record `PATCH` means the record is new. |
| `409` | `expected_revision` is stale, the `record_uid` already exists on `POST`, another change to your organization's custom lists is in progress, or another upload is publishing. `error` is `conflict`. | Read pending changes again and retry with the current revision. |
| `413` | The file or request is larger than `maxUploadFileSize` or `maxUploadRequestSize`. `error` is `limit_exceeded`. | Split the file. |
| `422` | A limit would be exceeded, such as `maxRecordsPerList`, `maxCustomListsPerTenant`, or `maxActiveCustomListRecordsPerWorkspace`. `error` is `limit_exceeded`, `limit` names the limit, and `max` is its maximum. On `DELETE`, a `422` means the list was archived but a limit was reached afterwards. | Archive records you no longer need, or contact [support@gominerva.com](mailto:support@gominerva.com) about your limits. On `DELETE`, do not retry; contact [support@gominerva.com](mailto:support@gominerva.com). |
| `408` | The file upload did not finish in time. `error` is `upload_request_timeout`. | Retry. |
| `429` | Upload capacity is full: "Custom List upload capacity is currently full. Please retry." `error` is `upload_capacity_exceeded`. | Retry with exponential backoff and jitter. |
| `503` | Minerva could not verify the key ("Authentication service is unavailable.") or upload storage is briefly unavailable (`upload_storage_unavailable`). Nothing was changed. | Retry with exponential backoff and jitter. |

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.

## Related documentation

* [Custom Lists Guide](https://docs.gominerva.com/custom-lists-guide.md) for the dashboard view of lists, versions, and activation
* [Workspace Management API](https://docs.gominerva.com/api-reference/workspace-management-api.md) for creating admin keys and configuring workspaces
* [API Keys](https://docs.gominerva.com/api-reference/api-keys.md) for the full key lifecycle
* [Match Scoring Guide](https://docs.gominerva.com/match-scoring-guide.md) for the Internal thresholds that Internal activation needs


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.