> ## Documentation Index
> Fetch the complete documentation index at: https://docs.virtuous.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Security & Credential Management

> How to store, rotate, and protect the credentials and secrets your CRM+ integration depends on — Virtuous API tokens, webhook secrets, third-party OAuth tokens, and customer data.

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:

| Class                            | Examples                                                                                |
| -------------------------------- | --------------------------------------------------------------------------------------- |
| **Virtuous API tokens**          | One per customer organization. Grants the integration's full configured permission set. |
| **Webhook secrets**              | One per customer per webhook subscription. Used to verify incoming events.              |
| **Source-platform credentials**  | Per-customer OAuth tokens or API keys for Stripe, Mailchimp, etc.                       |
| **Integration-internal secrets** | Database credentials, infrastructure secrets, internal service tokens.                  |
| **Donor PII**                    | Names, addresses, emails, phone numbers passing through the integration.                |
| **Financial data**               | Gift amounts, payment methods (typically tokenized), giving history.                    |
| **Health and demographic data**  | Some 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:

| Option                                                                                    | Use                                                                                                  |
| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| 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 key                                    | Acceptable when secrets manager isn't available. Requires careful KMS key rotation.                  |

Not acceptable:

| Anti-pattern                                  | Why                                                                               |
| --------------------------------------------- | --------------------------------------------------------------------------------- |
| Environment variables in deployment manifests | Visible to anyone with deploy access; logged in deployment system; not auditable. |
| `.env` files in source control                | Visible in git history forever; impossible to revoke.                             |
| Plaintext database columns                    | Anyone with read access to the database has all customer tokens.                  |
| Hardcoded in source                           | Compromised on every code commit, every CI artifact, every container image.       |

### Naming

Use a key naming convention that makes the secret's scope obvious:

```text theme={null}
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:

| Rule                                                                                                                                | Why                                                                    |
| ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| **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:

<Steps>
  <Step title="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.)
  </Step>

  <Step title="Add the new token alongside the existing one">
    Store the new token in the secrets manager. Mark it as "pending."
  </Step>

  <Step title="Cut over reads">
    Workers start reading the new token. Old token remains accessible to handle in-flight requests.
  </Step>

  <Step title="Validate">
    Confirm a successful API call with the new token. Confirm no errors from workers using the new token.
  </Step>

  <Step title="Retire the old token">
    Delete the old token from the secrets manager. Revoke it in Virtuous.
  </Step>
</Steps>

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 JavaScript theme={null}
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:

```text theme={null}
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](/crm/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:

```text theme={null}
{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 JavaScript theme={null}
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 JavaScript theme={null}
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 JavaScript theme={null}
// 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](/crm/webhooks/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 JavaScript theme={null}
// ❌ 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 JavaScript theme={null}
// ❌ 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:

| Data                                                    | Suggested retention                          |
| ------------------------------------------------------- | -------------------------------------------- |
| Active sync state (`partner_contacts`, `partner_gifts`) | Indefinite while the customer is active      |
| Dead-letter entries                                     | 90 days after resolution; archive thereafter |
| Reconciliation logs                                     | 1 year; archive thereafter                   |
| Worker logs containing identifiers                      | 30 days hot, 1 year cold                     |
| Webhook delivery captures                               | 30 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:

<Steps>
  <Step title="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.
  </Step>

  <Step title="Store in the secrets manager">
    Under the customer's scoped path. Confirm storage succeeded before returning to the customer.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Initial reconciliation">
    Run the initial bulk-load or cross-match reconciliation per the integration's architecture.
  </Step>

  <Step title="Notify the customer of completion">
    Surface in your platform's UI that sync is active.
  </Step>
</Steps>

### Offboarding

When a customer disconnects:

<Steps>
  <Step title="Stop syncing immediately">
    Pause the customer's submitter and reconciliation workers. Stop draining the queue.
  </Step>

  <Step title="Delete the webhook subscription in Virtuous">
    `DELETE /api/Webhook/{webhookId}` so Virtuous stops attempting deliveries to your endpoint.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Delete the customer's secrets">
    Remove the API token, webhook secret, and source-platform credentials from the secrets manager.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

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.

<Note>
  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.
</Note>

***

## 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:

<Steps>
  <Step title="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.
  </Step>

  <Step title="Audit the integration's recent writes">
    Pull the recent activity log for that customer. Identify any anomalous writes.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Report to the customer">
    Whether or not compromise is confirmed, the customer needs a transparent report of what was investigated and what was found.
  </Step>
</Steps>

### 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

<CardGroup cols={2}>
  <Card title="Versioning and Backward Compatibility" icon="code-branch" href="/crm/best-practices/versioning-and-backward-compatibility">
    The companion practices for long-term integration durability.
  </Card>

  <Card title="Signature Verification" icon="shield-check" href="/crm/webhooks/signature-verification">
    The verification pattern that the webhook security on this page builds on.
  </Card>

  <Card title="Authentication" icon="key" href="/crm/authentication">
    The authentication reference for the Virtuous API token format.
  </Card>

  <Card title="Sandbox Access" icon="vial" href="/crm/partners/sandbox-access">
    How to obtain Seeded Sandbox credentials for development without using production data.
  </Card>
</CardGroup>
