API Reference

REST API documentation for programmatic access to CalyxCRM

Overview

CalyxCRM provides a REST API for programmatic access to your organization's data. All API endpoints require authentication using an API key.

Authentication

All API requests must include an API key in the Authorization header:

Authorization: Bearer caly_your_api_key_here

Creating an API Key

  1. Navigate to Organization Settings → Developers
  2. Click Create API Key
  3. Enter a name, select scopes, and optionally set an expiration date
  4. Copy the key immediately - it won't be shown again

See API Keys for detailed instructions and security best practices.

Base URL

All API endpoints are prefixed with /api/v1/:

https://your-domain.com/api/v1/

Response Format

All responses are JSON. Successful responses include the requested data:

{
  "objects": [...],
  "pagination": { "total": 100, "limit": 50, "offset": 0 }
}

Error responses include an error code and message:

{
  "error": "not_found",
  "message": "Record not found"
}

HTTP Status Codes

CodeDescription
200Success
201Created
202Accepted (async operation started)
400Bad Request
401Unauthorized
403Forbidden (insufficient scope or permission)
404Not Found
409Conflict
429Too Many Requests (rate limited)
500Internal Server Error

Scopes

API keys use scopes to control access to specific resources. When creating an API key, you can choose Full access (all scopes) or select individual scopes.

ScopeDescription
objects:readRead objects
objects:writeCreate, update, and delete objects
records:readRead records
records:writeCreate, update, and delete records
attributes:readRead attributes
attributes:writeCreate, update, and delete attributes
activities:readRead activities
activities:writeCreate, update, and delete activities
reports:readRead reports
reports:runRun reports
workflows:readRead workflows
workflows:writeUpdate workflows
workflows:runRun workflows
members:readRead organization members
files:writeUpload files
webhooks:manageManage webhooks

If a request requires a scope the API key doesn't have, the API returns a 403 error:

{
  "error": "insufficient_scope",
  "message": "This API key does not have the required scope: records:read",
  "details": { "requiredScope": "records:read" }
}

Objects

Objects define the data structures in your CRM (e.g., People, Deals, custom objects).

Required scope: objects:read for read operations, objects:write for mutations.

List Objects

GET /api/v1/objects

Returns all active objects in your organization.

Response:

{
  "objects": [
    {
      "id": "uuid",
      "type": "standard",
      "pluralName": "People",
      "singularName": "Person",
      "description": "Contact records",
      "slug": "people",
      "isActive": true,
      "createdAt": "2024-01-01T00:00:00Z",
      "updatedAt": "2024-01-01T00:00:00Z"
    }
  ]
}

Get Object

GET /api/v1/objects/:objectId

Returns a single object with its attributes.

Create Object

POST /api/v1/objects

Create a new custom object.

Request Body:

{
  "pluralName": "Products",
  "singularName": "Product",
  "description": "Product catalog",
  "slug": "products"
}

Update Object

PATCH /api/v1/objects/:objectId

Update a custom object (standard objects cannot be modified).

Request Body:

{
  "pluralName": "Updated Name",
  "description": "Updated description",
  "isActive": false
}

Delete Object

DELETE /api/v1/objects/:objectId

Delete a custom object (standard objects cannot be deleted).


Attributes

Attributes define the fields on an object (e.g., "Email", "Phone Number", "Status").

Required scope: attributes:read for read operations, attributes:write for mutations.

List Attributes

GET /api/v1/objects/:objectId/attributes

Query Parameters:

  • includeInactive (optional): Set to true to include inactive attributes

Response:

{
  "attributes": [
    {
      "id": "uuid",
      "objectId": "uuid",
      "name": "Email",
      "slug": "email",
      "type": "email",
      "isRequired": true,
      "isUnique": true,
      "isList": false,
      "isActive": true,
      "displayOrder": 0,
      "config": null,
      "createdAt": "2024-01-01T00:00:00Z",
      "updatedAt": "2024-01-01T00:00:00Z"
    }
  ]
}

Get Attribute

GET /api/v1/objects/:objectId/attributes/:attributeId

Create Attribute

POST /api/v1/objects/:objectId/attributes

Request Body:

{
  "name": "Status",
  "type": "select",
  "isRequired": false,
  "isUnique": false,
  "config": {
    "options": ["Active", "Inactive", "Pending"]
  }
}

Attribute Types:

TypeDescriptionConfig
textPlain text-
emailEmail address-
numberNumeric value-
phone_numberPhone number-
checkboxBoolean-
nameName (first/last)-
addressPostal address-
currencyMonetary value-
dateDate value-
selectDropdown selectionoptions (array of strings, required)
listMulti-value list-
referenceLink to another objectallowed_object_id (UUID, required)

Update Attribute

PATCH /api/v1/objects/:objectId/attributes/:attributeId

Only the name can be updated. The slug and type are immutable after creation.

