KasarKasar Docs
API Reference

Records

Create, read, update, and delete records on any CRM object. Supports filtering, sorting, pagination, projection, and full-text search.

The Records API lets you work with data on any object in your CRM — contacts, companies, opportunities, or any custom object you have defined. All endpoints use the object's API name (e.g., contacts, companies) as a path parameter.

List records

GET /api/v1/records/{object}

Returns a paginated list of records with optional filtering, sorting, and field projection.

Query parameters

ParameterTypeDefaultDescription
limitnumber20Number of records per page. Max 100.
cursorstringPagination cursor returned as nextCursor from a previous response.
filter_fieldstringField name to filter on (simple single-field filter).
filter_operatorstringFilter operator. See operators below.
filter_valuestringValue to filter against. Use comma-separated values for in.
filtersstringJSON-encoded FilterGroup for complex filters with AND/OR nesting.
sort_bystringcreated_atField name to sort by.
sort_dirasc | descdescSort direction.
sortstringJSON array of [{field, direction}] for multi-column sorting.
fieldsstringComma-separated field names to return (projection).
searchstringFull-text search query. Mutually exclusive with filters.

Simple filters (filter_field / filter_operator / filter_value) and complex filters (filters) are mutually exclusive. Use one or the other, not both.

Response

{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "full_name": "Alice Martin",
      "email": "alice@example.com",
      "contact_status": "customer",
      "created_at": "2025-01-15T10:30:00Z"
    }
  ],
  "total": 142,
  "nextCursor": "eyJpZCI6IjU1MGU4NDAw..."
}

When nextCursor is absent or null, you have reached the last page.

Examples

# Basic list with limit
curl -X GET "https://kasar.app/api/v1/records/contacts?limit=10" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

# Simple filter
curl -X GET "https://kasar.app/api/v1/records/contacts?filter_field=contact_status&filter_operator=equals&filter_value=Lead&limit=50" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

# Pagination with cursor
curl -X GET "https://kasar.app/api/v1/records/contacts?cursor=eyJpZCI6IjU1MGU4NDAw...&limit=20" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

# Field projection and sorting
curl -X GET "https://kasar.app/api/v1/records/contacts?fields=full_name,email,contact_status&sort_by=full_name&sort_dir=asc" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

# Full-text search
curl -X GET "https://kasar.app/api/v1/records/contacts?search=martin" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const baseUrl = "https://kasar.app/api/v1";
const headers = { Authorization: "Bearer YOUR_API_TOKEN" };

// Basic list with limit
const response = await fetch(`${baseUrl}/records/contacts?limit=10`, { headers });
const { data, total, nextCursor } = await response.json();

// Simple filter
const leads = await fetch(
  `${baseUrl}/records/contacts?filter_field=contact_status&filter_operator=equals&filter_value=Lead`,
  { headers }
).then((r) => r.json());

// Paginate through all results
let cursor: string | undefined;
const allRecords = [];

do {
  const url = new URL(`${baseUrl}/records/contacts`);
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);

  const page = await fetch(url.toString(), { headers }).then((r) => r.json());
  allRecords.push(...page.data);
  cursor = page.nextCursor;
} while (cursor);

Complex filters

For advanced filtering with AND/OR logic, pass a JSON-encoded FilterGroup as the filters query parameter.

FilterGroup structure:

type FilterGroup = {
  type: "AND" | "OR";
  conditions: Array<FilterCondition | FilterGroup>;
};

type FilterCondition = {
  field: string;
  operator: string;
  value: string | number | boolean | null;
};

Example — contacts named "marc" who are either leads or customers:

