Developers
CRM REST API
Integrate with modules, records, files, and system metadata over a versioned JSON API. All responses use a consistent envelope so your clients can handle success and errors uniformly.
Base URL
https://crm.elabry.com/api/v1
Send requests with Content-Type: application/json unless uploading files.
Authentication
Include your API key in the X-Api-Key header on every call except the public health check.
Contact an administrator to obtain an API key.
Response format
Successful calls return success: true with a data payload. Lists include pagination in meta.
Quick start
Verify connectivity, then list modules with your API key.
1. Health check (no API key)
curl -s "https://crm.elabry.com/api/v1/system/ping"
2. Authenticated request
curl -s "https://crm.elabry.com/api/v1/modules" \
-H "X-Api-Key: YOUR_API_KEY"
{
"success": true,
"data": { }
}
{
"success": true,
"data": [ ],
"meta": {
"page": 1,
"pageSize": 25,
"total": 142,
"totalPages": 6
}
}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "One or more fields failed validation.",
"details": {
"fields": {
"email": ["Invalid email format."]
}
}
}
}
Shared models
Common response shapes referenced across endpoints.
ModuleDto
▼
{
"id": "00000000-0000-0000-0000-000000000001",
"slug": "customers",
"displayName": "Customer",
"pluralDisplayName": "Customers",
"icon": "user",
"isSystem": true,
"isActive": true,
"sortOrder": 1
}
FieldDto
▼
{
"id": "10000000-0000-0000-0000-000000000001",
"slug": "email",
"displayName": "Email",
"fieldType": "String",
"controlType": "TextBox",
"isRequired": true,
"isSystem": true,
"sortOrder": 2,
"controlOptions": { "placeholder": "name@example.com" }
}
RecordDto
▼
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"moduleSlug": "customers",
"createdAt": "2026-05-14T10:00:00Z",
"updatedAt": "2026-05-14T12:30:00Z",
"fields": {
"name": "Acme Corp",
"email": "contact@acme.com",
"status": "active",
"priority": 1,
"is_vip": true,
"due_date": "2026-06-01",
"attachment": {
"fileName": "contract.pdf",
"storageKey": "uploads/abc123.pdf",
"sizeBytes": 204800,
"contentType": "application/pdf"
}
}
}
FileValueModel
▼
{
"fileName": "document.pdf",
"storageKey": "uploads/abc123.pdf",
"sizeBytes": 204800,
"contentType": "application/pdf"
}
LayoutFieldItem
▼
{
"fieldDefinitionId": "10000000-0000-0000-0000-000000000001",
"sortOrder": 1,
"isVisible": true,
"groupName": "Overview",
"colSpan": 2,
"isSortable": true,
"columnWidthPct": 40
}
System
Health checks and server metadata.
GET
https://crm.elabry.com/api/v1/system/ping
No auth
200
▼
Health check. Does not require an API key.
Response (200)
Model: PingResponse
{
"success": true,
"data": {
"status": "ok",
"timestamp": "2026-05-14T12:00:00Z"
}
}
GET
https://crm.elabry.com/api/v1/system/info
200
▼
CRM version, module count, and record count.
Response (200)
Model: SystemInfo
{
"success": true,
"data": {
"version": "1.0.0.0",
"moduleCount": 3,
"recordCount": 128,
"serverTime": "2026-05-14T12:00:00Z"
}
}
Modules
Module definitions, fields, and layouts.
GET
https://crm.elabry.com/api/v1/modules
200
▼
List all modules.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
includeInactive |
boolean | No | When true, includes inactive modules. |
Response (200)
Model: ModuleDto[]
{
"success": true,
"data": [
{
"id": "00000000-0000-0000-0000-000000000001",
"slug": "customers",
"displayName": "Customer",
"pluralDisplayName": "Customers",
"icon": "user",
"isSystem": true,
"isActive": true,
"sortOrder": 1
}
]
}
GET
https://crm.elabry.com/api/v1/modules/{slug}
200
▼
Get a single module by slug.
Response (200)
Model: ModuleDto
{
"success": true,
"data": {
"id": "00000000-0000-0000-0000-000000000001",
"slug": "customers",
"displayName": "Customer",
"pluralDisplayName": "Customers",
"icon": "user",
"isSystem": true,
"isActive": true,
"sortOrder": 1
}
}
POST
https://crm.elabry.com/api/v1/modules
201
▼
Create a new custom module.
Returns 409 SLUG_CONFLICT if slug already exists.
Request (application/json)
Model: CreateModuleApiRequest
{
"displayName": "Invoice",
"pluralDisplayName": "Invoices",
"slug": "invoices",
"icon": "file"
}
Response (201)
Model: ModuleDto
PATCH
https://crm.elabry.com/api/v1/modules/{slug}
200
▼
Update module metadata.
Request (application/json)
Model: UpdateModuleApiRequest
{
"displayName": "Invoice",
"pluralDisplayName": "Invoices",
"icon": "file-text",
"isActive": true,
"sortOrder": 5
}
Response (200)
Model: ModuleDto
DELETE
https://crm.elabry.com/api/v1/modules/{slug}
204
▼
Delete a module (non-system only).
Returns 403 SYSTEM_MODULE for built-in modules.
Response (204)
Model: (empty body)
(HTTP 204 No Content)
GET
https://crm.elabry.com/api/v1/modules/{slug}/fields
200
▼
List field definitions for a module.
Response (200)
Model: FieldDto[]
POST
https://crm.elabry.com/api/v1/modules/{slug}/fields
201
▼
Add a field to a module.
fieldType: String | Number | Decimal | Boolean | Date | DateTime | Guid | File | Json | Reference. controlType: TextBox | TextArea | RichTextEditor | NumberInput | DatePicker | Upload | Dropdown | Checkbox | RadioGroup | SearchableDropdown.
Request (application/json)
Model: AddFieldApiRequest
{
"displayName": "Priority",
"slug": "priority",
"fieldType": "String",
"controlType": "Dropdown",
"isRequired": false,
"defaultValue": null,
"controlOptions": {
"sourceType": "static",
"staticOptions": [
{ "value": "low", "label": "Low" },
{ "value": "high", "label": "High" }
],
"allowEmpty": true,
"emptyLabel": "— Select —"
}
}
Response (201)
Model: FieldDto
PATCH
https://crm.elabry.com/api/v1/modules/{slug}/fields/{fieldSlug}
200
▼
Update a field definition.
System fields: control type cannot be changed. Returns 403 SYSTEM_FIELD when violated.
Request (application/json)
Model: UpdateFieldApiRequest
{
"displayName": "Priority Level",
"controlType": "Dropdown",
"isRequired": true,
"controlOptions": { "sourceType": "static", "staticOptions": [] }
}
Response (200)
Model: FieldDto
DELETE
https://crm.elabry.com/api/v1/modules/{slug}/fields/{fieldSlug}
204
▼
Delete a custom field.
Returns 403 SYSTEM_FIELD for system fields.
Response (204)
Model: (empty body)
(HTTP 204 No Content)
PUT
https://crm.elabry.com/api/v1/modules/{slug}/fields/order
200
▼
Reorder fields by slug list.
Request (application/json)
Model: string[]
["title", "status", "due_date", "description"]
Response (200)
Model: ReorderResult
{
"success": true,
"data": { "reordered": 4 }
}
GET
https://crm.elabry.com/api/v1/modules/{slug}/layouts/{type}
200
▼
Get list, detail, or form layout.
type must be list, detail, or form.
Response (200)
Model: LayoutFieldItem[]
PUT
https://crm.elabry.com/api/v1/modules/{slug}/layouts/{type}
200
▼
Replace a layout.
Request (application/json)
Model: LayoutFieldItem[]
[
{
"fieldDefinitionId": "10000000-0000-0000-0000-000000000001",
"sortOrder": 1,
"isVisible": true,
"groupName": "Overview",
"colSpan": 2,
"isSortable": true
}
]
Response (200)
Model: SaveLayoutResult
{
"success": true,
"data": { "saved": 1 }
}
Records
CRUD and bulk operations on module records.
GET
https://crm.elabry.com/api/v1/modules/{moduleSlug}/records
200
▼
List records (paginated, searchable).
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
page |
integer | No | Page number (default 1). |
pageSize |
integer | No | Items per page, 1–200 (default 25). |
sort |
string | No | Field slug to sort by. |
dir |
string | No | asc or desc (default asc). |
q |
string | No | Full-text search term. |
Response (200)
Model: RecordDto[] + meta
{
"success": true,
"data": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"moduleSlug": "tasks",
"createdAt": "2026-05-14T10:00:00Z",
"updatedAt": "2026-05-14T12:00:00Z",
"fields": {
"title": "Follow up with client",
"status": "open",
"priority": "high"
}
}
],
"meta": {
"page": 1,
"pageSize": 25,
"total": 42,
"totalPages": 2
}
}
GET
https://crm.elabry.com/api/v1/modules/{moduleSlug}/records/{id}
200
▼
Get a single record.
Response (200)
Model: RecordDto
POST
https://crm.elabry.com/api/v1/modules/{moduleSlug}/records
201
▼
Create a record.
Field keys are field slugs. Values are typed JSON (string, number, boolean, object for files). All required fields must be present.
Request (application/json)
Model: RecordFields (by slug)
{
"title": "New task",
"status": "open",
"priority": "medium",
"due_date": "2026-05-20T17:00:00Z",
"description": "Call the client back",
"is_active": true
}
Response (201)
Model: RecordDto
PATCH
https://crm.elabry.com/api/v1/modules/{moduleSlug}/records/{id}
200
▼
Partial update — only send fields to change.
Request (application/json)
Model: RecordFields (partial)
{
"status": "done"
}
Response (200)
Model: RecordDto
PUT
https://crm.elabry.com/api/v1/modules/{moduleSlug}/records/{id}
200
▼
Full replace — all fields required.
Request (application/json)
Model: RecordFields (full)
{
"title": "Updated task",
"status": "in_progress",
"priority": "high",
"due_date": null,
"description": ""
}
Response (200)
Model: RecordDto
DELETE
https://crm.elabry.com/api/v1/modules/{moduleSlug}/records/{id}
204
▼
Soft-delete a record.
Response (204)
Model: (empty body)
(HTTP 204 No Content)
POST
https://crm.elabry.com/api/v1/modules/{moduleSlug}/records/bulk
200
▼
Bulk create up to 500 records.
Per-record validation errors appear under errors["[index].fieldSlug"].
Request (application/json)
Model: RecordFields[]
[
{ "title": "Task A", "status": "open" },
{ "title": "Task B", "status": "open" }
]
Response (200)
Model: BulkCreateResult
{
"success": true,
"data": {
"created": 2,
"errors": {}
}
}
DELETE
https://crm.elabry.com/api/v1/modules/{moduleSlug}/records/bulk
200
▼
Bulk delete by record IDs.
Request (application/json)
Model: Guid[]
[
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"b2c3d4e5-f6a7-8901-bcde-f12345678901"
]
Response (200)
Model: BulkDeleteResult
{
"success": true,
"data": { "deleted": 2 }
}
Files
Upload, download, and delete file attachments.
POST
https://crm.elabry.com/api/v1/files/upload
200
▼
Upload a file attachment.
Use the returned storageKey in a record's file field. Max size and allowed extensions come from app settings.
Request (multipart/form-data)
Model: multipart: file
Form field: file (binary). No JSON body.
Response (200)
Model: FileValueModel
{
"success": true,
"data": {
"fileName": "document.pdf",
"storageKey": "uploads/abc123.pdf",
"sizeBytes": 204800,
"contentType": "application/pdf"
}
}
GET
https://crm.elabry.com/api/v1/files/download?key={storageKey}
200
▼
Download a file by storage key.
Not wrapped in the JSON envelope — returns raw file stream.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | Storage key from upload or record file field. |
Response (200)
Model: (binary stream)
Content-Type and Content-Disposition set; body is the file bytes.
DELETE
https://crm.elabry.com/api/v1/files?key={storageKey}
204
▼
Delete a file from storage.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | Storage key to delete. |
Response (204)
Model: (empty body)
(HTTP 204 No Content)
Outreach
Cold outreach email to prospects.
POST
https://crm.elabry.com/api/v1/outreach/send
200
▼
Send a one-off outreach email to a prospect using a configured email profile.
Requires at least one configured email profile in Admin → Settings → Email. Pass profileId from the profile list or edit page. Plain-text part is generated automatically.
Request (application/json)
Model: OutreachSendRequest
{
"profileId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"to": "prospect@company.com",
"subject": "Quick introduction",
"bodyHtml": "<p>Hi there,</p><p>I wanted to reach out about...</p>"
}
Response (200)
Model: OutreachSendResult
{
"success": true,
"data": {
"sent": true,
"profileId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"to": "prospect@company.com",
"subject": "Quick introduction"
}
}
Tracking
Public email open/click tracking for outreach-email records.
GET
https://crm.elabry.com/api/v1/track/open/{moduleSlug}/{id}
No auth
200
▼
Email open tracking pixel (1×1 transparent GIF). Updates the record matched by trackingid in the given module.
No API key. {moduleSlug} is the module slug (e.g. outreach-email). {id} is the trackingid field value, not the record GUID. Always returns the pixel (even if id is unknown).
Response (200)
Model: (binary GIF)
Returns a 1×1 transparent image/gif. Sets opened=true, increments opens, sets openedat on first hit, updates lastopenedat on every hit (when those fields exist).
GET
https://crm.elabry.com/api/v1/track/click/{moduleSlug}/{id}
No auth
302
▼
Link click tracking — logs click and redirects to the target URL.
No API key. {moduleSlug} is the module slug. {id} is the trackingid. Always redirects to url when valid; tracking is best-effort and skipped silently when the record is not found. Redirects to / when url is missing or invalid.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
url |
string | Yes | URL-encoded destination (http or https). Appended to clickedurls with timestamp. |
Response (302)
Model: (HTTP 302 redirect)
Redirects to the decoded url. Sets clicked=true, increments clicks, sets clickedat on first hit, appends { url, ts } to clickedurls (when those fields exist).