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

# Update Contact

> Updates a contact that belongs to the API key's company. Only basic profile, reachability and status fields are supported.

<Info>
  `PATCH /candidates/{id}` 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>

Updates an existing contact. Only the fields you send are changed; everything else is left alone.

## Overview

A partial update, so you send the fields you want to change and nothing more. Use it to correct a phone number, move someone's status, change their job assignments, or attach metadata.

## Use Cases

* **Status updates**: move a contact through your own process
* **Reachability**: change email, phone number or name
* **Gender override**: set or clear it for accurate addressing on the call
* **Job assignments**: attach, replace or clear them
* **Metadata**: add or change your own opaque key-value pairs — not the same thing as [contact fields](/api-reference/contacts/update-contact)
* **Refinement**: fill the record in as you learn more

## Partial Updates

You can update any combination of fields:

```javascript theme={null}
// Update only status
PATCH /contacts/{id}
{
  "status": "IN_PROCESS"
}

// Update how to reach them
PATCH /contacts/{id}
{
  "email": "newemail@example.com",
  "phoneNumber": "+1987654321"
}

// Assign to multiple jobs
PATCH /contacts/{id}
{
  "jobIds": ["job-uuid-1", "job-uuid-2", "job-uuid-3"]
}

// Remove all job assignments
PATCH /contacts/{id}
{
  "jobIds": []
}

// Set gender explicitly
PATCH /contacts/{id}
{
  "gender": "female"
}

// Clear gender (revert to auto-detection)
PATCH /contacts/{id}
{
  "gender": null
}

// Replace the work history
PATCH /contacts/{id}
{
  "workHistory": [
    {
      "companyName": "Tech Corp",
      "candidatePosition": "Senior Software Engineer",
      "referenceName": "Jane Smith",
      "referencePhone": "+1234567890",
      "startDate": "2020-01-01"
    }
  ]
}
```

<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>
  **Sending `metadata` never disturbs the contact's fields.** The two are stored separately, so
  replacing `metadata` wholesale cannot reach the catalogued values: they survive the write
  untouched, and the response shows them back to you under `fields`. A routine sync of your own
  payload cannot erase a value an agent depends on.

  It does not work as a back door either. Nothing on the `metadata` path checks a value against
  its field's type, enum or reserved-key rules, and nothing reads a contact field out of it —
  whatever you put there stays your own scratch space. `fields` below is the validated door, on
  this endpoint and on [`POST /contacts`](/api-reference/contacts/create-contact), and it answers
  `422` naming the key when a value does not fit.
</Info>

## Updating Field Values

Send `fields` to change a contact's catalogued values in the same request as anything else:

```javascript theme={null}
PATCH /contacts/{id}
{ "status": "contacted", "fields": { "order_number": "SO-40129" } }
```

<Info>
  **`fields` merges; `metadata` replaces.** A key you omit keeps its value, and an explicit
  `null` clears one. The two behave differently in the same request body on purpose:
  `metadata` is one opaque document you own, `fields` are individually catalogued values.
</Info>

```javascript theme={null}
// Change one value, leave every other alone
PATCH /contacts/{id}
{ "fields": { "order_number": "SO-40129" } }

// Clear one
PATCH /contacts/{id}
{ "fields": { "renewal_date": null } }
```

`"fields": null` is not a wipe — it reads as "no values in this request" and is a no-op.
Clearing is per key, which is what stops a partial update from blanking a contact by accident.

### Values are validated against the catalog

Every value is checked against its field's declared type, and an unknown key is refused. Both
answer `422` **naming the key** — never a silent drop, because a caller who believes it wrote a
value it did not is the worse outcome. `422` rather than `400`: the request is well formed, it
is the catalog that rejects its contents.

Validation runs in the same transaction as the contact update, so a rejection applies
**nothing** — not the status, not the metadata, not the other values.

