CRM Search Guide
Overview
CRM Search provides two complementary layers of search capability: a global search endpoint that queries all CRM entity types simultaneously, and per-entity search endpoints for contacts, customers, and opportunities that return paginated, relationship-enriched results. Together they support everything from a top-level search bar that surfaces results across the whole CRM to a filtered entity list within a specific module section.
All search endpoints perform full-text matching against the most relevant fields for each entity type - names, email addresses, phone numbers, company names, and opportunity titles - so results are intuitive and predictable without requiring knowledge of internal field naming.
Search Scope
| Endpoint | Entity Types Searched | Pagination | Relationships Returned |
|---|---|---|---|
GET /search/global | Leads, Customers, Contacts, Opportunities, Activities | No (top-N per type) | Minimal (id, name, email, status) |
GET /search/contacts | Contacts only | Yes | Organisation, Customer |
GET /search/customers | Customers only | Yes | Account Manager |
GET /search/opportunities | Opportunities only | Yes | Assigned User, Customer, Lead |
What Each Entity Searches Against
Leads: email address, first name, last name, company name Customers: company name, trading name, email, primary contact name Contacts: first name, last name, email, phone number, job title Opportunities: title, description, customer name Activities: subject, description, associated contact and customer names
API Endpoints
All search endpoints are under the base path /api/v1/crm.
Authentication: Required (Bearer token) for all endpoints.
1. Global Search
Search across all CRM entity types simultaneously. Designed for a single top-bar search input that surfaces the most relevant results from anywhere in the CRM.
Endpoint: GET /api/v1/crm/search/global
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
q | string | Yes | - | Search query. Minimum 2 characters |
limit | integer | No | 5 | Maximum results returned per entity type. Range: 1–20 |
Example Requests:
# Search for "acme" with default limit of 5 per type
GET /api/v1/crm/search/global?q=acme
# Search for "sarah johnson" with 10 results per type
GET /api/v1/crm/search/global?q=sarah+johnson&limit=10Response (200 OK):
{
"success": true,
"message": "Search completed successfully",
"data": {
"query": "acme",
"results": {
"leads": [
{
"id": "01hwlead0000000000000001",
"name": "James Miller",
"email": "james.miller@acmecorp.com",
"status": "qualified"
},
{
"id": "01hwlead0000000000000002",
"name": "Lisa Chen",
"email": "l.chen@acme-global.com",
"status": "new"
}
],
"customers": [
{
"id": "01hwcust0000000000000001",
"name": "Acme Corporation",
"email": "contact@acmecorp.com",
"status": "active"
}
],
"contacts": [
{
"id": "01hwcont0000000000000001",
"name": "Robert Acme",
"email": "r.acme@techpartners.com",
"status": null
}
],
"opportunities": [
{
"id": "01hwopp00000000000000001",
"name": "Acme Corp - Enterprise Renewal",
"email": null,
"status": "proposal"
}
],
"activities": []
},
"total_results": 5
},
"meta": {}
}Response Fields:
| Field | Description |
|---|---|
query | The search string as received (useful for confirming the request was processed correctly) |
results | Object keyed by entity type. Each key holds an array of up to limit matching records |
results[type][].id | ULID identifier for the matched record |
results[type][].name | Display name for the matched record |
results[type][].email | Primary email address, where applicable |
results[type][].status | Current status value, where applicable (null for entity types without a status field) |
total_results | Sum of all matched records across all entity types, up to limit per type |
Minimum Query Length
Queries shorter than 2 characters return a 422 Unprocessable Entity error. This prevents overly broad searches that would return noise. Build your UI to enforce a minimum of 2 characters before triggering the global search call.
Error Response (422 - Query Too Short):
{
"success": false,
"message": "The given data was invalid.",
"data": {},
"meta": {
"errors": {
"q": ["The search query must be at least 2 characters."]
}
}
}2. Contacts Search
Full-text search across contacts with eager-loaded organisation and customer relationships. Returns paginated results.
Endpoint: GET /api/v1/crm/search/contacts
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
q | string | Yes | - | Search query. Minimum 2 characters |
page | integer | No | 1 | Page number |
per_page | integer | No | 25 | Results per page. Maximum: 100 |
Example Requests:
# Search for contacts named "sarah"
GET /api/v1/crm/search/contacts?q=sarah
# Search for contacts at "techcorp" with 50 per page
GET /api/v1/crm/search/contacts?q=techcorp&per_page=50&page=1Response (200 OK):
{
"success": true,
"message": "Contacts search completed successfully",
"data": [
{
"id": "01hwcont0000000000000001",
"first_name": "Sarah",
"last_name": "Johnson",
"full_name": "Sarah Johnson",
"email": "sarah.johnson@techcorp.com",
"phone": "+1-555-0123",
"job_title": "VP of Engineering",
"customer": {
"id": "01hwcust0000000000000001",
"name": "TechCorp Inc"
},
"organisation": {
"id": "01hworgg0000000000000001",
"name": "TechCorp Inc",
"industry": "Software"
},
"created_at": "2025-08-14T09:20:00Z"
},
{
"id": "01hwcont0000000000000002",
"first_name": "Michael",
"last_name": "Ross",
"full_name": "Michael Ross",
"email": "m.ross@techcorp-europe.com",
"phone": "+44-20-7946-0123",
"job_title": "Head of Procurement",
"customer": null,
"organisation": {
"id": "01hworgg0000000000000002",
"name": "TechCorp Europe",
"industry": "Software"
},
"created_at": "2025-09-01T11:45:00Z"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 2,
"last_page": 1
}
}Response Fields:
| Field | Description |
|---|---|
customer | The CRM customer this contact is directly linked to, or null if not linked |
organisation | The organisation this contact belongs to, with industry classification |
meta | Standard pagination metadata |
Contacts Without Customers
A contact can belong to an organisation without being linked to a CRM customer record. This is common for contacts at prospect companies - they appear as customer: null until the company converts.
3. Customers Search
Full-text search across customer records with account manager relationship eager-loaded. Returns paginated results.
Endpoint: GET /api/v1/crm/search/customers
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
q | string | Yes | - | Search query. Minimum 2 characters |
page | integer | No | 1 | Page number |
per_page | integer | No | 25 | Results per page. Maximum: 100 |
Example Requests:
# Search for customers matching "global"
GET /api/v1/crm/search/customers?q=global
# Search for customers matching "software" with custom pagination
GET /api/v1/crm/search/customers?q=software&per_page=50Response (200 OK):
{
"success": true,
"message": "Customers search completed successfully",
"data": [
{
"id": "01hwcust0000000000000001",
"name": "Global Retail Group",
"trading_name": "GRG",
"email": "accounts@globalretailgroup.com",
"phone": "+1-800-555-0100",
"status": "active",
"type": "enterprise",
"account_manager": {
"id": "01hwuser0000000000000001",
"name": "Jane Davis",
"email": "jane.davis@yourcompany.com"
},
"created_at": "2024-11-03T08:00:00Z"
},
{
"id": "01hwcust0000000000000002",
"name": "Global Tech Solutions",
"trading_name": null,
"email": "info@globaltechsolutions.com",
"phone": "+1-555-0200",
"status": "active",
"type": "mid_market",
"account_manager": {
"id": "01hwuser0000000000000002",
"name": "John Smith",
"email": "john.smith@yourcompany.com"
},
"created_at": "2025-01-22T13:30:00Z"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 2,
"last_page": 1
}
}Response Fields:
| Field | Description |
|---|---|
trading_name | Alternative trading name, if different from the registered name. null if not set |
status | Customer status (active, inactive, churned, prospect) |
type | Customer tier/segment (enterprise, mid_market, smb) |
account_manager | The internal user assigned as account manager, or null if unassigned |
4. Opportunities Search
Full-text search across opportunity records with assigned user, customer, and originating lead relationships eager-loaded. Returns paginated results.
Endpoint: GET /api/v1/crm/search/opportunities
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
q | string | Yes | - | Search query. Minimum 2 characters |
page | integer | No | 1 | Page number |
per_page | integer | No | 25 | Results per page. Maximum: 100 |
Example Requests:
# Search for opportunities mentioning "renewal"
GET /api/v1/crm/search/opportunities?q=renewal
# Search for opportunities linked to "techcorp"
GET /api/v1/crm/search/opportunities?q=techcorp&per_page=10Response (200 OK):
{
"success": true,
"message": "Opportunities search completed successfully",
"data": [
{
"id": "01hwopp00000000000000001",
"title": "TechCorp Inc - Platform Renewal 2026",
"description": "Annual platform licence renewal with potential expansion to 3 additional divisions.",
"stage": "proposal",
"probability": 70,
"value": 120000.00,
"currency": "USD",
"expected_close_date": "2026-03-31",
"assigned_user": {
"id": "01hwuser0000000000000001",
"name": "Jane Davis",
"email": "jane.davis@yourcompany.com"
},
"customer": {
"id": "01hwcust0000000000000001",
"name": "TechCorp Inc"
},
"lead": {
"id": "01hwlead0000000000000001",
"email": "james.miller@techcorp.com"
},
"created_at": "2025-12-01T10:00:00Z",
"updated_at": "2026-02-15T14:20:00Z"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
}
}Response Fields:
| Field | Description |
|---|---|
stage | Current pipeline stage (prospect, proposal, negotiation, closing, won, lost) |
probability | Win probability percentage (0–100) |
value | Opportunity value in the specified currency |
expected_close_date | Target close date in ISO 8601 (YYYY-MM-DD) |
assigned_user | Rep responsible for this opportunity |
customer | The customer this opportunity is attached to |
lead | The originating lead, if the opportunity was created from a lead conversion. null for manually created opportunities |
Business Scenarios
Scenario 1: Global Search Bar Implementation
Context: A product team is building a top-bar search input that lets users find any CRM record from anywhere in the application.
Recommended Approach:
- Debounce the input to fire after 300–500 ms of inactivity
- Only trigger the API call once the query is 2+ characters
- Display up to 5 results per entity type in a dropdown, grouped by entity
- On selection, navigate to the detail page for that entity type and ID
# User types "sarah jo" - fires after debounce
GET /api/v1/crm/search/global?q=sarah+jo&limit=5
# Display results grouped:
# Leads (2)
# Sarah Johnson - sarah.johnson@company.com - qualified
# Contacts (1)
# Sarah Jones - s.jones@enterprise.com
# Customers (0)
# Opportunities (0)
# Activities (0)For "show all results" links within each group, route the user to the appropriate per-entity search endpoint with the same query string:
# "Show all Contacts for 'sarah jo'"
GET /api/v1/crm/search/contacts?q=sarah+jo&per_page=25Scenario 2: Contact Lookup Before Creating a Lead
Context: A sales rep wants to check whether a prospect already exists in the system before creating a new lead, to avoid duplicates.
Workflow:
- Before submitting the "Create Lead" form, search contacts and customers by email or company name
- If a match is found, link the new lead to the existing contact/customer rather than creating a duplicate
- If no match is found, proceed with lead creation
# Check for existing contact by email domain
GET /api/v1/crm/search/contacts?q=techcorp.com
# Check for existing customer by company name
GET /api/v1/crm/search/customers?q=TechCorp
# If customer found (id: 01hwcust0000000000000001), create lead linked to it:
POST /api/v1/crm/leads
{
"email": "new.contact@techcorp.com",
"source": "cold_outreach",
"contact_info": {
"first_name": "Alex",
"last_name": "Wu",
"company": "TechCorp Inc"
}
}Scenario 3: Account Manager Renewal Pipeline Search
Context: An account manager wants to pull up all open renewal opportunities across their accounts before a quarterly business review.
Workflow:
- Search opportunities for "renewal" to surface all renewal deals
- Filter the returned list by checking
assigned_user.idmatches the current user - Cross-reference with the customer records for context on account status
# Find all renewal opportunities
GET /api/v1/crm/search/opportunities?q=renewal&per_page=100
# The response includes assigned_user - filter client-side or add user_id
# filter parameter if available in your deployment version.
# For a specific account, search by customer name:
GET /api/v1/crm/search/opportunities?q=Global+Retail
# Then check each result's customer and expected_close_date
# to prioritise which accounts to cover in the QBR.Best Practices
1. Use Global Search for Discovery, Per-Entity Search for Depth
The global search endpoint is optimised for speed and breadth: it searches across all entity types but returns only a handful of results per type and minimal field data. When a user selects a search result or wants to browse more matches in a given category, route them to the per-entity endpoint, which returns full records with relationships and supports pagination.
2. Enforce Minimum Query Length in the UI
All search endpoints return 422 Unprocessable Entity for queries shorter than 2 characters. Build your UI to check this client-side before making the API call. Showing "Type at least 2 characters to search" is a better user experience than handling an API error on every keystroke.
3. Use per_page=100 for Bulk Lookup Workflows
For scripted or integration workflows that need to retrieve all matching records (e.g., a nightly sync that needs all customers matching a domain), set per_page=100 and iterate through pages until meta.current_page === meta.last_page. Avoid parallel page fetches without rate-limit awareness - they will compete for the same database read locks.
4. Prefer Quoted Multi-Word Queries
When searching for a full name or multi-word company name, pass the query URL-encoded as a single string (e.g., q=Global+Retail+Group). The search engine performs phrase-aware full-text matching: multi-word queries boost records where all terms appear together over records where the terms appear independently.
Integration Points
With Lead Management
The global search results for leads surface the same lead records managed through the Lead Management guide. Clicking a lead result from the search bar should navigate to the lead detail view using the returned id as the path parameter: GET /api/v1/crm/leads/{id}.
With Customer Management
Customer search results feed directly into the customer detail view. The account_manager relationship returned by contacts search and the account manager on a customer record are the same user field, enabling account-based filtering. Refer to the Customer Management guide.
With Opportunity Pipeline
Opportunity search complements the opportunity pipeline by providing text-based lookup in addition to the status/stage-based filtering available on the main opportunities list. When the pipeline report surfaces an anomaly (e.g., an unusually large stale deal), use opportunity search to locate that record quickly by title or customer name. Refer to the Opportunity Pipeline guide.
With CRM Reports
Search does not replace reports - they serve different purposes. Reports provide pre-aggregated analytics over time windows; search provides instant lookup of individual records. A common workflow is: use a CRM Report to identify a pattern (e.g., a specific channel producing high-value customers), then use search to locate and review the specific records that make up that pattern.
Troubleshooting
Search Returns No Results for a Known Record
Symptom: A record definitely exists in the system (visible in the entity list) but does not appear in search results for a query that should match it.
Possible Causes:
- The query contains a typo or special character that doesn't match the stored value
- The record's searchable fields are partially filled (e.g., a contact with no email and a short first name)
- The query is below the 2-character minimum after trimming whitespace
- The record was recently created and the search index has not yet caught up (if your deployment uses an async search index)
Diagnostic Steps:
# 1. Confirm the record exists
GET /api/v1/crm/contacts?per_page=5
# 2. Try a broader query - a single distinctive word from the record
GET /api/v1/crm/search/contacts?q=miller
# 3. Try searching by email if you know it exactly
GET /api/v1/crm/search/contacts?q=james.miller
# 4. Check that the query is at least 2 characters after trimmingIf the record consistently does not appear, retrieve it directly by ID and verify that the fields you are searching against are populated.
Global Search Returns Duplicate Results Across Entity Types
Symptom: The same person appears as both a leads result and a contacts result for the same query.
Cause: This is expected behaviour, not a bug. A person can simultaneously be a lead (an unqualified prospect record) and a contact (a relationship entry linked to a customer or organisation). The global search reports truthfully that both records matched the query.
Resolution:
- If the duplication represents a data quality issue (the same person has a lead and an independent contact record that should be merged), address it in the respective entity detail views.
- In your UI, consider de-duplicating results by email address when rendering the global search dropdown, favouring the customer or contact record over the lead record once a conversion has occurred.
Related Documentation
- Lead Management Guide - Managing leads returned by search
- Customer Management Guide - Managing customers returned by search
- Opportunity Pipeline Guide - Managing opportunities returned by search
- Activity Management Guide - Managing activities returned by global search
- CRM Reports Guide - Analytical aggregates over CRM data