API Reference

SurelyCrm exposes a set of JSON endpoints for programmatic integration. Use these APIs to create customers, search records, manage workflows, and interact with the telephony system from your own applications.

Authentication Required: Most endpoints require an active session cookie (standard browser login). Endpoints under /Api/ use API key authentication. Administrative endpoints require the Administrator role.

Command Line Tool: You can also interact with these endpoints using the SurelyCrm CLI. Install it with npm install -g @surelycrm/cli.

Authentication

API Key Authentication

Endpoints under /Api/ accept a GUID API key passed in the request header:

apiKey: {your-guid-api-key}

Administrators can view or regenerate the API key in Settings > Application Settings. Keep this key secure — treat it like a password. If compromised, regenerate it immediately.

Session Authentication

All other endpoints use standard ASP.NET Core cookie authentication. Log in via the web interface and use the resulting session cookie for subsequent API requests.

Customers API

Create Customer

Creates a new customer record in the system.

PropertyValue
EndpointPOST /Api/Create
AuthenticationAPI Key (apiKey header)
Content-Typeapplication/json

Required fields: Only title, firstname, surname and referenceNumber are required. All other fields are optional.

Request Body

{
  "title": "Mr",
  "firstname": "John",
  "surname": "Davies",
  "referenceNumber": "optional-override",
  "address1": "123 High Street",
  "address2": "",
  "address3": "",
  "address4": "",
  "town": "Manchester",
  "city": "Greater Manchester",
  "postcode": "M1 1AA",
  "homePhone": "01611234567",
  "mobilePhone": "07700900123",
  "emailAddress": "john.davies@example.com",
  "notificationsEnabled": true,
  "importantDate": "2025-06-15T00:00:00",
  "dateOfBirth": "1985-03-20T00:00:00",
  "website": "https://example.com",
  "statusId": "00000000-0000-0000-0000-000000000000",
  "ownerId": "00000000-0000-0000-0000-000000000000",
  "referredBy": "Google Ads",
  "customFields": {
    "ContractType": "Premium",
    "Priority": "High"
  }
}

Field Reference

FieldTypeRequiredDescription
titlestringYesMr, Mrs, Miss, or Ms
firstnamestringYesFirst name
surnamestringYesSurname
referenceNumberstringYesCustomer reference number
address1stringNoFirst line of address
address2stringNoSecond line of address
address3stringNoThird line of address
address4stringNoFourth line of address
townstringNoTown
citystringNoCity
postcodestringNoPostcode
homePhonestringNoHome telephone number
mobilePhonestringNoMobile telephone number
emailAddressstringNoEmail address
notificationsEnabledbooleanNoConsent to marketing communications
importantDatedatetimeNoKey date for the customer
dateOfBirthdatetimeNoDate of birth
websitestringNoWebsite URL
statusIdguidNoStatus GUID. Omit for default.
ownerIdguidNoAssigned user GUID. Omit for unassigned.
referredBystringNoReferral source
customFieldsobjectNoKey-value pairs of custom field data

Response

200 OK — Customer created successfully. Returns the new customer's GUID:

"550e8400-e29b-41d4-a716-446655440000"

400 Bad Request — Validation failed or customer could not be created. Returns error details:

{
  "Title": ["The Title field is required."],
  "Firstname": ["The Firstname field is required."],
  "Surname": ["The Surname field is required."],
  "ReferenceNumber": ["The ReferenceNumber field is required."]
}

401 Unauthorized — Missing or invalid API key.

Get Customer by GUID

Retrieves a single customer record by its unique identifier.

PropertyValue
EndpointGET /Api/customer/{id}
AuthenticationAPI Key (apiKey header)

Response

