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

# Create Contact

> Creates a contact (optionally associated with one or more jobs) in the API key's company.

<Info>
  `POST /candidates` is a permanent alias of this endpoint and keeps working unchanged, with the same scopes and the same response. See [Resource names](/api-reference/introduction#resource-names).
</Info>

Creates a contact — a person an agent can call. A contact can optionally be associated with a job, or exist on its own.

## Overview

A contact is the person on the other end of a conversation: the record holding their name, how to reach them, and whatever else you want to keep about them. Jobs are optional and can be attached at creation or later.

## Use Cases

* **Application processing**: create contacts from inbound applications, with a job attached
* **ATS integration**: sync contacts in from an external system
* **Manual entry**: add people you found some other way
* **Building a list**: keep contacts with no job assignment at all
* **Bulk import**: load many contacts at once

## Job Association (Optional)

<Info>
  **A job is optional.** Send `jobId` to attach one at creation, or leave it out.

  * **With a job**: the contact is associated with that job immediately.
  * **Without one**: the contact exists on its own, and jobs can be attached later through the update endpoint. Most callers outside hiring never send one.
  * If provided, the job must exist and belong to your API key's company.
</Info>

## Basic Contact Creation

### With Job Association

```javascript theme={null}
{
  "jobId": "job-uuid",
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane.doe@example.com",
  "phoneNumber": "+1234567890",
  "gdprExpiryDate": "2026-11-16"
}
```

### Without Job Association

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

## A Fuller Contact

```javascript theme={null}
{
  "jobId": "job-uuid",
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane.doe@example.com",
  "phoneNumber": "+1234567890",
  "gdprExpiryDate": "2026-11-16",
  "gender": "female",
  "metadata": {
    "source": "LinkedIn",
    "externalId": "CONTACT-12345",
    "notes": "Strong technical background",
    "resumeUrl": "https://storage.example.com/resumes/jane-doe.pdf",
    "linkedinUrl": "https://linkedin.com/in/janedoe"
  },
  "workHistory": [
    {
      "companyName": "Tech Corp",
      "candidatePosition": "Software Engineer",
      "referenceName": "Jane Smith",
      "referencePhone": "+1234567890",
      "startDate": "2020-01-01",
      "endDate": "2022-12-31"
    }
  ]
}
```

<Info>
  **Note**: `resumeUrl` and `linkedinUrl` are not built-in fields — they are shown inside the
  free-form `metadata` object, which is where anything of your own belongs.
</Info>

<Warning>
  **`metadata` is not readable by an agent.** It is your own scratch space — free-form,
  unvalidated, and never placed in a prompt. If you want a value an agent can *say*, it has to
  be a **contact field**: catalogued, typed, and addressable as `{{contact.<key>}}`. See
  [Update Contact](/api-reference/contacts/update-contact).
</Warning>

<Info>
  `workHistory` describes previous employment, and its `candidatePosition` field keeps that name
  because it is the name the API accepts. It exists for reference calls: each entry can carry a
  `referenceName` and `referencePhone` to call about that job.
</Info>

## Company Scoping

Contacts are scoped to your API key's company. When a job is attached, the job must belong to that company; a contact created without one is associated with your company anyway, and can only be reached through your company's API keys. That is what keeps companies fully isolated from each other.

## Required Fields

<Warning>
  **Required on every contact**:

  * `firstName` — 1-100 characters
  * `lastName` — 1-100 characters
  * `gdprExpiryDate` — GDPR retention date, ISO 8601, and in the future (e.g. `"2026-11-16"`)
  * At least one way to reach them: `email` or `phoneNumber` (or both)
</Warning>

## Optional Fields

* **jobId**: UUID of a job to attach (must exist and belong to your company)
* **email**: valid email address (required if `phoneNumber` is absent)
* **phoneNumber**: E.164 format, e.g. `+1234567890` (required if `email` is absent)
* **gender**: `"male"` or `"female"`. Used for gender-aware addressing on the call. Auto-detected from the name when omitted.
* **metadata**: your own key-value pairs (max 10KB, 5 levels deep, 50 keys)
* **fields**: catalogued field values, keyed by catalog key — validated against the catalog, and the only values an agent can speak

## Field Validation

The API enforces:

* **firstName/lastName**: 1-100 characters (required)
* **gdprExpiryDate**: a valid ISO 8601 date in the future (required)
* **email**: valid email format (at least one of email or phoneNumber required)
* **phoneNumber**: starts with `+`, E.164 format (at least one of email or phoneNumber required)
* **jobId**: a valid UUID naming an existing job in your company (optional)
* **gender**: `"male"` or `"female"` if present (optional)
* **metadata**: max 10KB, max depth 5, max 50 keys (optional)

## Setting Field Values on the Create

Send them as `fields`, keyed by catalog key, and the contact is created with its values in one
request:

```javascript theme={null}
POST /contacts
{
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane@example.com",
  "fields": {
    "is_vip": true,
    "order_number": "SO-40128",
    "renewal_date": "2026-04-01"
  }
}
```

The response's `fields` is what was stored, so there is nothing to go and check.

The shape is **flat** — the same one
[`GET /contacts/{id}`](/api-reference/contacts/get-contact) returns and
[`PATCH /contacts/{id}`](/api-reference/contacts/update-contact) accepts. There is no
seeded-versus-your-own split in the payload because there is none to make: every key this
accepts is a catalogued key with `writable: true`, whether InstaView seeded it or you defined
it. Read [`GET /contact-fields`](/api-reference/contact-fields/list-contact-fields) for the keys
your company has.

<Warning>
  **A rejected value takes the contact with it.** Values are validated against the catalog in
  the same transaction as the insert, so an unknown key or a wrong-typed value answers `422`
  naming it and the contact is **not** created. That is deliberate: a contact that exists
  without the values you sent is a half-write you cannot detect without re-reading.
</Warning>

An unknown key is a `422` by default. Pass
[`?createMissingFields=true`](/api-reference/contacts/update-contact#createmissingfields-true)
to have it **defined for you** as a `string` field instead — convenient for a first sync, and
opt-in per request because a typo would otherwise mint a permanent field in your catalog.

## `fields` Is Not `metadata`

They are stored separately and read separately. `fields` is the validated door: every value is
checked against its field's declared type, enum and reserved-key rules, and only values that
arrive through it are addressable as `{{contact.<key>}}`. `metadata` is your own scratch space,
free-form and never read by an agent, and nothing you put in it becomes a contact field however
you name the key.

## Response Format

The response carries both `jobId` (singular) and `jobIds` (array):

```javascript theme={null}
{
  "id": "contact-uuid",
  "jobId": "job-uuid",           // First job (for backward compatibility)
  "jobIds": ["job-uuid"],        // Array of all job associations
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane.doe@example.com",
  "phoneNumber": "+1234567890",
  "gdprExpiryDate": "2026-11-16",
  "gender": "female",
  "status": "APPLIED",
  "fields": {                    // Catalogued values, as stored
    "is_vip": true,
    "order_number": "SO-40128"
  },
  "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-28T10:30:00Z",
  "updatedAt": "2025-11-28T10:30:00Z"
}
```

## Related Resources

<CardGroup cols={2}>
  <Card title="Contacts Resource Guide" icon="user" href="/guides/resources/contacts">
    Learn about managing contacts and their workflows
  </Card>

  <Card title="Jobs Resource Guide" icon="briefcase" href="/guides/resources/jobs">
    Understand job management
  </Card>

  <Card title="Conversations Resource Guide" icon="microphone" href="/guides/resources/conversations">
    Start a conversation with a contact
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/contacts/list-contacts">
    List and manage your contacts
  </Card>
</CardGroup>


## OpenAPI

````yaml POST /contacts
openapi: 3.0.0
info:
  title: InstaView API
  description: |-
    InstaView API Documentation

    ## Authentication

    All endpoints require API key authentication using Bearer token:
    ```
    Authorization: Bearer sk_your_api_key_here
    ```

    ## API Key Management

    The API Key module provides comprehensive key management for:
    - **Direct Client Keys**: Company-scoped keys for your applications
    - **ATS Partner Keys**: Resource-scoped keys for ATS integrations

    ### Key Features
    - HMAC-SHA256 hashing for API key storage
    - Configurable rate limiting
    - Comprehensive audit logging
    - Company-level isolation
    - Resource scoping for ATS partners
  version: 1.0.0
  contact: {}
servers:
  - url: https://api.instaview.sk
    description: Production API Gateway
security: []
tags: []
paths:
  /contacts:
    post:
      tags:
        - Contacts
      summary: Create contact
      description: >-
        Creates a contact (optionally associated with one or more jobs) in the
        API key's company.
      operationId: PublicCandidatesController_createCandidate_v1
      parameters:
        - name: companyId
          required: false
          in: query
          description: >-
            Required for ATS API keys to specify which company to access.
            Ignored for standard company API keys.
          schema:
            type: string
        - name: createMissingFields
          required: false
          in: query
          description: >-
            Define any key in `fields` that your catalog does not know yet, as a
            STRING field, instead of rejecting the request. Off by default,
            because a typo would otherwise become a permanent field in your
            catalog — visible in every agent's variable palette until somebody
            deletes it. A key the platform reserves, a malformed key, and a
            company already at its field ceiling are all still refused.
          schema:
            type: boolean
            default: false
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicCreateCandidateDto'
      responses:
        '201':
          description: Contact created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicCandidateDto'
      security:
        - bearer: []
components:
  schemas:
    PublicCreateCandidateDto:
      type: object
      properties:
        firstName:
          type: string
          description: The contact's first name
          example: John
          minLength: 1
          maxLength: 100
        lastName:
          type: string
          description: The contact's last name
          example: Doe
          minLength: 1
          maxLength: 100
        email:
          type: string
          description: The contact's email address
          example: john.doe@example.com
        phoneNumber:
          type: string
          description: The contact's phone number in E.164 format
          example: '+421915123456'
          pattern: ^\+[1-9]\d{1,14}$
        gdprExpiryDate:
          type: string
          description: GDPR expiry date in ISO 8601 format (must be in the future)
          example: '2026-11-16'
          format: date
        jobId:
          type: string
          description: '[Deprecated - use jobIds] Optional single job ID to associate.'
          deprecated: true
          format: uuid
          example: 123e4567-e89b-12d3-a456-426614174000
        jobIds:
          type: array
          description: >-
            Optional array of job IDs to associate the contact with. If both
            jobId and jobIds are provided, jobIds takes precedence.
          items:
            type: string
            format: uuid
          uniqueItems: true
          minItems: 1
          maxItems: 50
        metadata:
          type: object
          description: >-
            Custom metadata for extensibility (key-value pairs, max 10KB, 5
            levels deep, 50 keys)
          example:
            source: LinkedIn
            referredBy: Jane Smith
            tags:
              - senior
              - remote-preferred
            externalId: CAND-12345
        fields:
          type: object
          description: >-
            Catalogued field values, keyed by catalog key — the same flat shape
            `GET /contacts/{id}` returns. Validated against each field's
            declared type, so an unknown key or a wrong-typed value is a 422
            naming it, and the whole request — contact included — is rolled
            back. Read `GET /contact-fields` for the keys your company has. Not
            `metadata`: these are typed, and they are the only values an agent
            can speak.
          additionalProperties: true
          example:
            is_vip: true
            order_number: SO-40128
            renewal_date: '2026-04-01'
        gender:
          type: string
          description: >-
            The contact's gender. Used for gender-aware addressing on the call.
            If not provided, it is auto-detected from the name.
          enum:
            - male
            - female
          example: female
        cvText:
          type: string
          description: >-
            The contact's CV in plain text. This will be automatically
            anonymized.
          example: |-
            Jane Doe
            Software Engineer
            Experience: ...
          maxLength: 50000
        workHistory:
          type: array
          description: Optional work history items for the contact.
          maxItems: 20
          items:
            $ref: '#/components/schemas/PublicCandidateWorkHistoryItemDto'
      required:
        - firstName
        - lastName
        - gdprExpiryDate
    PublicCandidateDto:
      type: object
      properties:
        id:
          type: string
          description: Contact ID
          example: 123e4567-e89b-12d3-a456-426614174000
        jobId:
          type: string
          description: '[Deprecated] Single job ID; use jobIds instead.'
          deprecated: true
          example: 987e6543-e21b-12d3-a456-426614174000
        jobIds:
          type: array
          description: Jobs the contact is assigned to.
          items:
            type: string
            format: uuid
          uniqueItems: true
          maxItems: 50
        firstName:
          type: string
          description: The contact's first name
          example: John
        lastName:
          type: string
          description: The contact's last name
          example: Doe
        email:
          type: string
          description: The contact's email address
          example: john.doe@example.com
        phoneNumber:
          type: string
          description: The contact's phone number
          example: '+421915123456'
        status:
          type: string
          description: Contact status
          enum:
            - UNDEFINED
            - APPLIED
            - IN_PROCESS
            - REJECTED
            - ACCEPTED
          example: APPLIED
        gdprExpiryDate:
          type: string
          description: >-
            GDPR expiry date. Currently returned as a date (no time). NOTE: We
            plan to migrate to a timestamp with timezone (timestamptz) for
            global correctness.
          example: '2026-11-16'
          format: date
        overallRating:
          type: number
          description: Overall rating/match score (0-100)
          example: 85
          minimum: 0
          maximum: 100
        metadata:
          type: object
          description: >-
            Your own scratch space on the contact: free-form, unvalidated, and
            never read by an agent. Replaced wholesale on update. Not the place
            for values you want an agent to say — those are `fields`.
          example:
            source: LinkedIn
            externalId: CAND-12345
        fields:
          type: object
          description: >-
            The contact's catalogued field values, keyed by field key — the
            counterpart of `metadata`, and the only half an agent can speak.
            Each key is defined in the company catalog (`GET /contact-fields`)
            and addressable in a flow as `{{contact.<key>}}`. Written through
            `fields` on `POST /contacts` and `PATCH /contacts/{id}`, never
            through `metadata`. A field with no value is absent rather than
            null; `{}` means none are set.
          example:
            order_number: SO-40128
            is_vip: true
            renewal_date: '2026-04-01'
          additionalProperties: true
        createdAt:
          type: string
          description: Created timestamp (UTC)
          example: '2025-11-20T10:30:00Z'
          format: date-time
        updatedAt:
          type: string
          description: Updated timestamp (UTC)
          example: '2025-11-20T10:30:00Z'
          format: date-time
        analysisCount:
          type: number
          description: >-
            Number of analyses for this contact. Not currently populated by any
            endpoint — treat as absent.
          example: 2
        interviewCount:
          type: number
          description: >-
            Number of conversations for this contact. Keeps its original field
            name, and is not currently populated by any endpoint — treat as
            absent.
          example: 3
        links:
          type: object
          description: >-
            Convenience links to related collections. Endpoints may be added
            incrementally.
          example:
            analyses: /v1/public/contacts/123e4567-e89b-12d3-a456-426614174000/analyses
            interviews: >-
              /v1/public/contacts/123e4567-e89b-12d3-a456-426614174000/conversations
        gender:
          type: string
          description: >-
            The contact's gender. Used for gender-aware addressing on the call.
            Null if not explicitly set (auto-detected from the name).
          enum:
            - male
            - female
          nullable: true
          example: female
        anonymizedCvText:
          type: string
          description: The contact's anonymized CV in plain text.
          example: |-
            [NAME]
            Software Engineer
            Experience: ...
        workHistory:
          type: array
          description: Work history items for the contact.
          maxItems: 20
          items:
            $ref: '#/components/schemas/PublicCandidateWorkHistoryItemDto'
      required:
        - id
        - firstName
        - lastName
        - status
        - fields
        - createdAt
        - updatedAt
    PublicCandidateWorkHistoryItemDto:
      type: object
      properties:
        id:
          type: string
          description: Work history item ID
          format: uuid
          example: 123e4567-e89b-12d3-a456-426614174000
        companyName:
          type: string
          description: Name of the employer company
          example: Google
          minLength: 1
          maxLength: 255
        candidatePosition:
          type: string
          description: >-
            The role held at that company. Keeps its original field name, which
            is the name the API accepts.
          example: Software Engineer
          minLength: 1
          maxLength: 255
        referenceName:
          type: string
          description: Name of the reference person
          example: Jane Smith
          maxLength: 255
        referencePhone:
          type: string
          description: Phone number of reference contact in E.164 format
          example: '+1987654321'
          pattern: ^\+[1-9]\d{1,14}$
        startDate:
          type: string
          description: Job start date in calendar format (yyyy-MM-dd)
          example: '2020-01-01'
          format: date
          pattern: ^\d{4}-\d{2}-\d{2}$
        endDate:
          type: string
          description: Job end date in calendar format (yyyy-MM-dd)
          example: '2022-12-31'
          format: date
          pattern: ^\d{4}-\d{2}-\d{2}$
      required:
        - companyName
        - candidatePosition
        - referencePhone
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: API key for authentication using Bearer scheme

````