StartCap.org Logo - Business Funding and Partner Program

StartCap Partners API Documentation

Version: 1.1.0 | Last Updated:

The StartCap Partners API allows you to programmatically submit leads, track referrals, and manage your partner account. This RESTful API uses JSON for request and response payloads and requires API key authentication. Integrate seamlessly with our partner program to automate lead submission, track referral performance, manage commissions, and access comprehensive analytics.

Base URL: All endpoints are prefixed with https://www.startcap.org/app/partners/api/v1/

Authentication

All API requests require authentication using an API key. You can obtain your API key from the API Integration page in your partner dashboard.

API Key Header

Include your API key in the request header (recommended method):

X-API-Key: your_api_key_here

API Key Parameter

Alternatively, you can pass the API key as a POST parameter:

api_key=your_api_key_here

Security Note: Keep your API key secure and never expose it in client-side code or public repositories. If your API key is compromised, rotate it immediately from your dashboard.

Request Format

All POST and PUT requests must send JSON data in the request body with the appropriate Content-Type header.

Content-Type Header

You must include the Content-Type: application/json header in all POST and PUT requests:

Content-Type: application/json

Request Body

JSON data should be sent in the request body (not as URL parameters or form data). For example:

{
  "first_name": "John",
  "last_name": "Doe",
  "email": "john@example.com",
  "phone": "5551234567"
}

Note: GET requests do not require a request body. Query parameters should be included in the URL for filtering and pagination.

Base URL

All API endpoints are prefixed with:

https://www.startcap.org/app/partners/api/v1/

For example, to submit a lead, the full URL would be:

https://www.startcap.org/app/partners/api/v1/leads/submit

Rate Limiting

API requests are limited to 1,000 requests per hour per API key. Rate limits are applied on a per-API-key basis.

Rate Limit Headers

Every API response includes rate limit information in the headers:

Header Description
X-RateLimit-Limit Maximum number of requests allowed per hour (1000)
X-RateLimit-Remaining Number of requests remaining in the current hour
X-RateLimit-Reset Unix timestamp when the rate limit resets

Rate Limit Exceeded

When you exceed the rate limit, you'll receive a 429 Too Many Requests status code with the following response:

{
  "error": "Rate limit exceeded"
}

Wait until the rate limit resets before making additional requests. The dev endpoint (/leads/validate) does not count against rate limits.

Data Models

Lead Model

The table below documents the fields you can include when submitting a lead via POST /leads/submit or POST /leads/validate. Required fields must be included in every submission; conditional and optional fields may be omitted.

Identifiers

All leads and referrals are identified by a reference_id in the format YYYYMMDD-id (e.g. 20240115-123). This ID is returned when you submit a lead and included in all API responses and webhook payloads. Use it for API lookups, display, and support tickets.

ID Lookup: All GET /leads/{id} and GET /referrals/{id} endpoints require the reference_id format (e.g. 20240115-123).
Field Type Required Description Constraints
first_name string Yes Lead's first name 2-50 characters, letters and spaces only
last_name string Yes Lead's last name 2-50 characters, letters and spaces only
date_of_birth string (date) Yes Lead's date of birth Format: YYYY-MM-DD (e.g. 1985-03-15)
email string (email) Conditional Lead's email address. Required when your account is set to "StartCap fully manages onboarding and funding"; optional otherwise. Valid email format, max 255 characters
phone string Conditional Lead's phone number. Required when your account is set to "StartCap fully manages onboarding and funding"; optional otherwise. Exactly 10 digits (digits only or formatted)
company_name string No Company name (if applicable) Max 100 characters
residential_street string Yes Street address (residential) Max 255 characters
residential_city string Yes City (residential) Max 100 characters
residential_state string Yes State (residential) 2-letter US state code (e.g. CA, NY, DC)
residential_zip string Yes ZIP code (residential) 5 or 9 digits (e.g. 12345 or 12345-6789)
notes string No Additional notes about the lead Max 1000 characters
funding_type string No Funding type for the lead. When provided, must be one of the platform's currently accepted (active) funding types; otherwise the request is rejected. When provided: must match an active funding type (see admin settings). Max 100 characters. Omit if not applicable.
utm_source string No UTM source (e.g. website name, channel). Use to attribute the lead to a specific source when you have multiple sites or campaigns. Max 255 characters. Omit if not applicable.
utm_medium string No UTM medium (e.g. email, cpc, referral). Max 255 characters. Omit if not applicable.
utm_campaign string No UTM campaign name. Max 255 characters. Omit if not applicable.
utm_term string No UTM term (e.g. paid search keyword). Max 255 characters. Omit if not applicable.
utm_content string No UTM content (e.g. ad variant). Max 255 characters. Omit if not applicable.

Lead Status (11-stage pipeline)

Lead state is represented by the pipeline status (11-stage flow). In API responses, use application_status when present, or effective_status, which is the canonical pipeline status for display and logic.

Pipeline status (application_status / effective_status) – One of the 11 values below. When a lead is not yet linked to an application, application_status may be null and effective_status falls back to the referral-level status.