200 OK — Returns the customer record:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "referenceNumber": "1234567890",
  "title": "Mr",
  "firstname": "John",
  "surname": "Davies",
  "address1": "123 High Street",
  "address2": "",
  "address3": "",
  "address4": "",
  "town": "Manchester",
  "city": "Greater Manchester",
  "postcode": "M1 1AA",
  "homePhone": "01611234567",
  "mobilePhone": "07700900123",
  "emailAddress": "john.davies@example.com",
  "notificationsEnabled": true,
  "importantDate": "2025-06-15T00:00:00",
  "dateOfBirth": "1985-03-20T00:00:00",
  "website": "https://example.com",
  "age": 40,
  "statusId": "00000000-0000-0000-0000-000000000000",
  "ownerId": "00000000-0000-0000-0000-000000000000",
  "referredBy": "Google Ads",
  "customFields": {
    "ContractType": "Premium",
    "Priority": "High"
  }
}

401 Unauthorized — Missing or invalid API key.

404 Not Found — Customer with the specified GUID does not exist.

Get Customer by Reference Number

Retrieves a single customer record by its reference number.

PropertyValue
EndpointGET /Api/customer/reference/{referenceNumber}
AuthenticationAPI Key (apiKey header)

Response

200 OK — Returns the customer record (same shape as Get Customer by GUID).

400 Bad Request — Reference number is missing.

401 Unauthorized — Missing or invalid API key.

404 Not Found — Customer with the specified reference number does not exist.

Get Customer by Email Address

Retrieves a single customer record by email address.

PropertyValue
EndpointGET /Api/customer/byEmail/{emailAddress}
AuthenticationAPI Key (apiKey header)

Response

200 OK — Returns the customer record (same shape as Get Customer by GUID).

400 Bad Request — Email address is missing.

401 Unauthorized — Missing or invalid API key.

404 Not Found — Customer with the specified email address does not exist.

Update Customer

Updates an existing customer record. The customer ID in the request body must match the ID in the route.

PropertyValue
EndpointPUT /Api/customer/{id}
AuthenticationAPI Key (apiKey header)
Content-Typeapplication/json

Required fields: Only title, firstname, surname and referenceNumber are required. All other fields are optional.

Request Body

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "title": "Mr",
  "firstname": "John",
  "surname": "Davies",
  "referenceNumber": "1234567890",
  "address1": "123 High Street",
  "address2": "",
  "address3": "",
  "address4": "",
  "town": "Manchester",
  "city": "Greater Manchester",
  "postcode": "M1 1AA",
  "homePhone": "01611234567",
  "mobilePhone": "07700900123",
  "emailAddress": "john.davies@example.com",
  "notificationsEnabled": true,
  "importantDate": "2025-06-15T00:00:00",
  "dateOfBirth": "1985-03-20T00:00:00",
  "website": "https://example.com",
  "statusId": "00000000-0000-0000-0000-000000000000",
  "ownerId": "00000000-0000-0000-0000-000000000000",
  "referredBy": "Google Ads",
  "customFields": {
    "ContractType": "Premium",
    "Priority": "High"
  }
}

Response

200 OK — Customer updated successfully.

400 Bad Request — Validation failed, IDs do not match, or update could not be completed.

401 Unauthorized — Missing or invalid API key.

404 Not Found — Customer with the specified GUID does not exist.

Change Account Status

Changes the status of a customer account.

PropertyValue
EndpointPOST /Api/customer/{customerId}/status
AuthenticationAPI Key (apiKey header)
Content-Typeapplication/json

Request Body

{
  "statusId": "550e8400-e29b-41d4-a716-446655440000"
}

Response

200 OK — Returns true when the status was changed successfully.

400 Bad Request — Invalid request or status could not be changed.

401 Unauthorized — Missing or invalid API key.

404 Not Found — Customer or status record not found.

Start Workflow for Customer

Starts a workflow instance for a customer. Provide either the workflow GUID or the workflow name; one is required.

PropertyValue
EndpointPOST /Api/customer/{customerId}/workflow
AuthenticationAPI Key (apiKey header)
Content-Typeapplication/json

Request Body

{
  "workflowId": "550e8400-e29b-41d4-a716-446655440000",
  "workflowName": "Welcome Sequence",
  "contextData": "{}"
}

