Skip to main content
Coming Soon - Webhook functionality is currently in development. This page documents the planned implementation.

Overview

The plan.cancelled event is triggered when a recurring donation plan is cancelled, either by the donor, an admin, or due to payment failures. This event marks the end of the recurring giving relationship.

When This Event Fires

  • Donor cancels their recurring plan
  • Admin cancels plan in the dashboard
  • Plan is cancelled due to multiple failed payments
  • Plan is cancelled via the API
  • Payment method expires and cannot be updated
When a plan is cancelled, no future payments will be processed. Past transactions remain in the system and this event does not indicate a refund.

Webhook Payload

{
  "id": "evt_cde345fgh678",
  "type": "plan.cancelled",
  "created_at": "2024-06-15T16:30:00Z",
  "data": {
    "id": "plan_789012",
    "amount": 2500,
    "currency": "USD",
    "frequency": "monthly",
    "interval": 1,
    "status": "cancelled",
    "payment_method": "card",
    "campaign": {
      "id": "camp_abc123",
      "title": "Monthly Giving Program",
      "url": "https://givebutter.com/monthly-giving"
    },
    "donor": {
      "id": "cont_987654321",
      "first_name": "Jane",
      "last_name": "Doe",
      "email": "[email protected]",
      "phone": "+1234567890"
    },
    "cancellation": {
      "reason": "donor_request",
      "note": "Moving to annual giving instead",
      "cancelled_by": "donor",
      "cancelled_at": "2024-06-15T16:30:00Z"
    },
    "total_payments": 5,
    "total_donated": 12500,
    "started_at": "2024-01-15T11:00:00Z",
    "created_at": "2024-01-15T11:00:00Z",
    "updated_at": "2024-06-15T16:30:00Z"
  },
  "account_id": "acct_xyz789"
}

Event Data Fields

id
string
required
Unique identifier for the recurring plan (prefixed with plan_)
amount
integer
required
Recurring donation amount in cents (e.g., 2500 = $25.00)
currency
string
required
Three-letter ISO currency code (e.g., USD, CAD, EUR)
frequency
string
required
Payment frequency: weekly, monthly, quarterly, or yearly
interval
integer
required
Number of frequency periods between payments
status
string
required
Plan status. Always cancelled for this event.
payment_method
string
required
Payment method that was used: card, paypal, ach, or bank_transfer
campaign
object
required
Campaign the recurring plan supported
donor
object
required
Information about the recurring donor
cancellation
object
required
Details about the cancellation
total_payments
integer
required
Total number of successful payments made during the plan
total_donated
integer
required
Total amount donated through this plan in cents
started_at
string
required
ISO 8601 timestamp when the plan began
created_at
string
required
ISO 8601 timestamp when the plan was originally created
updated_at
string
required
ISO 8601 timestamp when the plan was last updated (cancellation time)

Common Use Cases

Confirm cancellation and thank donors for their past support:
async function handlePlanCancelled(event) {
  const { donor, campaign, total_payments, total_donated, cancellation } = event.data;

  await sendEmail({
    to: donor.email,
    subject: 'Your recurring donation has been cancelled',
    template: 'plan-cancelled',
    data: {
      first_name: donor.first_name,
      campaign_name: campaign.title,
      total_payments: total_payments,
      total_donated: total_donated / 100,
      cancelled_at: cancellation.cancelled_at
    }
  });
}
Attempt to re-engage cancelled donors:
async function handlePlanCancelled(event) {
  const { donor, campaign, cancellation, total_donated } = event.data;

  // Wait 30 days, then send win-back email
  await scheduler.scheduleEmail({
    to: donor.email,
    template: 'winback-recurring',
    send_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
    data: {
      first_name: donor.first_name,
      campaign_name: campaign.title,
      past_impact: calculateImpact(total_donated),
      reactivate_url: campaign.url
    }
  });
}
Move donors from active to lapsed recurring segments:
async function handlePlanCancelled(event) {
  const { donor, cancellation, total_payments } = event.data;

  await crm.updateContact(donor.id, {
    donor_type: 'lapsed_recurring',
    recurring_status: 'cancelled',
    cancellation_reason: cancellation.reason,
    cancelled_at: cancellation.cancelled_at,
    lifetime_payments: total_payments,
    tags: ['former-monthly-donor']
  });

  // Remove from recurring lists
  await emailPlatform.removeFromList(donor.email, 'Monthly Donors Circle');
  await emailPlatform.addToList(donor.email, 'Lapsed Monthly Donors');
}
Notify fundraising team to follow up with high-value donors:
async function handlePlanCancelled(event) {
  const { donor, amount, total_donated, cancellation } = event.data;

  // Alert for significant donors
  if (total_donated >= 50000 || amount >= 10000) {
    await sendNotification({
      channel: '#major-donors',
      message: `⚠️ High-value recurring donor cancelled`,
      details: {
        donor: `${donor.first_name} ${donor.last_name}`,
        monthly_amount: `$${amount / 100}`,
        total_given: `$${total_donated / 100}`,
        reason: cancellation.reason,
        note: cancellation.note
      },
      action: 'Schedule personal follow-up call'
    });
  }
}
Track cancellation reasons for improvement insights:
async function handlePlanCancelled(event) {
  const { cancellation, frequency, amount, total_payments } = event.data;

  await analytics.trackEvent('recurring_plan_cancelled', {
    reason: cancellation.reason,
    cancelled_by: cancellation.cancelled_by,
    frequency: frequency,
    monthly_value: amount / 100,
    lifetime_payments: total_payments,
    days_active: calculateDaysActive(event.data.started_at, cancellation.cancelled_at)
  });

  // Update cancellation metrics
  await metrics.increment('recurring_cancellations', {
    reason: cancellation.reason,
    month: new Date().getMonth()
  });
}
Ask donors why they cancelled to improve retention:
async function handlePlanCancelled(event) {
  const { donor, cancellation } = event.data;

  // Only ask if donor cancelled themselves
  if (cancellation.cancelled_by === 'donor') {
    await sendEmail({
      to: donor.email,
      subject: 'We value your feedback',
      template: 'cancellation-survey',
      data: {
        first_name: donor.first_name,
        survey_url: generateSurveyLink(event.data.id),
        incentive: '$10 gift card for completing survey'
      }
    });
  }
}

Cancellation Reasons

Reason CodeDescription
donor_requestDonor chose to cancel
payment_failedMultiple payment failures
card_expiredPayment method expired and not updated
adminAdmin cancelled in dashboard
otherOther reason (check cancellation.note)

Who Can Cancel

Cancelled ByDescription
donorDonor cancelled via their account or email link
adminStaff member cancelled in the dashboard
systemAutomatically cancelled (e.g., payment failures)