Skip to main content
This recipe walks through building a Stripe-to-Virtuous integration: receiving payment events from Stripe, identifying or creating the corresponding Virtuous Contact, recording the donation as a Gift, and handling the lifecycle events that follow (refunds, subscription renewals, cancellations). The architecture instantiates the patterns from Sync External Donations into Virtuous for the Stripe-specific case. Read that page first if you have not — this recipe assumes familiarity with the outbound queue, submitter worker, webhook receiver, and reconciliation poller pattern.
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

Stripe Charge / PaymentIntent → Virtuous Gift

Pick either the Charge ID (ch_*) or the PaymentIntent ID (pi_*) as your transactionId and use it consistently across your integration’s lifetime. Mixing the two will produce duplicate Gifts in Virtuous when the same payment appears under both IDs. PaymentIntent is the modern Stripe primitive; new integrations should default to it.

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’s metadata on the Charge or PaymentIntent:
JavaScript
When your webhook receiver processes the 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: For subscription-based recurring donations, add:

The Stripe webhook receiver

The receiver verifies Stripe’s signature, enqueues the event for processing, and acknowledges immediately:
JavaScript
Three patterns to note:
  • 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 NOTHING to 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.
See Sync External Donations into Virtuous for the worker pattern that drains this queue.

Processing payment_intent.succeeded

The most common event — a donor made a one-time donation. The worker constructs and submits the Gift Transaction:
JavaScript
Two patterns worth calling out:
  • The Stripe Customer’s id becomes Virtuous’s referenceId. This is the bridge between the two platforms’ donor identifiers. Virtuous’s matching algorithm uses referenceSource: "Stripe" + referenceId: cus_xxx as its highest-priority match signal.
  • amount conversion 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
See Donations / Gifts — Reversals and refunds for the reasoning behind the reversing-transaction pattern.
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
The 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
See Sync Recurring Donor Updates for the full RecurringGift lifecycle treatment.

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
This is the most common partner-side reconciliation request — accountants want monthly confirmation that every Stripe deposit is reflected in Virtuous.

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.
  • transactionId on 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, not DELETE /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.
Last modified on May 21, 2026