This guide explains how to create, update, and manage records through the
`/api/v2/records` endpoints.

## Why this matters

Records are the actual data instances for your objects (for example candidates,
vacancies, companies). These endpoints gives you a complete lifecycle:
create/update, search, relation handling, archive/restore, and morphing.

## Prerequisites

- Bearer token or API Token with permissions for the relevant object scopes
- Object internal name (used as `{objectName}` in routes)


## Endpoint map

Core write and lifecycle endpoints:

- `POST /api/v2/records/{objectName}` create a record
- `PUT /api/v2/records/{objectName}/{uuid}` update a record
- `POST /api/v2/records/{objectName}/createOrUpdate` upsert by unique fields
- `DELETE /api/v2/records/{uuid}` archive, anonymize, or hard delete
- `POST /api/v2/records/{uuid}/restore` restore archived records
- `POST /api/v2/records/{uuid}/morph` move/add visibility across objects


List/search and projection endpoints:

- `POST /api/v2/records/index`
- `POST /api/v2/records/search`
- `GET /api/v2/records/{uuid}`
- `GET /api/v2/records/{uuid}/previews/{name}`


Relation endpoints:

- `POST /api/v2/records/relations/{fromUuid}/{relationName}/{toUuid}`
- `DELETE /api/v2/records/relations/{fromUuid}/{relationName}/{toUuid}`


## Step 1: Create a record


```bash
curl -X POST "https://your-company.caraer.com/api/v2/records/candidate?parse=true" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "properties": {
      "firstname": "Jane",
      "lastname": "Doe",
      "email": "jane.doe@example.com"
    }
  }'
```

What happens:

- The controller builds a `Record` from your DTO and object name.
- Property-level validation is applied in record services.
- You get a `CreateResponse` with the created record.
- When setting parse to true the values will be parsed to labels and human readable values. Leave parse out if you want the internal values when working on an integration for example. Parse is by default false.


## Step 2: Update a record


```bash
curl -X PUT "https://your-company.caraer.com/api/v2/records/candidate/RECORD_UUID?parse=true" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "properties": {
      "application_status": "in_progress"
    }
  }'
```

The endpoint returns `UpdateResponse<Record>`.

## Step 3: Use create-or-update for idempotent integrations

If your integration receives repeated events, `createOrUpdate` is usually the
safest write pattern:


```bash
curl -X POST "https://your-company.caraer.com/api/v2/records/candidate/createOrUpdate" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "values": {
      "email": "jane.doe@example.com",
      "firstname": "Jane",
      "lastname": "Doe"
    }
  }'
```

If a matching unique record exists, Caraer updates it; otherwise it creates a
new one. The matching is based on all the unique properties of a record.

## How to submit values by property type and format

When you submit `values`, the backend validates by **property type** and uses
**property format** for parsing/formatting behavior. The key is always the
property `name` of the property, and the value shape must match the type.

Common type rules from backend validation:

- `string`: send a string
- `number`: send a numeric value (for example `120000` or `"120000"`)
- `date`: send unix timestamp milliseconds in UTC (preferred). ISO date/date-time
inputs are also accepted by date parsing and normalized
- `checkbox`: send `true`/`false` (or string `"true"`/`"false"`)
- `single-select`: send one option name exactly as configured
- `multi-select`: send option names as one semicolon-separated string or as an array of the names
- `tag`: send tags as one semicolon-separated string or as an array of the names
- `range`: send `"min;max"` (both numeric)
- `structure`: send a valid JSON string (not a raw nested JSON object)
- `file`: send a file key string (needs to be under your company files path)
- `linked-property`: do not send a value (backend rejects manual values since they're read only and managed on the related records)


Example payload covering multiple types:


```json
{
  "properties": {
    "firstname": "Jane Doe",
    "salary": 4200,
    "start_date": 1742851200000, // "2025-04-24T00:00:00.000Z"
    "available_remote": true,
    "application_status": "in_progress",
    "skills": "java;spring;neo4j", // ['java', 'spring', 'neo4j']
    "labels": "high_priority;backend", // ['high_priority', 'backend']
    "salary_range": "3500;5000",
    "profile_data": "{\"github\":\"janedoe\",\"years_experience\":6}",
    "resume_file": "COMPANY_UUID/files/candidates/jane-doe-cv.pdf"
  }
}
```

### Type vs format (important distinction)

- **Type** controls what value shape is valid.
- **Format** controls how that value is interpreted or displayed.


Example: a string property may accept multiple string input styles, but the
normalized stored value is still validated as a string type value.

### Submission checklist before calling create/update

- Confirm property `name` keys match the object schema exactly.
- Confirm select values exist in property options.
- For `multi-select`, `tag`, and `range`, use semicolon delimiters or arrays with names of the options.
- For `structure`, serialize to a JSON string first.
- Do not send values for `linked-property` fields.


## Step 4: List records for UI and automation

Use `/index` for generic pagination, with optional `archived=true` and
`parse=true`.

**Note on archived records:**
The `archived` query parameter determines whether to fetch archived (soft-deleted) records or not. By default, only active (non-archived) records are returned (`archived=false`).

- Set `archived=true` to list only records that have been archived.
- Omitting this parameter (or setting `archived=false`) will fetch only currently active records.


**Example:**
To fetch only archived records, use:


```bash
curl -X GET "https://your-company.caraer.com/api/v2/records/OBJECT_NAME/index?archived=true" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

## Step 5: Connect records with relations

Use these endpoints to link two existing records with a named relation.
The route is:

- `POST /api/v2/records/relations/{fromUuid}/{relationName}/{toUuid}`
- `DELETE /api/v2/records/relations/{fromUuid}/{relationName}/{toUuid}`


### Create a relation between two records

Example: link candidate `FROM_UUID` to vacancy `TO_UUID` using relation
`applied_for`.


```bash
curl -X POST "https://your-company.caraer.com/api/v2/records/relations/FROM_UUID/applied_for/TO_UUID?primary=true" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

What happens on create:

- Backend loads both records and the relation by `relationName`.
- If one of them does not exist, the request fails with a not-found error.
- The relation direction is resolved against relation schema (`from`/`to`) in
backend logic, so use the relation name exactly as defined.
- The relation is merged (created if missing, updated if already present).


### Delete a relation between two records

This removes the relation edge between those record UUIDs for that relation
name.


```bash
curl -X DELETE "https://your-company.caraer.com/api/v2/records/relations/FROM_UUID/applied_for/TO_UUID" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

What happens on delete:

- It checks relation delete access on both involved objects.
- It deletes matching relation edge(s) for that relation between the two UUIDs.


### How `primary` works for relations

`primary` is a query parameter on relation creation:

- `primary=false` (default): creates/updates the relation without primary
selection behavior.
- `primary=true`: marks this specific relation edge as the primary one.


Important primary behavior:

- The created relation gets `primary = true`.
- For the same relation type and same target record (`toUuid`), other incoming
relation edges are set to `primary = false`.
- Practical result: you can enforce a single "main" linked record for a target
record within that relation type.


Example:


```bash
curl -X POST "https://your-company.caraer.com/api/v2/records/relations/CANDIDATE_UUID/applied_for/VACANCY_UUID?primary=true" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

### `primary` vs `primary_object` (do not confuse these)

- Relation `primary` is metadata on a **relation edge** between two records.
- `primary_object` is metadata on the **record node itself** and represents that
record's main object context.


They solve different problems and are managed separately.

## Step 6: Handle record lifecycle safely

Delete modes:

- `archive` (default): soft delete sets the deletedAt property on the record
- `anonymize`: removes most data values from the record, preserving only its structure in the database (useful for keeping references to submissions or campaign aggregates while ensuring privacy)
- `delete`: Permanently removes the record from the database (hard delete). Once deleted, the record cannot be recovered.



```bash
curl -X DELETE "https://your-company.caraer.com/api/v2/records/RECORD_UUID?mode=archive" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

Restore:

> **Note:** The restore endpoint (`POST /api/v2/records/{uuid}/restore`) can only fully restore records that were *archived* (soft deleted).
While you can technically call restore for records deleted with `anonymize`, most data values will **not** be recovered because they were irreversibly anonymized. Only records deleted with the `archive` (soft delete) mode can be restored with their original data intact.



```bash
curl -X POST "https://your-company.caraer.com/api/v2/records/RECORD_UUID/restore" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

## Step 7: Morph records across objects

Morphing lets one record be visible in other object contexts without
duplicating data. See [Object morph explained](/blog/2026-06-07-object-morph-explained)
for primary object vs morph objects, `extendsTo`, and the
person → contact/candidate pattern.


```bash
curl -X POST "https://your-company.caraer.com/api/v2/records/RECORD_UUID/morph" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "objects": [
      { "name": "candidate" },
      { "name": "lead" }
    ]
  }'
```

## Common implementation notes

- `create`, `update`, and `createOrUpdate` all support `parse=true` query
parsing for returned values.
- `/search` can return either full records or previews depending on request
payload.


> **Important:** Build integrations around validated property names and object
names from your environment, not hardcoded assumptions from examples.


## Related reading

- [Object morph explained](/blog/2026-06-07-object-morph-explained)
- [Submitting forms and embedding forms on your website](/blog/2026-03-25-submit-forms-and-embed-on-your-website)
- [Caraer data model: Object, Property, Relation](/blog/2026-03-25-caraer-data-model-object-property-relation)
- [API introduction](/apis/get-started)