Skip to main content
A Raise integration handles two distinct credentials per customer: the Raise API token (used to authenticate API requests) and the webhook security token (used to verify incoming webhook deliveries). Both must be stored securely, rotated periodically, and removed when the customer offboards. The patterns on this page cover the production practices that keep these credentials safe across the integration’s lifetime. Beyond credentials, the page covers the broader security considerations for partner integrations: protecting donor data in transit and at rest, logging and redaction, compliance (especially the implications of touching donation flows), and the practices that prevent the integration from becoming a vector for attacks against the customer’s account. The audience is integration security leads and engineers building production Raise integrations.

The two credentials

CredentialIssued byUsed forLives in
Raise API tokenCustomer’s Raise administratorBearer auth on all API requestsPartner secrets manager
Webhook security tokenPartner integration (set on subscription create/update)Signing outgoing webhook deliveries from RaiseBoth partner secrets manager and Raise’s stored subscription record
Each customer has their own tokens — never share tokens across customers, even within the same partner. The blast radius of a compromise should be one customer, not all of them.

API token storage

The customer’s Raise administrator generates an API token in the Raise admin UI and provides it to the partner integration through whatever onboarding flow the partner has built. From the moment the partner receives it, the token must be treated as a sensitive secret.

Storage practices

PracticeDescription
Store in a secrets managerAWS Secrets Manager, HashiCorp Vault, Google Secret Manager, Azure Key Vault. Never in a code repository, environment variable file, or unencrypted database column.
Encrypt at restThe secrets manager handles this; for database-backed storage, encrypt the column with a separate key.
Restrict access by IAM/RBACOnly the production sync workers should be able to read tokens. Developers should not have direct production access.
Audit accessEvery read of a customer’s token should be logged. Anomalies (a token read by an unfamiliar service or at an unusual time) should alert.
Never log the tokenLogging frameworks often serialize objects fully — make sure tokens are explicitly redacted before logging.

A storage abstraction

JavaScript
class CredentialStore {
  constructor(secretsManager) {
    this.secretsManager = secretsManager;
  }

  async getRaiseToken(customerId) {
    const secret = await this.secretsManager.getSecret(
      `raise-integration/${customerId}/api-token`
    );
    return secret.value;
  }

  async setRaiseToken(customerId, token) {
    await this.secretsManager.putSecret(
      `raise-integration/${customerId}/api-token`,
      { value: token, updatedAt: new Date().toISOString() }
    );
  }

  async getWebhookSecret(customerId) {
    const secret = await this.secretsManager.getSecret(
      `raise-integration/${customerId}/webhook-secret`
    );
    return secret.value;
  }

  async setWebhookSecret(customerId, secret) {
    await this.secretsManager.putSecret(
      `raise-integration/${customerId}/webhook-secret`,
      { value: secret, updatedAt: new Date().toISOString() }
    );
  }
}

// Usage — never reach into the secrets manager directly from worker code
const credentials = new CredentialStore(secretsManager);
const token = await credentials.getRaiseToken(customerId);
The abstraction makes it easy to add caching, audit logging, and other cross-cutting concerns in one place.

Token caching

For high-volume integrations, the secrets manager’s read latency can dominate request time. A short-TTL in-memory cache mitigates this:
JavaScript
class CachedCredentialStore {
  constructor(underlying, ttlSeconds = 300) {
    this.underlying = underlying;
    this.ttlMs = ttlSeconds * 1000;
    this.cache = new Map();
  }

  async getRaiseToken(customerId) {
    const cached = this.cache.get(`token:${customerId}`);
    if (cached && Date.now() - cached.cachedAt < this.ttlMs) {
      return cached.value;
    }
    const token = await this.underlying.getRaiseToken(customerId);
    this.cache.set(`token:${customerId}`, { value: token, cachedAt: Date.now() });
    return token;
  }
}
5-minute TTL is a reasonable default. Shorter TTLs reduce blast radius during rotation; longer TTLs reduce secrets-manager cost. Tune based on your specific platform.

API token rotation

Tokens should be rotated periodically — typically every 90 days for high-stakes integrations, every 6–12 months for lower-stakes ones. The rotation pattern is straightforward but requires coordination with the customer.

The rotation flow

1

Customer's Raise administrator generates a new token

In the Raise admin UI. The old token typically remains valid during the rotation window.
2

Customer provides the new token to the partner

Through your onboarding/settings flow.
3

Partner stores the new token alongside the old one

Both are marked valid in your credential store.
4

Partner switches to using the new token

Workers re-read credentials and start using the new token.
5

Confirm requests succeed with the new token

Monitor for 401 errors that would indicate a problem.
6

Customer revokes the old token in Raise

Once the new token is verified working.
7

