Understanding Webhook Payloads: All webhook events are sent as JSON arrays containing objects in the request body. Your endpoint should parse the JSON and respond with a 200 status code within 5 seconds.

Event Types Overview

Event Type Trigger Typical Use Cases
customer_created New customer is created CRM sync, welcome emails
customer_status_change Customer status is updated Sales pipeline, notifications
project_email_sent Email sent for a project Communication tracking
project_sms_sent SMS sent for a project Communication tracking
published_proposal_link Proposal link is published Team notifications, analytics
contract_signed Contract is signed by customer Sales tracking, notifications
recent_draft_project Draft project created or updated Work-in-progress tracking

customer_created

Triggered when a new customer is created in Solar Proof.

Note: Unlike other webhook events, customer_created sends a single object (not an array). Access fields directly without using [0].

Event Payload

Payload Structure (Single Object) JSON
{
  "customer_id": 141472,
  "customer_uid": "a1b2c3d4",
  "first_name": "Hussain",
  "surname": "Shahab",
  "customer_name": "Hussain Shahab",
  "phone_primary": "0412345678",
  "phone_secondary": "",
  "customer_email": "s.hussainshahab@gmail.com",
  "company_name": "",
  "company_phone": "",
  "abn": "",
  "gst_registered": 0,
  "customer_type_id": 1,
  "customer_status_id": 1,
  "site_address": "45 Thuringowa Dr, Kirwan QLD 4817, Australia",
  "customer_created_at": "2025-10-16T12:00:00Z",
  "customer_notes": "",
  "assigned_user_id": 16,
  "assigned_user_name": "Chris Taeni",
  "assigned_user_email": "phoenix.cst@gmail.com",
  "company_name_full": "Solar Proof"
}

Payload Fields

Field Type Description
customer_id integer Unique customer ID
customer_uid string Unique customer identifier (UID)
first_name string Customer's first name
surname string Customer's surname/last name
customer_name string Full customer name
customer_email string Customer email address
phone_primary string Primary phone number
phone_secondary string Secondary phone number
company_name string Customer's company name (if commercial)
gst_registered integer GST registration status (0 or 1)
customer_type_id integer Customer type: 1 = Residential, 2 = Commercial
customer_status_id integer Initial customer status ID
site_address string Customer's site address
customer_created_at string ISO 8601 timestamp when customer was created
assigned_user_id integer ID of the Solar Proof user assigned to this customer
assigned_user_name string Name of assigned user
company_name_full string Solar installation company name

Example Handler

Node.js/Express Handler javascript
app.post('/webhooks/solarproof', async (req, res) => {
  const customer = req.body; // Single object, NOT an array!
  
  // Sync to CRM
  await crm.createContact({
    id: customer.customer_id,
    email: customer.customer_email,
    firstName: customer.first_name,
    lastName: customer.surname,
    phone: customer.phone_primary,
    address: customer.site_address,
    assignedTo: customer.assigned_user_name,
    source: 'Solar Proof'
  });
  
  res.status(200).send('OK');
});

customer_status_change

Triggered when a customer's status is changed (e.g., from Lead to Signed).

This event now fires automatically, not only when a person changes a status. Solar Proof moves a deal along the pipeline on its own when: a proposal is published (moves to Quoted), the customer opens the proposal link or roughly two days pass with no response (moves to Follow Up), and a quote is signed (moves to Signed). Each of those sends this event. If your integration assumed a human triggered every status change - for example by notifying your team that "a rep moved this customer" - review it. Automated moves carry the opportunity_* fields described below; a status changed by hand from the contact list does not.

Event Payload

Payload Structure (Array Format) JSON
[
  {
    "customer_id": 141472,
    "first_name": "Hussain",
    "surname": "Shahab",
    "customer_name": "Hussain Shahab",
    "phone_primary": "",
    "phone_secondary": "",
    "customer_email": "s.hussainshahab@gmail.com",
    "company_name": "",
    "company_phone": "",
    "abn": "",
    "gst_registered": 0,
    "customer_type_id": 1,
    "customer_status_id": 1,
    "site_address": "45 Thuringowa Dr, Kirwan QLD 4817, Australia",
    "customer_created_at": "2025-10-16T00:00:00Z",
    "customer_notes": "",
    "assigned_user_id": 16,
    "assigned_user_name": "Chris Taeni",
    "assigned_user_email": "phoenix.cst@gmail.com",
    "assigned_user_phone": "0411 549 054",
    "company_name_full": "Solar Proof",
    "customer_status_name": "Lead",
    "opportunity_id": 4821,
    "opportunity_name": "6.6kW + battery",
    "opportunity_value": 15900.00,
    "opportunity_source": "Website enquiry",
    "opportunity_expected_close": "2026-09-30"
  }
]