{
  "type": "AND",
  "conditions": [
    { "field": "full_name", "operator": "contains", "value": "marc" },
    {
      "type": "OR",
      "conditions": [
        { "field": "contact_status", "operator": "equals", "value": "Lead" },
        { "field": "contact_status", "operator": "equals", "value": "customer" }
      ]
    }
  ]
}
# URL-encode the JSON filter
curl -G "https://kasar.app/api/v1/records/contacts" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  --data-urlencode 'filters={"type":"AND","conditions":[{"field":"full_name","operator":"contains","value":"marc"},{"type":"OR","conditions":[{"field":"contact_status","operator":"equals","value":"Lead"},{"field":"contact_status","operator":"equals","value":"customer"}]}]}'
const filters = {
  type: "AND",
  conditions: [
    { field: "full_name", operator: "contains", value: "marc" },
    {
      type: "OR",
      conditions: [
        { field: "contact_status", operator: "equals", value: "Lead" },
        { field: "contact_status", operator: "equals", value: "customer" },
      ],
    },
  ],
};

const url = new URL(`${baseUrl}/records/contacts`);
url.searchParams.set("filters", JSON.stringify(filters));

const response = await fetch(url.toString(), { headers });

Filter operators

OperatorDescriptionExample value
equalsExact match"Lead"
not_equalsNot equal"archived"
containsSubstring match (case-insensitive)"marc"
starts_withStarts with"Al"
ends_withEnds with"tin"
ilikeCase-insensitive LIKE pattern"%example%"
greater_thanGreater than100
less_thanLess than50
greater_equalGreater than or equal10
less_equalLess than or equal99
betweenBetween two values"10,100"
inMatches any value in list"Lead,customer,prospect"
not_inMatches none in list"archived,deleted"
is_nullField is null
is_not_nullField is not null
is_trueBoolean is true
is_falseBoolean is false
is_emptyEmpty string or null
is_not_emptyNot empty
date_equalsExact date match"2025-01-15"
date_beforeBefore date"2025-06-01"
date_afterAfter date"2025-01-01"
date_betweenBetween two dates"2025-01-01,2025-12-31"
date_todayToday's date
date_this_weekCurrent week
date_this_monthCurrent month

Operators like is_null, is_true, date_today, date_this_week, and date_this_month do not require a value.


Get a record

GET /api/v1/records/{object}/{id}

Returns a single record by ID with enriched data, including:

  • Display values for foreign key relations (e.g., company name instead of just the ID)
  • Compound fields expanded (EMAILS, PHONES, ADDRESS)
  • Many-to-many relations included

Response

{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "full_name": "Alice Martin",
    "email": "alice@example.com",
    "contact_status": "customer",
    "company_id": "7a2b3c4d-e5f6-7890-abcd-ef1234567890",
    "company_id__display": "Acme Corp",
    "emails": [
      { "email": "alice@example.com", "label": "work", "is_primary": true },
      { "email": "alice.m@gmail.com", "label": "personal", "is_primary": false }
    ],
    "phones": [
      { "phone": "+33612345678", "label": "mobile", "is_primary": true }
    ],
    "tags": ["vip", "enterprise"],
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-03-20T14:15:00Z"
  }
}

Examples

curl -X GET "https://kasar.app/api/v1/records/contacts/550e8400-e29b-41d4-a716-446655440000" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
const recordId = "550e8400-e29b-41d4-a716-446655440000";

const response = await fetch(`${baseUrl}/records/contacts/${recordId}`, {
  headers,
});
const { data } = await response.json();

Create a record

POST /api/v1/records/{object}

Creates a new record on the specified object. The request body contains field values as a JSON object.

Automatic behavior

  • Pipeline defaults: If the object has a pipeline, the default pipeline and its first step are auto-assigned.
  • User fields: owner and created_by are set to the authenticated user.
  • Field defaults: Default values from field metadata are applied for any omitted fields.

Request body

{
  "first_name": "Alice",
  "last_name": "Martin",
  "email": "alice@example.com",
  "company_id": "7a2b3c4d-e5f6-7890-abcd-ef1234567890",
  "contact_status": "Lead"
}

Response

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "action": "created",
  "record": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "first_name": "Alice",
    "last_name": "Martin",
    "email": "alice@example.com",
    "company_id": "7a2b3c4d-e5f6-7890-abcd-ef1234567890",
    "contact_status": "Lead",
    "owner": "user-uuid",
    "created_by": "user-uuid",
    "pipeline_id": "default-pipeline-uuid",
    "pipeline_step_id": "first-step-uuid",
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T10:30:00Z"
  }
}

