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

# Sourcing

> AI-powered asynchronous candidate sourcing and enrichment via the Public API

## Overview

The Sourcing API exposes InstaView's AI-driven candidate sourcing pipeline as a fully programmable, asynchronous REST interface. You can initiate sourcing runs, refine criteria mid-session, enrich shortlisted profiles for contact discovery, and embed beautifully styled candidate cards directly into your ATS or internal dashboards.

All sourcing operations are **asynchronous** — requests return immediately with a `requestId`, and results are delivered either via [webhooks](/guides/webhooks) or polled through a results endpoint.

## Asynchronous Request-Response Pattern

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Client as Client Application
    participant API as InstaView API
    participant Webhook as Your Webhook Endpoint

    Client->>API: POST /sourcing (job title, description, location)
    API-->>Client: 202 Accepted { requestId: "req_…" }

    Note over API: Agent runs sourcing pipeline<br/>in the background

    alt Webhook delivery
        API->>Webhook: POST (sourcing.completed payload)
        Webhook-->>API: 200 OK
    else Fallback polling
        Client->>API: GET /sourcing/{requestId}/results
        API-->>Client: 200 OK { status: "completed", candidates: […] }
    end
```

## Required Scopes

| Operation                       | Required Scope   |
| ------------------------------- | ---------------- |
| Initiate sourcing run           | `sourcing:write` |
| Patch / re-run sourcing session | `sourcing:write` |
| Get sourcing results            | `sourcing:read`  |
| Initiate enrichment run         | `enrich:write`   |
| Get enrichment results          | `enrich:write`   |
| Render HTML candidate cards     | `enrich:write`   |

## Execution Pipeline

When a sourcing run is initiated, the sourcing agent executes a fully automated pipeline — no clarifying questions, no interactive steps:

<Steps>
  <Step title="ICP Generation">
    The agent derives a complete Ideal Candidate Profile (ICP) from your `title`, `jobDescription`, `location`, and optional `query`. Skill expansion, title synonyms, location variants, and negative keywords are all populated automatically.
  </Step>

  <Step title="Query Compilation">
    A tailored search query is compiled against InstaView's sourcing index. Skill expansions and localized terms maximize recall.
  </Step>

  <Step title="Candidate Search">
    The search is executed against external platform indices. A strict 15-second timeout with 2 automatic retries applies — if the external index is unavailable, a circuit-breaker failover triggers immediately and a `sourcing.failed` webhook is dispatched.
  </Step>

  <Step title="Enrichment & Matching">
    Profiles are enriched sequentially. Failed individual profiles are isolated and marked unbilled — the batch continues with the remaining candidates.
  </Step>

  <Step title="Webhook / Polling Delivery">
    The completed, ranked candidate list is delivered to your webhook endpoint or made available via `GET /sourcing/{id}/results`.
  </Step>
</Steps>

## Initiating a Sourcing Run

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.instaview.sk/sourcing \
    -H "Authorization: Bearer sk_your_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "title": "Senior Staff ML Engineer",
      "jobDescription": "We are seeking a Staff ML Engineer to design and scale our agentic coding infrastructure...",
      "location": "Remote, USA",
      "query": "Prioritize candidates with active contributions to LangChain or vLLM.",
      "limit": 15,
      "webhookId": "3f9a12c7-44b8-4d32-b71f-e29a7d1e0f5c"
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.instaview.sk/sourcing', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.INSTAVIEW_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      title: 'Senior Staff ML Engineer',
      jobDescription: 'We are seeking a Staff ML Engineer...',
      location: 'Remote, USA',
      limit: 15,
      webhookId: '3f9a12c7-44b8-4d32-b71f-e29a7d1e0f5c',
    }),
  });

  const { requestId } = await response.json();
  console.log('Run started:', requestId); // "req_550e8400-…"
  ```

  ```python Python theme={null}
  import os
  import requests

  resp = requests.post(
      'https://api.instaview.sk/sourcing',
      headers={
          'Authorization': f'Bearer {os.environ["INSTAVIEW_API_KEY"]}',',
          'Content-Type': 'application/json',
      },
      json={
          'title': 'Senior Staff ML Engineer',
          'jobDescription': 'We are seeking a Staff ML Engineer...',
          'location': 'Remote, USA',
          'limit': 15,
          'webhookId': '3f9a12c7-44b8-4d32-b71f-e29a7d1e0f5c',
      },
  )

  data = resp.json()
  print('Run started:', data['requestId'])
  ```
