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

# Contacts

> Manage the people your agents call, with the Contacts API

<Info>
  This resource is also `/contacts`. `/candidates` is a permanent alias that keeps working unchanged; the newer name is the one to reach for in new code. See [Resource names](/api-reference/introduction#resource-names).
</Info>

## Overview

A contact is the person on the other end of a conversation. The Contacts API creates them, keeps them up to date, and finds them again. A contact may be associated with one or more jobs, or with none at all — outside hiring, none is the normal case.

## Resource Structure

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "jobId": "987e6543-e21b-12d3-a456-426614174000",
  "jobIds": ["987e6543-e21b-12d3-a456-426614174000"],
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane.doe@example.com",
  "phoneNumber": "+421915123456",
  "status": "APPLIED",
  "gender": "female",
  "gdprExpiryDate": "2026-11-16",
  "anonymizedCvText": "[NAME]\nSoftware Engineer\nExperience: ...",
  "overallRating": 85,
  "metadata": {
    "source": "LinkedIn",
    "externalId": "CAND-12345",
    "resumeUrl": "https://storage.example.com/resumes/jane-doe.pdf",
    "linkedinUrl": "https://linkedin.com/in/janedoe"
  },
  "fields": {
    "order_number": "SO-40128",
    "is_vip": true
  },
  "workHistory": [
    {
      "id": "work-history-uuid",
      "companyName": "Tech Corp",
      "candidatePosition": "Software Engineer",
      "referenceName": "Jane Smith",
      "referencePhone": "+1234567890",
      "startDate": "2020-01-01",
      "endDate": "2022-12-31"
    }
  ],
  "createdAt": "2025-11-20T10:30:00Z",
  "updatedAt": "2025-11-20T10:30:00Z"
}
```

<Info>
  **Free-form metadata**: anything you want to keep about a contact for your own use — resume
  URL, LinkedIn profile, skills — goes in `metadata` as key-value pairs. It is never
  validated and never read by an agent.

  **Custom fields are a different thing**, and they are the half an agent can speak: values
  defined in your company catalog, validated on write, and addressable as
  `{{contact.<key>}}`. Those live in `fields` — see
  [Update Contact](/api-reference/contacts/update-contact).
</Info>

<Warning>
  The contact schema also declares `analysisCount`, `interviewCount` and `links`, and they are
  **not currently returned** by any endpoint — nothing populates them. They are left out of the
  structure above deliberately; treat them as absent rather than as zero, and count a contact's
  conversations with [List Conversations](/api-reference/conversations/list-conversations).
</Warning>

## Required Scopes

| Operation         | Required Scope    | Additional Scopes           |
| ----------------- | ----------------- | --------------------------- |
| List contacts     | `read:contacts`   | `read:jobs` (for filtering) |
| Get contact by ID | `read:contacts`   |                             |
| Create contact    | `write:contacts`  | `read:jobs` (validation)    |
| Update contact    | `write:contacts`  |                             |
| Delete contact    | `delete:contacts` |                             |

The legacy aliases (`read:candidates`, `write:candidates`, `delete:candidates`) work identically and stay valid on keys that already carry them — see
[Scopes and Permissions](/guides/scopes-and-permissions).

## A Request Helper

Every JavaScript example below goes through this helper rather than calling `fetch` directly.
It does the two things a bare `fetch` does not: it gives the request a deadline, and it treats a
non-2xx answer as a failure. Without the first, a request to a stalled service never settles;
without the second, `response.json()` parses an error body and the code carries on as though it
had a contact.

```javascript theme={null}
async function apiFetch(url, options = {}) {
  const response = await fetch(url, {
    ...options,
    headers: {
      Authorization: `Bearer ${apiKey}`,
      ...options.headers,
    },
    // 10s is a starting point; pick one from your own latency budget.
    signal: options.signal ?? AbortSignal.timeout(10_000),
  });

  if (!response.ok) {
    const body = await response.json().catch(() => ({}));
    throw new Error(
      `${options.method ?? "GET"} ${url} failed: ${response.status} ${body.message ?? response.statusText}`,
    );
  }

  return response;
}
```

The Python examples use `requests`, whose `timeout` argument is the equivalent — pass it on
every call, and check `response.raise_for_status()` before reading the body.

## Creating Contacts

### Basic Contact Creation

A contact can be created with or without a job. Without one, the contact simply exists on its own and jobs can be attached later.

<CodeGroup>
  ```bash cURL (with job) theme={null}
  curl -X POST https://api.instaview.sk/contacts \
    -H "Authorization: Bearer sk_your_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "jobId": "job-uuid",
      "firstName": "Jane",
      "lastName": "Doe",
      "email": "jane.doe@example.com",
      "phoneNumber": "+421915123456",
      "gdprExpiryDate": "2026-11-16"
    }'
  ```

  ```bash cURL (without job) theme={null}
  curl -X POST https://api.instaview.sk/contacts \
    -H "Authorization: Bearer sk_your_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "firstName": "Jane",
      "lastName": "Doe",
      "email": "jane.doe@example.com",
      "phoneNumber": "+421915123456",
      "gdprExpiryDate": "2026-11-16"
    }'
  ```

  ```javascript Node.js (with job) theme={null}
  const contact = await fetch("https://api.instaview.sk/contacts", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.INSTAVIEW_API_KEY}`,
      "Content-Type": "application/json",
    },
    signal: AbortSignal.timeout(10_000),
    body: JSON.stringify({
      jobId: "job-uuid",
      firstName: "Jane",
      lastName: "Doe",
      email: "jane.doe@example.com",
      phoneNumber: "+421915123456",
      gdprExpiryDate: "2026-11-16",
    }),
  });

  if (!contact.ok) {
    throw new Error(`Creating the contact failed: ${contact.status}`);
  }

  const result = await contact.json();
  console.log("Contact created:", result.id);
  ```

  ```javascript Node.js (without job) theme={null}
  const contact = await fetch("https://api.instaview.sk/contacts", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.INSTAVIEW_API_KEY}`,
      "Content-Type": "application/json",
    },
    signal: AbortSignal.timeout(10_000),
    body: JSON.stringify({
      firstName: "Jane",
      lastName: "Doe",
      email: "jane.doe@example.com",
      phoneNumber: "+421915123456",
      gdprExpiryDate: "2026-11-16",
      metadata: {
        source: "Career Fair",
        skills: ["Python", "JavaScript"],
      },
    }),
  });

  if (!contact.ok) {
    throw new Error(`Creating the contact failed: ${contact.status}`);
  }

  const result = await contact.json();
  console.log("Contact created:", result.id);
  ```

  ```python Python (with job) theme={null}
  response = requests.post(
      'https://api.instaview.sk/contacts',
      headers={
          'Authorization': f'Bearer {os.environ["INSTAVIEW_API_KEY"]}',
          'Content-Type': 'application/json'
      },
      json={
          'jobId': 'job-uuid',
          'firstName': 'Jane',
          'lastName': 'Doe',
          'email': 'jane.doe@example.com',
          'phoneNumber': '+421915123456',
          'gdprExpiryDate': '2026-11-16'
      },
      timeout=10
  )
  response.raise_for_status()

  contact = response.json()
  print(f'Contact created: {contact["id"]}')
  ```

  ```python Python (without job) theme={null}
  response = requests.post(
      'https://api.instaview.sk/contacts',
      headers={
          'Authorization': f'Bearer {os.environ["INSTAVIEW_API_KEY"]}',
          'Content-Type': 'application/json'
      },
      json={
          'firstName': 'Jane',
          'lastName': 'Doe',
          'email': 'jane.doe@example.com',
          'phoneNumber': '+421915123456',
          'gdprExpiryDate': '2026-11-16'
      },
      timeout=10
  )
  response.raise_for_status()

  contact = response.json()
  print(f'Contact created: {contact["id"]}')
  ```
</CodeGroup>

### A Fuller Contact

```javascript theme={null}
const fullContact = {
  // Required fields
  firstName: "Jane",
  lastName: "Doe",
  email: "jane.doe@example.com",
  phoneNumber: "+1234567890",
  gdprExpiryDate: "2026-11-16",

  // Optional: Associate with job(s)
  jobId: "job-uuid", // Single job association

  // Store additional data in metadata (max 10KB)
  metadata: {
    // Professional information
    yearsOfExperience: 5,
    currentJobTitle: "Senior Software Engineer",
    currentCompany: "Tech Corp",
    skills: ["JavaScript", "React", "Node.js", "TypeScript", "AWS"],

    // Education
    educationLevel: "bachelors",

    // Location
    location: {
      city: "San Francisco",
      countryCode: "US",
    },

    // Availability
    availability: {
      canStart: "2024-03-01",
      noticePeriod: 30, // days
    },

    // Links
    resumeUrl: "https://storage.example.com/resumes/jane-doe.pdf",
    linkedinUrl: "https://linkedin.com/in/janedoe",
    portfolioUrl: "https://janedoe.dev",

    // Additional context
    notes: "Strong technical background with excellent communication skills",
  },
};

