Webhooks

Webhooks allow you to receive real-time notifications when events occur in Good Plan, such as when a plan is created, a payment is reported, or a plan is completed.

Overview

Good Plan sends HTTP POST requests to your webhook URL when subscribed events occur. This enables you to:

  • Update your accounting system when payments are confirmed
  • Trigger email notifications
  • Sync data to spreadsheets or databases
  • Build custom workflows

Setting Up Webhooks

  1. Log into Good Plan
  2. Navigate to Dashboard → Webhooks
  3. Click "Add Webhook"
  4. Enter your webhook URL (must be https:// in production)
  5. Select the events you want to subscribe to
  6. Save - your webhook secret is automatically generated

Webhook Events

Plan Events

Event When It Fires
plan.created New plan created (via UI or API)
plan.activated Customer selects payment option
plan.completed All payments finished
plan.abandoned Plan marked as abandoned
plan.cancelled Plan cancelled
plan.written_off Plan written off
plan.reactivated Plan reactivated

Installment Events

Event When It Fires
installment.paid Customer reports payment
installment.confirmed Business confirms receipt
installment.extension_granted Due date extended
installment.deferred Payment moved to later date

Proposal Events

Event When It Fires
proposal.created Customer proposes alternative
proposal.approved Business accepts proposal
proposal.countered Business sends counter-offer
proposal.declined Business rejects proposal
proposal.counter_accepted Customer accepts counter

Webhook Payload

All webhooks follow this structure:

{
  "event": "plan.activated",
  "timestamp": "2025-11-27T01:15:30Z",
  "data": {
    "plan": {
      "id": 42,
      "status": "active",
      "total_installments": 6,
      "payment_interval": "monthly",
      "start_date": "2025-12-01",
      "customer_portal_url": "https://..."
    },
    "invoice": {
      "id": 890,
      "invoice_number": "INV-2024-001",
      "amount": "1200.00"
    },
    "customer": {
      "id": 567,
      "name": "John Doe",
      "email": "john@example.com"
    },
    "business": {
      "id": 1,
      "name": "Your Business"
    }
  }
}

Security

Verifying Webhook Signatures

Always verify webhook signatures to ensure requests are from Good Plan.

Good Plan includes a signature header:

X-Webhook-Signature: sha256=abc123...

This is an HMAC-SHA256 hash of the raw request body using your webhook secret.

PHP Example

function verifyWebhookSignature($payload, $signature, $secret)
{
    // Remove 'sha256=' prefix if present
    $signature = str_replace('sha256=', '', $signature);
    
    // Compute HMAC
    $computed = hash_hmac('sha256', $payload, $secret);
    
    // Use timing-safe comparison
    return hash_equals($computed, $signature);
}

// Example webhook receiver
$rawPayload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$secret = 'your_webhook_secret_from_goodplan';

// Verify signature BEFORE processing
if (!verifyWebhookSignature($rawPayload, $signature, $secret)) {
    http_response_code(401);
    exit('Invalid signature');
}

// Parse and handle the event
$event = json_decode($rawPayload, true);

switch ($event['event']) {
    case 'plan.activated':
        handlePlanActivated($event['data']);
        break;
        
    case 'installment.confirmed':
        handleInstallmentConfirmed($event['data']);
        break;
}

http_response_code(200);
echo json_encode(['received' => true]);

Python Example

import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = 'your_webhook_secret'

def verify_signature(payload, signature, secret):
    if signature.startswith('sha256='):
        signature = signature[7:]
    
    computed = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(computed, signature)

@app.route('/webhooks/goodplan', methods=['POST'])
def handle_webhook():
    payload = request.get_data()
    signature = request.headers.get('X-Webhook-Signature', '')
    
    if not verify_signature(payload, signature, WEBHOOK_SECRET):
        return jsonify({'error': 'Invalid signature'}), 401
    
    event = request.get_json()
    
    if event['event'] == 'plan.activated':
        handle_plan_activated(event['data'])
    elif event['event'] == 'installment.confirmed':
        handle_installment_confirmed(event['data'])
    
    return jsonify({'received': True}), 200

Handling Events

Plan Activated

When a customer selects a payment option:

function handlePlanActivated($data)
{
    $planId = $data['plan']['id'];
    $installments = $data['plan']['total_installments'];
    $invoiceNumber = $data['invoice']['invoice_number'];
    
    // Update your system
    updateInvoiceStatus($invoiceNumber, 'payment_plan_active');
    
    // Send confirmation
    mail(
        $data['customer']['email'],
        'Payment Plan Activated',
        "Your {$installments}-payment plan is now active!"
    );
}

Installment Confirmed

When a payment is verified by the business:

function handleInstallmentConfirmed($data)
{
    $installmentNum = $data['installment']['installment_number'];
    $amount = $data['installment']['amount'];
    $invoiceNumber = $data['invoice']['invoice_number'];
    
    // Record payment in accounting system
    recordPayment(
        $invoiceNumber,
        $amount,
        "Installment {$installmentNum}"
    );
}

Plan Completed

When all payments are finished:

function handlePlanCompleted($data)
{
    $invoiceNumber = $data['invoice']['invoice_number'];
    
    // Mark invoice as paid in full
    updateInvoiceStatus($invoiceNumber, 'paid_in_full');
    
    // Send thank you email
    sendThankYou($data['customer']['email']);
}

Best Practices

Return 200 Quickly

// ✅ Good: Queue for background processing
$event = json_decode(file_get_contents('php://input'), true);
queue_job('ProcessWebhook', $event);
http_response_code(200);
echo json_encode(['queued' => true]);

// ❌ Bad: Long processing blocks response
processEverything();  // Takes 30 seconds
updateDatabase();     // May timeout

Handle Idempotency

function processWebhook($event)
{
    $eventId = $event['plan']['id'] . '-' . $event['timestamp'];
    
    // Check if already processed
    if (isEventProcessed($eventId)) {
        return;  // Skip duplicate
    }
    
    // Process event
    handleEvent($event);
    
    // Mark as processed
    markEventProcessed($eventId);
}

Use HTTPS

  • Good Plan requires https:// webhook URLs in production
  • Use Let's Encrypt for free SSL certificates
  • Test with webhook.site during development

Monitor Failures

Good Plan automatically retries failed webhooks:

  • 3 retry attempts with exponential backoff
  • Webhooks auto-disable after 10 consecutive failures
  • View delivery logs in the Dashboard → Webhooks page

Testing

Using webhook.site

  1. Go to webhook.site
  2. Copy your unique URL
  3. Add it as a webhook in Good Plan
  4. Click "Test" to send a test payload
  5. View the received payload on webhook.site

Test Events

When you click "Test" in the webhook UI, Good Plan sends:

{
  "event": "webhook.test",
  "timestamp": "2025-11-27T01:15:30Z",
  "data": {
    "message": "This is a test webhook from Good Plan",
    "business": {
      "id": 1,
      "name": "Your Business Name"
    }
  }
}

Zapier Integration

Combine webhooks with the API for full two-way sync:

  1. API - Your system → Good Plan (create plans)
  2. Webhooks - Good Plan → Your system (receive updates)

Example Zap:

  • Trigger: Webhooks by Zapier (Catch Hook)
  • Filter: Only when event equals installment.confirmed
  • Action: Update QuickBooks invoice

Troubleshooting

Webhooks not arriving?

  1. Check webhook is active in Dashboard → Webhooks
  2. Verify URL is accessible (test with curl)
  3. Check your server's firewall settings
  4. View delivery logs for error messages

Signature verification failing?

  1. Use the raw request body (before parsing)
  2. Get secret from Good Plan webhook settings
  3. Remove sha256= prefix from signature header
  4. Use constant-time comparison (e.g., hash_equals in PHP)

High failure rate?

  1. Return 200 OK within 10 seconds
  2. Queue long-running tasks for background processing
  3. Handle network errors gracefully
  4. Log errors for debugging

Next Steps

Related Articles

Need more help?

Can't find what you're looking for?

Contact Support →