Skip to main content
CRM+ webhooks deliver real-time notifications to a URL you control when records in a nonprofit’s Virtuous organization change. They are the recommended pattern for partner integrations that need to react to events — new Gifts, updated Contacts, deleted Projects — without polling the API on a schedule. This page covers what webhooks are, how to subscribe to them, the lifecycle of a webhook subscription, and the requirements your receiving endpoint must meet. The subsequent pages cover event types, signature verification, retry behavior, idempotency, and local testing in detail.

Why webhooks

For a typical partner integration, webhooks are strictly better than polling on three dimensions:
  • Freshness. A webhook delivers within seconds of the source event. A polled sync runs on whatever interval you configure — typically minutes or hours.
  • Cost. Webhook deliveries do not count against your hourly rate limit. A poll that scans for recent changes does — even when nothing has changed.
  • Completeness. Webhooks fire for events generated by any source — your own integration, other integrations, manual entry by a Virtuous user, the nightly Transaction batch. A poll only sees changes that landed before your last query timestamp.
The Virtuous platform team explicitly recommends webhooks over polling in the API description itself. For any partner integration that needs to detect changes, plan on webhooks as the primary mechanism — and use Query endpoints with modifiedDateTimeUtc filters as a reconciliation backstop rather than the primary signal.

How the flow works

The full lifecycle of a webhook integration: The four pieces:
  1. Subscribe. Your integration calls POST /api/Webhook to register a payload URL and select which events you want delivered. Virtuous returns the subscription details, including the secret used for signature verification.
  2. Receive. When a subscribed event occurs in the nonprofit’s Virtuous organization, Virtuous sends an HTTP POST to your payloadUrl with the event payload.
  3. Verify and process. Your endpoint verifies the request signature (proving the request came from Virtuous and was not tampered with), then processes the event.
  4. Acknowledge. Your endpoint returns a 2xx HTTP status to confirm delivery. Non-2xx responses trigger retries — see Retry Behavior.

Subscribing to webhooks

A webhook subscription specifies (a) where to deliver events and (b) which events to deliver. Create one with POST /api/Webhook:
curl -X POST https://api.virtuoussoftware.com/api/Webhook \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "payloadUrl": "https://your-integration.example.com/virtuous/webhook",
    "secret": "your-randomly-generated-secret",
    "contactCreate": true,
    "contactUpdate": true,
    "giftCreate": true,
    "giftUpdate": true,
    "giftDelete": false,
    "projectCreate": false,
    "projectUpdate": false,
    "projectDelete": false,
    "formSubmission": false,
    "contactNoteCreate": false,
    "contactNoteUpdate": false,
    "contactNoteDelete": false,
    "eventCreate": false,
    "eventUpdate": false,
    "eventDelete": false,
    "active": true
  }'
A successful response returns the subscription record:
{
  "id": 1042,
  "payloadUrl": "https://your-integration.example.com/virtuous/webhook",
  "secret": "your-randomly-generated-secret",
  "contactCreate": true,
  "contactUpdate": true,
  "giftCreate": true,
  "giftUpdate": true,
  "giftDelete": false,
  "projectCreate": false,
  "projectUpdate": false,
  "projectDelete": false,
  "formSubmission": false,
  "contactNoteCreate": false,
  "contactNoteUpdate": false,
  "contactNoteDelete": false,
  "eventCreate": false,
  "eventUpdate": false,
  "eventDelete": false,
  "active": true
}
Store the id for later management (updating event selections, deactivating, deleting). Store the secret securely — it is used to verify the authenticity of every incoming webhook delivery.

The subscription fields

FieldTypeDescription
idintegerThe webhook subscription’s primary key. Assigned at creation.
payloadUrlstringThe HTTPS URL Virtuous will POST events to. Must accept POST requests with a JSON body.
secretstringA shared secret used for signature verification. You provide this on subscription creation. Keep it in a secrets manager.
activebooleanWhen true, Virtuous delivers events to this subscription. When false, events are not delivered (and not queued for later — they are dropped).
contactCreate / contactUpdatebooleanSubscribe to Contact lifecycle events.
giftCreate / giftUpdate / giftDeletebooleanSubscribe to Gift lifecycle events.
projectCreate / projectUpdate / projectDeletebooleanSubscribe to Project lifecycle events.
formSubmissionbooleanSubscribe to form submission events from Virtuous-hosted forms.
contactNoteCreate / contactNoteUpdate / contactNoteDeletebooleanSubscribe to ContactNote lifecycle events.
eventCreate / eventUpdate / eventDeletebooleanSubscribe to Event (calendar/program event) lifecycle events.
See Event Types for what each event represents and when it fires.
When deleting a webhook subscription, events are dropped, not queued. Setting active: false (or deleting the subscription entirely) stops delivery immediately and Virtuous does not buffer events to deliver later if you reactivate. If your endpoint is temporarily unavailable, rely on the retry mechanism rather than deactivating the subscription — see Retry Behavior.

Generating the secret

You provide the secret value when creating the subscription. Virtuous does not generate it for you. Use a cryptographically secure random string of at least 32 bytes:
import crypto from 'crypto';