Pipeline values: click, lead, in_underwriting, need_info, not_approved, offer_ready, call_scheduled, sign_docs, processing, in_funding, funded.

Display labels: Click, Lead, In Underwriting, Need Info, Declined, Offer Emailed, Call Scheduled, Agreement Sent, Processing, In Funding, Funded.

Coarse status (status) – Legacy referral-level field. Used only for the GET /leads query filter; accepted values for that parameter are click, lead, application, funded. Prefer effective_status for display and filtering in your application.

Quotes

When retrieving a lead via GET /leads/{id}, the response may include a quotes array if quotes exist for that lead. Each quote object contains:

Field Type Description
quote_type string The funding type for this quote (e.g. "Term Loan", "Line of Credit")
amount number (nullable) The quote amount in USD. May be null if the quote is still pending.
status string Quote status: pending, sent, received, declined, not_approved
quote_group_status string Overall quote group status: waiting (quotes being prepared), ready (ready to send), sent (sent to applicant)

Referral Model

A referral represents a tracked referral event. The following table describes all available fields:

Field Type Required Description
email string (email) Yes Email of the referred user
utm_source string No UTM source parameter for tracking
utm_medium string No UTM medium parameter for tracking
utm_campaign string No UTM campaign parameter for tracking

Endpoints

POST /leads/validate

Dev Endpoint: Validate lead data without saving to database. Perfect for testing your integration before going live.

Full URL:

Features:
  • Validates all required and optional fields
  • Returns detailed validation errors and warnings
  • Does not save data to database
  • Does not count against rate limits
  • Does not log requests
Request Body:

Required: first_name, last_name, date_of_birth, residential_street, residential_city, residential_state, residential_zip. Optional: email, phone, company_name, notes, funding_type, utm_source, utm_medium, utm_campaign, utm_term, utm_content. If funding_type is provided, it must be an accepted funding type or validation fails.

{
  "first_name": "John",
  "last_name": "Doe",
  "date_of_birth": "1985-03-15",
  "email": "john@example.com",
  "phone": "5551234567",
  "company_name": "Example Corp",
  "residential_street": "123 Main St",
  "residential_city": "Los Angeles",
  "residential_state": "CA",
  "residential_zip": "90001",
  "notes": "Optional notes",
  "funding_type": "Term Loan",
  "utm_source": "my-website",
  "utm_medium": "api",
  "utm_campaign": "q1-leads"
}
Response (Valid) - HTTP 200:
{
  "valid": true,
  "message": "Lead data is valid",
  "errors": [],
  "warnings": [],
  "field_validations": {
    "first_name": { "field": "first_name", "valid": true, "errors": [], "warnings": [] },
    "last_name": { "field": "last_name", "valid": true, "errors": [], "warnings": [] },
    "email": { "field": "email", "valid": true, "errors": [], "warnings": [] },
    "phone": { "field": "phone", "valid": true, "errors": [], "warnings": [] },
    "residential_street": { "field": "residential_street", "valid": true, "errors": [], "warnings": [] },
    "residential_city": { "field": "residential_city", "valid": true, "errors": [], "warnings": [] },
    "residential_state": { "field": "residential_state", "valid": true, "errors": [], "warnings": [] },
    "residential_zip": { "field": "residential_zip", "valid": true, "errors": [], "warnings": [] }
  },
  "data_received": {
    "first_name": "John",
    "last_name": "Doe",
    "email": "john@example.com",
    "phone": "5551234567",
    "company_name": "Example Corp",
    "residential_street": "123 Main St",
    "residential_city": "Los Angeles",
    "residential_state": "CA",
    "residential_zip": "90001",
    "notes": "Optional notes",
    "funding_type": "Term Loan",
    "utm_source": "my-website",
    "utm_medium": "api",
    "utm_campaign": "q1-leads"
  }
}
Response (Invalid) - HTTP 400:
{
  "valid": false,
  "message": "Lead data has validation errors",
  "errors": [
    "Residential address: State is required",
    "Field 'residential_zip' must be 5 or 9 digits"
  ],
  "warnings": [],
  "field_validations": {
    "residential_state": { "field": "residential_state", "valid": false, "errors": ["This field is required"], "warnings": [] },
    "residential_zip": { "field": "residential_zip", "valid": false, "errors": ["Must be 5 or 9 digits"], "warnings": [] }
  },
  "data_received": { ... }
}

When funding_type is provided but is not an accepted funding type, errors will include: "This funding type is not currently accepted for lead submissions. Please use an allowed funding type or omit funding_type." and field_validations.funding_type will have "valid": false and "errors": ["This funding type is not currently accepted"].

POST /leads/submit

Submit a single lead for processing. The lead will be created and assigned to your partner account.

Full URL: https://www.startcap.org/app/partners/api/v1/leads/submit

Why are email and phone sometimes required?
When your account is set to StartCap fully manages onboarding and funding, StartCap contacts the lead directly by email and phone for onboarding, documents, and status updates. To support that, email and phone are required on lead submission. When your account is set to Partner handles onboarding; StartCap handles funding, you are responsible for communicating with the lead, so email and phone can be optional.

