# Kolvera developer guide

Version: 2026-10-06. Public REST v2 routes, OAuth scopes, webhook event registry and MCP connection checked against current application source.

This saved copy may age. Current documentation: [Developer portal](https://www.kolvera.io/developers), [REST API](https://www.kolvera.io/integrations/api), [MCP setup](https://www.kolvera.io/integrations/mcp), [webhooks](https://www.kolvera.io/integrations/webhooks), and [help guide](https://www.kolvera.io/guide/api).

## Pick a connection

- **REST API**: Read and update records from your own application. Base URL: `https://www.kolvera.io/api/v2`.
- **MCP**: Let an AI assistant call Kolvera tools. Follow the [MCP setup guide](https://www.kolvera.io/integrations/mcp); do not send MCP requests to REST paths.
- **Webhooks**: Receive change notifications at your HTTPS endpoint. Use REST to fetch full records and reconcile periodically.

## REST quick start

Create a `kv_` token in **Settings → API Tokens**. Keep it server side. Send `Authorization: Bearer kv_...` and JSON for write requests. OAuth access tokens also work on REST v2 when granted each endpoint's required scope.

```sh
TOKEN=kv_your_token
curl -H "Authorization: Bearer $TOKEN" \
  "https://www.kolvera.io/api/v2/contacts?q=test&page=1&per_page=25"
```

List endpoints use `page` and `per_page`; response metadata includes `total`, `page`, `per_page`, and `pages`. The usual page size is 25, with a per-endpoint maximum of 100. Check each endpoint's response for its data key. Limits differ by endpoint: common list reads allow 60 requests/minute, detail reads 120, and writes less. Respect HTTP `429` and `Retry-After`; see rate-limit headers. Errors use JSON such as `{"ok":false,"error":"...","code":"..."}`.

Quick route map (full catalogue below; prefix paths with `/api/v2`):

| Resource | Routes |
| --- | --- |
| Contacts | `GET, POST /contacts`; `GET, PATCH, DELETE /contacts/{id}`; `POST /contacts/{id}/find-email`, `/contacts/{id}/find-phone` |
| Companies | `GET /companies`; `GET, PATCH /companies/{id}`; `GET /companies/{id}/contacts`; `POST /companies/{id}/research`, `/companies/{id}/find-contacts` |
| Campaigns | `GET /campaigns`, `/campaigns/{id}`, `/campaigns/{id}/stats`; `POST /campaigns/{id}/activate`, `/pause`, `/resume`, `/enrol` |
| Pipeline and research | `GET /pipeline`, `/pipeline/{id}`, `/research`, `/research/{id}`; `POST /research` |
| Hot lists and ICP | `GET, POST /hot-lists`; `GET /hot-lists/{id}`; `POST /hot-lists/{id}/members`; `GET, POST /icp`; `GET, PATCH /icp/{id}` |
| Meetings and credits | `GET, POST /meetings`; `GET /meetings/{id}`; `PATCH /meetings/{id}/status`; `GET /credits` |
| Webhooks | `GET, POST /webhooks`; `PATCH, DELETE /webhooks/{id}` |

See the full catalogue and common parameters below; the [online REST reference](https://www.kolvera.io/integrations/api) carries current limits. Credit-consuming actions require an eligible account and may charge credits; check the product quote or current documentation before triggering them.

### Job descriptions

`GET /pipeline` returns summary fields; requesting `fields=description` does not turn it into a detail response. Fetch `GET /pipeline/{id}` and read `job.description` for the stored description. Availability depends on the job board and record; descriptions may be excerpts, and a missing value must not be treated as an empty job advert. Keep the job's source URL when your integration needs the original listing.

## Subscribe to webhooks

Register a public HTTPS receiver. `secret` is optional but recommended. Supply and retain your own random secret: subscription responses do not return it. OAuth tokens need `webhooks:manage`.

```sh
curl -X POST https://www.kolvera.io/api/v2/webhooks \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"url":"https://your-domain.example/webhook","events":["campaign.replied","contact.enriched"],"secret":"your-own-long-random-secret"}'

curl -H "Authorization: Bearer $TOKEN" https://www.kolvera.io/api/v2/webhooks
curl -X PATCH https://www.kolvera.io/api/v2/webhooks/SUBSCRIPTION_ID \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"is_active":false}'
curl -X DELETE https://www.kolvera.io/api/v2/webhooks/SUBSCRIPTION_ID \
  -H "Authorization: Bearer $TOKEN"
```

Registered event types (18):

| Category | Events |
| --- | --- |
| Contact | `contact.created`, `contact.updated`, `contact.enriched` |
| Company | `company.created`, `company.updated` |
| Campaign | `campaign.activated`, `campaign.paused`, `campaign.completed`, `campaign.replied`, `campaign.bounced`, `enrolment.complete` |
| Meeting | `meeting.created`, `meeting.status_changed` |
| Research and credit | `research.completed`, `credit.low` |
| Engagement | `link.clicked`, `email.opened` |
| Scraping | `scrape.complete` |

Every delivery is a JSON POST with `id`, `event`, `timestamp`, and event-specific `data`. Example:

```json
{"id":"evt_...","event":"enrolment.complete","timestamp":"2026-10-05T00:00:00+00:00","data":{"campaign_id":"...","enrolled":3}}
```

With a secret, `X-Kolvera-Signature` is `sha256=<lowercase hex HMAC-SHA256>` of the **raw request body bytes**, keyed by your secret's UTF-8 bytes. Compare the computed value with the header using a constant-time comparison before parsing or processing. No secret means no signature header. Return a 2xx status promptly. Transient delivery failures are retried; `id` remains stable across retry attempts, so persist processed IDs to deduplicate. Repeated delivery failures can disable a subscription. Inspect `last_status_code`, `failure_count`, and `is_active` through `GET /webhooks`.

Event registration does not guarantee every matching UI or import action emits an event. In particular, contact and company write events primarily cover API writes, and `contact.enriched` covers API email lookup. Treat webhooks as change hints; use periodic REST reads to reconcile your local copy. See the [webhook guide](https://www.kolvera.io/integrations/webhooks) for payload fields, coverage, verification examples, and retry policy.

## REST endpoint catalogue

All 57 publicly advertised REST v2 route/method pairs follow. Prefix paths with `https://www.kolvera.io/api/v2`. `{...}` is a path parameter. OAuth calls require the shown scope; API token permissions, tenant access, plan gates and credit checks also apply. Operation labels describe intent; validate JSON request and response fields for your use case. Pilot sourcing endpoints require pilot availability.

| Method | Path | OAuth scope | Operation |
| --- | --- | --- | --- |
| `GET` | `/people/sourcing/quote` | `contacts:read` | Person sourcing quote |
| `POST` | `/people/sourcing/runs` | `contacts:enrich` | Person sourcing start |
| `GET` | `/people/sourcing/runs/{run_id}` | `contacts:read` | Person sourcing status |
| `POST` | `/people/sourcing/runs/{run_id}/resume` | `contacts:enrich` | Person sourcing resume |
| `GET` | `/contacts` | `contacts:read` | Contacts list |
| `GET` | `/contacts/{contact_id}` | `contacts:read` | Contacts get |
| `POST` | `/contacts` | `contacts:write` | Contacts create |
| `PATCH` | `/contacts/{contact_id}` | `contacts:write` | Contacts update |
| `DELETE` | `/contacts/{contact_id}` | `contacts:write` | Contacts delete |
| `POST` | `/contacts/{contact_id}/find-email` | `contacts:enrich` | Contacts find email |
| `POST` | `/contacts/{contact_id}/find-phone` | `contacts:enrich` | Contacts find phone |
| `GET` | `/companies` | `companies:read` | Companies list |
| `GET` | `/companies/{company_id}` | `companies:read` | Companies get |
| `GET` | `/companies/{company_id}/contacts` | `companies:read` | Companies contacts |
| `PATCH` | `/companies/{company_id}` | `contacts:write` | Companies update |
| `POST` | `/companies/{company_id}/research` | `research:write` | Companies research |
| `POST` | `/companies/{company_id}/find-contacts` | `contacts:enrich` | Companies find contacts |
| `GET` | `/campaigns` | `campaigns:read` | Campaigns list |
| `GET` | `/campaigns/{campaign_id}` | `campaigns:read` | Campaigns get |
| `GET` | `/campaigns/{campaign_id}/stats` | `campaigns:read` | Campaigns stats |
| `POST` | `/campaigns/{campaign_id}/activate` | `campaigns:write` | Campaigns activate |
| `POST` | `/campaigns/{campaign_id}/pause` | `campaigns:write` | Campaigns pause |
| `POST` | `/campaigns/{campaign_id}/resume` | `campaigns:write` | Campaigns resume |
| `POST` | `/campaigns/{campaign_id}/enrol` | `campaigns:write` | Campaigns enrol |
| `GET` | `/pipeline` | `pipeline:read` | Pipeline list |
| `GET` | `/pipeline/{job_id}` | `pipeline:read` | Pipeline get |
| `GET` | `/scrape-configs` | `pipeline:read` | Scrape config list |
| `GET` | `/scrape-configs/{config_id}` | `pipeline:read` | Scrape config get |
| `GET` | `/scrape-quality` | `pipeline:read` | Scrape quality |
| `POST` | `/scrape-configs/{config_id}/excludes` | `pipeline:write` | Scrape config update excludes |
| `GET` | `/research` | `research:read` | Research list |
| `GET` | `/research/{report_id}` | `research:read` | Research get |
| `POST` | `/research` | `research:write` | Research trigger |
| `GET` | `/hot-lists` | `hot_lists:read` | Hot lists list |
| `GET` | `/hot-lists/{list_id}` | `hot_lists:read` | Hot lists get |
| `POST` | `/hot-lists` | `hot_lists:write` | Hot lists create |
| `POST` | `/hot-lists/{list_id}/members` | `hot_lists:write` | Hot lists add members |
| `DELETE` | `/hot-lists/{list_id}/members/{member_id}` | `hot_lists:write` | Hot lists remove member |
| `PATCH` | `/hot-lists/{list_id}/members/{member_id}` | `hot_lists:write` | Hot lists update member |
| `GET` | `/icp` | `icp:read` | Icp list |
| `GET` | `/icp/{profile_id}` | `icp:read` | Icp get |
| `POST` | `/icp` | `icp:write` | Icp create |
| `PATCH` | `/icp/{profile_id}` | `icp:write` | Icp update |
| `POST` | `/icp/generate` | `icp:write` | Icp generate |
| `GET` | `/icp/{profile_id}/prospects` | `icp:read` | Icp prospects |
| `POST` | `/icp/{icp_id}/build-hotlist` | `hot_lists:write` | Build icp hotlist |
| `GET` | `/meetings` | `meetings:read` | Meetings list |
| `GET` | `/meetings/{meeting_id}` | `meetings:read` | Meetings get |
| `POST` | `/meetings` | `meetings:write` | Meetings create |
| `PATCH` | `/meetings/{meeting_id}/status` | `meetings:write` | Meetings update status |
| `POST` | `/fathom/sync` | `meetings:write` | Fathom sync |
| `GET` | `/fathom/notes` | `meetings:read` | Fathom notes list |
| `GET` | `/credits` | `credits:read` | Credits balance |
| `GET` | `/webhooks` | `webhooks:manage` | Webhooks list |
| `POST` | `/webhooks` | `webhooks:manage` | Webhooks create |
| `PATCH` | `/webhooks/{sub_id}` | `webhooks:manage` | Webhooks update |
| `DELETE` | `/webhooks/{sub_id}` | `webhooks:manage` | Webhooks delete |

### Authentication, pagination and errors

Create a named `kv_` token under **Settings → Developers → API tokens** and keep it server side. Send `Authorization: Bearer kv_...`; send `Content-Type: application/json` for JSON writes. For a multi-account app, register a developer app and redirect URI under **Settings → Developer Apps**, then use OAuth 2.0 authorization code with PKCE (`code_challenge_method=S256`). Authorization URL: `https://www.kolvera.io/oauth/authorize`; token URL: `https://www.kolvera.io/oauth/token`. Documented access token lifetime: 1 hour; refresh token: 30 days. Request the scopes you need from the catalogue.

Most list calls accept `page` and `per_page`; typical default 25 and maximum 100. List responses include `total`, `page`, `per_page`, and `pages`, with a resource-specific data key. Successful JSON responses have `ok: true`; errors have `ok: false`, `error`, and a machine-readable `code`. Rate limits vary: common list reads allow 60 requests/minute, detail reads 120, and writes less. Respect rate headers and `Retry-After` on `429`; do not assume a single global limit. Back off, and retry mutations only when your workflow can safely deduplicate them.

### Common request fields

| Operation | Query or JSON fields |
| --- | --- |
| `GET /contacts` | `q`, `page`, `per_page`, `has_email`, `has_phone`, `is_prospect`, `is_candidate`, `state`, `sort`, `order` |
| `POST /contacts` | `first_name`, `last_name`, `email`, `phone`, `title`, `company_name`, `linkedin_url`, `location`, `is_prospect`, `is_candidate` |
| `GET /companies` | `q`, `page`, `per_page`, `industry`, `state`, `bd_status`, `sort`, `order` |
| `GET /pipeline` | `q`, `page`, `per_page`, `created_after`, `sort`, `order`, `icp_id` |
| `POST /scrape-configs/{config_id}/excludes` | `companies`, `keywords` |
| `POST /campaigns/{campaign_id}/enrol` | `contact_ids` |
| `POST /research` | `brief`, `icp_id`, `mpc_id` |
| `POST /hot-lists` | `name`, `description`, `list_type`, `icp_id`, `mpc_id`, `colour` |
| `POST /hot-lists/{list_id}/members` | `source_ids`, `source_type`, `tier`, `status`, `source`, `notes` |
| `POST /icp` | `title`, `target_industries`, `target_locations`, `target_company_sizes`, `target_titles`, `seek_keywords` |
| `POST /meetings` | `title`, `starts_at_utc`, `duration_minutes`, `contact_id`, `location`, `notes` |
| `PATCH /meetings/{meeting_id}/status` | `status` |

PATCH calls accept validated partial fields. Omitted fields remain unchanged. Credit-consuming actions include enrichment, sourcing, research and some generation or campaign actions. Check `GET /credits`, pilot quote where relevant, and your plan before calling. `GET /people/sourcing/quote` reports pilot availability and price; a start call needs an approved credit cap.

### Job pipeline example

```sh
curl -H "Authorization: Bearer $TOKEN" \
  "https://www.kolvera.io/api/v2/pipeline?icp_id=DESK_ID&page=1&per_page=100"
curl -H "Authorization: Bearer $TOKEN" \
  "https://www.kolvera.io/api/v2/pipeline/JOB_ID"
curl -H "Authorization: Bearer $TOKEN" \
  "https://www.kolvera.io/api/v2/scrape-quality?config_id=CONFIG_ID"
```

`GET /pipeline` is a summary feed. Fetch `GET /pipeline/{job_id}` for `job.description`; `fields=description` on the list does not fetch detail. Stored descriptions depend on source and can be excerpts; LinkedIn search results can lack them. Retain the source URL and handle missing descriptions as missing data. Scrape-config and quality reads do not trigger scans. `POST /scrape-configs/{config_id}/excludes` changes excluded companies and keywords.

## Complete webhook event and payload reference

Every JSON POST has `id`, `event`, `timestamp`, and event-specific `data`. The same event `id` persists across retries. Store processed IDs and make handlers idempotent.

| Event | When emitted | Typical `data` fields |
| --- | --- | --- |
| `contact.created` | API creates contact | Created contact object |
| `contact.updated` | API changes contact | `contact_id`, changed `fields` names |
| `contact.enriched` | API email lookup finds address | `contact_id`, `type` (`email`), `email` |
| `company.created` | API contact creation creates company | `company_id`, `name` |
| `company.updated` | API changes company | `company_id`, changed `fields` names |
| `campaign.activated` | Campaign starts or resumes | `campaign_id`, `name` |
| `campaign.paused` | Campaign pauses | `campaign_id`, `name` |
| `campaign.completed` | One contact finishes sequence | `campaign_id`, `contact_id`, `enrollment_id` |
| `campaign.replied` | Campaign reply detected | `campaign_id`, `contact_id`, `enrollment_id` |
| `campaign.bounced` | Campaign email hard-bounces | `campaign_id`, `contact_id`, `email`, `error` |
| `meeting.created` | Meeting scheduled | Created meeting object |
| `meeting.status_changed` | Meeting status changes | `meeting_id`, `status`, `contact_id` |
| `research.completed` | Deep Research completes | `report_id`, `brief`, `companies_found` |
| `credit.low` | Credit balance falls to 10 or below | `balance`, source-dependent `action` |
| `link.clicked` | Tracked campaign link clicked | `contact_id`, `enrollment_id`, `token` |
| `email.opened` | Campaign email opened | `contact_id`, `enrollment_id` |
| `enrolment.complete` | Contacts enrolled successfully | `campaign_id`, `enrolled` count |
| `scrape.complete` | Job-board scrape finishes | `source`, `search_config_ids` |

`enrolment.complete` means successful enrolment; `campaign.completed` means a contact finishes the sequence. There is no per-email-sent event. Delivery coverage varies by workflow. Contact and company write events chiefly cover API writes; UI and imports can bypass them. `contact.enriched` covers API email lookup, not phone lookup. Use periodic REST reconciliation for a complete local copy.

### Verify, acknowledge, retry

With a signing secret, `X-Kolvera-Signature` equals `sha256=` followed by lowercase hex HMAC-SHA256 over the **raw request body bytes**, keyed by your secret's UTF-8 bytes. Verify with a constant-time comparison before parsing or applying changes. Without a secret, no signature header is sent. Return `2xx` promptly and queue longer work inside your app.

```python
import hashlib
import hmac

def valid_kolvera_webhook(body: bytes, signature: str | None, secret: str) -> bool:
    if not signature or not signature.startswith("sha256="):
        return False
    digest = signature[7:]
    if len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest):
        return False
    expected = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(digest, expected)
```

Kolvera retries HTTP `408`, `425`, `429`, `500`, `502`, `503` and `504` after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 60 minutes, up to five retries. Other `4xx` responses are not retried. Ten failed events disable a subscription; attempts for one event count as one failure. Broker-unavailable fallback makes one attempt without retries. Inspect `last_status_code`, `failure_count` and `is_active` through `GET /webhooks`. Reject invalid signatures with `401`.

## MCP setup and fit

Kolvera MCP uses Streamable HTTP at `https://mcp.kolvera.io/mcp`, registry name `io.kolvera/kolvera`. It is available on paid plans, uses account permissions and shares the dashboard credit balance. MCP is for assistant-driven tools; REST and webhooks suit a durable application pipeline.

- **Claude connector:** Customize → Connectors → Add custom connector; enter MCP URL, sign in to Kolvera and authorize.
- **Claude Code:** `claude mcp add --transport http kolvera https://mcp.kolvera.io/mcp`; complete OAuth prompt.
- **ChatGPT:** Settings → Connectors → Developer mode → Add MCP server; enter MCP URL and authenticate.
- **Token-only Streamable HTTP clients:** Configure `https://mcp.kolvera.io/mcp?token=YOUR_API_TOKEN`. Keep that URL secret and out of logs. Transport must be `streamable-http`.

See the [MCP setup guide](https://www.kolvera.io/integrations/mcp) for current client setup and tool catalogue. MCP protocol calls do not go to `/api/v2`.

## Integration checklist

1. Create a server-side token or OAuth app with needed scopes; keep secrets out of client code.
2. Test safe REST reads, check pagination, response keys and rate headers.
3. Subscribe to needed webhook events, verify raw-body signatures and deduplicate IDs.
4. Reconcile through REST on a schedule; not every write emits an event.
5. Fetch job detail for `description`; preserve source URLs and tolerate missing descriptions.
6. Check credits and eligibility before enrichment, research, sourcing or campaign actions.