const response = await createContact(fullContact);
```

## Listing Contacts

### List All Contacts

```javascript theme={null}
async function listContacts(page = 1, limit = 20) {
  const response = await apiFetch(
    `https://api.instaview.sk/contacts?page=${page}&limit=${limit}`,
  );

  // { data, total, page, limit, totalPages }
  return await response.json();
}

// Usage
const { data, total } = await listContacts(1, 50);
console.log(`Found ${total} contacts, ${data.length} on this page`);
```

### Filter by Job

```javascript theme={null}
async function getContactsForJob(jobId) {
  const response = await apiFetch(
    `https://api.instaview.sk/contacts?jobId=${jobId}&limit=100`,
  );

  return await response.json();
}
```

### Filter by Status

```javascript theme={null}
async function getAppliedContacts() {
  const response = await apiFetch(
    "https://api.instaview.sk/contacts?status=APPLIED",
  );

  return await response.json();
}
```

### Search Contacts

```javascript theme={null}
async function searchContacts(query) {
  const response = await apiFetch(
    `https://api.instaview.sk/contacts?search=${encodeURIComponent(query)}`,
  );

  return await response.json();
}

// Search by name or email
const results = await searchContacts("jane doe");
```

## Updating Contacts

### Partial Update

```javascript theme={null}
async function updateContact(contactId, updates) {
  const response = await apiFetch(
    `https://api.instaview.sk/contacts/${contactId}`,
    {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(updates),
    },
  );

  return await response.json();
}

