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 (single)
{
  "success": true,
  "data": { }
}
Success (list)
{
  "success": true,
  "data": [ ],
  "meta": {
    "page": 1,
    "pageSize": 25,
    "total": 142,
    "totalPages": 6
  }
}
Error
{
  "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).