How to change this setting:
In the partner portal, go to Settings. In the Lead submission & handling section, choose either StartCap fully manages onboarding and funding or Partner handles onboarding; StartCap handles funding, then click Save. The API requirement for email/phone (required vs optional) applies immediately after saving.

Request Body:

Required: first_name, last_name, date_of_birth (YYYY-MM-DD), residential_street, residential_city, residential_state (2-letter US state code), residential_zip (5 or 9 digits). email and phone are required when your account is set to StartCap fully manages onboarding and funding (lead handling preference = full_management). When your account is set to Partner handles onboarding; StartCap handles funding (funding_only) or the preference is not set, email and phone are optional. This preference is configured under Settings → Lead submission & handling (/app/partners/settings). Optional: company_name, notes, funding_type, utm_source, utm_medium, utm_campaign, utm_term, utm_content. If funding_type is provided, it must be an accepted funding type or the request is rejected with HTTP 400. See Lead Model for full specifications.

Summary: Required when full_management → email, phone. Optional when funding_only or unset → email, phone.

{
  "first_name": "John",
  "last_name": "Doe",
  "date_of_birth": "1985-03-15",
  "email": "john@example.com",
  "phone": "5551234567",
  "company_name": "Example Corp",
  "residential_street": "123 Main St",
  "residential_city": "Los Angeles",
  "residential_state": "CA",
  "residential_zip": "90001",
  "notes": "Optional notes",
  "funding_type": "Term Loan",
  "utm_source": "my-website",
  "utm_medium": "api",
  "utm_campaign": "q1-leads"
}
Response (Success) - HTTP 200:
{
  "success": true,
  "lead_id": 123,
  "reference_id": "20240115-123"
}

Use reference_id to fetch the lead later: GET /leads/20240115-123

Response (Error) - HTTP 400:

Example – missing required field:

{
  "error": "Residential address: Street is required"
}

When your account uses StartCap fully manages onboarding and funding and email or phone is missing or invalid, the API returns 400 with a JSON body containing: error (string), settings_note (string – where to change the setting in the partner portal), and settings_url (string – URL to Settings). Example – missing email when full_management:

{
  "error": "Field 'email' is required when your account is set to 'StartCap fully manages onboarding and funding'.",
  "settings_note": "You can change this in your partner portal under Settings → Lead submission & handling.",
  "settings_url": "https://www.startcap.org/app/partners/settings"
}

Example – non-accepted funding type:

{
  "error": "This funding type is not currently accepted for lead submissions. Please use an allowed funding type or omit funding_type."
}

GET /leads

Retrieve a list of all leads associated with your partner account. Supports optional filtering by date range and status.

Full URL: https://www.startcap.org/app/partners/api/v1/leads

Query Parameters:
Parameter Type Required Description
start_date string (date) No Filter by start date (format: YYYY-MM-DD)
end_date string (date) No Filter by end date (format: YYYY-MM-DD)
status string No Coarse filter by referral-level status only. Allowed: click, lead, application, funded. Use each lead’s application_status or effective_status for pipeline-stage display or client-side filtering.
Example Request:
GET /app/partners/api/v1/leads?start_date=2024-01-01&end_date=2024-12-31&status=funded
Response (Success) - HTTP 200:
{
  "success": true,
  "leads": [
    {
      "reference_id": "20240115-123",
      "first_name": "John",
      "last_name": "Doe",
      "email": "john@example.com",
      "phone": "5551234567",
      "company_name": "Example Corp",
      "residential_street": "123 Main St",
      "residential_city": "Los Angeles",
      "residential_state": "CA",
      "residential_zip": "90001",
      "status": "funded",
      "application_status": "funded",
      "effective_status": "funded",
      "created_at": "2024-01-15 10:30:00"
    }
  ]
}

The status query param is a coarse filter (click, lead, application, funded). Each lead in the response includes application_status and effective_status for the full 13-stage pipeline; use those for display and for filtering by pipeline stage in your application. Lead objects may include residential_street, residential_city, residential_state, residential_zip when submitted with address data.

GET /leads/{id}

Retrieve detailed information about a specific lead by ID.

Full URL: https://www.startcap.org/app/partners/api/v1/leads/{id}

Path Parameters:
Parameter Type Description
id string The lead reference_id in YYYYMMDD-id format (e.g. 20240115-123).
Example Request:
GET /app/partners/api/v1/leads/20240115-123
Response (Success) - HTTP 200:
{
  "success": true,
  "lead": {
    "reference_id": "20240115-123",
    "first_name": "John",
    "last_name": "Doe",
    "email": "john@example.com",
    "phone": "5551234567",
    "company_name": "Example Corp",
    "residential_street": "123 Main St",
    "residential_city": "Los Angeles",
    "residential_state": "CA",
    "residential_zip": "90001",
    "status": "lead",
    "application_status": null,
    "effective_status": "lead",
    "created_at": "2024-01-15 10:30:00",
    "updated_at": "2024-01-15 10:30:00"
  },
  "quotes": [
    {
      "quote_type": "Term Loan",
      "amount": 50000.00,
      "status": "sent",
      "quote_group_status": "sent"
    }
  ]
}