Partner removes the old token from secrets manager

Completing the rotation.

A rotation helper

JavaScript
async function rotateRaiseToken(customerId, newToken) {
  // Validate the new token works before committing
  const testResponse = await fetch(
    'https://prod-api.raisedonors.com/api/Donor/list?Take=1',
    { headers: { Authorization: `Bearer ${newToken}` } }
  );

  if (!testResponse.ok) {
    throw new Error(`New token failed validation: ${testResponse.status}`);
  }

  // Mark the old token as previous (kept for rollback)
  const oldToken = await credentials.getRaiseToken(customerId);
  await credentials.setRaiseToken(customerId, newToken);
  await credentials.setPreviousRaiseToken(customerId, oldToken);

  // Invalidate any cached credentials
  await credentials.invalidateCache(customerId);

  // Schedule deletion of the old token after a grace period
  await scheduleTokenCleanup(customerId, 'previous', '7 days');

  await audit.log({
    customerId,
    action: 'token_rotated',
    timestamp: new Date(),
  });
}
The validation step catches “the customer pasted the wrong thing” before the new token gets stored. The previous-token retention provides a rollback window in case something goes wrong.

Triggering rotation reminders

For partner integrations with many customers, rotation should be tracked centrally:
JavaScript
async function findCustomersNeedingRotation(maxAgeDays = 90) {
  const all = await credentials.listAll();
  const cutoff = new Date(Date.now() - maxAgeDays * 24 * 60 * 60 * 1000);

  return all.filter((c) => c.tokenUpdatedAt < cutoff);
}

// Run weekly
async function rotationReminderJob() {
  const needRotation = await findCustomersNeedingRotation(80); // 10-day warning
  for (const customer of needRotation) {
    await emailService.send({
      to: customer.adminEmail,
      template: 'token-rotation-reminder',
      data: { lastRotated: customer.tokenUpdatedAt },
    });
  }
}
Reminders to the customer 10 days before the rotation deadline give them time to act without urgency. Customers who don’t rotate by the deadline get more urgent reminders; customers who don’t rotate within a longer window get escalated.

Webhook secret management

The webhook security token has different lifecycle than the API token — the partner integration generates it (rather than receiving it from the customer), stores it on both sides (partner secrets manager and Raise’s subscription record), and rotates it differently.

Generating a strong secret

JavaScript
import crypto from 'crypto';

function generateWebhookSecret() {
  // 32 bytes of cryptographically random data = 256 bits of entropy
  return crypto.randomBytes(32).toString('base64');
}
256 bits of entropy is essentially impossible to brute-force. Don’t reduce below 128 bits.

Storing both sides

JavaScript
async function createWebhookSubscription(customerId) {
  const settings = customerSettings[customerId];
  const secret = generateWebhookSecret();

  // 1. Store on the partner side first
  await credentials.setWebhookSecret(customerId, secret);

  // 2. Create the subscription in Raise
  const response = await fetch('https://prod-api.raisedonors.com/api/Webhook', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${settings.raiseApiToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: `Partner integration — ${customerId}`,
      notificationUrl: `https://partner.example.com/raise-webhooks/${customerId}`,
      eventTypesList: [10, 11, 20, 22],
      format: 1,
      status: 1,
      securityToken: secret,
    }),
  });

  const webhook = await response.json();

  // 3. Store the webhook ID for management
  await persistCustomerSettings(customerId, {
    raiseWebhookId: webhook.id,
  });

  return webhook;
}
The order matters — store the secret on the partner side before creating the subscription. If the subscription create succeeds but the secret storage fails, you have a working webhook delivery you can’t verify. If the secret storage succeeds but the subscription create fails, you have an orphaned secret that’s easy to clean up.

Webhook secret rotation

The dual-secret pattern (verify against either current or previous secret during the rotation window):
1

Generate a new secret

Cryptographically random, 32 bytes.
2

Update the partner-side storage with both secrets

Both are marked valid for verification.
3

Update the webhook subscription with the new secret via PUT

Raise begins signing with the new secret immediately.
4

Confirm new deliveries verify with the new secret

Inspect webhook logs and partner-side verification metrics.
5

Remove the old secret from partner-side storage

