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.
| Property | Value |
|---|---|
| Endpoint | POST /Api/Create |
| Authentication | API Key (apiKey header) |
| Content-Type | application/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
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Mr, Mrs, Miss, or Ms |
firstname | string | Yes | First name |
surname | string | Yes | Surname |
referenceNumber | string | Yes | Customer reference number |
address1 | string | No | First line of address |
address2 | string | No | Second line of address |
address3 | string | No | Third line of address |
address4 | string | No | Fourth line of address |
town | string | No | Town |
city | string | No | City |
postcode | string | No | Postcode |
homePhone | string | No | Home telephone number |
mobilePhone | string | No | Mobile telephone number |
emailAddress | string | No | Email address |
notificationsEnabled | boolean | No | Consent to marketing communications |
importantDate | datetime | No | Key date for the customer |
dateOfBirth | datetime | No | Date of birth |
website | string | No | Website URL |
statusId | guid | No | Status GUID. Omit for default. |
ownerId | guid | No | Assigned user GUID. Omit for unassigned. |
referredBy | string | No | Referral source |
customFields | object | No | Key-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.
| Property | Value |
|---|---|
| Endpoint | GET /Api/customer/{id} |
| Authentication | API 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.
| Property | Value |
|---|---|
| Endpoint | GET /Api/customer/reference/{referenceNumber} |
| Authentication | API 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.
| Property | Value |
|---|---|
| Endpoint | GET /Api/customer/byEmail/{emailAddress} |
| Authentication | API 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.
| Property | Value |
|---|---|
| Endpoint | PUT /Api/customer/{id} |
| Authentication | API Key (apiKey header) |
| Content-Type | application/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.
| Property | Value |
|---|---|
| Endpoint | POST /Api/customer/{customerId}/status |
| Authentication | API Key (apiKey header) |
| Content-Type | application/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.
| Property | Value |
|---|---|
| Endpoint | POST /Api/customer/{customerId}/workflow |
| Authentication | API Key (apiKey header) |
| Content-Type | application/json |
Request Body
{
"workflowId": "550e8400-e29b-41d4-a716-446655440000",
"workflowName": "Welcome Sequence",
"contextData": "{}"
}
Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
workflowId | guid | One of workflowId or workflowName | GUID of the active workflow to start |
workflowName | string | One of workflowId or workflowName | Name of the active workflow to start |
contextData | string | No | JSON 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.
| Property | Value |
|---|---|
| Endpoint | GET /Api/support/tickets/{id} |
| Authentication | API 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.
| Property | Value |
|---|---|
| Endpoint | GET /Api/support/tickets |
| Authentication | API Key (apiKey header) |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
newTickets | boolean | One mode selector only | Return open tickets ordered by creation date |
recentlyReplied | boolean | One mode selector only | Return tickets with messages in the recent period |
customerId | guid | One mode selector only | Return all tickets for the customer with this GUID |
customerEmailAddress | string | One mode selector only | Return all tickets for the customer with this email address |
hours | integer | No | Number of hours to look back when recentlyReplied is true. Defaults to 24. |
page | integer | No | Page number. Defaults to 1. |
pageSize | integer | No | Page 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.
| Property | Value |
|---|---|
| Endpoint | POST /Api/support/tickets/{id}/reply |
| Authentication | API Key (apiKey header) |
| Content-Type | application/json |
Request Body
{
"message": "Thanks for getting in touch. We are looking into this now.",
"isInternal": false,
"closeAfterReply": false
}
Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | The reply text |
isInternal | boolean | No | Whether the reply is internal-only. Defaults to false. |
closeAfterReply | boolean | No | Close 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.
| Property | Value |
|---|---|
| Endpoint | GET /Api/customers/byAge?age={age}&comparison={comparison} |
| Authentication | API Key (apiKey header) |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
age | integer | Yes | The age value to compare against |
comparison | string | No | One 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.
| Property | Value |
|---|---|
| Endpoint | GET /Customer/SearchByPhone?phoneNumber={number} |
| Authentication | Session 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.
| Property | Value |
|---|---|
| Endpoint | GET /TwilioToken/Generate or POST /TwilioToken/Generate |
| Authentication | Session cookie |
Response
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Change Agent Status
Updates the agent's availability status in the call queue.
| Property | Value |
|---|---|
| Endpoint | GET /TwilioAgent/ChangeStatus?status={Ready|Busy} |
| Authentication | Session cookie |
Workflow API
Save Workflow
Creates or updates a workflow definition.
| Property | Value |
|---|---|
| Endpoint | POST /Workflow/SaveWorkflow |
| Authentication | Session cookie (Admin) |
| Content-Type | application/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
| Code | Meaning |
|---|---|
| 200 OK | Request succeeded |
| 400 Bad Request | Validation failed or malformed request |
| 401 Unauthorized | Missing or invalid authentication |
| 403 Forbidden | Authenticated but insufficient permissions |
| 404 Not Found | Resource does not exist |
| 409 Conflict | Request conflicts with existing data |
| 422 Unprocessable Entity | Selection is valid but unavailable |
| 500 Internal Server Error | Unexpected 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 key404— 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}
| Parameter | Required | Notes |
|---|---|---|
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 key400— missing/invalid status name, or malformed cursor404— 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
New → Processing 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 key400— missing or blank external reference404— feature disabled, Lead missing, or foreign-tenant reference409— Lead exists but is no longerNew(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.