Note: The quotes array is only included when the lead has one or more quotes. Each quote contains quote_type (funding type), amount (quote amount, may be null if pending), status (quote status: pending, sent, received, declined, not_approved), and quote_group_status (overall group status: waiting, ready, sent).

Response (Not Found) - HTTP 404:
{
  "error": "Lead not found"
}

GET /referrals/{id}

Get details and status of a specific referral.

Full URL: https://www.startcap.org/app/partners/api/v1/referrals/{id}

Path Parameters:
Parameter Type Description
id string The referral reference_id in YYYYMMDD-id format (e.g. 20240115-456).
Example Request:
GET /app/partners/api/v1/referrals/20240115-456

Data Privacy Note: For partner-submitted leads (api_lead, manual_lead), full contact details are returned. For traffic referrals (clicks, organic traffic), limited data is returned for privacy reasons: last_name shows only the initial (e.g. "S."), and email, phone, and address fields are omitted.

Response (Partner Lead) - HTTP 200:
{
  "success": true,
  "referral": {
    "reference_id": "20240115-456",
    "referral_type": "api_lead",
    "first_name": "John",
    "last_name": "Smith",
    "email": "john@example.com",
    "phone": "5551234567",
    "company_name": "Example Corp",
    "status": "lead",
    "application_status": null,
    "effective_status": "lead",
    "created_at": "2024-01-15 10:30:00",
    "updated_at": "2024-01-15 10:30:00"
  }
}
Response (Traffic Referral) - HTTP 200:
{
  "success": true,
  "referral": {
    "reference_id": "20240115-789",
    "referral_type": "traffic",
    "first_name": "Jane",
    "last_name": "D.",
    "company_name": "",
    "status": "click",
    "application_status": null,
    "effective_status": "click",
    "created_at": "2024-01-15 10:30:00",
    "updated_at": "2024-01-15 10:30:00"
  }
}

GET /referrals/stats

Get statistics about your referrals. Optional date range filters available.

Full URL: https://www.startcap.org/app/partners/api/v1/referrals/stats

Query Parameters:
Parameter Type Required Description
start_date string (date) No Filter by start date (format: YYYY-MM-DD)
end_date string (date) No Filter by end date (format: YYYY-MM-DD)
Response (Success) - HTTP 200:
{
  "success": true,
  "stats": {
    "total_referrals": 150,
    "total_clicks": 120,
    "total_leads": 25,
    "total_applications": 10,
    "total_funded": 5,
    "conversion_rate": 0.033
  }
}

GET /referrer/stats

Get overall statistics for your referrer account, including earnings and performance metrics.

Full URL: https://www.startcap.org/app/partners/api/v1/referrer/stats

Response (Success) - HTTP 200:
{
  "success": true,
  "stats": {
    "total_earnings": 5000.00,
    "pending_earnings": 1250.00,
    "paid_earnings": 3750.00,
    "total_referrals": 150,
    "total_funded": 5
  }
}

GET /referrer/commissions

Get detailed commission information for your account, including commission structure and history.

Full URL: https://www.startcap.org/app/partners/api/v1/referrer/commissions

Response (Success) - HTTP 200:
{
  "success": true,
  "commissions": [
    {
      "id": 1,
      "level": 1,
      "percentage": 5.0,
      "description": "Direct referral commission"
    }
  ]
}

GET /referrer/payouts

Get history of all payouts for your account.

Full URL: https://www.startcap.org/app/partners/api/v1/referrer/payouts

Response (Success) - HTTP 200:
{
  "success": true,
  "payouts": [
    {
      "id": 1,
      "amount": 1000.00,
      "status": "completed",
      "processed_at": "2024-01-15 10:30:00"
    }
  ]
}

GET /referrer/downline