After confirmation, typically a few hours later.
The dual-secret window is brief but essential — without it, in-flight deliveries signed with the old secret would fail during the cutover.
JavaScript
async function rotateWebhookSecret(customerId) {
  const settings = customerSettings[customerId];
  const newSecret = generateWebhookSecret();
  const oldSecret = await credentials.getWebhookSecret(customerId);

  // 1. Store both secrets (dual-verify window starts)
  await credentials.setWebhookSecret(customerId, newSecret);
  await credentials.setPreviousWebhookSecret(customerId, oldSecret);

  // 2. Update the subscription in Raise
  await fetch(
    `https://prod-api.raisedonors.com/api/Webhook/${settings.raiseWebhookId}`,
    {
      method: 'PUT',
      headers: {
        Authorization: `Bearer ${settings.raiseApiToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        // ... existing subscription fields ...
        securityToken: newSecret,
      }),
    }
  );

  // 3. Schedule cleanup of the old secret
  await scheduleSecretCleanup(customerId, 'previous-webhook', '1 hour');
}
And the dual-verify function during the rotation window:
JavaScript
async function verifyRaiseWebhook(req, customerId) {
  const primary = await credentials.getWebhookSecret(customerId);
  if (verifyWithSecret(req, primary)) return true;

  const previous = await credentials.getPreviousWebhookSecret(customerId);
  if (previous && verifyWithSecret(req, previous)) return true;

  return false;
}
See Signature Verification: Rotating the secret for the broader rotation pattern.

Customer offboarding

When a customer cancels the integration, both credentials should be removed cleanly:
1

Delete the webhook subscription in Raise

DELETE /api/Webhook/{id} to stop event deliveries.
2

Remove the API token from partner secrets manager

No reason to retain it.
3

Remove the webhook secret from partner secrets manager

Same.
4

Mark the customer as offboarded in your system

Subsequent sync attempts should fail loudly rather than silently — the lack of credentials is intentional.
5

Document the offboarding for audit

When the offboarding happened, who triggered it, why.
JavaScript
async function offboardCustomer(customerId, reason) {
  const settings = customerSettings[customerId];

  // 1. Delete the webhook subscription
  if (settings.raiseWebhookId) {
    try {
      await fetch(
        `https://prod-api.raisedonors.com/api/Webhook/${settings.raiseWebhookId}`,
        {
          method: 'DELETE',
          headers: { Authorization: `Bearer ${settings.raiseApiToken}` },
        }
      );
    } catch (err) {
      console.warn(`Failed to delete webhook for ${customerId}:`, err);
      // Continue with offboarding even if this fails — we'll clean up our side
    }
  }

  // 2. Remove credentials
  await credentials.deleteAllForCustomer(customerId);

  // 3. Mark customer offboarded
  await customerStore.markOffboarded(customerId, {
    offboardedAt: new Date(),
    reason,
  });

  // 4. Audit
  await audit.log({
    customerId,
    action: 'offboarded',
    reason,
    timestamp: new Date(),
  });
}
Don’t retain credentials “just in case the customer comes back.” If they do, they’ll generate new tokens. Retaining old credentials adds compliance risk for no operational benefit.

Donor data handling

Beyond credentials, Raise integrations handle donor data (PII) that requires its own protections.

Encryption in transit

All API calls to Raise use HTTPS. All webhook deliveries from Raise should arrive over HTTPS to the partner’s endpoint. Verify both directions:
JavaScript
// Confirm partner-side endpoint enforces HTTPS
app.use((req, res, next) => {
  if (!req.secure && process.env.NODE_ENV === 'production') {
    return res.status(403).send('HTTPS required');
  }
  next();
});
The webhook subscription’s notificationUrl must be https://. Don’t try to use plain HTTP even for development — set up local HTTPS tunneling instead. See Local Testing.

Encryption at rest

For partner-side databases storing donor data, encrypt at rest:
FieldSensitivityEncryption approach
EmailPIIApplication-level encryption recommended; required for some jurisdictions
NamePIIApplication-level encryption recommended
PhonePIIApplication-level encryption recommended
AddressPIIApplication-level encryption recommended
Gift amountFinancialStandard database encryption usually sufficient
Card detailsPCINever stored on the partner side — Raise handles tokenization
Don’t store raw card details under any circumstances. Card tokenization happens at Raise’s hosted donation form; the partner only sees paymentMethodId (a non-sensitive token) and metadata like cardBrand and expMonthAndYear.

Logging and redaction

Logs are a common source of accidental credential exposure. Build redaction into the logging framework:
JavaScript
function safeLog(message, data) {
  const redacted = redactSensitive(data);
  logger.info(message, redacted);
}

function redactSensitive(obj) {
  const SENSITIVE_KEYS = [
    'token', 'apiToken', 'authorization', 'auth',
    'securityToken', 'webhookSecret', 'secret',
    'password', 'apiKey', 'privateKey',
    'cardNumber', 'cvv', 'cvc',
  ];

  if (typeof obj !== 'object' || obj === null) return obj;
  if (Array.isArray(obj)) return obj.map(redactSensitive);

  return Object.fromEntries(
    Object.entries(obj).map(([key, value]) => {
      if (SENSITIVE_KEYS.some((s) => key.toLowerCase().includes(s.toLowerCase()))) {
        return [key, '[REDACTED]'];
      }
      return [key, redactSensitive(value)];
    })
  );
}
Apply redaction at the logging-framework level so it can’t be forgotten in individual call sites. Audit logs periodically for missed cases.

