Skip to main content
A partner integration handles credentials that, if mishandled, could expose customer donor data to attackers, lock customers out of their own systems, or produce data breaches that the customer is legally responsible for. This page covers the practices that keep credentials, secrets, and sensitive data safe across the lifecycle of an integration — from initial customer onboarding through eventual offboarding. The audience is the engineering team responsible for the integration’s security posture, plus the operations team handling customer credential lifecycle events.

What needs protection

A partner integration typically handles four classes of secrets and three classes of sensitive data:
ClassExamples
Virtuous API tokensOne per customer organization. Grants the integration’s full configured permission set.
Webhook secretsOne per customer per webhook subscription. Used to verify incoming events.
Source-platform credentialsPer-customer OAuth tokens or API keys for Stripe, Mailchimp, etc.
Integration-internal secretsDatabase credentials, infrastructure secrets, internal service tokens.
Donor PIINames, addresses, emails, phone numbers passing through the integration.
Financial dataGift amounts, payment methods (typically tokenized), giving history.
Health and demographic dataSome donor records include sensitive attributes. Handle with extra care.
Each class needs different controls. The next sections walk through them.

Storing Virtuous API tokens

Virtuous API tokens are the integration’s primary credential. A leaked token can be used to read or write any data the token’s permission set grants — including donor PII, financial history, and webhook configuration.

Storage

Store tokens in a dedicated secrets manager. The acceptable options:
OptionUse
Cloud-provider secrets manager (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault)The default for most cloud deployments. Integrates with IAM, audit logging, and rotation primitives.
Self-hosted secrets manager (Vault, Bitwarden Secrets Manager)For environments not running in a cloud provider.
Encrypted database column with rotated KMS-managed keyAcceptable when secrets manager isn’t available. Requires careful KMS key rotation.
Not acceptable:
Anti-patternWhy
Environment variables in deployment manifestsVisible to anyone with deploy access; logged in deployment system; not auditable.
.env files in source controlVisible in git history forever; impossible to revoke.
Plaintext database columnsAnyone with read access to the database has all customer tokens.
Hardcoded in sourceCompromised on every code commit, every CI artifact, every container image.

Naming

Use a key naming convention that makes the secret’s scope obvious:
virtuous/{environment}/{customer_id}/api_token
virtuous/production/customer_acme/api_token
virtuous/staging/customer_acme/api_token
This structure supports per-customer-per-environment access control and makes the secret’s purpose clear in audit logs.

Access control

The principle: only the workers that need a customer’s token can access it. Three rules:
RuleWhy
Per-customer access scoping. The submitter for customer A can read customer A’s token but not customer B’s.Limits blast radius of a compromised worker.
No human read access in production. Engineers should never need to view a customer’s plaintext token to debug.Tokens leak through screenshots, terminal scrollback, support tickets.
All access audit-logged. Every read of a customer’s token produces an audit log entry with the accessor, timestamp, and reason.Detects misuse; supports incident response.
The cloud secrets managers all support these controls natively. Use them.

Rotation

API tokens should be rotated periodically. The cadence depends on the integration’s threat model — typical recommendations are every 90 days for high-sensitivity integrations, every 6–12 months for lower-sensitivity ones. The rotation flow:
1

Generate a new token in Virtuous

The customer’s Virtuous admin generates a new API key. (Or the partner does it on the customer’s behalf if the partner has admin access.)
2

Add the new token alongside the existing one

Store the new token in the secrets manager. Mark it as “pending.”
3

Cut over reads

Workers start reading the new token. Old token remains accessible to handle in-flight requests.
4

Validate

Confirm a successful API call with the new token. Confirm no errors from workers using the new token.
5

Retire the old token

Delete the old token from the secrets manager. Revoke it in Virtuous.
Build this flow into the integration from the start — bolting it on later is much harder.

Storing webhook secrets

Webhook secrets are different from API tokens in one important way: the integration generates them rather than receiving them from Virtuous. When you call POST /api/Webhook to subscribe, you provide the secret field.

Generation

Generate webhook secrets with a cryptographically secure random source — at least 32 bytes of entropy:
JavaScript
import crypto from 'crypto';

function generateWebhookSecret() {
  return crypto.randomBytes(32).toString('base64');
}
Never reuse the same secret across customers. Each customer’s webhook subscription has its own randomly-generated secret.

Storage

The same secrets manager that holds API tokens holds webhook secrets. Use a parallel naming convention:
virtuous/{environment}/{customer_id}/webhook_secret

Rotation

