Prospect Search (ICP) Editing¶
Update prospect search profiles with permission-based field access.
Overview¶
The Prospect Search Editing API allows you to update Ideal Customer Profile (ICP) configurations. This endpoint implements:
- Field Mask Pattern for partial updates (Google AIP-134)
- Permission-Based Access with field-level restrictions
- Optimistic Locking to prevent concurrent modification conflicts
How It Works¶
graph LR
A[GET Prospect Search] --> B[Get version field]
B --> C[PATCH with version]
C --> D{Version Match?}
D -->|Yes| E[Update Success]
D -->|No| F[409 Conflict]
F --> A
- Get current state - Retrieve the prospect search to get the current
version - Send PATCH request - Include
versionand only the fields you want to update - Handle response - Success returns updated data; conflict requires re-fetch
Required Scopes¶
| Scope | Description |
|---|---|
prospect_searches:read |
Read prospect searches and their settings |
prospect_searches:write |
Create and update prospect searches |
Field Categories¶
The API enforces permission-based field editing:
| Category | Fields | Who Can Edit |
|---|---|---|
| User-Editable | name, ideal_customer_profile |
Any user with edit permission |
| Admin-Only | geo_locations, search_keywords |
Admins and owners only |
| Non-Editable | language_config, *_extraction_commands |
AI-generated, read-only |
Admin Field Stripping
If you include admin-only fields without proper permissions, they are silently removed from the update. The stripped_fields response array indicates which fields were removed.
Endpoints¶
| Endpoint | Method | Description |
|---|---|---|
/prospect-searches/{id} |
GET |
Get prospect search with current version |
/prospect-searches/{id} |
PATCH |
Update specific fields |
Update Prospect Search¶
Partially update a prospect search with optimistic locking.
Request¶
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
version |
integer | Yes | Current version for optimistic locking |
update_mask |
string[] | No | Fields to update (uses all non-null if omitted) |
name |
string | No | New name for the prospect search |
ideal_customer_profile |
string | No | Updated ICP description |
geo_locations |
object[] | No | Geographic targeting (admin only) |
search_keywords |
string[] | No | Search keywords (admin only) |
Code Examples¶
# First, get the current version
curl -X GET "https://api.openprospect.io/api/v1/prospect-searches/${PROSPECT_SEARCH_ID}" \
-H "Authorization: Bearer ${OPENPROSPECT_API_KEY}" \
-H "Content-Type: application/json"
# Then update with the version
curl -X PATCH "https://api.openprospect.io/api/v1/prospect-searches/${PROSPECT_SEARCH_ID}" \
-H "Authorization: Bearer ${OPENPROSPECT_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"version": 1,
"name": "Updated Search Name",
"ideal_customer_profile": "B2B SaaS companies with 50-500 employees in the DACH region"
}'
import httpx
API_KEY = "lnc_live_your_api_key_here"
BASE_URL = "https://api.openprospect.io/api/v1"
prospect_search_id = "a45e6452-496e-4bb3-86a0-3c99475c0fc3"
# Step 1: Get current state and version
response = httpx.get(
f"{BASE_URL}/prospect-searches/{prospect_search_id}",
headers={"Authorization": f"Bearer {API_KEY}"}
)
current = response.json()
version = current["version"]
print(f"Current version: {version}")
# Step 2: Update with the version
update_response = httpx.patch(
f"{BASE_URL}/prospect-searches/{prospect_search_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"version": version,
"name": "Updated Search Name",
"ideal_customer_profile": "B2B SaaS companies with 50-500 employees"
}
)
if update_response.status_code == 200:
result = update_response.json()
print(f"Updated fields: {result['updated_fields']}")
print(f"Stripped fields: {result['stripped_fields']}")
print(f"New version: {result['prospect_search']['version']}")
elif update_response.status_code == 409:
print("Version conflict - re-fetch and retry")
else:
print(f"Error: {update_response.status_code}")
const API_KEY = "lnc_live_your_api_key_here";
const BASE_URL = "https://api.openprospect.io/api/v1";
const prospectSearchId = "a45e6452-496e-4bb3-86a0-3c99475c0fc3";
// Step 1: Get current state and version
const currentResponse = await fetch(
`${BASE_URL}/prospect-searches/${prospectSearchId}`,
{ headers: { "Authorization": `Bearer ${API_KEY}` } }
);
const current = await currentResponse.json();
const version = current.version;
console.log(`Current version: ${version}`);
// Step 2: Update with the version
const updateResponse = await fetch(
`${BASE_URL}/prospect-searches/${prospectSearchId}`,
{
method: "PATCH",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
version: version,
name: "Updated Search Name",
ideal_customer_profile: "B2B SaaS companies with 50-500 employees"
})
}
);
if (updateResponse.ok) {
const result = await updateResponse.json();
console.log(`Updated fields: ${result.updated_fields.join(", ")}`);
console.log(`Stripped fields: ${result.stripped_fields.join(", ")}`);
console.log(`New version: ${result.prospect_search.version}`);
} else if (updateResponse.status === 409) {
console.log("Version conflict - re-fetch and retry");
}
const API_KEY = "lnc_live_your_api_key_here";
const BASE_URL = "https://api.openprospect.io/api/v1";
interface ProspectSearch {
id: string;
name: string;
ideal_customer_profile: string | null;
geo_locations: Array<{city: string; country: string; lat: number; lng: number}> | null;
search_keywords: string[] | null;
version: number;
}
interface UpdateResponse {
prospect_search: ProspectSearch;
updated_fields: string[];
stripped_fields: string[];
}
async function updateProspectSearch(
id: string,
updates: {name?: string; ideal_customer_profile?: string}
): Promise<UpdateResponse> {
// Get current version
const currentResponse = await fetch(`${BASE_URL}/prospect-searches/${id}`, {
headers: { Authorization: `Bearer ${API_KEY}` }
});
if (!currentResponse.ok) {
throw new Error(`Failed to fetch prospect search: ${currentResponse.status}`);
}
const current = await currentResponse.json() as ProspectSearch;
// Update with version
const response = await fetch(`${BASE_URL}/prospect-searches/${id}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
version: current.version,
...updates
})
});
if (response.status === 409) {
throw new Error("Version conflict - re-fetch and retry");
}
if (!response.ok) {
throw new Error(`Update failed: ${response.status}`);
}
return response.json() as Promise<UpdateResponse>;
}
// Usage
const result = await updateProspectSearch(
"a45e6452-496e-4bb3-86a0-3c99475c0fc3",
{ name: "Updated Search Name" }
);
console.log(`New version: ${result.prospect_search.version}`);
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
public class ProspectSearchClient
{
private readonly HttpClient _client;
private readonly string _baseUrl = "https://api.openprospect.io/api/v1";
public ProspectSearchClient(string apiKey)
{
_client = new HttpClient();
_client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
}
public async Task<UpdateResponse> UpdateProspectSearchAsync(
string id,
string? name = null,
string? idealCustomerProfile = null)
{
// Get current version
var current = await _client.GetFromJsonAsync<ProspectSearch>(
$"{_baseUrl}/prospect-searches/{id}");
// Update with version
var updateData = new Dictionary<string, object?>
{
["version"] = current!.Version,
["name"] = name,
["ideal_customer_profile"] = idealCustomerProfile
};
var response = await _client.PatchAsJsonAsync(
$"{_baseUrl}/prospect-searches/{id}",
updateData);
if (response.StatusCode == System.Net.HttpStatusCode.Conflict)
{
throw new InvalidOperationException("Version conflict - re-fetch and retry");
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<UpdateResponse>()!;
}
}
public record ProspectSearch(string Id, string Name, int Version);
public record UpdateResponse(
ProspectSearch ProspectSearch,
string[] UpdatedFields,
string[] StrippedFields);
Response¶
Success (200 OK):
{
"prospect_search": {
"id": "a45e6452-496e-4bb3-86a0-3c99475c0fc3",
"user_id": "b23f5678-e89b-12d3-a456-426614174000",
"name": "Updated Search Name",
"ideal_customer_profile": "B2B SaaS companies with 50-500 employees",
"geo_locations": [{"city": "Berlin", "country": "Germany", "lat": 52.52, "lng": 13.405}],
"search_keywords": ["tech", "startup", "saas"],
"status": "active",
"version": 2,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-16T14:22:00Z"
},
"updated_fields": ["name", "ideal_customer_profile"],
"stripped_fields": []
}
Error Handling¶
Version Conflict (409)¶
Returned when the version in your request doesn't match the current version in the database.
{
"detail": {
"code": "VERSION_CONFLICT",
"message": "ProspectSearch with id a45e6452-496e-4bb3-86a0-3c99475c0fc3 has been modified. Expected version 1, but found version 2. Please refresh and try again.",
"prospect_search_id": "a45e6452-496e-4bb3-86a0-3c99475c0fc3",
"expected_version": 1,
"actual_version": 2
}
}
Resolution: Re-fetch the prospect search to get the current data and version, then retry your update.
Permission Denied (403)¶
Returned when the user lacks can_edit_prospect_searches permission or isn't in the same organization.
{
"detail": {
"code": "PERMISSION_DENIED",
"message": "You do not have permission to edit this prospect search",
"prospect_search_id": "a45e6452-496e-4bb3-86a0-3c99475c0fc3"
}
}
Not Found (404)¶
Returned when the prospect search doesn't exist or has been deleted.
{
"detail": {
"code": "PROSPECT_SEARCH_NOT_FOUND",
"message": "Prospect search with ID a45e6452-496e-4bb3-86a0-3c99475c0fc3 not found",
"prospect_search_id": "a45e6452-496e-4bb3-86a0-3c99475c0fc3"
}
}
Validation Error (422)¶
Returned when the request body contains invalid fields.
{
"detail": {
"code": "VALIDATION_ERROR",
"message": "Invalid fields in update_mask: invalid_field_name",
"field": "update_mask",
"allowed_fields": ["name", "ideal_customer_profile", "geo_locations", "search_keywords"]
}
}
Best Practices¶
Handling Concurrent Updates¶
When multiple users or systems may update the same prospect search, implement retry logic with exponential backoff:
#!/bin/bash
# Retry wrapper for prospect search updates
PROSPECT_SEARCH_ID="a45e6452-496e-4bb3-86a0-3c99475c0fc3"
MAX_RETRIES=3
BACKOFF=0.5
update_with_retry() {
local attempt=0
local updates="$1"
while [ $attempt -lt $MAX_RETRIES ]; do
# Get current version
current=$(curl -s -X GET \
"https://api.openprospect.io/api/v1/prospect-searches/${PROSPECT_SEARCH_ID}" \
-H "Authorization: Bearer ${OPENPROSPECT_API_KEY}")
version=$(echo "$current" | jq -r '.version')
# Attempt update with version
response=$(curl -s -w "\n%{http_code}" -X PATCH \
"https://api.openprospect.io/api/v1/prospect-searches/${PROSPECT_SEARCH_ID}" \
-H "Authorization: Bearer ${OPENPROSPECT_API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"version\": $version, $updates}")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" -eq 200 ]; then
echo "$body"
return 0
elif [ "$http_code" -eq 409 ]; then
attempt=$((attempt + 1))
echo "Version conflict, retrying... (attempt $attempt)" >&2
sleep $(echo "$BACKOFF * $attempt" | bc)
else
echo "Error: HTTP $http_code" >&2
echo "$body" >&2
return 1
fi
done
echo "Failed after $MAX_RETRIES retries" >&2
return 1
}
# Usage
update_with_retry '"name": "Updated Name"'
import httpx
from time import sleep
def update_with_retry(prospect_search_id: str, updates: dict, max_retries: int = 3):
"""Update with automatic retry on version conflict."""
for attempt in range(max_retries):
# Get current state
current = httpx.get(
f"{BASE_URL}/prospect-searches/{prospect_search_id}",
headers={"Authorization": f"Bearer {API_KEY}"}
).json()
# Attempt update
response = httpx.patch(
f"{BASE_URL}/prospect-searches/{prospect_search_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"version": current["version"], **updates}
)
if response.status_code == 200:
return response.json()
elif response.status_code == 409:
print(f"Version conflict, retrying... (attempt {attempt + 1})")
sleep(0.5 * (attempt + 1)) # Exponential backoff
else:
response.raise_for_status()
raise Exception(f"Failed after {max_retries} retries")
async function updateWithRetry(prospectSearchId, updates, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
// Get current state
const currentResponse = await fetch(
`${BASE_URL}/prospect-searches/${prospectSearchId}`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
const current = await currentResponse.json();
// Attempt update
const updateResponse = await fetch(
`${BASE_URL}/prospect-searches/${prospectSearchId}`,
{
method: "PATCH",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ version: current.version, ...updates })
}
);
if (updateResponse.ok) {
return await updateResponse.json();
} else if (updateResponse.status === 409) {
console.log(`Version conflict, retrying... (attempt ${attempt + 1})`);
await new Promise(r => setTimeout(r, 500 * (attempt + 1))); // Exponential backoff
} else {
throw new Error(`HTTP ${updateResponse.status}: ${await updateResponse.text()}`);
}
}
throw new Error(`Failed after ${maxRetries} retries`);
}
// Usage
const result = await updateWithRetry(
"a45e6452-496e-4bb3-86a0-3c99475c0fc3",
{ name: "Updated Name" }
);
interface UpdateResponse {
prospect_search: ProspectSearch;
updated_fields: string[];
stripped_fields: string[];
}
async function updateWithRetry(
prospectSearchId: string,
updates: Record<string, unknown>,
maxRetries = 3
): Promise<UpdateResponse> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
// Get current state
const currentResponse = await fetch(
`${BASE_URL}/prospect-searches/${prospectSearchId}`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
if (!currentResponse.ok) {
throw new Error(`Failed to get current state: ${currentResponse.status}`);
}
const current = await currentResponse.json() as ProspectSearch;
// Attempt update
const updateResponse = await fetch(
`${BASE_URL}/prospect-searches/${prospectSearchId}`,
{
method: "PATCH",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ version: current.version, ...updates })
}
);
if (updateResponse.ok) {
return await updateResponse.json() as UpdateResponse;
} else if (updateResponse.status === 409) {
console.log(`Version conflict, retrying... (attempt ${attempt + 1})`);
await new Promise(r => setTimeout(r, 500 * (attempt + 1)));
} else {
const errorBody = await updateResponse.text();
throw new Error(`HTTP ${updateResponse.status}: ${errorBody}`);
}
}
throw new Error(`Failed after ${maxRetries} retries`);
}
// Usage
const result = await updateWithRetry(
"a45e6452-496e-4bb3-86a0-3c99475c0fc3",
{ name: "Updated Name" }
);
console.log(`New version: ${result.prospect_search.version}`);
public async Task<UpdateResponse> UpdateWithRetryAsync(
string prospectSearchId,
Dictionary<string, object?> updates,
int maxRetries = 3)
{
for (int attempt = 0; attempt < maxRetries; attempt++)
{
// Get current state
var current = await _client.GetFromJsonAsync<ProspectSearch>(
$"{_baseUrl}/prospect-searches/{prospectSearchId}");
// Attempt update with version
var updateData = new Dictionary<string, object?>(updates)
{
["version"] = current!.Version
};
var response = await _client.PatchAsJsonAsync(
$"{_baseUrl}/prospect-searches/{prospectSearchId}",
updateData);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<UpdateResponse>()!;
}
else if (response.StatusCode == System.Net.HttpStatusCode.Conflict)
{
Console.WriteLine($"Version conflict, retrying... (attempt {attempt + 1})");
await Task.Delay(TimeSpan.FromMilliseconds(500 * (attempt + 1)));
}
else
{
response.EnsureSuccessStatusCode();
}
}
throw new InvalidOperationException($"Failed after {maxRetries} retries");
}
// Usage
var result = await client.UpdateWithRetryAsync(
"a45e6452-496e-4bb3-86a0-3c99475c0fc3",
new Dictionary<string, object?> { ["name"] = "Updated Name" }
);
Using Field Masks¶
Only update specific fields by providing update_mask:
{
"version": 1,
"update_mask": ["name"],
"name": "New Name",
"ideal_customer_profile": "This will be ignored because it's not in update_mask"
}
Field Mask Edge Cases¶
| Scenario | Behavior |
|---|---|
update_mask contains field not in request body |
Field is ignored; no update occurs for that field |
update_mask is empty [] |
No fields are updated; returns success with empty updated_fields |
update_mask is omitted |
All non-null fields in the request body are used as the mask |
null value in request body |
Field is set to null (if allowed by schema) |
| Field omitted from request body | Field is not updated (preserves existing value) |
update_mask contains non-editable field |
Returns 422 Validation Error |
update_mask contains admin-only field (non-admin user) |
Field is silently removed; listed in stripped_fields |
Rate Limits¶
| Tier | Requests/Hour |
|---|---|
| Free | 100 |
| Pro | 1,000 |
| Enterprise | Unlimited |
Next Steps¶
- Authentication Guide - Set up API key authentication
- Error Handling - Complete error reference
- Client Integration - Full CRM sync workflow