Skip to content

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
  1. Get current state - Retrieve the prospect search to get the current version
  2. Send PATCH request - Include version and only the fields you want to update
  3. 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

Partially update a prospect search with optimistic locking.

Request

PATCH /api/v1/prospect-searches/{prospect_search_id}

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