Error responses

Duplicate record (HTTP 409):

{
  "error": true,
  "code": "DUPLICATE_RECORD",
  "message": "A contact with this email already exists"
}

Validation error (HTTP 400):

{
  "error": true,
  "code": "VALIDATION_ERROR",
  "message": "Validation failed",
  "field_errors": [
    { "field": "email", "message": "Must be a valid email address" },
    { "field": "first_name", "message": "This field is required" }
  ]
}

Examples

curl -X POST "https://kasar.app/api/v1/records/contacts" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Alice",
    "last_name": "Martin",
    "email": "alice@example.com",
    "company_id": "7a2b3c4d-e5f6-7890-abcd-ef1234567890",
    "contact_status": "Lead"
  }'
const response = await fetch(`${baseUrl}/records/contacts`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    first_name: "Alice",
    last_name: "Martin",
    email: "alice@example.com",
    company_id: "7a2b3c4d-e5f6-7890-abcd-ef1234567890",
    contact_status: "Lead",
  }),
});

const result = await response.json();

if (!response.ok) {
  if (result.code === "DUPLICATE_RECORD") {
    console.error("Duplicate detected:", result.message);
  } else if (result.code === "VALIDATION_ERROR") {
    console.error("Validation errors:", result.field_errors);
  }
} else {
  console.log("Created record:", result.id);
}

Update a record

PUT /api/v1/records/{object}/{id}

Updates an existing record. Only include the fields you want to change — omitted fields are left unchanged.

Automatic behavior

  • Pipeline tracking: If the pipeline_step_id changes, an activity log entry is recorded.
  • Enriched response: The returned record includes display values for relations, just like the GET endpoint.

Request body

{
  "contact_status": "customer",
  "email": "alice.martin@newdomain.com"
}

Response

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "action": "updated",
  "record": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "first_name": "Alice",
    "last_name": "Martin",
    "email": "alice.martin@newdomain.com",
    "contact_status": "customer",
    "company_id": "7a2b3c4d-e5f6-7890-abcd-ef1234567890",
    "company_id__display": "Acme Corp",
    "updated_at": "2025-03-20T14:15:00Z"
  }
}

Examples

curl -X PUT "https://kasar.app/api/v1/records/contacts/550e8400-e29b-41d4-a716-446655440000" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_status": "customer",
    "email": "alice.martin@newdomain.com"
  }'
const recordId = "550e8400-e29b-41d4-a716-446655440000";

const response = await fetch(`${baseUrl}/records/contacts/${recordId}`, {
  method: "PUT",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    contact_status: "customer",
    email: "alice.martin@newdomain.com",
  }),
});

const { id, action, record } = await response.json();

Delete records

DELETE /api/v1/records/{object}

Deletes one or more records in a single request. Deletion is subject to per-record RBAC checks — if the authenticated user does not have permission to delete a specific record, that record is skipped and reported in the errors array.

For contacts with synced email accounts, the sync state is cleaned up automatically.

Request body

{
  "record_ids": [
    "550e8400-e29b-41d4-a716-446655440000",
    "661f9511-f30c-52e5-b827-557766551111"
  ]
}

Response

{
  "deleted": 2,
  "failed": 0
}

Partial failure:

{
  "deleted": 1,
  "failed": 1,
  "errors": [
    {
      "id": "661f9511-f30c-52e5-b827-557766551111",
      "reason": "Permission denied"
    }
  ]
}

Examples

curl -X DELETE "https://kasar.app/api/v1/records/contacts" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "record_ids": [
      "550e8400-e29b-41d4-a716-446655440000",
      "661f9511-f30c-52e5-b827-557766551111"
    ]
  }'
const response = await fetch(`${baseUrl}/records/contacts`, {
  method: "DELETE",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    record_ids: [
      "550e8400-e29b-41d4-a716-446655440000",
      "661f9511-f30c-52e5-b827-557766551111",
    ],
  }),
});

const { deleted, failed, errors } = await response.json();