Webhook secret rotation is more complex than API token rotation because the verifier must accept both old and new during a rotation window. See Webhooks Overview: Rotating the secret for the pattern.

OAuth and source-platform credentials

For source platforms that use OAuth (Mailchimp, Constant Contact, Stripe Connect, etc.), the integration typically holds:
  • A long-lived refresh token that can produce fresh access tokens.
  • Short-lived access tokens issued from the refresh token.

Refresh token storage

Refresh tokens are the long-term credential — protect them like API tokens. Store in the secrets manager:
{source}/{environment}/{customer_id}/refresh_token
mailchimp/production/customer_acme/refresh_token

Access token handling

Access tokens are short-lived. Cache them in memory or a short-TTL cache (e.g., Redis with the token’s natural expiration as TTL). Don’t store access tokens in the secrets manager — they expire and the constant write churn is wasteful.
JavaScript
async function getAccessToken(customerId, source) {
  // Check in-memory cache first
  const cached = accessTokenCache.get(`${source}:${customerId}`);
  if (cached && cached.expiresAt > Date.now()) {
    return cached.token;
  }

  // Refresh from the refresh token
  const refreshToken = await secretsManager.get(`${source}/${env}/${customerId}/refresh_token`);
  const fresh = await refreshAccessToken(source, refreshToken);

  accessTokenCache.set(`${source}:${customerId}`, {
    token: fresh.access_token,
    expiresAt: Date.now() + fresh.expires_in * 1000,
  });

  // If the refresh returned a new refresh token (some OAuth flows rotate them), store it
  if (fresh.refresh_token && fresh.refresh_token !== refreshToken) {
    await secretsManager.set(`${source}/${env}/${customerId}/refresh_token`, fresh.refresh_token);
  }

  return fresh.access_token;
}

Revoked credentials

Customers can revoke a partner integration’s OAuth grant from the source platform’s dashboard. When this happens, refresh attempts return errors. Detect and surface:
JavaScript
try {
  const token = await getAccessToken(customerId, 'mailchimp');
  await callMailchimp(token, ...);
} catch (err) {
  if (err.code === 'invalid_grant' || err.code === 'unauthorized') {
    await alertCustomer(customerId, 'Mailchimp access has been revoked. Re-authorize to resume sync.');
    await pauseCustomerSync(customerId);
  }
  throw err;
}
A revoked credential is a customer-side event, not an integration bug. The right response is to pause the customer’s sync and notify them through your platform’s UI.

Network and transport security

A few practices outside of secret storage:

TLS everywhere

Every external API call uses HTTPS with a valid TLS certificate from a public CA. Reject endpoints that present invalid certificates or use plain HTTP.
JavaScript
// Use a fetch implementation that rejects invalid certificates
// (this is the default for Node.js fetch and most other runtimes;
// don't override it)

Egress IP allowlisting

Some customers want to allowlist the egress IPs your integration uses to talk to Virtuous. Plan to support this:
  • Run your workers behind a NAT gateway with a stable IP, or use a cloud-provider feature that produces stable egress IPs.
  • Document the egress IPs in your customer documentation so customer firewall teams can allowlist them.
  • Notify customers in advance when egress IPs change.

Webhook receiver hardening

Your webhook receiver is internet-facing. Standard web-application hardening applies:
  • HTTPS-only with a valid certificate.
  • Rate limiting at the edge to prevent abuse.
  • A WAF or similar to filter obvious attacks.
  • Signature verification on every incoming request (see Signature Verification).
  • Reject requests with unexpected Content-Type or oversize bodies.

PII and sensitive data handling

Beyond credentials, the integration handles a lot of donor PII. Treat it accordingly.

Don’t log PII

Avoid logging fields that contain donor identifiers. Structured logs should reference donors by stable identifiers (Virtuous Contact ID, your platform’s user ID) rather than PII.
JavaScript
// ❌ Bad — logs donor PII
console.log(`Synced contact: ${donor.email} ${donor.firstName} ${donor.lastName}`);

// ✅ Good — logs identifiers only
console.log('Synced contact', {
  virtuous_contact_id: contact.id,
  partner_id: donor.platformId,
});
If a specific debug investigation requires PII, do it in a controlled debug-mode flag — not as production-default logging.

Don’t log request bodies in error paths

A common pattern that leaks PII:
JavaScript
// ❌ Bad — error path includes the full request body
catch (err) {
  console.error('Request failed', {
    body: requestBody,                       // includes donor PII
    error: err.message,
  });
}
Log error context (status code, error message, request ID) but redact request bodies. If the body is needed for investigation, store it in a secure debug-data store with retention and access controls.

