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
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 20 | Number of records per page. Max 100. |
cursor | string | — | Pagination cursor returned as nextCursor from a previous response. |
filter_field | string | — | Field name to filter on (simple single-field filter). |
filter_operator | string | — | Filter operator. See operators below. |
filter_value | string | — | Value to filter against. Use comma-separated values for in. |
filters | string | — | JSON-encoded FilterGroup for complex filters with AND/OR nesting. |
sort_by | string | created_at | Field name to sort by. |
sort_dir | asc | desc | desc | Sort direction. |
sort | string | — | JSON array of [{field, direction}] for multi-column sorting. |
fields | string | — | Comma-separated field names to return (projection). |
search | string | — | Full-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
| Operator | Description | Example value |
|---|---|---|
equals | Exact match | "Lead" |
not_equals | Not equal | "archived" |
contains | Substring match (case-insensitive) | "marc" |
starts_with | Starts with | "Al" |
ends_with | Ends with | "tin" |
ilike | Case-insensitive LIKE pattern | "%example%" |
greater_than | Greater than | 100 |
less_than | Less than | 50 |
greater_equal | Greater than or equal | 10 |
less_equal | Less than or equal | 99 |
between | Between two values | "10,100" |
in | Matches any value in list | "Lead,customer,prospect" |
not_in | Matches none in list | "archived,deleted" |
is_null | Field is null | — |
is_not_null | Field is not null | — |
is_true | Boolean is true | — |
is_false | Boolean is false | — |
is_empty | Empty string or null | — |
is_not_empty | Not empty | — |
date_equals | Exact date match | "2025-01-15" |
date_before | Before date | "2025-06-01" |
date_after | After date | "2025-01-01" |
date_between | Between two dates | "2025-01-01,2025-12-31" |
date_today | Today's date | — |
date_this_week | Current week | — |
date_this_month | Current 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:
ownerandcreated_byare 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_idchanges, 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}/searchAccept 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/createCreate 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/updateUpdate 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/upsertCreate 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.
idPropertymust 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
errorsarray — 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 }.