API Authentication¶
Secure access to the OpenProspect API using API keys for external integrations or OAuth 2.0 for internal tools.
🔑 Which Authentication Method Should I Use?¶
| Use Case | Method | Best For |
|---|---|---|
| Server-to-server integration | API Keys | External developers, automation, production systems |
| Web application (internal) | OAuth 2.0 + PKCE | Internal tools, admin dashboards |
| CLI / Development | OAuth 2.0 + PKCE | Developer tools, local testing |
| CI/CD pipelines | API Keys | Automated workflows, deployment scripts |
For external developers: Use API Keys - they're simpler, more secure for server-to-server use, and don't require user interaction.
🚀 Quick Start: API Keys¶
1. Get Your API Key¶
Contact your OpenProspect account manager or create an API key in the dashboard:
- Log in to app.openprospect.io
- Go to Settings → Developer → API Keys
- Click Create API Key
- Select the required scopes and save the key securely
Alternatively, use the CLI:
CLI Tool Name
The OpenProspect CLI is currently named lince for backward compatibility.
# Create an API key
lince api-keys create "Production Integration" \
--scopes companies:read deliveries:write \
--tier pro
# Output:
# ✓ API Key created successfully!
#
# Key: lnc_live_A3mK9pX2vL8hQ5nR7zT1wY4jB6cE0dF
# Name: Production Integration
# Scopes: companies:read, deliveries:write
#
# ⚠️ IMPORTANT: Store this key securely. It will not be shown again!
Key Format:
- Production:
lnc_live_...(32-character base62) - Testing:
lnc_test_...(32-character base62)
The prefix prevents accidental use of test keys in production.
2. Store Your API Key Securely¶
# Environment variable (recommended)
export OPENPROSPECT_API_KEY="lnc_live_A3mK9pX2vL8hQ5nR7zT1wY4jB6cE0dF"
# Or use a .env file (never commit to git!)
echo "OPENPROSPECT_API_KEY=lnc_live_..." >> .env
3. Make Your First API Call¶
import os
import httpx
api_key = os.getenv("OPENPROSPECT_API_KEY")
response = httpx.get(
"https://api.openprospect.io/api/v1/companies",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
},
params={"limit": 10}
)
companies = response.json()
print(f"Found {len(companies['items'])} companies")
const apiKey = process.env.OPENPROSPECT_API_KEY;
const response = await fetch(
"https://api.openprospect.io/api/v1/companies?limit=10",
{
method: "GET",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
}
}
);
const companies = await response.json();
console.log(`Found ${companies.items.length} companies`);
const apiKey = process.env.OPENPROSPECT_API_KEY!;
interface CompaniesResponse {
items: Array<{id: string; name: string}>;
total: number;
}
const response = await fetch(
"https://api.openprospect.io/api/v1/companies?limit=10",
{
method: "GET",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
}
}
);
const companies: CompaniesResponse = await response.json();
console.log(`Found ${companies.items.length} companies`);
using System.Net.Http.Headers;
var apiKey = Environment.GetEnvironmentVariable("OPENPROSPECT_API_KEY");
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
var response = await client.GetAsync(
"https://api.openprospect.io/api/v1/companies?limit=10");
var companies = await response.Content.ReadFromJsonAsync<CompaniesResponse>();
Console.WriteLine($"Found {companies.Items.Count} companies");
📋 API Key Management¶
Validating Your API Key¶
Check if your API key is valid without making a production API call:
Expected Response:
{
"valid": true,
"organization_id": "org_a1b2c3d4",
"scopes": ["companies:read", "deliveries:write"],
"tier": "pro",
"expires_at": null
}
Rotating API Keys¶
Important: API keys do NOT auto-refresh like JWT tokens. They are long-lived credentials that must be manually rotated.
When to rotate:
- Every 90 days (minimum security policy)
- Immediately if compromised
- When changing team members with access
- Before decommissioning old systems
How to rotate:
import os
import httpx
api_key = os.getenv("OPENPROSPECT_API_KEY")
key_id = "key_abc123"
response = httpx.post(
f"https://api.openprospect.io/api/v1/api-keys/{key_id}/rotate",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
)
if response.status_code == 200:
new_key_data = response.json()
print(f"✓ New API key: {new_key_data['key']}")
print("⚠️ Update your environment variables immediately!")
print(f"✓ Old key revoked: {new_key_data['revoked_key_id']}")
const apiKey = process.env.OPENPROSPECT_API_KEY;
const keyId = "key_abc123";
const response = await fetch(
`https://api.openprospect.io/api/v1/api-keys/${keyId}/rotate`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
}
}
);
if (response.ok) {
const newKeyData = await response.json();
console.log(`✓ New API key: ${newKeyData.key}`);
console.log("⚠️ Update your environment variables immediately!");
}
const apiKey = process.env.OPENPROSPECT_API_KEY!;
const keyId = "key_abc123";
interface KeyRotationResponse {
key: string;
key_id: string;
revoked_key_id: string;
created_at: string;
}
const response = await fetch(
`https://api.openprospect.io/api/v1/api-keys/${keyId}/rotate`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
}
}
);
if (response.ok) {
const newKeyData: KeyRotationResponse = await response.json();
console.log(`✓ New API key: ${newKeyData.key}`);
console.log("⚠️ Update your environment variables immediately!");
}
var apiKey = Environment.GetEnvironmentVariable("OPENPROSPECT_API_KEY");
var keyId = "key_abc123";
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
var response = await client.PostAsync(
$"https://api.openprospect.io/api/v1/api-keys/{keyId}/rotate",
null);
if (response.IsSuccessStatusCode)
{
var newKeyData = await response.Content
.ReadFromJsonAsync<KeyRotationResponse>();
Console.WriteLine($"✓ New API key: {newKeyData.Key}");
Console.WriteLine("⚠️ Update your environment variables immediately!");
}
Rotation Response:
{
"key": "lnc_live_9XyZ2wQ5pL8vM3nK7hR1tA6jB4cF0dG",
"key_id": "key_new789",
"name": "Production Integration",
"scopes": ["companies:read", "deliveries:write"],
"revoked_key_id": "key_abc123",
"created_at": "2025-11-30T12:00:00Z"
}
Best Practice: Keep the old key active for 24-48 hours during rotation to prevent service disruption. Update all systems, then revoke the old key.
Revoking API Keys¶
Immediately deactivate a compromised or unused API key:
# Revoke a specific key
curl -X DELETE "https://api.openprospect.io/api/v1/api-keys/${KEY_ID}" \
-H "Authorization: Bearer ${OPENPROSPECT_API_KEY}"
Effect is immediate - the key stops working instantly.
🔐 Scopes and Permissions¶
API keys use scope-based permissions for fine-grained access control.
Available Scopes¶
| Scope | Permissions | Typical Use Case |
|---|---|---|
companies:read |
List and view company data | Read-only integrations |
companies:write |
Update company records | Data enrichment services |
prospects:read |
Access prospect information | Lead management systems |
deliveries:read |
Check delivery status and history | Monitoring dashboards |
deliveries:write |
Configure webhook destinations | Integration setup |
Scope Enforcement¶
Insufficient scopes return 403 Forbidden:
{
"error": {
"code": "insufficient_scope",
"message": "Insufficient permissions. Required scope: deliveries:write",
"required_scope": "deliveries:write",
"granted_scopes": ["companies:read", "prospects:read"]
}
}
Example: Scope-Restricted Request¶
# This will fail with 403 if API key lacks 'deliveries:write' scope
response = httpx.post(
"https://api.openprospect.io/api/v1/deliveries/123/destinations", # noqa: MD034
headers={"Authorization": f"Bearer {api_key}"},
json={"url": "https://your-app.com/webhooks/leads"} # noqa: MD034
)
if response.status_code == 403:
error = response.json()["error"]
print(f"❌ Missing scope: {error['required_scope']}")
print(f"You have: {error['granted_scopes']}")
const response = await fetch(
"https://api.openprospect.io/api/v1/deliveries/123/destinations", // noqa: MD034
{
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
url: "https://your-app.com/webhooks/leads" // noqa: MD034
})
}
);
if (response.status === 403) {
const error = (await response.json()).error;
console.log(`❌ Missing scope: ${error.required_scope}`);
console.log(`You have: ${error.granted_scopes}`);
}
🔒 Security Best Practices¶
1. Storage¶
✅ DO:
- Store in environment variables
- Use secret management systems (AWS Secrets Manager, HashiCorp Vault, 1Password)
- Encrypt at rest in configuration files
- Use different keys for development, staging, and production
❌ DON'T:
- Hard-code in source code
- Commit to version control (add
*.envto.gitignore) - Share keys between team members
- Log keys in plain text
- Send keys via email or Slack
2. Rotation Policy¶
Recommended schedule:
- Minimum: Every 90 days
- Recommended: Every 30 days for production systems
- Immediately: If compromised, team member leaves, or system decommissioned
3. Scope Principle of Least Privilege¶
Only grant the minimum scopes needed:
# Good: Only read access for monitoring
lince api-keys create "Monitoring Dashboard" \
--scopes companies:read deliveries:read
# Bad: Overly permissive for monitoring
lince api-keys create "Monitoring Dashboard" \
--scopes companies:write deliveries:write prospects:write
4. Environment Separation¶
Always use the correct environment:
# Development
OPENPROSPECT_API_KEY="lnc_test_..."
OPENPROSPECT_API_URL="http://localhost:8000/api/v1"
# Production
OPENPROSPECT_API_KEY="lnc_live_..."
OPENPROSPECT_API_URL="https://api.openprospect.io/api/v1"
Test keys (lnc_test_) work only against test environments. Production keys (lnc_live_) prevent accidental test key usage in production.
🚨 Error Handling¶
Common Authentication Errors¶
| Status | Error Code | Cause | Solution |
|---|---|---|---|
| 401 | invalid_api_key |
Invalid, expired, or revoked key | Validate key, check expiration, generate new key |
| 401 | missing_authentication |
No Authorization header | Add Authorization: Bearer <key> header |
| 403 | insufficient_scope |
API key lacks required scope | Request scope or use key with proper permissions |
| 429 | rate_limit_exceeded |
Too many requests | Implement exponential backoff, upgrade tier |
Security Note: All authentication failures return generic messages to prevent enumeration attacks. The API does NOT distinguish between "key doesn't exist" vs "key is revoked" vs "key is expired".
Error Handling Examples¶
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10)
)
def make_api_request(endpoint: str, api_key: str):
response = httpx.get(
f"https://api.openprospect.io/api/v1/{endpoint}",
headers={"Authorization": f"Bearer {api_key}"}
)
# Handle authentication errors
if response.status_code == 401:
error = response.json().get("error", {})
raise AuthenticationError(
f"Authentication failed: {error.get('message')}"
)
# Handle authorization errors
if response.status_code == 403:
error = response.json().get("error", {})
raise PermissionError(
f"Insufficient permissions: {error.get('required_scope')}"
)
# Handle rate limiting
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
raise RateLimitError(f"Rate limited. Retry after {retry_after}s")
response.raise_for_status()
return response.json()
async function makeAPIRequest(endpoint, apiKey) {
const response = await fetch(
`https://api.openprospect.io/api/v1/${endpoint}`,
{
headers: {
"Authorization": `Bearer ${apiKey}`
}
}
);
if (response.status === 401) {
const error = (await response.json()).error;
throw new Error(`Authentication failed: ${error.message}`);
}
if (response.status === 403) {
const error = (await response.json()).error;
throw new Error(`Insufficient permissions: ${error.required_scope}`);
}
if (response.status === 429) {
const retryAfter = response.headers.get("Retry-After") || "60";
throw new Error(`Rate limited. Retry after ${retryAfter}s`);
}
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
return await response.json();
}
class APIError extends Error {
constructor(
message: string,
public statusCode: number,
public errorCode?: string
) {
super(message);
}
}
async function makeAPIRequest<T>(
endpoint: string,
apiKey: string
): Promise<T> {
const response = await fetch(
`https://api.openprospect.io/api/v1/${endpoint}`,
{
headers: {
"Authorization": `Bearer ${apiKey}`
}
}
);
if (response.status === 401) {
const error = (await response.json()).error;
throw new APIError(
`Authentication failed: ${error.message}`,
401,
error.code
);
}
if (response.status === 403) {
const error = (await response.json()).error;
throw new APIError(
`Insufficient permissions: ${error.required_scope}`,
403,
error.code
);
}
if (response.status === 429) {
const retryAfter = response.headers.get("Retry-After") || "60";
throw new APIError(
`Rate limited. Retry after ${retryAfter}s`,
429,
"rate_limit_exceeded"
);
}
if (!response.ok) {
throw new APIError(`API error: ${response.statusText}`, response.status);
}
return await response.json();
}
public class APIClient
{
private readonly HttpClient _client;
public async Task<T> MakeAPIRequest<T>(string endpoint, string apiKey)
{
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
var response = await _client.GetAsync(
$"https://api.openprospect.io/api/v1/{endpoint}");
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
var error = await response.Content
.ReadFromJsonAsync<ErrorResponse>();
throw new AuthenticationException(
$"Authentication failed: {error.Error.Message}");
}
if (response.StatusCode == HttpStatusCode.Forbidden)
{
var error = await response.Content
.ReadFromJsonAsync<ErrorResponse>();
throw new UnauthorizedAccessException(
$"Insufficient permissions: {error.Error.RequiredScope}");
}
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
var retryAfter = response.Headers.RetryAfter?.Delta?.TotalSeconds ?? 60;
throw new RateLimitException(
$"Rate limited. Retry after {retryAfter}s");
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<T>();
}
}
🔄 Migration: OAuth → API Keys¶
If you're currently using OAuth/JWT tokens and want to switch to API keys:
Key Differences¶
| Feature | OAuth/JWT | API Keys |
|---|---|---|
| Lifetime | 1 hour (auto-refreshed) | Long-lived (manual rotation) |
| Refresh | Automatic with refresh token | No refresh - manually rotate |
| Use Case | User-facing applications | Server-to-server integrations |
| Headers | Authorization: Bearer <jwt> |
Authorization: Bearer lnc_live_... |
| Scopes | All internal scopes | Restricted external scopes |
Migration Steps¶
- Create API key with required scopes
- Update authentication code - headers remain the same!
- Remove refresh logic - not needed for API keys
- Implement rotation schedule - every 90 days minimum
- Test thoroughly before revoking OAuth credentials
🌐 Internal Authentication (OAuth 2.0 + PKCE)¶
For internal tools only - External developers should use API keys.
The OpenProspect CLI and admin dashboard use OAuth 2.0 with PKCE for user authentication.
CLI Authentication¶
# Login (opens browser for OAuth flow)
lince auth login
# Token saved to ~/.lince/auth_token.json
cat ~/.lince/auth_token.json | jq .access_token
# Use in API calls
export OPENPROSPECT_API_KEY=$(cat ~/.lince/auth_token.json | jq -r .access_token)
curl -H "Authorization: Bearer $OPENPROSPECT_API_KEY" https://api.openprospect.io/api/v1/users/me
OAuth 2.0 is available for internal tool development. For external integrations, API keys are the recommended authentication method.
🚀 Next Steps¶
With authentication configured:
- API Quick Start - Make your first authenticated API calls
- Delivery Integration - Set up webhook or API delivery
- Error Reference - Handle API errors gracefully
💡 Pro Tips¶
- Test keys first: Use
lnc_test_keys against test environments before production - Monitor expiration: Set up alerts 30 days before key expiration
- Rotate regularly: Automate key rotation every 30-90 days
- Use minimal scopes: Grant only what's needed
- Separate keys per service: Don't share one key across multiple services
- Version your integration: Include version in User-Agent header
Questions? Check our Error Reference or contact support.