| The field is | Accepted                                       | Refused                                     |
| ------------ | ---------------------------------------------- | ------------------------------------------- |
| `string`     | any text (trimmed)                             | an object                                   |
| `number`     | `42`, `"42"`, `"-7.5"`                         | `"twelve"`, `"12abc"`                       |
| `bool`       | `true`, `"true"`, `"yes"`, `"1"` and negatives | `"maybe"`                                   |
| `date`       | `"2026-04-20"`, any parseable date             | `"not a date"`                              |
| `enum`       | one of the field's `enumOptions`               | anything else — the error lists the options |
| `tags`       | `["vip","renewal"]`, **or** `"vip, renewal"`   | `42`, `true`, an object                     |

Reserved and read-only keys are refused too: `first_name`, `email` and the rest are set as the
contact's own properties above, and `company_description` and `agent_name` resolve from your
company and your agent, so a value stored under them would be shadowed on every call.

### `tags` takes two shapes

A `tags` field stores a `string[]` and accepts either the array or **one comma-separated
string** — so a single CSV cell holding several tags maps onto one field without your side
splitting it first. The two are equivalent:

```javascript theme={null}
PATCH /contacts/{id}
{ "fields": { "tags": ["vip", "renewal"] } }

// Same result
PATCH /contacts/{id}
{ "fields": { "tags": "vip, renewal" } }
```

Entries are trimmed, blanks are dropped (`"vip,, renewal,"` is two tags) and duplicates are
removed case-insensitively, keeping the spelling that came first — `["VIP","vip"]` stores
`["VIP"]`.

<Warning>
  **A comma is always a separator, so a tag cannot contain one.** That holds inside an array
  element too: `["north, east"]` stores two tags, exactly as `"north, east"` does. The two
  shapes would otherwise mean different things, and a value edited in the dashboard — which
  renders the list as one comma-joined line — would split on the next save with no warning.
</Warning>

Bounded, because these are stored on the contact and read on every call: at most **50 tags**,
**60 characters** per tag, and **2000 characters** of raw tag text per request. Each is a `422`
naming the limit.

<Info>
  **An empty list is the erase.** `[]`, `[""]` and `","` all say the contact has no tags, so
  they clear the field exactly as `null` and `""` do — you never get a key present with an
  empty array behind it.
</Info>

What it will not do is guess: a number, a boolean or an object is a `422` rather than a
one-element list, because a caller sending `42` for a tags field has a mapping bug and a
silent `["42"]` would be spoken on a call.

### `?createMissingFields=true`

Off by default. When passed, a key in `fields` that your catalog does not know yet is
**defined for you** as a `string` field, instead of the request being rejected.

```javascript theme={null}
PATCH /contacts/{id}?createMissingFields=true
{ "fields": { "warranty_ref": "W-9921" } }
// warranty_ref now exists in your catalog, labelled "Warranty ref"
```

It works the same way on [`POST /contacts`](/api-reference/contacts/create-contact).

<Warning>
  **A typo becomes a permanent field.** `oder_number` creates `oder_number`, visible in every
  agent's variable palette until somebody deletes it. That is why the option is opt-in per
  request rather than a mode you can leave on.
</Warning>

Two things to know:

* **It needs no extra scope.** The same `write:contacts` that lets you define a field
  explicitly lets you define one implicitly. What it will still never do is quietly ignore an
  unknown key you did not ask it to create.
* **The field is always `string`.** The value is the only evidence available and it is a poor
  one: `"1"` could be a number, a boolean or an order reference, and a wrong guess becomes
  validation that rejects your next row.
  [`PATCH /contact-fields/{id}`](/api-reference/contact-fields/update-contact-field) retypes it
  afterwards. Reserved keys, malformed keys and a company already at its field ceiling are all
  still refused.

For anything you control, define the fields explicitly with
[`POST /contact-fields`](/api-reference/contact-fields/create-contact-field) — you get the right
type and label, and a typo fails loudly.

## Managing Job Assignments

`jobIds` sets which jobs a contact is associated with. It **replaces** the array rather than adding to it:

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

// Add one to the existing assignments
// Note: fetch current state first, since PATCH replaces the whole jobIds array
const contact = await getContact("contact-uuid");
await updateContact("contact-uuid", {
  jobIds: [...contact.jobIds, "new-job-uuid"],
});