// Change how to reach them
await updateContact("contact-uuid", {
  email: "jane.newemail@example.com",
  phoneNumber: "+1987654321",
});
```

### Update Status

```javascript theme={null}
// Move the contact through your own process
await updateContact("contact-uuid", { status: "IN_PROCESS" });
await updateContact("contact-uuid", { status: "ACCEPTED" });
```

### Update Job Assignments

```javascript theme={null}
// Assign the contact to several jobs
await updateContact("contact-uuid", {
  jobIds: ["job-uuid-1", "job-uuid-2", "job-uuid-3"],
});

// Add one more, keeping the existing assignments
const contact = await getContact("contact-uuid");
await updateContact("contact-uuid", {
  jobIds: [...contact.jobIds, "new-job-uuid"],
});

// Remove every job assignment
await updateContact("contact-uuid", {
  jobIds: [],
});
```

## Status Management

### Available Statuses

`status` is yours to move as your own process moves — InstaView does not act on it. The five
values predate the neutral vocabulary and keep their hiring names, because they are the values
the API accepts.

| Value        | Typical meaning                            |
| ------------ | ------------------------------------------ |
| `UNDEFINED`  | No status set.                             |
| `APPLIED`    | The starting point for an inbound contact. |
| `IN_PROCESS` | Somewhere in the middle of your process.   |
| `REJECTED`   | Closed out unsuccessfully.                 |
| `ACCEPTED`   | Closed out successfully.                   |

### Status Workflow

```javascript theme={null}
class ContactWorkflow {
  async startProcess(contactId) {
    return await updateContact(contactId, {
      status: "IN_PROCESS",
    });
  }

  async accept(contactId, startDate) {
    return await updateContact(contactId, {
      status: "ACCEPTED",
      metadata: {
        startDate: startDate,
      },
    });
  }