Compliance considerations

Partner integrations handling donation data inherit compliance obligations from their customers.

PCI DSS

If your integrationPCI scope
Embeds the Raise hosted form and lets Raise tokenizeOut of scope — never sees card data
Submits to POST /api/Raise/give with a paymentMethodId tokenOut of scope — tokens aren’t card data
Implements its own card capture and sends raw card dataIn scope — full PCI DSS compliance required
For most partner integrations, stay out of PCI scope by letting Raise tokenize. The compliance overhead of being in PCI scope is substantial; the value of capturing card details yourself is rarely worth it.

GDPR / data privacy

For customers operating in jurisdictions with strict data privacy laws (EU GDPR, California CCPA, etc.):
PracticeRequired by
Data minimization — store only what’s neededGDPR Art. 5
Right to erasure — delete on customer/donor requestGDPR Art. 17
Data processing agreement with the customerGDPR Art. 28
Breach notification within 72 hoursGDPR Art. 33
Consent tracking for marketingGDPR Art. 7
Partner integrations should:
  • Document what donor data they retain and for how long.
  • Provide a deletion mechanism for individual donor data.
  • Sign data processing agreements with customers in regulated jurisdictions.
  • Implement appropriate technical and organizational measures (TOMs).

SOC 2 / similar certifications

For partner integrations serving enterprise nonprofit customers, SOC 2 Type II certification is increasingly expected. The certification documents controls in areas like:
  • Access controls and authentication
  • Encryption in transit and at rest
  • Change management
  • Incident response
  • Vendor management
The credential management practices on this page directly support several SOC 2 control areas.

Attack surface considerations

Beyond credential hygiene, partner integrations should be designed to limit attack surface:

Endpoint hardening

PracticeDescription
Rate limit incoming webhooksPrevent abuse where an attacker hits the endpoint repeatedly
Validate signatures before processingAn unsigned request is a forged request
Limit request body sizeDon’t accept arbitrarily large bodies
Disable verbose error messages in productionStack traces leak implementation details
Use a Web Application FirewallAn extra defense against common attack patterns

Principle of least privilege

Each component of the integration should have the minimum credentials it needs:
ComponentCredentials
Sync workerRead-only access to credentials, read/write to sync state
Onboarding serviceWrite access to credentials (new tokens), no production data access
Customer support toolsRead-only access to sync state, no credential access
Analytics / reportingRead-only access to sync state, no credential or PII access
A compromise of any single component should not provide access to credentials or PII it doesn’t need.

Audit logging

Every credential access, every customer offboarding, every token rotation should produce an audit log entry:
JavaScript
await audit.log({
  customerId,
  actor: 'sync-worker-prod-3',
  action: 'token_read',
  resource: 'raise_api_token',
  timestamp: new Date(),
  context: { workerInstance: process.env.HOSTNAME },
});
Audit logs should be tamper-resistant — typically stored in a separate system from the application’s main database, with access tightly restricted. Retain for at least a year, longer in regulated jurisdictions.

A security checklist

Walk through this when designing or auditing the integration:
  • Raise API tokens stored in a secrets manager, never in code or environment files
  • Webhook secrets generated with 32 bytes of cryptographic random
  • Per-customer credentials — no shared tokens across customers
  • HTTPS everywhere — API calls and webhook deliveries
  • Signature verification on every incoming webhook before any processing
  • Logging framework includes credential redaction by default
  • No raw card data ever stored on the partner side
  • Customer offboarding deletes credentials and the webhook subscription
  • Token rotation tracked centrally with reminders
  • Webhook secret rotation uses the dual-verify pattern
  • Sensitive donor data (email, name, address) encrypted at rest
  • Audit logs for all credential access and customer lifecycle events
  • Production access controlled by IAM/RBAC; developers don’t have direct production access
  • PCI scope is out — Raise handles all card tokenization
  • Data processing agreements signed with customers in regulated jurisdictions
  • Incident response runbook exists for credential compromise
Most of these are policy decisions as much as technical ones. The investment in getting them right early is much smaller than the cost of fixing them after a breach.

Where to go next

Authentication

The reference for the Raise API token authentication pattern.

Signature Verification

The webhook signature verification pattern that uses the secrets discussed here.

Versioning and Backward Compatibility

The durability practices that complement security practices.

Error Recovery Patterns

The 401-handling and credential-refresh patterns referenced in this page.
Last modified on May 21, 2026