// Replace every assignment with one job
await updateContact("contact-uuid", {
  jobIds: ["only-this-job-uuid"],
});

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

<Info>
  **Job validation**: every id in `jobIds` must name an existing job belonging to your API key's
  company. An unknown id is a validation error, not a silent omission.
</Info>

<Warning>
  **Concurrent `jobIds` updates lose assignments.** Because the field replaces the array, adding
  one job is a read-modify-write, and the endpoint offers no precondition to make that safe — no
  `If-Match`, no version field. Two requests that both read `["job-1"]` and each append their own
  job leave the contact with whichever wrote last, and the other assignment is gone. It is not
  reported: both calls answer `200`.

  Serialise `jobIds` writes for a given contact — a queue or a lock of your own, keyed on the
  contact id — rather than issuing them in parallel from several workers. Fields that are not
  read-modify-write (`email`, `status`, `metadata`) do not have this problem.
</Warning>

## Status Values

`status` is one of:

| Value        | Meaning                                   |
| ------------ | ----------------------------------------- |
| `UNDEFINED`  | No status set                             |
| `APPLIED`    | The starting point for an inbound contact |
| `IN_PROCESS` | Somewhere in your process                 |
| `REJECTED`   | Closed out unsuccessfully                 |
| `ACCEPTED`   | Closed out successfully                   |

<Info>
  The names are hiring-flavoured because they predate the neutral vocabulary, and they are the
  values the API accepts, so they are unchanged. Sending anything else is a `400`.
</Info>

## Company Isolation

You can only update contacts belonging to your API key's company. Ownership is validated before anything is written.

## Error Scenarios

* **404 Not Found**: the contact does not exist, or has been deleted
* **403 Forbidden**: the contact belongs to a different company, or a `jobIds` entry names another company's job
* **400 Bad Request**: invalid field values, or a `jobIds` entry naming no job at all

## Related Resources

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

  <Card title="Get Contact" icon="eye" href="/api-reference/contacts/get-contact">
    Read the current state first
  </Card>

  <Card title="Create Contact" icon="plus" href="/api-reference/contacts/create-contact">
    Create a new contact
  </Card>
</CardGroup>


## OpenAPI

````yaml PATCH /contacts/{id}
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/{id}:
    patch:
      tags:
        - Contacts
      summary: Update contact
      description: >-
        Updates a contact that belongs to the API key's company. Only basic
        profile, reachability and status fields are supported.
      operationId: PublicCandidatesController_updateCandidate_v1
      parameters:
        - name: id
          required: true
          in: path
          schema:
            type: string
            format: uuid
        - 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/PublicUpdateCandidateDto'
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicCandidateDto'
      security:
        - bearer: []
components:
  schemas:
    PublicUpdateCandidateDto:
      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: '+421901234567'
          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
        status:
          type: string
          description: Contact status
          enum:
            - UNDEFINED
            - APPLIED
            - IN_PROCESS
            - REJECTED
            - ACCEPTED
          example: IN_PROCESS
        jobIds:
          description: >-
            Array of job IDs to assign the contact to (replaces existing
            assignments)
          example:
            - 123e4567-e89b-12d3-a456-426614174000
            - 987e6543-e21b-12d3-a456-426614174001
          type: array
          items:
            type: string
            format: uuid
        metadata:
          type: object
          description: >-
            Custom metadata (replaces existing metadata, max 10KB, 5 levels
            deep, 50 keys)
          example:
            source: LinkedIn
            referredBy: John Smith
        fields:
          type: object
          description: >-
            Catalogued field values, keyed by catalog key. MERGED key by key,
            unlike `metadata` which replaces: a key you omit keeps its value,
            and an explicit `null` clears one. 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 is rolled back.
          additionalProperties: true
          example:
            order_number: SO-40129
            renewal_date: null
        gender:
          type: string
          description: >-
            The contact's gender. Set to 'male' or 'female' to override
            auto-detection, or null to clear and revert to auto-detection.
          enum:
            - male
            - female
          nullable: true
          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 (replaces the existing
            ones).
          maxItems: 20
          items:
            $ref: '#/components/schemas/PublicCandidateWorkHistoryItemDto'
    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

````