// Generate a 32-byte random string, base64-encoded
const secret = crypto.randomBytes(32).toString('base64');
Store the secret in your integration’s secrets manager — never commit it to source control. The secret is the foundation of trust for every incoming webhook; treat it like an API key. For details on how the secret is used to validate incoming requests, see Signature Verification.

Managing webhook subscriptions

The standard CRUD endpoints work as expected:
EndpointUse
GET /api/Webhook/{webhookId}Retrieve a single subscription by ID.
POST /api/WebhookCreate a new subscription.
PUT /api/Webhook/{webhookId}Update a subscription — change the payload URL, rotate the secret, or modify the event selections.
PUT /api/Webhook/{webhookId}/ActiveToggle the active flag in a single call. Pass ?active=true or ?active=false as a query parameter.
DELETE /api/Webhook/{webhookId}Permanently delete the subscription.
The CRM+ spec does not include an endpoint to list all webhook subscriptions for an organization. To find the IDs of existing subscriptions, store the id returned from POST /api/Webhook in your integration’s database at subscription-creation time.

Updating the event selections

To subscribe to additional event types after the initial creation, send a PUT /api/Webhook/{webhookId} with the full set of fields you want enabled:
cURL
curl -X PUT https://api.virtuoussoftware.com/api/Webhook/1042 \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "payloadUrl": "https://your-integration.example.com/virtuous/webhook",
    "secret": "your-randomly-generated-secret",
    "contactCreate": true,
    "contactUpdate": true,
    "giftCreate": true,
    "giftUpdate": true,
    "giftDelete": true,
    "projectUpdate": true,
    "active": true
  }'
Send every field you want to retain — including the payload URL, secret, and existing event subscriptions. Following the same conservative GET-then-PUT pattern used elsewhere in CRM+ minimizes the risk of inadvertently disabling event types you previously subscribed to.

Rotating the secret

To rotate the webhook secret, generate a new value and PUT the subscription with the new secret. The new secret takes effect on the next webhook delivery. Your endpoint must accept both the old and new secret during the rotation window — at minimum the time between the API call and your verification code being updated. A safer rotation pattern: maintain a list of valid current and recent secrets in your verifier, accept signatures matching any of them, and drop the old secret from the list once you have confirmation that no deliveries with the old signature have arrived.

Endpoint requirements

Your payloadUrl must meet a few requirements to receive webhooks reliably:
RequirementWhy
HTTPS onlyVirtuous does not deliver to plain HTTP URLs. Use a valid TLS certificate signed by a public certificate authority.
Accept POST with JSON bodyAll webhook deliveries are HTTP POST with Content-Type: application/json.
Return 2xx on successAny 2xx status (most commonly 200 or 204) acknowledges receipt. Non-2xx triggers retries.
Respond quicklyIndustry-standard webhook timeouts are 10–30 seconds. Slow endpoints risk timeout-triggered retries even when processing succeeds. Defer heavy processing to a background queue.
Idempotent processingRetries can deliver the same event multiple times. Your handler must produce the same result if invoked twice with the same payload. See Idempotency and Safe Reprocessing.
The exact delivery timeout enforced by Virtuous is not documented in the OpenAPI spec.⚠️ Human input required: Confirm the exact delivery timeout (seconds before Virtuous considers a delivery failed and triggers retry) so partners can size their endpoints appropriately.

The receiver pattern

The recommended shape of a webhook receiver:
JavaScript
import express from 'express';
import { verifyVirtuousSignature } from './signature-verification.js';

const app = express();

// Capture the raw body before JSON parsing — needed for signature verification
app.post(
  '/virtuous/webhook',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    // 1. Verify the signature — see /crm/webhooks/signature-verification
    const valid = verifyVirtuousSignature(req.body, req.headers, process.env.VIRTUOUS_WEBHOOK_SECRET);
    if (!valid) {
      return res.status(401).send('Invalid signature');
    }

    // 2. Parse the event payload
    const event = JSON.parse(req.body.toString('utf8'));

    // 3. Acknowledge immediately — defer processing
    res.status(200).send('OK');

    // 4. Process asynchronously (queue, background job, etc.)
    await processVirtuousEvent(event);
  }
);
Three points worth emphasizing:
  • Capture the raw body. Signature verification runs over the exact bytes Virtuous sent. JSON-parsing-then-re-serializing changes whitespace and ordering, which breaks the signature.
  • Acknowledge before processing. Return 2xx as soon as you have verified and stored the event. Heavy processing in the request handler increases the risk of timeout-triggered retries.
  • Queue the work. A robust integration pushes the event onto a background queue (SQS, Cloud Tasks, Redis Stream, etc.) and processes asynchronously. This isolates the webhook endpoint’s latency from the cost of downstream processing.

Where to go next

Event Types

The 15 event types CRM+ delivers and what each one represents.

Signature Verification

How to verify that an incoming webhook came from Virtuous and was not tampered with.

Retry Behavior

What happens when your endpoint is unavailable or returns non-2xx.

Idempotency and Safe Reprocessing

Why and how to make your handler safe against duplicate deliveries.

Local Testing

Receive Virtuous webhooks on localhost using a tunneling tool.

Rate Limits

Why webhooks are the recommended pattern when the alternative is polling.
Last modified on May 27, 2026