  async reject(contactId, reason) {
    return await updateContact(contactId, {
      status: "REJECTED",
      metadata: {
        rejectionReason: reason,
      },
    });
  }
}
```

## Multiple Job Assignments

A contact can be associated with several jobs through `jobIds`, so one person can be tracked across more than one opening.

```javascript theme={null}
// Create the contact with its first job assignment
const contact = await createContact({
  firstName: "Jane",
  lastName: "Doe",
  email: "jane@example.com",
  phoneNumber: "+1234567890",
  gdprExpiryDate: "2026-11-16",
  jobId: "job-1-uuid", // Initial job assignment
});

// Later, assign more
await updateContact(contact.id, {
  jobIds: ["job-1-uuid", "job-2-uuid", "job-3-uuid"],
});

// The response carries both fields
// {
//   "id": "contact-uuid",
//   "jobId": "job-1-uuid",        // First job (backward compatible)
//   "jobIds": ["job-1-uuid", "job-2-uuid", "job-3-uuid"],
//   ...
// }
```

<Info>
  **Job associations**: `jobIds` is the array to manage. `jobId` (singular) is kept for backward
  compatibility and holds the first of them.
</Info>

## Deleting Contacts

```javascript theme={null}
async function deleteContact(contactId) {
  const response = await apiFetch(
    `https://api.instaview.sk/contacts/${contactId}`,
    { method: "DELETE" },
  );

  return await response.json();
}
```

<Warning>
  **This is a hard delete, and it cascades.** The contact goes, and so does every conversation
  they had — transcripts, recordings, analyses — along with their job assignments. None of it is
  readable through the API afterwards, and no endpoint brings it back. Export what you need
  first. See [Delete Contact](/api-reference/contacts/delete-contact).
</Warning>

<Info>
  **GDPR**: this is the deletion the API offers, and the one to reach for on an erasure request.
  Deleted rows are archived internally first, for our own compliance obligations, and that
  archive is not reachable through the API — see [Delete
  Contact](/api-reference/contacts/delete-contact) before promising a data subject that nothing
  remains anywhere.
</Info>

### Bulk Delete

```javascript theme={null}
async function bulkDeleteContacts(contactIds) {
  const results = [];

  for (const id of contactIds) {
    try {
      await deleteContact(id);
      results.push({ id, success: true });
      await sleep(200); // Rate limiting
    } catch (error) {
      results.push({ id, success: false, error: error.message });
    }
  }

  return results;
}
```

## Common Patterns

### Importing Contacts from an ATS

```javascript theme={null}
async function importContactsFromATS(atsData, jobMapping) {
  const imported = [];
  const errors = [];

  for (const atsRecord of atsData) {
    try {
      const instaviewJobId = jobMapping[atsRecord.jobId];

      if (!instaviewJobId) {
        throw new Error(`No mapping for job ${atsRecord.jobId}`);
      }

      const contact = await createContact({
        jobId: instaviewJobId,
        firstName: atsRecord.firstName,
        lastName: atsRecord.lastName,
        email: atsRecord.email,
        phoneNumber: atsRecord.phone,
        metadata: {
          resumeUrl: atsRecord.resumeUrl,
        },
        status: mapAtsStatus(atsRecord.status),
      });

      imported.push(contact);
      await sleep(200);
    } catch (error) {
      errors.push({
        record: atsRecord,
        error: error.message,
      });
    }
  }

  return { imported, errors };
}

function mapAtsStatus(atsStatus) {
  const statusMap = {
    new: "APPLIED",
    in_review: "IN_PROCESS",
    interview: "IN_PROCESS",
    offer: "IN_PROCESS",
    hired: "ACCEPTED",
    rejected: "REJECTED",
  };

  return statusMap[atsStatus] || "APPLIED";
}
```

### Duplicate Detection

```javascript theme={null}
async function findDuplicates(contact) {
  // Search by email
  const byEmail = await searchContacts(contact.email);

  // Search by phone
  const byPhone = await searchContacts(contact.phoneNumber);

  // Combine and deduplicate
  const allMatches = [...byEmail.data, ...byPhone.data];
  const uniqueMatches = Array.from(
    new Map(allMatches.map((c) => [c.id, c])).values(),
  );

  return uniqueMatches.filter((c) => c.id !== contact.id);
}