if (failed > 0) {
  console.warn(`${failed} records could not be deleted:`, errors);
}

Deletion is permanent. There is no soft-delete or trash. Make sure to confirm the operation before calling this endpoint in user-facing applications.


Delete a single record

DELETE /api/v1/records/{object}/{id}

Deletes a single record by ID. This is a convenience alternative to the batch delete endpoint above.

Response

{
  "deleted": 1,
  "failed": 0
}

Update a record (PATCH)

PATCH /api/v1/records/{object}/{id}

PATCH is an alias of PUT — both perform partial updates. Only include the fields you want to change; omitted fields are left unchanged.


Search records

POST /api/v1/records/{object}/search

Accept a FilterGroup and sorting in the request body instead of query parameters. This is cleaner for complex filters that would be awkward to URL-encode.

Request body

{
  "filters": {
    "type": "AND",
    "conditions": [
      { "field": "contact_status", "operator": "equals", "value": "Lead" },
      { "field": "full_name", "operator": "contains", "value": "marc" }
    ]
  },
  "sort": [{ "field": "created_at", "direction": "desc" }],
  "fields": ["id", "full_name", "email"],
  "limit": 20,
  "after": "eyJ..."
}

The response format is the same as the List records endpoint: { data, total, nextCursor }.


Associations

PUT /api/v1/records/{object}/{id}/associations/{toObject}/{toId}
DELETE /api/v1/records/{object}/{id}/associations/{toObject}/{toId}

Explicitly manage many-to-many relationships between records.

Example

Link a contact to a company:

curl -X PUT "https://kasar.app/api/v1/records/contacts/UUID/associations/companies/UUID2" \
  -H "Authorization: Bearer ksr_..."

Request body (optional)

Pass junction field data in the request body:

{
  "position": "CEO & Chairman"
}

The available junction fields depend on the M2M relation configuration. The response includes a junctionFields array listing available fields:

{
  "from": "UUID",
  "to": "UUID2",
  "type": "contacts_to_companies",
  "action": "created",
  "junctionFields": [
    { "name": "position", "type": "TEXT", "label": "Poste" }
  ]
}

Response

{
  "from": "UUID",
  "to": "UUID2",
  "type": "contacts -> companies",
  "action": "created"
}

To remove the association, use the same path with DELETE.


Batch create

POST /api/v1/records/{object}/batch/create

Create multiple records in a single request. Maximum 100 records per call.

Request body

{
  "inputs": [
    { "data": { "first_name": "Alice", "last_name": "Martin", "email": "alice@example.com" } },
    { "data": { "first_name": "Bob", "last_name": "Dupont", "email": "bob@example.com" } }
  ]
}

Response

{
  "results": [ { "id": "...", "action": "created", "record": { ... } } ],
  "errors": [],
  "total": 2,
  "created": 2,
  "failed": 0
}

Batch update

POST /api/v1/records/{object}/batch/update

Update multiple records in a single request. Maximum 100 records per call.

Request body

{
  "inputs": [
    { "id": "550e8400-e29b-41d4-a716-446655440000", "data": { "contact_status": "customer" } },
    { "id": "661f9511-f30c-52e5-b827-557766551111", "data": { "contact_status": "Lead" } }
  ]
}

Response

Same structure as batch create: { results, errors, total, created, failed }.


Batch upsert

POST /api/v1/records/{object}/batch/upsert

Create or update records in a single request. Maximum 100 records per call. Each record is looked up by idProperty — if found, it is updated; if not, it is created.

  • idProperty must be a field that uniquely identifies records (e.g. email, linkedin_url, name).
  • If the lookup finds multiple matches, the first matching record is updated.
  • Partial failures are reported per-record in the errors array — successful records in the same batch are still committed.

Request body

{
  "inputs": [
    { "data": { "email": "alice@example.com", "first_name": "Alice", "contact_status": "customer" } },
    { "data": { "email": "new@example.com", "first_name": "New Contact", "contact_status": "Lead" } }
  ],
  "idProperty": "email"
}

Response

Same structure as batch create: { results, errors, total, created, failed }.

On this page