PII in error responses

If your integration’s own API surface returns errors to your customers’ developers, scrub PII from error messages. A 400 Bad Request response should explain what was wrong with the schema, not echo back the donor data that was rejected.

Retention

Establish retention policies for sync state and dead-letter records:
DataSuggested retention
Active sync state (partner_contacts, partner_gifts)Indefinite while the customer is active
Dead-letter entries90 days after resolution; archive thereafter
Reconciliation logs1 year; archive thereafter
Worker logs containing identifiers30 days hot, 1 year cold
Webhook delivery captures30 days
Retention also applies to your secrets manager — old rotated credentials should be deleted, not just marked inactive.

Customer onboarding and offboarding

The credential lifecycle starts at onboarding and ends at offboarding. Both need explicit handling.

Onboarding

When a customer connects your integration:
1

Receive their Virtuous API token securely

Through your platform’s UI (not via email). Validate immediately with a low-impact API call (e.g., GET /api/Health or equivalent) before storing.
2

Store in the secrets manager

Under the customer’s scoped path. Confirm storage succeeded before returning to the customer.
3

Configure webhook subscription

Generate a webhook secret, store it, and call POST /api/Webhook with the customer’s payload URL pointing at your integration’s endpoint.
4

Initial reconciliation

Run the initial bulk-load or cross-match reconciliation per the integration’s architecture.
5

Notify the customer of completion

Surface in your platform’s UI that sync is active.

Offboarding

When a customer disconnects:
1

Stop syncing immediately

Pause the customer’s submitter and reconciliation workers. Stop draining the queue.
2

Delete the webhook subscription in Virtuous

DELETE /api/Webhook/{webhookId} so Virtuous stops attempting deliveries to your endpoint.
3

Revoke the customer's API token

Have the customer’s admin revoke the token in Virtuous, or do it for them if your integration has the access.
4

Delete the customer's secrets

Remove the API token, webhook secret, and source-platform credentials from the secrets manager.
5

Delete or anonymize the customer's data

Per your retention policy and the customer’s data-protection requirements (GDPR, CCPA, etc.). If the customer requests full deletion, that includes sync state, dead-letter entries, and historical logs.
6

Confirm to the customer

Send a confirmation that all customer data has been removed and provide a record of the deletion for their compliance file.
The deletion step is the one most integrations get wrong — old sync records, dead-letter entries, and operational logs linger long after the customer leaves. Have a documented deletion process that’s tested periodically.

Permissions: least privilege

Virtuous API tokens carry the permission set configured on the underlying API key. The principle: request only the permissions your integration actually uses. If your integration only writes Gifts and reads Contacts, request a permission set that allows those operations and nothing else. Don’t request “all access” if you only need a subset — a compromised token with narrower permissions is less damaging than one with broader permissions. Document the permission requirements in your customer-facing setup documentation so customers can configure their API keys appropriately.
The exact permission groups and operations available in CRM+ are an admin-side concern in the Virtuous UI. Walk through the configuration with the customer’s admin during onboarding.⚠️ Human input required: Document the specific permission groups CRM+ exposes for API keys (read-only vs. write-enabled, by resource type) so partner integrations can recommend the minimum-required configuration.

Incident response

Plan for the credential-incident scenarios before they happen:

Suspected credential compromise

A customer reports unexpected data in their Virtuous account that might trace to your integration’s writes:
1

Rotate the customer's API token immediately

Even if compromise isn’t confirmed, rotation is cheap. Generate a new token, update the secrets manager, retire the old one.
2

Audit the integration's recent writes

Pull the recent activity log for that customer. Identify any anomalous writes.
3

Check audit logs for unexpected token access

The secrets manager’s audit log shows every read of the token. Look for unexpected accessors or unusual access patterns.
4

Report to the customer

Whether or not compromise is confirmed, the customer needs a transparent report of what was investigated and what was found.

Source-platform credential revoked

Already covered above. The integration detects the revoke automatically, pauses sync, and notifies the customer.

Webhook signature failures spike

A sudden increase in signature verification failures suggests either an attack (someone trying to forge webhooks) or a rotation problem (old secret no longer accepted). Alert on this; investigate quickly.

Where to go next

Versioning and Backward Compatibility

The companion practices for long-term integration durability.

Signature Verification

The verification pattern that the webhook security on this page builds on.

Authentication

The authentication reference for the Virtuous API token format.

Sandbox Access

How to obtain Seeded Sandbox credentials for development without using production data.
Last modified on May 21, 2026