How to handle Raise API tokens, webhook secrets, and other credentials in partner integrations — secure storage, rotation, customer offboarding, and the platform-specific considerations for handling donation data.
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.
Partner integration (set on subscription create/update)
Signing outgoing webhook deliveries from Raise
Both 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.
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.
AWS Secrets Manager, HashiCorp Vault, Google Secret Manager, Azure Key Vault. Never in a code repository, environment variable file, or unencrypted database column.
Encrypt at rest
The secrets manager handles this; for database-backed storage, encrypt the column with a separate key.
Restrict access by IAM/RBAC
Only the production sync workers should be able to read tokens. Developers should not have direct production access.
Audit access
Every 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 token
Logging frameworks often serialize objects fully — make sure tokens are explicitly redacted before logging.
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.
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.
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.
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 weeklyasync 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.
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.
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.
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.
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.
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.
For partner-side databases storing donor data, encrypt at rest:
Field
Sensitivity
Encryption approach
Email
PII
Application-level encryption recommended; required for some jurisdictions
Name
PII
Application-level encryption recommended
Phone
PII
Application-level encryption recommended
Address
PII
Application-level encryption recommended
Gift amount
Financial
Standard database encryption usually sufficient
Card details
PCI
Never 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.
Embeds the Raise hosted form and lets Raise tokenize
Out of scope — never sees card data
Submits to POST /api/Raise/give with a paymentMethodId token
Out of scope — tokens aren’t card data
Implements its own card capture and sends raw card data
In 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.
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.
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.
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.