Field Reference

FieldTypeRequiredDescription
workflowIdguidOne of workflowId or workflowNameGUID of the active workflow to start
workflowNamestringOne of workflowId or workflowNameName of the active workflow to start
contextDatastringNoJSON context data for the workflow instance. Defaults to "{}".

Response

200 OK — Workflow started successfully:

{
  "success": true,
  "instanceId": "550e8400-e29b-41d4-a716-446655440001"
}

400 Bad Request — Neither workflow ID nor name was provided, the workflow is not active, or the workflow could not be started.

Get Support Ticket by Id

Retrieves a single support ticket, including its messages.

PropertyValue
EndpointGET /Api/support/tickets/{id}
AuthenticationAPI Key (apiKey header)

Response

200 OK — Returns the support ticket record with messages:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "customerId": "550e8400-e29b-41d4-a716-446655440001",
  "customerName": "John Davies",
  "customerEmail": "john@example.com",
  "subject": "Login issue",
  "referenceNumber": "SR-001",
  "status": "Open",
  "priority": "Normal",
  "categoryName": "Technical Support",
  "assignedToName": "Jane Smith",
  "messageCount": 3,
  "unreadMessageCount": 1,
  "lastMessageDate": "2025-06-15T14:30:00",
  "createDate": "2025-06-15T10:00:00",
  "messages": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440002",
      "supportRequestId": "550e8400-e29b-41d4-a716-446655440000",
      "message": "I cannot log in.",
      "isFromCustomer": true,
      "isInternal": false,
      "sentByName": "John Davies",
      "readAt": null,
      "createDate": "2025-06-15T10:00:00"
    }
  ]
}

401 Unauthorized — Missing or invalid API key.

404 Not Found — Support ticket not found.

Get Support Tickets

Returns support tickets. Use global list filters (newTickets or recentlyReplied) for open/recently-active tickets, or customer filters (customerId or customerEmailAddress) to list all tickets for a specific customer. Customer filters and list filters are mutually exclusive.

PropertyValue
EndpointGET /Api/support/tickets
AuthenticationAPI Key (apiKey header)

Query Parameters

ParameterTypeRequiredDescription
newTicketsbooleanOne mode selector onlyReturn open tickets ordered by creation date
recentlyRepliedbooleanOne mode selector onlyReturn tickets with messages in the recent period
customerIdguidOne mode selector onlyReturn all tickets for the customer with this GUID
customerEmailAddressstringOne mode selector onlyReturn all tickets for the customer with this email address
hoursintegerNoNumber of hours to look back when recentlyReplied is true. Defaults to 24.
pageintegerNoPage number. Defaults to 1.
pageSizeintegerNoPage size. Defaults to 25.

Response

200 OK — Returns an array of support ticket records:

[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "customerId": "550e8400-e29b-41d4-a716-446655440001",
    "customerName": "John Davies",
    "customerEmail": "john@example.com",
    "subject": "Login issue",
    "referenceNumber": "SR-001",
    "status": "Open",
    "priority": "Normal",
    "categoryName": "Technical Support",
    "assignedToName": "Jane Smith",
    "messageCount": 3,
    "unreadMessageCount": 1,
    "lastMessageDate": "2025-06-15T14:30:00",
    "createDate": "2025-06-15T10:00:00"
  }
]

400 Bad Request — Missing selector, conflicting filters, or customer not found.

401 Unauthorized — Missing or invalid API key.

Reply to Support Ticket

Adds a staff reply to an existing support ticket. Optionally closes the ticket after the reply.

PropertyValue
EndpointPOST /Api/support/tickets/{id}/reply
AuthenticationAPI Key (apiKey header)
Content-Typeapplication/json

Request Body

{
  "message": "Thanks for getting in touch. We are looking into this now.",
  "isInternal": false,
  "closeAfterReply": false
}

Field Reference

FieldTypeRequiredDescription
messagestringYesThe reply text
isInternalbooleanNoWhether the reply is internal-only. Defaults to false.
closeAfterReplybooleanNoClose the ticket after posting the reply. Defaults to false.

