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_hereCreating an API Key
- Navigate to Organization Settings → Developers
- Click Create API Key
- Enter a name, select scopes, and optionally set an expiration date
- 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
| Code | Description |
|---|---|
| 200 | Success |
| 201 | Created |
| 202 | Accepted (async operation started) |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden (insufficient scope or permission) |
| 404 | Not Found |
| 409 | Conflict |
| 429 | Too Many Requests (rate limited) |
| 500 | Internal 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.
| Scope | Description |
|---|---|
objects:read | Read objects |
objects:write | Create, update, and delete objects |
records:read | Read records |
records:write | Create, update, and delete records |
attributes:read | Read attributes |
attributes:write | Create, update, and delete attributes |
activities:read | Read activities |
activities:write | Create, update, and delete activities |
reports:read | Read reports |
reports:run | Run reports |
workflows:read | Read workflows |
workflows:write | Update workflows |
workflows:run | Run workflows |
members:read | Read organization members |
files:write | Upload files |
webhooks:manage | Manage 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/objectsReturns 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/:objectIdReturns a single object with its attributes.
Create Object
POST /api/v1/objectsCreate a new custom object.
Request Body:
{
"pluralName": "Products",
"singularName": "Product",
"description": "Product catalog",
"slug": "products"
}Update Object
PATCH /api/v1/objects/:objectIdUpdate a custom object (standard objects cannot be modified).
Request Body:
{
"pluralName": "Updated Name",
"description": "Updated description",
"isActive": false
}Delete Object
DELETE /api/v1/objects/:objectIdDelete 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/attributesQuery Parameters:
includeInactive(optional): Set totrueto 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/:attributeIdCreate Attribute
POST /api/v1/objects/:objectId/attributesRequest Body:
{
"name": "Status",
"type": "select",
"isRequired": false,
"isUnique": false,
"config": {
"options": ["Active", "Inactive", "Pending"]
}
}Attribute Types:
| Type | Description | Config |
|---|---|---|
text | Plain text | - |
email | Email address | - |
number | Numeric value | - |
phone_number | Phone number | - |
checkbox | Boolean | - |
name | Name (first/last) | - |
address | Postal address | - |
currency | Monetary value | - |
date | Date value | - |
select | Dropdown selection | options (array of strings, required) |
list | Multi-value list | - |
reference | Link to another object | allowed_object_id (UUID, required) |
Update Attribute
PATCH /api/v1/objects/:objectId/attributes/:attributeIdOnly 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/:attributeIdDeleting 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/recordsQuery 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_atorupdated_at(defaultcreated_at)order(optional): Sort direction -ascordesc(defaultdesc)filter[slug][operator]=value(optional): Filter records by attribute values
Filter Operators:
| Operator | Description | Example |
|---|---|---|
equals | Exact match | ?filter[status][equals]=active |
notEquals | Not equal | ?filter[status][notEquals]=deleted |
contains | String contains | ?filter[email][contains]=@example.com |
notContains | String doesn't contain | ?filter[name][notContains]=test |
startsWith | String starts with | ?filter[name][startsWith]=John |
endsWith | String ends with | ?filter[email][endsWith]=.com |
greaterThan | Greater than | ?filter[age][greaterThan]=25 |
lessThan | Less than | ?filter[price][lessThan]=100 |
greaterThanOrEqual | Greater than or equal | ?filter[score][greaterThanOrEqual]=90 |
lessThanOrEqual | Less than or equal | ?filter[score][lessThanOrEqual]=50 |
isEmpty | Field is empty | ?filter[notes][isEmpty]=true |
isNotEmpty | Field is not empty | ?filter[email][isNotEmpty]=true |
isTrue | Boolean is true | ?filter[active][isTrue]=true |
isFalse | Boolean 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/searchFor 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/:recordIdCreate Record
POST /api/v1/objects/:objectId/recordsRequest Body:
{
"data": {
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com"
}
}Update Record
PATCH /api/v1/objects/:objectId/records/:recordIdUpdates are merged with existing data.
Request Body:
{
"data": {
"email": "newemail@example.com"
}
}Delete Record
DELETE /api/v1/objects/:objectId/records/:recordIdBulk Create/Upsert Records
POST /api/v1/objects/:objectId/records/bulkCreate 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 providedidis ignored.upsert- If a record has anid, 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/bulkDelete 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/activitiesQuery 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 IDobjectId(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/:activityIdCreate Activity
POST /api/v1/activitiesRequest 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/:activityIdAll fields are optional. Only provided fields are updated.
Request Body:
{
"title": "Updated Title",
"summary": "Updated notes"
}Delete Activity
DELETE /api/v1/activities/:activityIdMembers
Organization members with access to the CRM.
Required scope: members:read
List Members
GET /api/v1/membersResponse:
{
"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/reportsResponse:
{
"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/:reportIdRun Report
POST /api/v1/reports/:reportId/runExecutes 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/runsQuery Parameters:
limit(optional): Max 100, default 20offset(optional): Default 0
Get Report Run
GET /api/v1/reports/:reportId/runs/:runIdReturns 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/workflowsResponse:
{
"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/:workflowIdRun Workflow
POST /api/v1/workflows/:workflowId/runManually 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/runsQuery Parameters:
limit(optional): Max 100, default 20offset(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-urlReturns 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.pngWebhooks
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/webhooksReturns 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: 1704067200When 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()