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.
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: https://www.startcap.org/app/partners/api/v1/leads/validate
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."- Afunding_typewas 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 (includeslead_reference_id,old_application_status,new_application_status, andlead_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).
| Field | Type | Description |
|---|---|---|
lead_reference_id | string | Lead identifier in YYYYMMDD-id format (e.g. 20240115-123). Use for API calls and display. |
old_status | string or null | Previous coarse referral-level status (legacy). Pipeline detail is in old_application_status. |
new_status | string | Current coarse referral-level status (legacy). Pipeline detail is in new_application_status. |
old_application_status | string or null | Previous pipeline status (see 11-stage list below). |
new_application_status | string or null | Current pipeline status (e.g. in_underwriting, reviewing, funded). |
lead_details | object | Snapshot of the lead; see sub-fields below. |
test | boolean | Present and true when this is a test webhook from the dashboard. |
lead_details may contain:
| Field | Type | Description |
|---|---|---|
reference_id | string | Lead identifier in YYYYMMDD-id format (e.g. 20240115-123). |
email | string | Lead email. |
status | string | Coarse referral-level (legacy). Use application_status for pipeline. |
application_status | string or null | Current pipeline status (11-stage). |
first_name | string | Optional; e.g. on new lead submit. |
last_name | string | Optional; 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).
| Field | Type | Description |
|---|---|---|
commission_id | string | Optional; e.g. referralRefId_level for level-based. |
referral_reference_id | string | Referral/lead reference ID (YYYYMMDD-id format) that generated the commission. |
amount | number | Commission amount. |
level | integer | Optional; downline level (1, 2, 3) for multi-level commissions. |
breakdown | array | Optional; per-level or component breakdown of the commission. |
referral_details | object or null | Optional; reference_id, email, status of the referral. |
payout.processed
Sent when a payout is processed and completed.
| Field | Type | Description |
|---|---|---|
payout_id | integer/string | Payout record ID. |
amount | number | Payout amount. |
status | string | E.g. paid. |
processed_at | string | ISO 8601 time when the payout was processed. |
account.status_changed
Sent when your partner account status changes (e.g. activation, suspension).
| Field | Type | Description |
|---|---|---|
referrer_id | integer | Your referrer/partner ID. |
old_status | string | Previous account status. |
new_status | string | Current account status. |
account_details | object | id, 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();
}
}