Skip to content

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:

  1. Log in to app.openprospect.io
  2. Go to SettingsDeveloperAPI Keys
  3. Click Create API Key
  4. 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

curl -X GET "https://api.openprospect.io/api/v1/companies?limit=10" \
  -H "Authorization: Bearer ${OPENPROSPECT_API_KEY}" \
  -H "Content-Type: application/json"
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:

--8<-- "authentication/api-key-auth.sh"
--8<-- "authentication/api-key-auth.py"
--8<-- "authentication/api-key-auth.js"
--8<-- "authentication/api-key-auth.ts"
--8<-- "authentication/api-key-auth.cs"

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:

# Rotate key (generates new key, revokes old one)
curl -X POST "https://api.openprospect.io/api/v1/api-keys/${KEY_ID}/rotate" \
  -H "Authorization: Bearer ${OPENPROSPECT_API_KEY}" \
  -H "Content-Type: application/json"
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 *.env to .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

  1. Create API key with required scopes
  2. Update authentication code - headers remain the same!
  3. Remove refresh logic - not needed for API keys
  4. Implement rotation schedule - every 90 days minimum
  5. 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:

  1. API Quick Start - Make your first authenticated API calls
  2. Delivery Integration - Set up webhook or API delivery
  3. Error Reference - Handle API errors gracefully

💡 Pro Tips

  1. Test keys first: Use lnc_test_ keys against test environments before production
  2. Monitor expiration: Set up alerts 30 days before key expiration
  3. Rotate regularly: Automate key rotation every 30-90 days
  4. Use minimal scopes: Grant only what's needed
  5. Separate keys per service: Don't share one key across multiple services
  6. Version your integration: Include version in User-Agent header

Questions? Check our Error Reference or contact support.