This recipe is an architectural pattern, not a copy-paste implementation. The specifics of Stripe’s API (event names, ID prefixes, field shapes) evolve over time — confirm against Stripe’s current documentation before using any specific field name in production. The Virtuous-side mapping is the part of the recipe that’s most durable.
Architecture
Two webhook receivers — one for inbound Stripe events, one for outbound confirmations from Virtuous — with an outbound queue between them. The queue decouples Stripe’s delivery rate from Virtuous’s processing rate and provides durable storage if any component is temporarily down.Field mapping
The core of the integration is mapping Stripe’s data model to Virtuous’s. Stripe organizes data around Customers (donors) and Charges or PaymentIntents (donations); Virtuous organizes around Contacts and Gifts. The mapping:Stripe Customer → Virtuous Contact
| Virtuous field | Source from Stripe |
|---|---|
referenceSource | The literal string "Stripe" — your platform identifier. |
referenceId | The Stripe Customer ID (typically prefixed cus_). |
firstName / lastName | Parsed from Stripe Customer name field, or from Charge billing details. |
email (with emailType: "Home Email") | Stripe Customer email or Charge receipt_email. |
phone (with phoneType: "Mobile Phone") | Stripe Customer phone. |
address1, city, state, postal, country | Stripe Customer address or Charge billing_details.address. |
Stripe Charge / PaymentIntent → Virtuous Gift
| Virtuous field | Source from Stripe |
|---|---|
transactionSource | The literal string "Stripe". |
transactionId | The Stripe Charge ID (ch_*) or PaymentIntent ID (pi_*) — pick one and use it consistently. |
giftDate | Charge created timestamp, converted to a date in the customer’s organization timezone. |
giftType | "Cash" for credit-card or ACH charges, "EFT" for bank transfers if you distinguish. |
amount | Charge amount divided by 100 (Stripe stores cents; Virtuous expects dollars). |
currencyCode | Charge currency, uppercased. |
giftDesignations[].projectCode | Project code resolved from Charge metadata (see below). |
giftDesignations[].amountDesignated | Matches amount for single-designation gifts. |
Capturing the project designation
A typical donor flow on a Stripe-powered donation form includes a “Designate to” field that picks a Virtuous Project. Pass the Project code in Stripe’smetadata on the Charge or PaymentIntent:
JavaScript
charge.succeeded (or payment_intent.succeeded) event, it reads metadata.virtuous_project_code and uses it in the Virtuous Gift Transaction’s designation.
Stripe events to subscribe to
Subscribe your Stripe webhook endpoint to the events your integration acts on. A minimal set for a one-time-gift integration:| Stripe event | What it represents | Virtuous action |
|---|---|---|
payment_intent.succeeded | A payment completed successfully. | Submit POST /api/v2/Gift/Transaction. |
charge.refunded | A previously-recorded payment was fully or partially refunded. | Submit a reversing transaction via POST /api/Gift/ReversingTransaction. |
charge.dispute.created | A donor disputed a charge. | Surface to the customer for review; optionally tag the Gift. |
| Stripe event | What it represents | Virtuous action |
|---|---|---|
customer.subscription.created | A new recurring donation schedule began. | Submit POST /api/RecurringGift to create the schedule in Virtuous. See Sync Recurring Donor Updates. |
invoice.paid | A subscription’s recurring payment succeeded. | Submit POST /api/v2/Gift/Transaction with recurringGiftTransactionId linking to the schedule. |
customer.subscription.deleted | A recurring donation was cancelled. | Cancel the corresponding RecurringGift via PUT /api/RecurringGift/Cancel/{recurringGiftId}. |
The Stripe webhook receiver
The receiver verifies Stripe’s signature, enqueues the event for processing, and acknowledges immediately:JavaScript
- Stripe’s event ID is your idempotency anchor on the Stripe side. Use it as the primary key in your queue with
ON CONFLICT DO NOTHINGto handle Stripe’s webhook retries cleanly. - The Stripe event payload is stored verbatim. Your worker reads from the queue and constructs the Virtuous submission from the stored payload — meaning replay and reprocessing are possible without re-fetching from Stripe.
- Acknowledge before processing. The Virtuous submission happens out-of-band in the worker, not synchronously in the webhook handler.
Processing payment_intent.succeeded
The most common event — a donor made a one-time donation. The worker constructs and submits the Gift Transaction:
JavaScript
- The Stripe Customer’s
idbecomes Virtuous’sreferenceId. This is the bridge between the two platforms’ donor identifiers. Virtuous’s matching algorithm usesreferenceSource: "Stripe"+referenceId: cus_xxxas its highest-priority match signal. amountconversion is critical. Stripe stores money as integer cents; Virtuous expects dollar decimals. Off-by-100 errors are the most common partner bug — every gift recorded at 100x its intended value. Convert defensively and ideally unit-test the conversion.
Processing charge.refunded
When a donor’s payment is refunded — by the customer’s staff in the Stripe dashboard, by a chargeback resolution, or by your platform’s refund logic — record a reversing transaction in Virtuous to keep the accounting accurate:
JavaScript
The exact field set for
POST /api/Gift/ReversingTransaction is not fully enumerated in the spec. The fields shown above (reversedGiftId, giftDate, notes) reflect the conceptual pattern; confirm the authoritative request shape before going to production. See Handle Duplicate Records.Subscription-based recurring donations
For donors who set up a recurring donation through a Stripe Subscription, your integration tracks two related concepts in Virtuous:- A RecurringGift representing the schedule itself (the donor’s commitment to give $50/month forever).
- One Gift per successful payment — generated automatically by Stripe each billing cycle, recorded in Virtuous as a Gift linked to the schedule.
When the subscription is created
JavaScript
The valid
frequency values for POST /api/RecurringGift are not enumerated in the CRM+ spec. The mapping above is a typical pattern but may need adjustment.⚠️ Human input required: Confirm the canonical frequency enum values for RecurringGift, and update both this recipe and the Statuses and Lifecycle States page.When a subscription payment succeeds
JavaScript
recurringGiftTransactionId field on the Gift Transaction links the new Gift to the existing RecurringGift schedule. Virtuous’s matching algorithm uses this to associate the payment with the right donor recurring history.
When a subscription is cancelled
JavaScript
Reconciliation specific to Stripe
The general reconciliation patterns from Reconcile Failed Syncs apply. A few Stripe-specific reconciliation queries:Stripe daily payout reconciliation
Stripe’s daily payouts list every charge included in that day’s deposit. For accounting reconciliation, compare Stripe’s payout report with Virtuous’s gifts:JavaScript
Security checklist
Before deploying a Stripe-to-Virtuous integration to production, confirm:- Stripe webhook signature verification runs on every incoming request.
- Virtuous webhook signature verification runs on every incoming request. See Signature Verification.
- The Stripe webhook secret is loaded from a secrets manager — never hardcoded.
- The Virtuous API token is loaded from a secrets manager, scoped per customer.
- Stripe Customer IDs are stored in your database alongside Virtuous Contact IDs — both are needed for reconciliation.
transactionIdon Virtuous submissions is always the stable Stripe identifier (PaymentIntent ID is recommended).- The amount-conversion (cents → dollars) is unit-tested.
- Refunds use
POST /api/Gift/ReversingTransaction, notDELETE /api/Gift/{giftId}. - Subscription cancellations use
PUT /api/RecurringGift/Cancel/{id}, not deletion. - Reconciliation queries run on a schedule and produce reports the customer’s accounting team can review.
Where to go next
Sync Recurring Donor Updates
The deeper treatment of RecurringGift lifecycle management — paused subscriptions, failed payments, and donor-driven amount changes.
Import Historical Gifts
Backfill historical Stripe charges into Virtuous as part of the initial integration setup.
Sync External Donations into Virtuous
The general architecture this Stripe recipe instantiates.
Reconcile Failed Syncs
Handle the inevitable Stripe-to-Virtuous sync gaps that emerge over time.