Response

200 OK — Reply posted successfully:

{
  "success": true
}

400 Bad Request — Missing message or reply could not be posted.

401 Unauthorized — Missing or invalid API key.

404 Not Found — Support ticket not found.

Get Customers by Age

Returns all customers whose age matches the supplied comparison.

PropertyValue
EndpointGET /Api/customers/byAge?age={age}&comparison={comparison}
AuthenticationAPI Key (apiKey header)

Query Parameters

ParameterTypeRequiredDescription
ageintegerYesThe age value to compare against
comparisonstringNoOne of equal, greater, less, greaterthanorequal, or lessthanorequal. Defaults to equal.

Response

200 OK — Returns an array of matching customer records:

[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "referenceNumber": "1234567890",
    "title": "Mr",
    "firstname": "John",
    "surname": "Davies",
    "age": 40,
    "emailAddress": "john.davies@example.com",
    "statusId": "00000000-0000-0000-0000-000000000000"
  }
]

400 Bad Request — Invalid comparison value supplied.

401 Unauthorized — Missing or invalid API key.

Search Customer by Phone

Searches for customers by phone number across mobile and home phone fields. Automatically handles UK format variations.

PropertyValue
EndpointGET /Customer/SearchByPhone?phoneNumber={number}
AuthenticationSession cookie

Response

{
  "success": true,
  "customers": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "referenceNumber": "1234567890",
      "title": "Mr",
      "firstname": "John",
      "surname": "Davies",
      "dateOfBirth": "20/03/1985",
      "address": "123 High Street, Manchester, M1 1AA",
      "mobilePhone": "07700900123",
      "homePhone": "01611234567",
      "status": "Active"
    }
  ]
}

Telephony API

Generate Twilio Token

Generates a Twilio access token for the authenticated agent to use the embedded dialer.

PropertyValue
EndpointGET /TwilioToken/Generate or POST /TwilioToken/Generate
AuthenticationSession cookie

Response

"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Change Agent Status

Updates the agent's availability status in the call queue.

PropertyValue
EndpointGET /TwilioAgent/ChangeStatus?status={Ready|Busy}
AuthenticationSession cookie

Workflow API

Save Workflow

Creates or updates a workflow definition.

PropertyValue
EndpointPOST /Workflow/SaveWorkflow
AuthenticationSession cookie (Admin)
Content-Typeapplication/json

Request Body

{
  "id": "00000000-0000-0000-0000-000000000000",
  "name": "Welcome Sequence",
  "description": "Send welcome email and SMS to new leads",
  "isActive": true,
  "stages": [
    {
      "name": "Send Welcome Email",
      "actionType": "Email",
      "actionConfiguration": "{\"templateId\":\"guid-here\"}",
      "waitDuration": "00:00:00",
      "sortOrder": 1
    }
  ]
}

Response

{
  "success": true,
  "id": "550e8400-e29b-41d4-a716-446655440000"
}

Activate / Deactivate Workflow

POST /Workflow/ActivateWorkflow?id={workflowId}
POST /Workflow/DeactivateWorkflow?id={workflowId}

Both return { "success": true } on success.

Delete Workflow

DELETE /Workflow/DeleteWorkflow?id={workflowId}

Start Workflow Instance

POST /Workflow/StartWorkflow

Body:
{
  "workflowId": "guid",
  "customerId": "guid"
}

Returns { "success": true, "instanceId": "guid" }.

Control Instance

POST /Workflow/PauseInstance?id={instanceId}
POST /Workflow/ResumeInstance?id={instanceId}
POST /Workflow/CancelInstance?id={instanceId}
POST /Workflow/AdvanceInstance?id={instanceId}

All return { "success": true } on success.

Bulk Action API

Preview Bulk Action

Returns matching customer count and sample before execution.

POST /BulkAction/Preview

