An end-to-end recipe for adding a Raise donation form to a customer’s website and wiring up the integration that reacts to completed donations.
This recipe walks through the complete project of adding a Raise donation form to a customer’s website — from the admin-team setup work through the partner integration’s wiring and into production monitoring. It’s the most common partner integration scenario for Raise, and the patterns here apply with small variations to many partner-built fundraising experiences.The audience is a partner integration team building or maintaining a website-integration product for a nonprofit customer. The customer has (or is creating) a Raise account; the partner is responsible for the technical wiring.
Customer: A mid-size nonprofit with a marketing website and a Raise account. They want a donation form on their /donate page that captures gifts directly in Raise.Partner: A web platform or agency that hosts (or develops) the customer’s website. The customer has asked the partner to wire up the donation form, react to completed gifts, and send custom thank-you emails.Constraints:
The donation form itself is configured in Raise (form fields, designations, payment processor) — not via API.
The partner needs to embed the form on the customer’s existing website without breaking the rest of the site.
After each donation, the partner needs to send a custom-branded thank-you email and notify the customer’s team in Slack.
The integration must work for the customer’s test donations during development and for real donations in production.
Donation flow (left to right): donor visits the customer’s site, the embed loads from Raise, the donor completes the form, Raise processes the payment.
Post-donation flow (right side): Raise fires a webhook to the partner, the partner triggers downstream actions.
The partner integration sits entirely on the post-donation side. The donation form itself is Raise’s hosted product, embedded on the customer’s site.
The customer’s Raise administrator opens the form in the admin UI and copies the embed code. They provide it to the partner through whatever onboarding flow the partner has built — typically a settings field in the partner’s product.
Customer settings (partner UI)─────────────────────────────────────────────Raise donation form embed code:[ <iframe src="..." ... ></iframe> ]─────────────────────────────────────────────Validate the code: must be non-empty and contain an iframe or script tag pointing at a Raise domain.
Store the embed code as a per-customer setting:
JavaScript
// settings/customer-config.ts (example)const customerSettings = { customer_wayne_foundation: { displayName: 'Wayne Foundation', raiseApiToken: '...', // From secrets manager raiseEmbedCode: '<iframe src="..." ></iframe>', raiseFormId: 1234, // Captured from first received webhook raiseWebhookId: null, // Populated in step 3 raiseWebhookSecret: null, // Populated in step 3 thankYouEmailTemplate: 'wayne-foundation-thank-you', slackChannel: '#wayne-foundation-donations', majorGiftThreshold: 1000, },};
The customer’s raiseFormId may not be known yet — it will be visible in the first webhook event the partner receives. Leave it as null initially and capture it from the first delivery.
The partner serves the customer’s /donate page with the embed code dropped in. A minimal Express handler:
JavaScript
import express from 'express';const app = express();app.get('/donate', async (req, res) => { const customerId = identifyCustomer(req); // E.g., from domain or session const settings = customerSettings[customerId]; if (!settings?.raiseEmbedCode) { return res.status(404).send('Donation form not configured'); } res.render('donation-page', { displayName: settings.displayName, embedCode: settings.raiseEmbedCode, });});
The template:
donation-page.ejs
<!DOCTYPE html><html><head> <title>Support <%= displayName %></title> <link rel="stylesheet" href="/styles.css"></head><body> <header> <h1>Support <%= displayName %></h1> <p>Your gift makes a difference.</p> </header> <main> <!-- Begin Raise form embed --> <%- embedCode %> <!-- End Raise form embed --> </main> <footer> <p>Thank you for your support!</p> </footer></body></html>
Three template practices that matter:
Use raw output (<%- %> in EJS, {{{ }}} in Handlebars, etc.). The embed code is HTML; escaping it would prevent the embed from rendering.
Serve the page over HTTPS. Raise’s embed loads resources over HTTPS; embedding on HTTP produces mixed-content warnings or blocks the form.
Don’t wrap the embed in another iframe or apply transforms. The embed code is designed to work as Raise provides it.
The partner integration needs to know when donations complete. Subscribe to gift events for this customer:
JavaScript
import crypto from 'crypto';async function setupWebhookForCustomer(customerId) { const settings = customerSettings[customerId]; const webhookSecret = crypto.randomBytes(32).toString('base64'); 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} — gift events`, notificationUrl: `https://partner.example.com/raise-webhooks/${customerId}`, eventTypesList: [10, 11], // Gift Created and Gift Updated (per working hypothesis) format: 1, status: 1, securityToken: webhookSecret, }), } ); const webhook = await response.json(); // Persist webhook ID and secret in the customer settings await persistCustomerSettings(customerId, { raiseWebhookId: webhook.id, raiseWebhookSecret: webhookSecret, }); return webhook;}
Notes:
The notificationUrl includes customerId in the path so the receiver can identify which customer’s event is arriving.
The secret is generated fresh per customer — never reuse secrets across customers.
Event type 10 is the working-hypothesis value for “Gift Created”; confirm via discovery against the customer’s test events. See Event Types: Discovering the mapping.
Run this once during customer onboarding. Store the returned webhook.id and the secret in the customer’s settings.
The receiver handles incoming events. Three responsibilities: verify the signature, acknowledge quickly, queue for processing.
JavaScript
import express from 'express';import crypto from 'crypto';const app = express();// Capture raw body for signature verificationapp.use('/raise-webhooks/:customerId', express.raw({ type: 'application/json' }));function verifySignature(req, secret) { const signature = req.headers['x-raise-signature']; // Confirm name against live data if (!signature) return false; const expected = crypto .createHmac('sha256', secret) .update(req.body) .digest('hex'); if (signature.length !== expected.length) return false; return crypto.timingSafeEqual( Buffer.from(signature, 'utf8'), Buffer.from(expected, 'utf8') );}app.post('/raise-webhooks/:customerId', async (req, res) => { const { customerId } = req.params; const settings = customerSettings[customerId]; if (!settings) { return res.status(404).send('Unknown customer'); } if (!verifySignature(req, settings.raiseWebhookSecret)) { return res.status(401).send('Invalid signature'); } // Acknowledge first res.status(200).send('OK'); // Then queue for async processing const event = JSON.parse(req.body.toString('utf8')); await eventQueue.publish({ customerId, eventType: event.eventType, payload: event.payload ?? event, receivedAt: new Date(), });});
See Signature Verification for the full verification pattern. Confirm the header name (x-raise-signature above is a placeholder) by inspecting real deliveries.
The idempotency_key field (supported by Slack and many other systems) provides an extra defense against duplicate notifications even if the dedup store check is bypassed.
For gifts over the customer’s configured threshold, a higher-touch alert:
JavaScript
async function alertMajorDonorTeam(customerId, gift) { const settings = customerSettings[customerId]; // Major donor channel (typically more private than the general donations channel) await slackApi.postMessage({ channel: '#major-donor-alerts', text: `🌟 Major gift: ${gift.formattedAmount} from ${gift.donor?.name}`, blocks: [ // ... formatted detail blocks ... ], }); // Email the team lead await emailService.send({ to: settings.majorDonorTeamEmail, subject: `Major gift alert: ${gift.formattedAmount}`, template: 'major-donor-alert', data: { gift, donorRecentGifts: await fetchDonorRecentGifts(customerId, gift.donorId), }, });}
The major-donor email might include the donor’s recent giving history — useful context for the cultivation team. Use GET /api/Donor/{donorId}/gifts to fetch the donor’s recent gifts.
Before going live, run a complete end-to-end test:
1
Generate a test payment method
curl -X POST https://prod-api.raisedonors.com/api/Raise/generate-test-payment-method \ -H "Authorization: Bearer $CUSTOMER_TOKEN"
Save the returned paymentMethodId for the next step.
2
Submit a test donation through the embedded form
Open the customer’s /donate page. Fill out the form with test details (name, email, amount). Use the test card details the customer’s gateway provides for test mode (typically 4242 4242 4242 4242 or similar; consult the gateway’s documentation).
3
Confirm the event arrives
Watch your webhook receiver’s logs. Within seconds of submission, a webhook event should arrive. If it doesn’t, check GET /api/Webhook/{id}/log/list to see whether Raise attempted delivery and what status code your receiver returned.
4
Confirm signature verification passes
Check that the event made it past the signature verification gate. If not, see Signature Verification for the troubleshooting flow.
5
Confirm the thank-you email sent
Check the inbox of the email address you submitted. The thank-you email should arrive within a minute of the donation.
6
Confirm Slack notification posted
Check the customer’s Slack channel for the donation notification.
7
Confirm dedup works
Use the webhook log endpoint to trigger a redelivery (some platforms support this; if Raise doesn’t, manually replay the captured payload through your queue). Confirm the second processing produces no second email and no second Slack post.
Each step confirms one piece of the integration. Running them in order makes it easy to identify which piece is failing if the end-to-end flow doesn’t work the first time.
Once test mode validates cleanly, switch to production:
Setting
Test value
Production value
Webhook subscription status
1 (active during test)
1 (active)
isTestMode filter in handler
Process test events
Skip test events
Email recipient
Test email account
Real donor email
Slack channel
#dev-notifications
Customer’s real channel
Major-donor team email
Test email account
Real team lead
The cutover is typically just a configuration change — flip the settings to production values and let real donations flow through. Monitor closely for the first few real donations to confirm the production setup works as it did in test.
Frequency of duplicate deliveries — small spikes are normal, large spikes indicate issues
Failed signature verifications
Should be zero in steady state; non-zero indicates configuration drift or attacker probing
Build dashboards for these and alert on anomalies. The investment pays off the first time a customer asks “did the donations from Tuesday’s appeal make it through?” and you can answer with confidence.