async function createOrUpdateContact(contactData) {
  const duplicates = await findDuplicates(contactData);

  if (duplicates.length > 0) {
    console.log(`Found ${duplicates.length} potential duplicates`);
    // Update the one we already have
    return await updateContact(duplicates[0].id, contactData);
  }

  return await createContact(contactData);
}
```

### Rolling Up a Contact's Calls

```javascript theme={null}
async function getContactSummary(contactId) {
  const [contact, conversations] = await Promise.all([
    getContact(contactId),
    listConversations({ contactId }),
  ]);

  return {
    contact,
    totalConversations: conversations.total,
    completedConversations: conversations.data.filter(
      (c) => c.status === "COMPLETED",
    ).length,
    averageScore: calculateAverageScore(conversations.data),
    lastConversationDate: getLastConversationDate(conversations.data),
  };
}

// `overallRating` is null on a conversation we could not score, so test the type rather than
// the truthiness — a genuine 0 is a score, and null is the absence of one.
function calculateAverageScore(conversations) {
  const scored = conversations.filter(
    (c) => typeof c.analysis?.general?.overallRating === "number",
  );
  if (scored.length === 0) return null;

  const sum = scored.reduce(
    (acc, c) => acc + c.analysis.general.overallRating,
    0,
  );
  return sum / scored.length;
}
```

### Resume Processing

```javascript theme={null}
async function uploadAndAttachResume(contactId, resumeFile) {
  // 1. Upload resume to your storage
  const resumeUrl = await uploadToStorage(resumeFile);

  // 2. Record the URL on the contact
  const updated = await updateContact(contactId, {
    metadata: {
      resumeUrl: resumeUrl,
    },
  });

  // 3. Optional: Trigger resume parsing
  await triggerResumeParser(contactId, resumeUrl);

  return updated;
}