Get information about your downline members (partners you've referred).

Full URL: https://www.startcap.org/app/partners/api/v1/referrer/downline

Response (Success) - HTTP 200:
{
  "success": true,
  "downline": [
    {
      "id": 2,
      "name": "Jane Smith",
      "email": "jane@example.com",
      "status": "active",
      "total_referrals": 50
    }
  ]
}

Error Handling

All errors follow a consistent format. The API uses standard HTTP status codes to indicate the type of error.

Error Response Format

All error responses follow this structure:

{
  "error": "Error message here"
}

HTTP Status Codes

Status Code Meaning Description
200 OK Request succeeded
400 Bad Request Invalid request parameters or missing required fields
401 Unauthorized Invalid or missing API key
404 Not Found Resource not found (e.g., lead ID doesn't exist)
405 Method Not Allowed HTTP method not supported for this endpoint
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Server error occurred

Common Error Messages

  • "API key required" - No API key provided in request
  • "Invalid or inactive API key" - API key is invalid or account is inactive
  • "Field 'X' is required" - Required field is missing
  • "Field 'email' must be a valid email address" - Invalid email format
  • "This funding type is not currently accepted for lead submissions. Please use an allowed funding type or omit funding_type." - A funding_type was sent that is not currently accepted for lead submissions; omit it or use an accepted value
  • "Rate limit exceeded" - Too many requests in the current hour
  • "Lead not found" - Lead ID doesn't exist or doesn't belong to your account

Code Examples

Here are complete code examples for submitting leads using the API in common programming languages:

<?php
$ch = curl_init('https://www.startcap.org/app/partners/api/v1/leads/submit');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'first_name' => 'John',
    'last_name' => 'Doe',
    'date_of_birth' => '1985-03-15',
    'email' => 'john@example.com',
    'phone' => '5551234567',
    'company_name' => 'Example Corp',
    'residential_street' => '123 Main St',
    'residential_city' => 'Los Angeles',
    'residential_state' => 'CA',
    'residential_zip' => '90001',
    'notes' => 'Optional notes'
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'X-API-Key: your_api_key_here'
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$data = json_decode($response, true);
if ($httpCode === 200 && isset($data['success'])) {
    echo "Lead submitted successfully. Reference ID: " . $data['reference_id'];
    // Use reference_id (e.g. "20240115-123") to fetch the lead later:
    // GET /app/partners/api/v1/leads/20240115-123
} else {
    echo "Error: " . ($data['error'] ?? 'Unknown error');
}
?>
async function submitLead() {
    const response = await fetch('https://www.startcap.org/app/partners/api/v1/leads/submit', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-API-Key': 'your_api_key_here'
        },
        body: JSON.stringify({
            first_name: 'John',
            last_name: 'Doe',
            date_of_birth: '1985-03-15',
            email: 'john@example.com',
            phone: '5551234567',
            company_name: 'Example Corp',
            residential_street: '123 Main St',
            residential_city: 'Los Angeles',
            residential_state: 'CA',
            residential_zip: '90001',
            notes: 'Optional notes'
        })
    });
    
    const data = await response.json();
    
    if (response.ok && data.success) {
        console.log('Lead submitted successfully. Reference ID:', data.reference_id);
        // Use reference_id (e.g. "20240115-123") to fetch the lead later:
        // GET /app/partners/api/v1/leads/20240115-123
    } else {
        console.error('Error:', data.error || 'Unknown error');
    }
}

submitLead();
import requests
import json

url = 'https://www.startcap.org/app/partners/api/v1/leads/submit'
headers = {
    'Content-Type': 'application/json',
    'X-API-Key': 'your_api_key_here'
}
data = {
    'first_name': 'John',
    'last_name': 'Doe',
    'date_of_birth': '1985-03-15',
    'email': 'john@example.com',
    'phone': '5551234567',
    'company_name': 'Example Corp',
    'residential_street': '123 Main St',
    'residential_city': 'Los Angeles',
    'residential_state': 'CA',
    'residential_zip': '90001',
    'notes': 'Optional notes'
}

response = requests.post(url, json=data, headers=headers)

if response.status_code == 200:
    result = response.json()
    if result.get('success'):
        print(f"Lead submitted successfully. Reference ID: {result['reference_id']}")
        # Use reference_id (e.g. "20240115-123") to fetch the lead later:
        # GET /app/partners/api/v1/leads/20240115-123
    else:
        print(f"Error: {result.get('error', 'Unknown error')}")
else:
    print(f"HTTP Error {response.status_code}: {response.text}")
curl -X POST https://www.startcap.org/app/partners/api/v1/leads/submit \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key_here" \
  -d '{
    "first_name": "John",
    "last_name": "Doe",
    "date_of_birth": "1985-03-15",
    "email": "john@example.com",
    "phone": "5551234567",
    "company_name": "Example Corp",
    "residential_street": "123 Main St",
    "residential_city": "Los Angeles",
    "residential_state": "CA",
    "residential_zip": "90001",
    "notes": "Optional notes"
  }'

Webhooks

Webhooks allow you to receive real-time notifications when events occur in your partner account. Configure webhooks from your API Integration dashboard.

Supported Events

  • Lead Status Changes (lead.status_changed) – When a lead is submitted or moves through the pipeline (includes lead_reference_id, old_application_status, new_application_status, and lead_details).
  • Commission Earned (commission.earned) – When a commission is earned (e.g. from a funded application or downline level).
  • Payout Processed (payout.processed) – When a payout is processed and completed.
  • Account Status Changes (account.status_changed) – When your account status changes (e.g. activation, suspension).

Webhook Payload

Webhook payloads are sent as JSON POST requests to your configured endpoint. Every request has this top-level structure:

  • event (string) – Event type (e.g. lead.status_changed, commission.earned).
  • timestamp (string) – ISO 8601 time when the event was sent.
  • data (object) – Event-specific payload; see Possible fields by event below.

Possible fields by event

Below are the possible fields that may be sent in data for each event type. Fields may be omitted when not applicable (e.g. application_id only when a lead is linked to an application).

lead.status_changed

Sent when a lead’s status changes (new lead submitted, pipeline stage change, or funded).