Request Body:

{
  "name": "Updated Name"
}

Delete Attribute

DELETE /api/v1/objects/:objectId/attributes/:attributeId

Deleting a reference attribute will clean up associated data in records.


Records

Records are the data entries within objects.

Required scope: records:read for read operations, records:write for mutations.

List Records

GET /api/v1/objects/:objectId/records

Query Parameters:

  • limit (optional): Number of records to return (max 100, default 50)
  • offset (optional): Number of records to skip (default 0)
  • sort (optional): Sort field - created_at or updated_at (default created_at)
  • order (optional): Sort direction - asc or desc (default desc)
  • filter[slug][operator]=value (optional): Filter records by attribute values

Filter Operators:

OperatorDescriptionExample
equalsExact match?filter[status][equals]=active
notEqualsNot equal?filter[status][notEquals]=deleted
containsString contains?filter[email][contains]=@example.com
notContainsString doesn't contain?filter[name][notContains]=test
startsWithString starts with?filter[name][startsWith]=John
endsWithString ends with?filter[email][endsWith]=.com
greaterThanGreater than?filter[age][greaterThan]=25
lessThanLess than?filter[price][lessThan]=100
greaterThanOrEqualGreater than or equal?filter[score][greaterThanOrEqual]=90
lessThanOrEqualLess than or equal?filter[score][lessThanOrEqual]=50
isEmptyField is empty?filter[notes][isEmpty]=true
isNotEmptyField is not empty?filter[email][isNotEmpty]=true
isTrueBoolean is true?filter[active][isTrue]=true
isFalseBoolean is false?filter[active][isFalse]=true

Response:

{
  "records": [
    {
      "id": "uuid",
      "objectId": "uuid",
      "data": {
        "first_name": "John",
        "last_name": "Doe",
        "email": "john@example.com"
      },
      "createdAt": "2024-01-01T00:00:00Z",
      "updatedAt": "2024-01-01T00:00:00Z"
    }
  ],
  "pagination": {
    "total": 150,
    "limit": 50,
    "offset": 0
  }
}

Search Records

POST /api/v1/objects/:objectId/records/search

For complex filter combinations, use the search endpoint with a JSON body instead of query parameters.

Request Body:

{
  "filters": [
    { "attributeSlug": "email", "operator": "contains", "value": "@example.com" },
    { "attributeSlug": "status", "operator": "equals", "value": "active" }
  ],
  "sort": { "field": "created_at", "order": "desc" },
  "limit": 50,
  "offset": 0
}

Response: Same format as List Records.

Get Record

GET /api/v1/objects/:objectId/records/:recordId

Create Record

POST /api/v1/objects/:objectId/records

Request Body:

{
  "data": {
    "first_name": "Jane",
    "last_name": "Smith",
    "email": "jane@example.com"
  }
}

Update Record

PATCH /api/v1/objects/:objectId/records/:recordId

Updates are merged with existing data.

Request Body:

{
  "data": {
    "email": "newemail@example.com"
  }
}

Delete Record

DELETE /api/v1/objects/:objectId/records/:recordId

Bulk Create/Upsert Records

POST /api/v1/objects/:objectId/records/bulk

Create or upsert up to 100 records in a single request.

Request Body:

{
  "mode": "create",
  "records": [
    { "data": { "first_name": "Alice", "email": "alice@example.com" } },
    { "data": { "first_name": "Bob", "email": "bob@example.com" } }
  ]
}

Modes:

  • create (default) - Always create new records. Any provided id is ignored.
  • upsert - If a record has an id, update it. Otherwise create a new record.

Response:

{
  "created": 2,
  "updated": 0,
  "failed": []
}

If some records fail, the failed array contains the index and error message:

{
  "created": 1,
  "updated": 0,
  "failed": [{ "index": 1, "error": "Record not found in your organization" }]
}

Bulk Delete Records

DELETE /api/v1/objects/:objectId/records/bulk

Delete up to 100 records in a single request.

Request Body:

{
  "ids": ["uuid1", "uuid2", "uuid3"]
}

Response:

{
  "deleted": 3
}

Activities

Activities are notes, meetings, calls, and other interactions linked to records.

Required scope: activities:read for read operations, activities:write for mutations.

List Activities

GET /api/v1/activities

Query Parameters:

  • limit (optional): Number of activities to return (max 100, default 50)
  • offset (optional): Number of activities to skip (default 0)
  • recordId (optional): Filter by linked record ID
  • objectId (optional): Filter by linked object ID

Response:

{
  "activities": [
    {
      "id": "uuid",
      "title": "Initial Call",
      "summary": "Discussed product requirements",
      "activityDate": "2024-01-15T10:00:00Z",
      "linkedRecords": [
        { "objectId": "uuid", "recordId": "uuid" }
      ],
      "participants": [
        { "type": "user", "id": "uuid" }
      ],
      "createdBy": {
        "id": "uuid",
        "name": "John Doe",
        "email": "john@example.com"
      },
      "createdAt": "2024-01-15T10:00:00Z",
      "updatedAt": "2024-01-15T10:00:00Z"
    }
  ]
}

Get Activity

GET /api/v1/activities/:activityId

Create Activity

POST /api/v1/activities

Request Body:

{
  "title": "Follow-up Call",
  "summary": "Discussed pricing options",
  "activityDate": "2024-01-16T14:00:00Z",
  "linkedRecords": [
    { "objectId": "uuid", "recordId": "uuid" }
  ],
  "participants": [
    { "type": "user", "id": "uuid" },
    { "type": "person", "id": "uuid" }
  ]
}

Participant Types:

  • user - An organization member (referenced by user ID)
  • person - A record in the CRM (referenced by record ID)

Update Activity

PATCH /api/v1/activities/:activityId

All fields are optional. Only provided fields are updated.

Request Body:

{
  "title": "Updated Title",
  "summary": "Updated notes"
}

Delete Activity

DELETE /api/v1/activities/:activityId

Members

Organization members with access to the CRM.

Required scope: members:read

List Members

GET /api/v1/members

Response:

{
  "members": [
    {
      "id": "uuid",
      "role": "owner",
      "createdAt": "2024-01-01T00:00:00Z",
      "user": {
        "id": "uuid",
        "name": "John Doe",
        "email": "john@example.com",
        "image": "https://..."
      }
    }
  ]
}

Reports

Custom reports with multi-object queries.

Required scope: reports:read for read operations, reports:run for executing reports.

List Reports

GET /api/v1/reports

Response:

{
  "reports": [
    {
      "id": "uuid",
      "name": "Monthly Sales",
      "description": "Sales by region",
      "lastRunAt": "2024-01-15T10:00:00Z",
      "createdAt": "2024-01-01T00:00:00Z",
      "creator": {
        "id": "uuid",
        "name": "John Doe",
        "email": "john@example.com"
      }
    }
  ]
}

Get Report

GET /api/v1/reports/:reportId

Run Report

POST /api/v1/reports/:reportId/run

Executes the report and stores the results. Returns immediately with run metadata.

Response (201):

{
  "run": {
    "id": "uuid",
    "status": "completed",
    "resultCount": 42,
    "startedAt": "2024-01-15T10:00:00Z",
    "completedAt": "2024-01-15T10:00:05Z"
  }
}

List Report Runs

GET /api/v1/reports/:reportId/runs

Query Parameters:

  • limit (optional): Max 100, default 20
  • offset (optional): Default 0

Get Report Run

GET /api/v1/reports/:reportId/runs/:runId

Returns the run with full results data.


Workflows

Automation workflows triggered by events or schedules.

Required scope: workflows:read for read operations, workflows:write for updates, workflows:run for execution.

List Workflows

GET /api/v1/workflows

Response:

{
  "workflows": [
    {
      "id": "uuid",
      "name": "Welcome Email",
      "description": "Send welcome email to new contacts",
      "trigger": "record/created",
      "enabled": true,
      "createdAt": "2024-01-01T00:00:00Z",
      "object": {
        "id": "uuid",
        "pluralName": "People",
        "slug": "people"
      },
      "createdBy": {
        "id": "uuid",
        "name": "John Doe",
        "email": "john@example.com"
      }
    }
  ]
}

Get Workflow

GET /api/v1/workflows/:workflowId

Run Workflow

POST /api/v1/workflows/:workflowId/run

Manually trigger a workflow execution. The workflow must be enabled.

Request Body (optional):

{
  "data": {
    "customParam": "value"
  }
}

Response (202):

{
  "message": "Workflow execution triggered",
  "workflowId": "uuid",
  "workflowName": "Welcome Email"
}

List Workflow Runs

GET /api/v1/workflows/:workflowId/runs

Query Parameters:

  • limit (optional): Max 100, default 20
  • offset (optional): Default 0

Response:

{
  "runs": [
    {
      "id": "uuid",
      "status": "completed",
      "title": "API: Welcome Email",
      "triggerEvent": "api",
      "error": null,
      "startedAt": "2024-01-15T10:00:00Z",
      "completedAt": "2024-01-15T10:00:02Z",
      "createdAt": "2024-01-15T10:00:00Z"
    }
  ]
}

Files

Upload files to storage using signed URLs.

Required scope: files:write

Get Upload URL

POST /api/v1/files/upload-url

Returns a pre-signed URL for uploading a file directly to storage.

Request Body:

{
  "path": "uploads/image.png",
  "bucket": "bucket-name"
}

Response:

{
  "signedUrl": "https://storage.example.com/...?signature=...",
  "publicUrl": "https://storage.example.com/bucket-name/uploads/image.png"
}