async function parseResumeData(resumeUrl) {
  // Use a resume parsing service
  const parsed = await resumeParsingService.parse(resumeUrl);

  return {
    skills: parsed.skills,
    yearsOfExperience: parsed.totalYears,
    educationLevel: parsed.highestDegree,
    currentJobTitle: parsed.currentPosition,
    currentCompany: parsed.currentEmployer,
  };
}
```

## Validation Rules

<ResponseField name="firstName" type="string" required>
  First name (1-100 characters)
</ResponseField>

<ResponseField name="lastName" type="string" required>
  Last name (1-100 characters)
</ResponseField>

<ResponseField name="gdprExpiryDate" type="string" required>
  GDPR expiry date in ISO 8601 format (must be in the future, e.g.,
  "2026-11-16")
</ResponseField>

<ResponseField name="email" type="string">
  Valid email address (at least email or phoneNumber required)
</ResponseField>

<ResponseField name="phoneNumber" type="string">
  Phone number in E.164 format (+1234567890) (at least email or phoneNumber
  required)
</ResponseField>

<ResponseField name="jobId" type="string">
  UUID of the job to associate with (optional - must exist and belong to your
  company if provided)
</ResponseField>

<ResponseField name="jobIds" type="array">
  Array of job UUIDs for multi-job assignments (used in update operations)
</ResponseField>

<ResponseField name="metadata" type="object">
  Custom key-value pairs for extensibility (max 10KB, max 5 levels deep, max 50
  keys). Store additional fields like resumeUrl, skills, education, etc.
</ResponseField>

<ResponseField name="gender" type="string">
  `"male"` or `"female"`. Used for gender-aware addressing on the call (Slovak and Czech formal
  titles, for instance). Auto-detected from the name when omitted.
</ResponseField>

<ResponseField name="anonymizedCvText" type="string">
  The contact's anonymized CV as plain text. Send the raw text as `cvText` on create or update
  and it is anonymized for you using Google Gemini.
</ResponseField>

<ResponseField name="workHistory" type="array">
  Previous employment, one entry per job. Each entry can carry a reference to call about it:

  * **id** (string, optional): UUID of the work history record
  * **companyName** (string, required): Name of the company/employer
  * **candidatePosition** (string, required): The role held there. Keeps its original name, which is the name the API accepts.
  * **referenceName** (string, optional): Name of the reference to call
  * **referencePhone** (string, required): That reference's phone number in E.164 format (e.g. `+1234567890`)
  * **startDate** (string, optional): Job start date in calendar format (`yyyy-MM-dd`)
  * **endDate** (string, optional): Job end date in calendar format (`yyyy-MM-dd`)
</ResponseField>

## Error Scenarios

Errors answer with the flat body described in [Error Handling](/guides/error-handling): a
`statusCode`, a `message`, a `traceId`, and — on a rejected request body — an `errors` array
with one entry per rejected constraint.

<AccordionGroup>
  <Accordion title="Invalid Job ID" icon="circle-xmark">
    ```json theme={null}
    {
      "statusCode": 404,
      "message": "Job not found",
      "traceId": "4b1f0c9d2e6a47f8b3c5d7e9a1b2c3d4",
      "timestamp": "2026-08-03T10:30:00.000Z"
    }
    ```
  </Accordion>

  <Accordion title="Duplicate Phone Number" icon="copy">
    A phone number is unique per company, across every job. An email address is not — two
    contacts may share one.

    ```json theme={null}
    {
      "statusCode": 409,
      "message": "Candidate with this phone number already exists for this company.",
      "traceId": "4b1f0c9d2e6a47f8b3c5d7e9a1b2c3d4",
      "timestamp": "2026-08-03T10:30:00.000Z"
    }
    ```
  </Accordion>

  <Accordion title="Invalid Phone Number" icon="phone">
    ```json theme={null}
    {
      "statusCode": 400,
      "message": "Validation failed",
      "errors": [
        "Phone number must be in valid E.164 format starting with + (e.g. +1234567890)"
      ],
      "traceId": "4b1f0c9d2e6a47f8b3c5d7e9a1b2c3d4"
    }
    ```
  </Accordion>
</AccordionGroup>

## Documents

A contact can hold documents an agent may look up during a call — a cover letter, application notes, and the contact's own processed CV. They are managed under `/contacts/{id}/documents` with the same three-step presigned upload as an [agent's knowledge base](/api-reference/agents/knowledge/start-upload).

<Steps>
  <Step title="Reserve a slot">
    `POST /contacts/{id}/documents/uploads` with `fileName`, `mimeType` and the file's exact `sizeBytes`. Both the slot and the bytes are reserved against your company's limits from this moment.
  </Step>

  <Step title="Upload the file">
    `PUT` the bytes to the returned `uploadUrl` with the returned `contentType`. No API key on this request.
  </Step>

  <Step title="Complete the upload">
    `POST /contacts/{id}/documents/{documentId}/complete`. The file is verified and the document becomes `READY`.
  </Step>
</Steps>

Two things distinguish contact documents from agent documents:

* **The CV appears on its own.** Once a CV has been processed, the listing shows a document with `source: "CV"`. It is produced from the CV's anonymized text and cannot be renamed or deleted through the API — deactivate it (`PATCH` with `isActive: false`) to keep it out of calls.
* **Two limits.** At most **5** uploaded documents per contact, and a plan-tiered ceiling on your company's total contact-document storage. Both are reserved when an upload is started, and the `400` you get when the company ceiling is the problem names the bytes in use, the ceiling and the size requested.

<Warning>
  **Listing a document does not put it on a call.** The agent must have
  `contextConfig.useContactContext` enabled. With it off, calls run exactly as they did before,
  documents or not.
</Warning>

Contact documents follow the contact's retention: when the contact reaches its GDPR expiry date, its documents are deleted with it, stored files and voice-provider copies included.

See the [Contact Documents reference](/api-reference/contacts/documents/list-documents) for the full endpoint set.

## Best Practices

<CardGroup cols={2}>
  <Card title="Validate Data" icon="check">
    Validate email and phone formats before submission
  </Card>

  <Card title="Handle Duplicates" icon="copy">
    Implement duplicate detection logic
  </Card>

  <Card title="Update Status" icon="arrows-rotate">
    Keep each contact's status current as your process moves
  </Card>

  <Card title="Secure Resume URLs" icon="lock">
    Use signed URLs with expiration for resume access
  </Card>
</CardGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Conversations" icon="microphone" href="/guides/resources/conversations">
    Start a conversation with a contact
  </Card>

  <Card title="Jobs" icon="briefcase" href="/guides/resources/jobs">
    Learn about job management
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/contacts/list-contacts">
    The full Contacts API reference
  </Card>

  <Card title="Best Practices" icon="star" href="/guides/best-practices">
    Production integration patterns
  </Card>
</CardGroup>