FieldTypeDescription
lead_reference_idstringLead identifier in YYYYMMDD-id format (e.g. 20240115-123). Use for API calls and display.
old_statusstring or nullPrevious coarse referral-level status (legacy). Pipeline detail is in old_application_status.
new_statusstringCurrent coarse referral-level status (legacy). Pipeline detail is in new_application_status.
old_application_statusstring or nullPrevious pipeline status (see 11-stage list below).
new_application_statusstring or nullCurrent pipeline status (e.g. in_underwriting, reviewing, funded).
lead_detailsobjectSnapshot of the lead; see sub-fields below.
testbooleanPresent and true when this is a test webhook from the dashboard.

lead_details may contain:

FieldTypeDescription
reference_idstringLead identifier in YYYYMMDD-id format (e.g. 20240115-123).
emailstringLead email.
statusstringCoarse referral-level (legacy). Use application_status for pipeline.
application_statusstring or nullCurrent pipeline status (11-stage).
first_namestringOptional; e.g. on new lead submit.
last_namestringOptional; e.g. on new lead submit.

Pipeline status values (11-stage): click, lead, in_underwriting, need_info, not_approved, offer_ready, call_scheduled, sign_docs, processing, in_funding, funded.

commission.earned

Sent when a commission is earned (e.g. from a funded application or level-based distribution).

FieldTypeDescription
commission_idstringOptional; e.g. referralRefId_level for level-based.
referral_reference_idstringReferral/lead reference ID (YYYYMMDD-id format) that generated the commission.
amountnumberCommission amount.
levelintegerOptional; downline level (1, 2, 3) for multi-level commissions.
breakdownarrayOptional; per-level or component breakdown of the commission.
referral_detailsobject or nullOptional; reference_id, email, status of the referral.

payout.processed

Sent when a payout is processed and completed.

FieldTypeDescription
payout_idinteger/stringPayout record ID.
amountnumberPayout amount.
statusstringE.g. paid.
processed_atstringISO 8601 time when the payout was processed.

account.status_changed

Sent when your partner account status changes (e.g. activation, suspension).

FieldTypeDescription
referrer_idintegerYour referrer/partner ID.
old_statusstringPrevious account status.
new_statusstringCurrent account status.
account_detailsobjectid, email, status.

Example payload (lead.status_changed)

{
  "event": "lead.status_changed",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "lead_reference_id": "20240115-123",
    "old_status": "lead",
    "new_status": "application",
    "old_application_status": "lead",
    "new_application_status": "in_underwriting",
    "lead_details": {
      "reference_id": "20240115-123",
      "email": "lead@example.com",
      "status": "application",
      "application_status": "in_underwriting"
    }
  }
}

Webhook Security

All webhook requests include a signature header that you can verify to ensure the request is authentic. The signature is generated using HMAC-SHA256 with your webhook secret.

Code Examples

Below are complete, ready-to-use webhook receiver examples in popular programming languages. Each example includes signature verification, event handling, and proper error responses.

const express = require('express');
const crypto = require('crypto');
const app = express();

// Your webhook secret from the API Integration dashboard
const WEBHOOK_SECRET = 'your_webhook_secret_here';

app.use(express.json({ verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
}}));