Upload the file by making a PUT request to the signedUrl:

curl -X PUT "SIGNED_URL" \
  -H "Content-Type: image/png" \
  --data-binary @image.png

Webhooks

Manage webhook endpoints and subscribe to CRM events. Webhooks are delivered via Svix with automatic retries, signing, and delivery tracking.

Required scope: webhooks:manage

Get Webhook Portal

GET /api/v1/webhooks

Returns the webhook management portal URL and available event types.

Response:

{
  "portalUrl": "https://app.svix.com/...",
  "eventTypes": [
    { "name": "record.created", "description": "Fired when a record is created" },
    { "name": "record.updated", "description": "Fired when a record is updated" },
    { "name": "record.deleted", "description": "Fired when a record is deleted" },
    { "name": "activity.created", "description": "Fired when an activity is created" },
    { "name": "activity.updated", "description": "Fired when an activity is updated" },
    { "name": "activity.deleted", "description": "Fired when an activity is deleted" },
    { "name": "workflow.completed", "description": "Fired when a workflow completes" },
    { "name": "workflow.failed", "description": "Fired when a workflow fails" }
  ]
}

Managing Webhooks

Use the webhook management portal (accessible from Organization Settings → Developers → Webhooks tab) to:

  • Create and configure webhook endpoints
  • Subscribe to specific event types
  • View delivery history with request/response details
  • Replay failed deliveries
  • Test endpoints

Webhook Event Payload

All webhook events include the following structure:

{
  "organizationId": "uuid",
  "recordId": "uuid",
  "objectId": "uuid",
  "data": { ... }
}

The data field contains the full record or activity data at the time of the event.


Rate Limiting

API requests are rate limited to 100 requests per minute per API key.

Rate limit headers are included in every response:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1704067200

When rate limited, the API returns a 429 Too Many Requests response with a Retry-After header indicating how many seconds to wait.


Examples

cURL

# List all objects
curl -X GET "https://your-domain.com/api/v1/objects" \
  -H "Authorization: Bearer caly_your_api_key"

# Create a record
curl -X POST "https://your-domain.com/api/v1/objects/{objectId}/records" \
  -H "Authorization: Bearer caly_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"data": {"first_name": "John", "last_name": "Doe"}}'

# Filter records
curl -X GET "https://your-domain.com/api/v1/objects/{objectId}/records?filter[status][equals]=active&filter[email][contains]=@example.com&sort=created_at&order=desc" \
  -H "Authorization: Bearer caly_your_api_key"

# Search records with complex filters
curl -X POST "https://your-domain.com/api/v1/objects/{objectId}/records/search" \
  -H "Authorization: Bearer caly_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": [
      {"attributeSlug": "status", "operator": "equals", "value": "active"},
      {"attributeSlug": "score", "operator": "greaterThan", "value": "80"}
    ],
    "sort": {"field": "updated_at", "order": "desc"},
    "limit": 25
  }'

# Bulk create records
curl -X POST "https://your-domain.com/api/v1/objects/{objectId}/records/bulk" \
  -H "Authorization: Bearer caly_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "create",
    "records": [
      {"data": {"name": "Alice"}},
      {"data": {"name": "Bob"}}
    ]
  }'

JavaScript/TypeScript

const API_KEY = "caly_your_api_key";
const BASE_URL = "https://your-domain.com/api/v1";

async function listRecords(objectId: string) {
  const response = await fetch(`${BASE_URL}/objects/${objectId}/records`, {
    headers: {
      Authorization: `Bearer ${API_KEY}`,
    },
  });
  return response.json();
}

async function searchRecords(objectId: string, filters: object[]) {
  const response = await fetch(
    `${BASE_URL}/objects/${objectId}/records/search`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ filters }),
    },
  );
  return response.json();
}

async function createRecord(
  objectId: string,
  data: Record<string, unknown>,
) {
  const response = await fetch(`${BASE_URL}/objects/${objectId}/records`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ data }),
  });
  return response.json();
}

Python

import requests

API_KEY = "caly_your_api_key"
BASE_URL = "https://your-domain.com/api/v1"

headers = {"Authorization": f"Bearer {API_KEY}"}

# List objects
response = requests.get(f"{BASE_URL}/objects", headers=headers)
objects = response.json()

# Filter records
params = {
    "filter[status][equals]": "active",
    "filter[email][contains]": "@example.com",
    "sort": "created_at",
    "order": "desc",
}
response = requests.get(
    f"{BASE_URL}/objects/{object_id}/records",
    headers=headers,
    params=params,
)
records = response.json()

# Create a record
data = {"data": {"first_name": "John", "last_name": "Doe"}}
response = requests.post(
    f"{BASE_URL}/objects/{object_id}/records",
    headers=headers,
    json=data,
)
record = response.json()

On this page