Body:
{
  "statusFilterOperator": "In",
  "statusIds": ["guid-1", "guid-2"],
  "ownerIds": ["guid-3"],
  "importantDateFrom": "2025-01-01",
  "importantDateTo": "2025-12-31",
  "balanceFrom": 0,
  "balanceTo": 1000,
  "notificationsEnabledOnly": true
}

Response

{
  "success": true,
  "data": {
    "totalCustomersMatched": 245,
    "sampleCustomers": [ /* first 10 matches */ ]
  }
}

Create Bulk Action Job

POST /BulkAction/CreateJob

Body:
{
  "actionRequest": {
    "actionType": "Email",
    "useTemplate": true,
    "emailTemplateId": "guid",
    "filter": { /* same filter structure as Preview */ }
  }
}

Manage Jobs

GET    /BulkAction/GetJobs
GET    /BulkAction/GetJob?id={jobId}
POST   /BulkAction/PauseJob?id={jobId}
POST   /BulkAction/ResumeJob?id={jobId}
POST   /BulkAction/CancelJob?id={jobId}
POST   /BulkAction/DeleteJob?id={jobId}
POST   /BulkAction/RetryJob?id={jobId}

Support API

List Support Requests

GET /support?status={status}&assignedTo={userId}

Returns HTML view. For JSON data, use the existing authenticated session.

Create Support Request

POST /support/new

Body (form data):
- customerId
- categoryId
- subject
- message
- priority (Low, Normal, High, Urgent)

Reply to Request

POST /support/{id}/reply

Body (form data):
- message
- isInternal (boolean)
- closeAfterReply (boolean)

Update Status / Assign / Close

POST /support/{id}/status   (form: status)
POST /support/{id}/assign   (form: userId)
POST /support/{id}/close

HTTP Status Codes

CodeMeaning
200 OKRequest succeeded
400 Bad RequestValidation failed or malformed request
401 UnauthorizedMissing or invalid authentication
403 ForbiddenAuthenticated but insufficient permissions
404 Not FoundResource does not exist
409 ConflictRequest conflicts with existing data
422 Unprocessable EntitySelection is valid but unavailable
500 Internal Server ErrorUnexpected server error

cURL Examples

Create Customer

curl -X POST https://app.surelycrm.co.uk/Api/Create \
  -H "Content-Type: application/json" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "title": "Mr",
    "firstname": "John",
    "surname": "Davies",
    "address1": "123 High Street",
    "town": "Manchester",
    "city": "Greater Manchester",
    "postcode": "M1 1AA",
    "emailAddress": "john@example.com",
    "notificationsEnabled": true,
    "mobilePhone": "07700900123"
  }'

Search by Phone

curl -X GET "https://app.surelycrm.co.uk/Customer/SearchByPhone?phoneNumber=07700900123" \
  -H "Cookie: .AspNetCore.Cookies={your-session-cookie}"

Start Workflow

curl -X POST https://app.surelycrm.co.uk/Workflow/StartWorkflow \
  -H "Content-Type: application/json" \
  -H "Cookie: .AspNetCore.Cookies={your-session-cookie}" \
  -d '{
    "workflowId": "550e8400-e29b-41d4-a716-446655440000",
    "customerId": "550e8400-e29b-41d4-a716-446655440001"
  }'

Get Customer by GUID

curl -X GET "https://app.surelycrm.co.uk/Api/customer/550e8400-e29b-41d4-a716-446655440000" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000"

Get Customer by Reference Number

curl -X GET "https://app.surelycrm.co.uk/Api/customer/reference/1234567890" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000"

Update Customer

curl -X PUT "https://app.surelycrm.co.uk/Api/customer/550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Mr",
    "firstname": "John",
    "surname": "Davies",
    "address1": "123 High Street",
    "town": "Manchester",
    "city": "Greater Manchester",
    "postcode": "M1 1AA",
    "emailAddress": "john@example.com",
    "notificationsEnabled": true,
    "mobilePhone": "07700900123"
  }'

Start Workflow for Customer