</CodeGroup>

### Response (202 Accepted)

```json theme={null}
{
  "success": true,
  "requestId": "req_9f3b827e-8c31-4bda-a7a2-b2d99d14f4e2",
  "status": "processing",
  "message": "Sourcing run accepted and queued for processing.",
  "webhookId": "3f9a12c7-44b8-4d32-b71f-e29a7d1e0f5c",
  "createdAt": "2026-05-20T13:20:45.000Z"
}
```

## Polling for Results

If you do not use webhooks, poll `GET /sourcing/{requestId}/results` until `status` transitions from `"processing"` to `"completed"` or `"failed"`.

```javascript theme={null}
async function pollResults(requestId) {
  while (true) {
    const res = await fetch(`https://api.instaview.sk/sourcing/${requestId}/results`, {
      headers: { 'Authorization': `Bearer ${process.env.INSTAVIEW_API_KEY}` },
    });

    const body = await res.json();

    if (body.status === 'completed') {
      return body.candidates;
    }

    if (body.status === 'failed') {
      throw new Error(body.errorMessage);
    }

    // Still processing — wait before polling again
    await new Promise(resolve => setTimeout(resolve, 5000));
  }
}
```

<Warning>
  Avoid polling more frequently than once every 5 seconds. Excessive polling can trigger the `GET /sourcing/*/results` rate limit (120 requests/minute per API key).
</Warning>

## Refining a Sourcing Session

After a run completes, use `PATCH /sourcing/{requestId}` to refine criteria or request additional candidates from the same session context. At least one of `query` or `limit` must be provided.

<Warning>
  A PATCH request is only accepted after the current run transitions to `"completed"` or `"failed"`. If a run is still `"processing"`, the request returns `409 Conflict`.
</Warning>

```json theme={null}
{
  "query": "Focus on startup founders with Kubernetes experience.",
  "limit": 5
}
```

The agent applies the updated `query` as a context patch and then evaluates `limit` additional profiles — all in a single asynchronous pass.

## Enriching Candidates

Enrichment performs deep profile analysis including **email discovery**. It is fully asynchronous: submit profile IDs, receive a `requestId`, and poll `GET /sourcing/enrich/{requestId}/results` or await the `enrich.completed` webhook.

```bash theme={null}
curl -X POST https://api.instaview.sk/sourcing/enrich \
  -H "Authorization: Bearer sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "profileIds": ["prof_a9238f82-a723", "prof_e8321c12-b912"],
    "webhookId": "3f9a12c7-44b8-4d32-b71f-e29a7d1e0f5c"
  }'
```

### Response (202 Accepted)

```json theme={null}
{
  "success": true,
  "requestId": "enr_550e8400-e29b-41d4-a716-446655440001",
  "status": "processing",
  "message": "Enrichment run accepted and queued for processing.",
  "createdAt": "2026-05-20T13:21:00.000Z"
}
```

<Info>
  Each successfully enriched profile consumes **1 credit**. Profiles that fail to enrich are not billed. For prepaid accounts, your balance must cover the full batch size upfront.
</Info>

### Enrichment Resilience

Individual profile failures within a batch are isolated: if one profile fails to enrich, the system marks it as failed, skips billing for it, and continues processing the rest of the batch.

## Rendering HTML Candidate Cards

Generate self-contained, embeddable HTML profile cards for your ATS or dashboards. This endpoint is **synchronous** — it returns immediately with rendered cards.

```bash theme={null}
curl -X POST https://api.instaview.sk/sourcing/render-html \
  -H "Authorization: Bearer sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "profileIds": ["prof_a9238f82-a723"],
    "theme": "dark"
  }'
```

Supported themes: `"dark"` (default) and `"light"`.

## Webhook Events

Configure a [WebhookConfig](/guides/webhooks) and pass its `id` as `webhookId` when initiating runs. The webhook must subscribe to at least one of the relevant event types.

### Sourcing Events

| Registration Key (`events[]`) | Payload Label (`event`) | Triggered When                                 |
| ----------------------------- | ----------------------- | ---------------------------------------------- |
| `"SOURCING_COMPLETED"`        | `"sourcing.completed"`  | Sourcing run finishes with scored candidates   |
| `"SOURCING_FAILED"`           | `"sourcing.failed"`     | Sourcing run encounters an unrecoverable error |

### Enrichment Events

| Registration Key (`events[]`) | Payload Label (`event`) | Triggered When                                                |
| ----------------------------- | ----------------------- | ------------------------------------------------------------- |
| `"ENRICH_COMPLETED"`          | `"enrich.completed"`    | Enrichment run completes (partial success is still completed) |
| `"ENRICH_FAILED"`             | `"enrich.failed"`       | Enrichment run encounters an unrecoverable error              |

<Info>
  A webhook config that subscribes to a mix of event types (e.g. `["ANALYSIS_COMPLETED", "SOURCING_COMPLETED", "ENRICH_COMPLETED"]`) is fully valid — all subscriptions are honored independently.
</Info>

## Billing & Credits

* **Sourcing**: 1 credit per successfully sourced and matched candidate, billed based on the `limit` parameter.
* **Enrichment**: 1 credit per successfully enriched profile (failed profiles are not billed).
* **Prepaid accounts**: balance must cover the full `limit` before a run starts. Insufficient funds → `402 Payment Required`.
* **Postpaid accounts**: run immediately regardless of balance; invoiced at end of the billing cycle.

## Multi-Tenant Security

All sourcing resources (`requestId`s, candidate lists, enrichment results) are strictly bound to the `companyId` associated with the authenticating API key:

* An API key from Company A **cannot** access, patch, or enrich resources belonging to Company B.
* Attempting to access another company's `requestId` returns `404 REQUEST_NOT_FOUND` — not `403` — to prevent resource enumeration.
* A `webhookId` submitted with a sourcing run must belong to the **same company** as the API key; mismatched ownership returns `403 WEBHOOK_COMPANY_MISMATCH`.

## Rate Limits

| Endpoint                            | Rate Limit            |
| ----------------------------------- | --------------------- |
| `POST /sourcing`                    | 10 requests / minute  |
| `PATCH /sourcing/{id}`              | 30 requests / minute  |
| `GET /sourcing/{id}/results`        | 120 requests / minute |
| `POST /sourcing/enrich`             | 50 requests / minute  |
| `GET /sourcing/enrich/{id}/results` | 120 requests / minute |
| `POST /sourcing/render-html`        | 50 requests / minute  |

## Error Codes

| HTTP Status | Error Code                         | Description                                                             |
| ----------- | ---------------------------------- | ----------------------------------------------------------------------- |
| `400`       | `INVALID_PAYLOAD`                  | Missing required fields (`title` or `jobDescription`) or invalid syntax |
| `400`       | `INVALID_PATCH_PAYLOAD`            | PATCH body omits both `query` and `limit`                               |
| `401`       | `UNAUTHORIZED`                     | API key is missing, expired, or deactivated                             |
| `402`       | `INSUFFICIENT_FUNDS`               | Prepaid account balance too low to cover the run's `limit`              |
| `403`       | `INSUFFICIENT_SCOPES`              | API key does not have the required scope                                |
| `403`       | `WEBHOOK_COMPANY_MISMATCH`         | Provided `webhookId` belongs to a different company                     |
| `404`       | `REQUEST_NOT_FOUND`                | `requestId` does not exist, has expired, or belongs to another company  |
| `404`       | `WEBHOOK_NOT_FOUND`                | Provided `webhookId` does not exist or is inaccessible                  |
| `409`       | `RUN_IN_PROGRESS`                  | PATCH attempted while a run is still `"processing"`                     |
| `422`       | `WEBHOOK_EVENT_NOT_SUBSCRIBED`     | Webhook config has no overlap with sourcing or enrichment event types   |
| `429`       | `RATE_LIMIT_EXCEEDED`              | Request rate exceeded sliding window limits                             |
| `503`       | `PROVIDER_TEMPORARILY_UNAVAILABLE` | External sourcing platforms experiencing outages                        |