app.post('/webhook', (req, res) => {
    const signature = req.headers['x-webhook-signature'];
    const eventType = req.headers['x-webhook-event'];
    
    if (!signature) {
        return res.status(401).json({ error: 'Missing signature' });
    }
    
    // Verify signature using raw body
    const expectedSignature = crypto
        .createHmac('sha256', WEBHOOK_SECRET)
        .update(req.rawBody)
        .digest('hex');
    
    if (signature !== expectedSignature) {
        return res.status(401).json({ error: 'Invalid signature' });
    }
    
    // Parse payload
    const { event, timestamp, data } = req.body;
    
    console.log(`Received ${event} at ${timestamp}`);
    
    // Handle different event types
    switch (event) {
        case 'lead.status_changed':
            console.log(`Lead ${data.lead_reference_id} changed from ${data.old_application_status || data.old_status} to ${data.new_application_status || data.new_status}`);
            // Add your business logic here
            break;
        case 'commission.earned':
            console.log(`Commission earned: $${data.amount}`);
            // Add your business logic here
            break;
        case 'payout.processed':
            console.log(`Payout processed: $${data.amount}`);
            // Add your business logic here
            break;
        case 'account.status_changed':
            console.log(`Account status changed from ${data.old_status} to ${data.new_status}`);
            // Add your business logic here
            break;
        default:
            console.log(`Unknown event type: ${event}`);
    }
    
    // Always return 200 OK to acknowledge receipt
    res.status(200).json({ received: true });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Webhook server running on port ${PORT}`);
});
from flask import Flask, request, jsonify
import hmac
import hashlib
import json

app = Flask(__name__)

# Your webhook secret from the API Integration dashboard
WEBHOOK_SECRET = 'your_webhook_secret_here'

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Webhook-Signature')
    event_type = request.headers.get('X-Webhook-Event')
    
    if not signature:
        return jsonify({'error': 'Missing signature'}), 401
    
    # Get raw body for signature verification
    payload = request.get_data()
    
    # Verify signature
    expected_signature = hmac.new(
        WEBHOOK_SECRET.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()
    
    if not hmac.compare_digest(signature, expected_signature):
        return jsonify({'error': 'Invalid signature'}), 401
    
    # Parse payload
    data = request.get_json()
    event = data.get('event')
    timestamp = data.get('timestamp')
    event_data = data.get('data', {})
    
    print(f"Received {event} at {timestamp}")
    
    # Handle different event types
    if event == 'lead.status_changed':
        print(f"Lead {event_data.get('lead_reference_id')} changed from {event_data.get('old_application_status') or event_data.get('old_status')} to {event_data.get('new_application_status') or event_data.get('new_status')}")
        # Add your business logic here
    elif event == 'commission.earned':
        print(f"Commission earned: ${event_data['amount']}")
        # Add your business logic here
    elif event == 'payout.processed':
        print(f"Payout processed: ${event_data['amount']}")
        # Add your business logic here
    elif event == 'account.status_changed':
        print(f"Account status changed from {event_data['old_status']} to {event_data['new_status']}")
        # Add your business logic here
    else:
        print(f"Unknown event type: {event}")
    
    # Always return 200 OK to acknowledge receipt
    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(port=3000, debug=True)
<?php
// Your webhook secret from the API Integration dashboard
$webhook_secret = 'your_webhook_secret_here';

// Get headers
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$event_type = $_SERVER['HTTP_X_WEBHOOK_EVENT'] ?? '';

if (empty($signature)) {
    http_response_code(401);
    header('Content-Type: application/json');
    echo json_encode(['error' => 'Missing signature']);
    exit;
}

// Get raw body for signature verification
$payload = file_get_contents('php://input');
$data = json_decode($payload, true);

if (json_last_error() !== JSON_ERROR_NONE) {
    http_response_code(400);
    header('Content-Type: application/json');
    echo json_encode(['error' => 'Invalid JSON']);
    exit;
}

// Verify signature
$expected_signature = hash_hmac('sha256', $payload, $webhook_secret);

if (!hash_equals($expected_signature, $signature)) {
    http_response_code(401);
    header('Content-Type: application/json');
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Process webhook
$event = $data['event'] ?? '';
$timestamp = $data['timestamp'] ?? '';
$event_data = $data['data'] ?? [];

error_log("Received {$event} at {$timestamp}");

// Handle different event types
switch ($event) {
    case 'lead.status_changed':
        error_log("Lead " . $event_data['lead_reference_id'] . " changed from " . ($event_data['old_application_status'] ?? $event_data['old_status']) . " to " . ($event_data['new_application_status'] ?? $event_data['new_status']));
        // Add your business logic here
        break;
    case 'commission.earned':
        error_log("Commission earned: $" . $event_data['amount']);
        // Add your business logic here
        break;
    case 'payout.processed':
        error_log("Payout processed: $" . $event_data['amount']);
        // Add your business logic here
        break;
    case 'account.status_changed':
        error_log("Account status changed from {$event_data['old_status']} to {$event_data['new_status']}");
        // Add your business logic here
        break;
    default:
        error_log("Unknown event type: {$event}");
}

// Always return 200 OK to acknowledge receipt
http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['received' => true]);
?>
require 'sinatra'
require 'json'
require 'openssl'
require 'rack'

# Your webhook secret from the API Integration dashboard
WEBHOOK_SECRET = 'your_webhook_secret_here'

post '/webhook' do
    signature = request.env['HTTP_X_WEBHOOK_SIGNATURE']
    event_type = request.env['HTTP_X_WEBHOOK_EVENT']
    
    if signature.nil? || signature.empty?
        status 401
        return { error: 'Missing signature' }.to_json
    end
    
    # Get raw body for signature verification
    payload = request.body.read
    request.body.rewind
    
    # Verify signature
    expected_signature = OpenSSL::HMAC.hexdigest(
        OpenSSL::Digest.new('sha256'),
        WEBHOOK_SECRET,
        payload
    )
    
    unless Rack::Utils.secure_compare(signature, expected_signature)
        status 401
        return { error: 'Invalid signature' }.to_json
    end
    
    # Parse payload
    data = JSON.parse(payload)
    event = data['event']
    timestamp = data['timestamp']
    event_data = data['data'] || {}
    
    puts "Received #{event} at #{timestamp}"
    
    # Handle different event types
    case event
    when 'lead.status_changed'
        puts "Lead #{event_data['lead_reference_id']} changed from #{event_data['old_application_status'] || event_data['old_status']} to #{event_data['new_application_status'] || event_data['new_status']}"
        # Add your business logic here
    when 'commission.earned'
        puts "Commission earned: $#{event_data['amount']}"
        # Add your business logic here
    when 'payout.processed'
        puts "Payout processed: $#{event_data['amount']}"
        # Add your business logic here
    when 'account.status_changed'
        puts "Account status changed from #{event_data['old_status']} to #{event_data['new_status']}"
        # Add your business logic here
    else
        puts "Unknown event type: #{event}"
    end
    
    # Always return 200 OK to acknowledge receipt
    status 200
    { received: true }.to_json
end
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)

// Your webhook secret from the API Integration dashboard
const webhookSecret = "your_webhook_secret_here"

type WebhookPayload struct {
    Event     string                 `json:"event"`
    Timestamp string                 `json:"timestamp"`
    Data      map[string]interface{} `json:"data"`
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    signature := r.Header.Get("X-Webhook-Signature")
    eventType := r.Header.Get("X-Webhook-Event")
    
    if signature == "" {
        http.Error(w, "Missing signature", http.StatusUnauthorized)
        return
    }
    
    // Get raw body for signature verification
    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Error reading body", http.StatusBadRequest)
        return
    }
    
    // Verify signature
    mac := hmac.New(sha256.New, []byte(webhookSecret))
    mac.Write(body)
    expectedSignature := hex.EncodeToString(mac.Sum(nil))
    
    if !hmac.Equal([]byte(signature), []byte(expectedSignature)) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }
    
    // Parse payload
    var payload WebhookPayload
    if err := json.Unmarshal(body, &payload); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    log.Printf("Received %s at %s", payload.Event, payload.Timestamp)
    
    // Handle different event types
    switch payload.Event {
    case "lead.status_changed":
        leadRefID := payload.Data["lead_reference_id"]
        oldStatus := payload.Data["old_application_status"]
        if oldStatus == nil {
            oldStatus = payload.Data["old_status"]
        }
        newStatus := payload.Data["new_application_status"]
        if newStatus == nil {
            newStatus = payload.Data["new_status"]
        }
        log.Printf("Lead %v changed from %v to %v", leadRefID, oldStatus, newStatus)
        // Add your business logic here
    case "commission.earned":
        amount := payload.Data["amount"]
        log.Printf("Commission earned: $%v", amount)
        // Add your business logic here
    case "payout.processed":
        amount := payload.Data["amount"]
        log.Printf("Payout processed: $%v", amount)
        // Add your business logic here
    case "account.status_changed":
        oldStatus := payload.Data["old_status"]
        newStatus := payload.Data["new_status"]
        log.Printf("Account status changed from %v to %v", oldStatus, newStatus)
        // Add your business logic here
    default:
        log.Printf("Unknown event type: %s", payload.Event)
    }
    
    // Always return 200 OK to acknowledge receipt
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, `{"received":true}`)
}

func main() {
    http.HandleFunc("/webhook", webhookHandler)
    log.Println("Webhook server running on :3000")
    log.Fatal(http.ListenAndServe(":3000", nil))
}
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;

@RestController
@RequestMapping("/webhook")
public class WebhookController {
    
    // Your webhook secret from the API Integration dashboard
    private static final String WEBHOOK_SECRET = "your_webhook_secret_here";
    
    @PostMapping
    public ResponseEntity<?> handleWebhook(
            @RequestHeader(value = "X-Webhook-Signature", required = false) String signature,
            @RequestHeader(value = "X-Webhook-Event", required = false) String eventType,
            @RequestBody String rawBody) {
        
        if (signature == null || signature.isEmpty()) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(Map.of("error", "Missing signature"));
        }
        
        // Verify signature
        try {
            String expectedSignature = calculateSignature(rawBody, WEBHOOK_SECRET);
            if (!signature.equals(expectedSignature)) {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                        .body(Map.of("error", "Invalid signature"));
            }
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(Map.of("error", "Signature verification failed"));
        }
        
        // Parse payload
        ObjectMapper mapper = new ObjectMapper();
        try {
            Map<String, Object> payload = mapper.readValue(rawBody, Map.class);
            String event = (String) payload.get("event");
            String timestamp = (String) payload.get("timestamp");
            Map<String, Object> data = (Map<String, Object>) payload.get("data");
            
            System.out.println("Received " + event + " at " + timestamp);
            
            // Handle different event types
            switch (event) {
                case "lead.status_changed":
                    System.out.println("Lead " + data.get("lead_reference_id") + 
                            " changed from " + (data.get("old_application_status") != null ? data.get("old_application_status") : data.get("old_status")) + 
                            " to " + (data.get("new_application_status") != null ? data.get("new_application_status") : data.get("new_status")));
                    // Add your business logic here
                    break;
                case "commission.earned":
                    System.out.println("Commission earned: $" + data.get("amount"));
                    // Add your business logic here
                    break;
                case "payout.processed":
                    System.out.println("Payout processed: $" + data.get("amount"));
                    // Add your business logic here
                    break;
                case "account.status_changed":
                    System.out.println("Account status changed from " + 
                            data.get("old_status") + " to " + data.get("new_status"));
                    // Add your business logic here
                    break;
                default:
                    System.out.println("Unknown event type: " + event);
            }
            
            // Always return 200 OK to acknowledge receipt
            return ResponseEntity.ok(Map.of("received", true));
            
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body(Map.of("error", "Invalid JSON"));
        }
    }
    
    private String calculateSignature(String payload, String secret) 
            throws NoSuchAlgorithmException, InvalidKeyException {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKey = new SecretKeySpec(
                secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        mac.init(secretKey);
        byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
        return bytesToHex(hash);
    }
    
    private String bytesToHex(byte[] bytes) {
        StringBuilder result = new StringBuilder();
        for (byte b : bytes) {
            result.append(String.format("%02x", b));
        }
        return result.toString();
    }
}