Payload Fields

Field Type Description
customer_id integer Unique customer ID in Solar Proof
first_name string Customer's first name
surname string Customer's surname/last name
customer_name string Full customer name
customer_email string Customer email address
phone_primary string Primary phone number
customer_status_id integer New status ID
customer_status_name string New status name (e.g., "Lead", "Signed")
site_address string Customer's site address
assigned_user_id integer ID of the Solar Proof user assigned to this customer
assigned_user_name string Name of assigned user
opportunity_id integer The deal this status change belongs to. Present only when the change came from the pipeline (a board move, or one of the automatic moves described above). Absent when a status was changed by hand from the contact list.
opportunity_name string Name of the deal, e.g. "6.6kW + battery". May be null on deals that were never named.
opportunity_value decimal Deal value. An explicit override if one was set, otherwise it is left null and the value shown in Solar Proof comes from the latest quote.
opportunity_source string Where the deal came from, e.g. "Website enquiry". May be null.
opportunity_expected_close date Expected close date (YYYY-MM-DD). May be null.
A contact can now hold more than one deal. A customer who bought panels last year and is being quoted a battery today has two deals, each with its own stage. customer_status_id always reflects the deal that just moved, so keying your integration on opportunity_id rather than customer_id keeps repeat business from overwriting itself. Deals themselves are not yet exposed as API endpoints - only through these webhook fields.

Example Handler

Node.js/Express Handler javascript
app.post('/webhooks/solarproof', async (req, res) => {
  const events = req.body; // Array of events
  const event = events[0]; // Get first event
  
  // Update CRM status
  await crm.updateContact(event.customer_id, {
    status: event.customer_status_name,
    assignedTo: event.assigned_user_name
  });
  
  res.status(200).send('OK');
});

project_email_sent

Triggered when an email is sent for a project (e.g., proposal email, follow-up). Returns comprehensive project, customer, equipment, and company information.

Event Payload

Payload Structure (Array Format) JSON
[
  {
    "activity_id": 266268,
    "activity_type": "email_sent",
    "email_sent_at": "2025-10-16T12:37:44Z",
    "project_id": 118825,
    "project_name": "45 Thuringowa Dr - Solar project (#118825)",
    "project_address": "{\"streetnum\":\"45\",\"street\":\"Thuringowa Dr\",\"suburb\":\"Kirwan\",\"state\":\"QLD\",\"postcode\":\"4817\",\"country\":\"AU\"}",
    "proposal_url": null,
    "viewing_link": "uploads/fa00dfba91388ede4483011b455d93/pdfs/118825/proposal_118825.pdf",
    "signed_at": null,
    "installation_date": null,
    "system_size_kw": 0,
    "panel_quantity": 21,
    "panel_wattage": "400",
    "quote_amount": "20830.00",
    "inverter_quantity": 1,
    "battery_quantity": 1,
    "deposit_amount": 4166,
    "deposit_percentage": 20,
    "has_signed_pdf": 0,
    "panel_brand": "TCL SunPower Global",
    "panel_model": "SPR-MAX3-400",
    "panel_wattage_spec": "400",
    "inverter_brand": "SMA Australia Pty Ltd",
    "inverter_model": "STP10000TL-20",
    "inverter_capacity": "10",
    "inverter_series": "Sunny Tripower",
    "battery_brand": "Alpha-ESS",
    "battery_model": "SMILE-G3-BAT-10.1P",
    "battery_name": "Alpha-ESS SMILE-G3-BAT-10.1P",
    "battery_capacity": 10.1,
    "customer_id": 141472,
    "customer_name": "Hussain Shahab",
    "customer_email": "s.hussainshahab@gmail.com",
    "customer_phone": "",
    "customer_company_name": "",
    "user_id": 16,
    "company_name": "Solar Proof",
    "user_name": "Chris Taeni",
    "user_email": "phoenix.cst@gmail.com",
    "company_phone": "08 7160 0127",
    "signed_pdf_url": null,
    "payment_terms": {
      "type": "cash",
      "description": null,
      "term_months": 0,
      "repayment_amount": 0,
      "repayment_frequency": "Weekly"
    }
  }
]

Payload Fields