curl -X POST "https://app.surelycrm.co.uk/Api/customer/550e8400-e29b-41d4-a716-446655440000/workflow" \
  -H "Content-Type: application/json" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "workflowName": "Welcome Sequence",
    "contextData": "{}"
  }'

Get Customers by Age

curl -X GET "https://app.surelycrm.co.uk/Api/customers/byAge?age=30&comparison=greater" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000"

Get Support Ticket by Id

curl -X GET "https://app.surelycrm.co.uk/Api/support/tickets/550e8400-e29b-41d4-a716-446655440000" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000"

Get Support Tickets for Customer

# By customer id
curl -X GET "https://app.surelycrm.co.uk/Api/support/tickets?customerId=550e8400-e29b-41d4-a716-446655440001" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000"

# By customer email
curl -X GET "https://app.surelycrm.co.uk/Api/support/tickets?customerEmailAddress=john@example.com" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000"

Get New Support Tickets

curl -X GET "https://app.surelycrm.co.uk/Api/support/tickets?newTickets=true&page=1&pageSize=25" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000"

Opportunities API

Manage the sales pipeline. Opportunities are linked to leads and track deal value, stage, probability, and expected close date.

List Opportunities

curl -X GET "https://app.surelycrm.co.uk/Api/opportunities?search=&leadId=&stage=&status=&page=1" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000"

Get Opportunity by Id

curl -X GET "https://app.surelycrm.co.uk/Api/opportunities/550e8400-e29b-41d4-a716-446655440000" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000"

Get Opportunities for Lead

curl -X GET "https://app.surelycrm.co.uk/Api/leads/550e8400-e29b-41d4-a716-446655440000/opportunities" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000"

Create Opportunity

curl -X POST "https://app.surelycrm.co.uk/Api/opportunities" \
  -H "Content-Type: application/json" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "leadId": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Website redesign project",
    "description": "Full redesign of company website",
    "value": 5000.00,
    "currency": "GBP",
    "probability": 50,
    "stage": "Prospecting",
    "status": "Open",
    "expectedCloseDate": "2026-12-31"
  }'

Update Opportunity

curl -X PUT "https://app.surelycrm.co.uk/Api/opportunities/550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "leadId": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Website redesign project",
    "value": 7500.00,
    "probability": 75,
    "stage": "Proposal",
    "status": "Open"
  }'

Change Opportunity Stage

curl -X POST "https://app.surelycrm.co.uk/Api/opportunities/550e8400-e29b-41d4-a716-446655440000/stage" \
  -H "Content-Type: application/json" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "stage": "ClosedWon",
    "notes": "Verbal agreement received"
  }'

External Campaign Selection API

The reference contract is available only when the ReferenceBackedCampaignPromotion feature is enabled for the current tenant. References are opaque and tenant-scoped; do not infer meaning from them. A campaign's referenceCode is its CRM display/search code, not its external identity.

List Active Campaign Locations

GET /Api/external/campaign-locations

Returns active locations only. Each item contains externalReference, name, and description.

Look Up an Active Location

GET /Api/external/campaign-locations/{externalReference}

Returns 404 when the reference is unknown, inactive, belongs to another tenant, or the feature is disabled.

List Eligible Campaigns

GET /Api/external/campaigns?locationReference={locationExternalReference}

Returns campaigns for the active location that are New or InProgress. Finished campaigns are not returned.

Create or Reconcile a Campaign

POST /Api/external/campaigns
{
  "externalReference": "dark-campaign-100",
  "name": "Autumn outreach",
  "referenceCode": "AUT-2026-001",
  "locationExternalReference": "loc_opaque_reference"
}

A first request returns 201; an identical retry returns 200 and the same campaign. A reused external reference with a changed name, display reference code, or location returns 409. An unknown or inactive location returns 422. Do not substitute referenceCode for externalReference.

External Promotion API

The same feature enables an atomic, reference-only handoff from Dark Leads. It is the only contract Dark Leads uses: it neither receives CRM UUIDs nor uses legacy UUID-based Lead or Opportunity endpoints.