Field Type Description
Activity Information
activity_id integer Unique activity log ID
activity_type string Always "email_sent" for this event
email_sent_at string ISO 8601 timestamp when email was sent
Project Information
project_id integer Unique project/quotation ID
project_name string Name/nickname of the project
project_address string JSON-encoded installation site address
system_size_kw number System capacity in kilowatts
quote_amount string Total quote amount after rebates
deposit_amount number Deposit amount required
signed_at string ISO timestamp when contract was signed (null if not signed)
installation_date string ISO timestamp of planned installation (null if not set)
Equipment Details
panel_brand string Solar panel manufacturer
panel_model string Solar panel model number
panel_quantity integer Number of solar panels
panel_wattage string Wattage per panel
inverter_brand string Inverter manufacturer
inverter_model string Inverter model number
inverter_capacity string Inverter capacity in kW
battery_brand string Battery manufacturer (null if no battery)
battery_model string Battery model number (null if no battery)
battery_capacity number Battery capacity in kWh (null if no battery)
Customer Information
customer_id integer Unique customer ID
customer_name string Customer's full name
customer_email string Customer's email address
customer_phone string Customer's phone number
User/Company Information
user_id integer ID of the Solar Proof user who sent the email
user_name string Name of the Solar Proof user
company_name string Solar installation company name
company_phone string Company phone number
Payment Information
payment_terms object Payment terms object containing type, description, term_months, repayment_amount, and repayment_frequency

Example Handler

Track Communication History javascript
app.post('/webhooks/solarproof', async (req, res) => {
  const events = req.body;
  const event = events[0];
  
  await analytics.trackEvent('project_email_sent', {
    customer_id: event.customer_id,
    project_id: event.project_id,
    user_id: event.user_id,
    system_size: event.system_size_kw,
    quote_amount: event.quote_amount,
    timestamp: new Date(event.email_sent_at)
  });
  
  res.status(200).send('OK');
});

project_sms_sent

Triggered when an SMS is sent for a project. Returns comprehensive project and customer information.

Event Payload

Payload Structure (Array Format) JSON
[
  {
    "activity_id": 264533,
    "activity_type": "sms_sent",
    "sms_sent_at": "2025-10-10T14:23:52Z",
    "project_id": 117573,
    "project_name": "48 Wright St - Battery project (#117573)",
    "project_address": "{\"streetnum\":\"48\",\"street\":\"Wright St\",\"suburb\":\"Paradise\",\"state\":\"SA\",\"postcode\":\"5075\",\"country\":\"AU\"}",
    "proposal_url": null,
    "viewing_link": "uploads/fa00dfba91388ede4483011b455d93/pdfs/117573/proposal_117573.pdf",
    "signed_at": "2025-10-15T00:00:00Z",
    "installation_date": null,
    "system_size_kw": 0,
    "quote_amount": "14974.00",
    "panel_quantity": 28,
    "inverter_quantity": 1,
    "battery_quantity": 1,
    "deposit_amount": 4000,
    "deposit_percentage": 20,
    "has_signed_pdf": 1,
    "panel_brand": "Trina Solar Co Ltd",
    "panel_model": "TSM-450NEG9R.25",
    "panel_wattage_spec": "450",
    "inverter_brand": "FOX ESS",
    "inverter_model": "KH10/KA10",
    "inverter_capacity": "10",
    "battery_brand": "Fox ESS",
    "battery_model": "ECS4800-H7",
    "battery_capacity": 32.61,
    "customer_id": 141145,
    "customer_name": "John Wilkinson",
    "customer_email": "jwilksy@solarproof.com.au",
    "customer_phone": "0411 222 222",
    "customer_phone_alternate": "",
    "customer_company_name": "",
    "user_id": 16,
    "company_name": "Solar Proof",
    "user_name": "Chris Taeni",
    "user_email": "phoenix.cst@gmail.com",
    "company_sender_phone": "08 7160 0127",
    "signed_pdf_url": "https://solarproof.com.au/uploads/fa00dfba91388ede4483011b455d93/pdfs/117573/proposal_117573.pdf",
    "payment_terms": {
      "type": "cash",
      "description": "",
      "term_months": 0,
      "repayment_amount": 0,
      "repayment_frequency": "Weekly",
      "balloon_payment": 0
    }
  }
]

Payload Fields

Similar fields to project_email_sent, with sms_sent_at instead of email_sent_at, and includes company_sender_phone (the phone number used to send the SMS).

Example Handler

Track SMS Communications javascript
app.post('/webhooks/solarproof', async (req, res) => {
  const events = req.body;
  const event = events[0];
  
  await communicationLog.record({
    type: 'sms',
    customer_id: event.customer_id,
    project_id: event.project_id,
    sent_by: event.user_name,
    sent_from: event.company_sender_phone,
    timestamp: new Date(event.sms_sent_at)
  });
  
  res.status(200).send('OK');
});