Create or Reconcile a Lead and Initial Opportunity

POST /Api/external/promotions
{
  "locationExternalReference": "loc_opaque_reference",
  "campaignExternalReference": "dark-campaign-100",
  "leadExternalReference": "dark-handoff-100",
  "businessName": "Example Business",
  "contactPerson": "Business contact",
  "contactEmail": "contact@example.test",
  "websiteUrl": "https://example.test",
  "salesBrief": "Sales-ready snapshot..."
}

Each reference is required, is 1 to 64 characters, starts with an ASCII letter or digit, and then contains only ASCII letters, digits, underscores, or hyphens. businessName is required and is at most 200 characters. Contact fields are optional. The optional salesBrief is plain text or simple Markdown treated as untrusted text, at most 8,000 characters, trimmed with blank values stored as null.

Surely locks and verifies the location and campaign, then creates the Lead, its initial Prospecting/Open Opportunity, and activity history in one database transaction. The location must be active, the campaign must belong to it, and the campaign must be New or InProgress. The Opportunity title is the business name; its value is 0 GBP, with no owner, probability, or date.

Retries and conflicts

The supplied leadExternalReference is the handoff key. Strings are trimmed; blank optional contact fields become null. References and email are compared case-insensitively. Other fields are compared after trimming with their exact case.

While the selected location remains active and the campaign remains eligible, an identical canonical retry returns 200 with both leadCreated and opportunityCreated set to false. A changed canonical payload returns 409 with only the differing field names. A location/campaign mismatch or an incomplete existing pair also returns 409; it exposes no internal IDs or field values. Do not retry with a different payload or fall back to a UUID endpoint.

The salesBrief is a creation-time snapshot: it is stored only when the Lead is created, and a canonical replay — with no brief or a different brief — reconciles exactly as before, never comparing or overwriting the stored brief, and never appearing in differingFields. The promotion endpoint stores a supplied brief even while the SalesBrief feature toggle is disabled; only the form and read API are gated.

Unknown locations or campaigns return 404. Inactive locations and finished campaigns return 422, including a replay after the selection has become ineligible. Invalid input returns 400. The feature being disabled returns 404.

Response

{
  "leadExternalReference": "dark-handoff-100",
  "opportunityExternalReference": "opp_opaque_reference",
  "leadCreated": true,
  "opportunityCreated": true
}

Responses contain external references only, never CRM UUIDs. The same reference string may be used by another tenant, but a tenant can only select and promote records in its own CRM database.

Lead Read API

Bounded read of a Lead by its opaque external reference for trusted callers. Available only when the SalesBrief feature is enabled for the current tenant; disabled access returns 404. The response contains exactly the external reference, business name, SalesBrief, and optional contact email and phone — never CRM UUIDs, contact-person names, or other PII.

Read a Lead by External Reference

GET /Api/external/leads/{externalReference}

Authentication uses the apiKey header. The request host selects the tenant database. The read is read-only — it never recomputes or rewrites anything.

Response

{
  "externalReference": "lead_opaque_reference",
  "businessName": "Example Business",
  "salesBrief": "Sales-ready snapshot...",
  "contactEmail": "contact@example.test",
  "contactPhone": "01234 567890"
}

salesBrief, contactEmail, and contactPhone are null when the Lead has none stored.

Errors

  • 401 — missing, wrong, or cross-tenant API key
  • 404 — feature disabled, Lead missing, or foreign-tenant reference

Lead Status List API

Bounded, status-filtered Lead listing for automated sample-site claiming. Available only when the AutomatedSampleSiteClaim feature is enabled for the current tenant. Disabled access returns 404. Responses contain opaque external references only — never CRM UUIDs, campaign IDs, contact details, or other PII.

List Leads by Status