contract_signed

Triggered when a customer signs a contract/proposal. Includes extensive project details and customer information.

Event Payload

Payload Structure (Array Format) JSON
[
  {
    "activity_id": 265815,
    "activity_type": "document_signed",
    "contract_signed_at": "2025-10-15T03:46:15Z",
    "project_id": 118692,
    "project_name": "48 Wright St - Upgrade Battery project",
    "project_address": "{\"streetnum\":\"48\",\"street\":\"Wright St\",\"suburb\":\"Paradise\",\"state\":\"SA\",\"postcode\":\"5075\",\"country\":\"AU\"}",
    "proposal_url": null,
    "viewing_link": "uploads/fa00dfba91388ede4483011b455d93/pdfs/118692/proposal_118692.pdf",
    "signed_at": "2025-10-15T00:00:00Z",
    "installation_date": null,
    "system_size_kw": 0,
    "panel_quantity": 28,
    "panel_wattage": "450",
    "quote_amount": "14974.00",
    "inverter_quantity": 1,
    "battery_quantity": 1,
    "deposit_amount": 4000,
    "deposit_percentage": 20,
    "stc_value": 37,
    "stc_quantity": 376,
    "has_signed_pdf": 1,
    "has_original_pdf": 1,
    "panel_brand": "Trina Solar Co Ltd",
    "panel_model": "TSM-450NEG9R.25",
    "panel_wattage_spec": "450",
    "inverter_brand": "FOX ESS",
    "inverter_model": "KH10/KA10",
    "inverter_capacity": "10",
    "inverter_series": "KH/KA",
    "battery_brand": "Fox ESS",
    "battery_model": "ECS4800-H7",
    "battery_name": "Fox ESS ECS4800-H7",
    "battery_capacity": "32.61",
    "customer_id": 141201,
    "customer_name": "Julia Foghorn",
    "customer_first_name": "Julia",
    "customer_last_name": "Foghorn",
    "customer_email": "juliafoghorn@solarproof.com.au",
    "customer_phone": "0499 333 444",
    "customer_phone_alternate": "",
    "customer_company_name": "",
    "customer_company_phone": "",
    "customer_abn": "",
    "user_id": 16,
    "company_name": "Solar Proof",
    "user_name": "Chris Taeni",
    "user_email": "phoenix.cst@gmail.com",
    "company_phone": "08 7160 0127",
    "company_email": "admin@solarproof.com.au",
    "company_address": "764 Cudgen Rd, Kingscliff NSW 2487, Australia",
    "signed_pdf_url": null,
    "proposal_link": "https://solarproof.com.au/uploads/fa00dfba91388ede4483011b455d93/pdfs/118692/proposal_118692.pdf",
    "payment_terms": {
      "type": "cash",
      "description": "",
      "term_months": "0",
      "repayment_amount": "0",
      "repayment_frequency": "Weekly",
      "balloon_payment": "0"
    },
    "customer_type": "Residential",
    "customer_gst_status": "Not GST Registered"
  }
]

Additional Fields

Field Type Description
contract_signed_at string ISO timestamp when contract was signed
stc_value number Small-scale Technology Certificate value
stc_quantity number Number of STCs
customer_type string "Residential" or "Commercial"
customer_gst_status string "GST Registered" or "Not GST Registered"
company_email string Installation company email
company_address string Installation company address

Example Handler

Process Signed Contract javascript
app.post('/webhooks/solarproof', async (req, res) => {
  const events = req.body;
  const event = events[0];
  
  // Update CRM
  await crm.markAsSigned(event.customer_id, {
    signedDate: event.contract_signed_at,
    projectId: event.project_id,
    quoteAmount: event.quote_amount,
    depositAmount: event.deposit_amount
  });
  
  // Notify installation team
  await notifications.sendToTeam({
    subject: 'New Contract Signed!',
    customer: event.customer_name,
    systemSize: event.system_size_kw,
    installDate: event.installation_date
  });
  
  res.status(200).send('OK');
});

recent_draft_project

Triggered when a draft project is created or updated. Useful for tracking work-in-progress.

Event Payload

Payload Structure (Array Format) JSON
[
  {
    "project_id": 118871,
    "project_name": "80 Collins St - Solar project (#118871)",
    "project_address": "{\"streetnum\":\"132\",\"street\":\"Grenfell St\",\"suburb\":\"Adelaide\",\"state\":\"SA\",\"postcode\":\"5000\",\"country\":\"AU\"}",
    "proposal_url": null,
    "created_at": "2025-10-16T03:35:10Z",
    "last_edited": "2025-10-16T14:43:53Z",
    "system_size_kw": 0,
    "panel_quantity": 0,
    "panel_wattage": "250",
    "quote_amount": "6500.00",
    "inverter_quantity": 1,
    "battery_quantity": 0,
    "deposit_amount": 0,
    "deposit_percentage": 0,
    "is_draft": 1,
    "draft_stage": 8,
    "panel_brand": "Jinko Solar Co Ltd",
    "panel_model": "JKM250P-60",
    "panel_wattage_spec": "250",
    "inverter_brand": "Fronius International GmbH",
    "inverter_model": "Symo 10.0-3-M",
    "inverter_capacity": "10",
    "inverter_series": "Symo",
    "battery_brand": null,
    "battery_model": null,
    "battery_name": null,
    "battery_capacity": null,
    "customer_id": null,
    "customer_name": null,
    "customer_email": null,
    "customer_phone": null,
    "customer_company_name": null,
    "user_id": 53179,
    "company_name": "Solar Proof",
    "user_name": "Sam Lawson",
    "user_email": "sam@solarproof.com.au",
    "payment_terms": {
      "type": "cash",
      "description": "",
      "term_months": 0,
      "repayment_amount": 0,
      "repayment_frequency": "Weekly",
      "balloon_payment": 0
    }
  }
]

Key Payload Fields

Field Type Description
is_draft integer Always 1 for draft projects
draft_stage integer Current stage of the draft (1-10)
created_at string ISO timestamp when draft was created
last_edited string ISO timestamp of last modification
customer_id null May be null if customer not yet assigned

Example Handler

Track Draft Progress javascript
app.post('/webhooks/solarproof', async (req, res) => {
  const events = req.body;
  const event = events[0];
  
  await dashboard.updateDraft({
    projectId: event.project_id,
    projectName: event.project_name,
    stage: event.draft_stage,
    lastEdited: event.last_edited,
    assignedTo: event.user_name
  });
  
  res.status(200).send('OK');
});

Testing Webhooks

Create Test Events

Trigger events in your Solar Proof account to generate webhook payloads:

  • Change a customer's status to trigger customer_status_change
  • Send a project email to trigger project_email_sent
  • Send an SMS to trigger project_sms_sent
  • Publish a proposal to trigger published_proposal_link
  • Create or edit a draft project to trigger recent_draft_project

Webhook Best Practices

Respond Quickly

Your webhook endpoint must respond within 5 seconds. Process complex operations asynchronously:

Async Processing Pattern javascript
app.post('/webhooks/solarproof', async (req, res) => {
  const events = req.body;
  
  // Respond immediately
  res.status(200).send('OK');
  
  // Process asynchronously
  await queue.add('process-webhook', events);
});

Handle Array vs Object Format

Important: Most webhook payloads are sent as arrays, but customer_created sends a single object. Handle each accordingly:

Correct Way to Access Data javascript
app.post('/webhooks/solarproof', async (req, res) => {
  const payload = req.body;
  
  // Check if it's customer_created (single object)
  if (payload.customer_id && !Array.isArray(payload)) {
    // Direct access for customer_created
    console.log(payload.customer_id);
  } 
  // All other events (array format)
  else if (Array.isArray(payload)) {
    const event = payload[0];
    console.log(event.customer_id);
  }
  
  res.status(200).send('OK');
});

Handle Duplicate Events

Webhooks may be delivered more than once. Use idempotency keys to prevent duplicate processing:

Idempotency Pattern javascript
app.post('/webhooks/solarproof', async (req, res) => {
  const events = req.body;
  const event = events[0];
  const idempotencyKey = `${event.activity_type}_${event.activity_id}`;
  
  // Check if already processed
  const exists = await db.checkProcessed(idempotencyKey);
  if (exists) {
    return res.status(200).send('Already processed');
  }
  
  // Process and mark as complete
  await processEvent(event);
  await db.markProcessed(idempotencyKey);
  
  res.status(200).send('OK');
});

Parse JSON Addresses

The project_address field contains JSON-encoded address data. Parse it to access individual components:

Parse Address Field javascript
const events = req.body;
const event = events[0];

// Parse the address JSON
const address = JSON.parse(event.project_address);

console.log(address.streetnum); // "45"
console.log(address.street);    // "Thuringowa Dr"
console.log(address.suburb);    // "Kirwan"
console.log(address.state);     // "QLD"
console.log(address.postcode);  // "4817"
console.log(address.country);   // "AU"