GET /Api/leads/byStatus?status={LeadStatus}&pageSize={1-20}&cursor={opaque}&hasSalesBrief={true|false}
ParameterRequiredNotes
status Yes Lead status name only (case-insensitive), e.g. New, Processing. Numeric values and comma-separated combinations are rejected (400).
pageSize No Defaults to 20. Values above 20 are clamped to 20; values below 1 become 1.
cursor No Opaque position data from a previous nextCursor; do not edit or rely on its contents. Malformed cursors return 400.
hasSalesBrief No When true, restricts results to Leads that have a SalesBrief. Null, blank, or whitespace-only briefs are not brief-ready. Omitted or false keeps current behaviour exactly.

Results are ordered oldest first by (createdAt, externalReference) and are tenant-isolated by the request host. Listing is read-only — it does not change Lead status, attempt counts, or ownership. Authentication uses the apiKey header.

Response

{
  "leads": [
    {
      "externalReference": "lead_opaque_reference",
      "status": "New",
      "createdAt": "2026-01-01T00:00:00Z"
    }
  ],
  "nextCursor": "opaque-or-null"
}

nextCursor is null on the last page. Walk pages by passing the previous nextCursor until it is null. Do not invent or edit cursors.

Errors

  • 401 — missing, wrong, or cross-tenant API key
  • 400 — missing/invalid status name, or malformed cursor
  • 404 — feature disabled for the tenant

Lead Claim API

Atomic per-Lead compare-and-set claim for automated sample-site claiming. Available only when the AutomatedSampleSiteClaim feature is enabled for the current tenant. Disabled access returns 404. The Lead is identified solely by its opaque external reference — never a CRM UUID. A successful claim transitions NewProcessing only when the Lead is still New.

Claim Lead by External Reference

POST /Api/leads/claim
{
  "externalReference": "lead_opaque_reference"
}

Authentication uses the apiKey header. The request host selects the tenant database. Exactly one of concurrent claim attempts wins; the rest receive 409. The claim updates only the Lead status column.

Response

{
  "externalReference": "lead_opaque_reference",
  "status": "Processing"
}

Responses contain the opaque external reference and new status only — never CRM UUIDs, campaign IDs, contact details, or other PII.

Errors

  • 401 — missing, wrong, or cross-tenant API key
  • 400 — missing or blank external reference
  • 404 — feature disabled, Lead missing, or foreign-tenant reference
  • 409 — Lead exists but is no longer New (bounded conflict; does not reveal the current status beyond "not New")

Lead Tasks API

Create and manage tasks linked to leads. Tasks support assignment, due dates, priorities, and status tracking.

List Lead Tasks

curl -X GET "https://app.surelycrm.co.uk/Api/lead-tasks?search=&leadId=&assignedToUserId=&status=&priority=&page=1" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000"

Get Lead Task by Id

curl -X GET "https://app.surelycrm.co.uk/Api/lead-tasks/550e8400-e29b-41d4-a716-446655440000" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000"

Get Tasks for Lead

curl -X GET "https://app.surelycrm.co.uk/Api/leads/550e8400-e29b-41d4-a716-446655440000/tasks" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000"

Create Lead Task

curl -X POST "https://app.surelycrm.co.uk/Api/lead-tasks" \
  -H "Content-Type: application/json" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "leadId": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Follow up call",
    "description": "Call to discuss proposal",
    "assignedToUserId": "550e8400-e29b-41d4-a716-446655440001",
    "status": "NotStarted",
    "priority": "High",
    "dueDate": "2026-08-10"
  }'

Update Lead Task

curl -X PUT "https://app.surelycrm.co.uk/Api/lead-tasks/550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "leadId": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Follow up call",
    "status": "InProgress",
    "priority": "High",
    "dueDate": "2026-08-10"
  }'

Change Lead Task Status

curl -X POST "https://app.surelycrm.co.uk/Api/lead-tasks/550e8400-e29b-41d4-a716-446655440000/status" \
  -H "Content-Type: application/json" \
  -H "apiKey: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "status": "Completed"
  }'

Need more endpoints? Contact SurelyCrm Support to discuss custom API integrations or webhook requirements.