Built for startups,
scaled for unicorns
Successfully submitted!
Error! Please try again

Webhooks are the most flexible way to connect GrowSurf to your custom tech stack. Every time a referral event occurs β a participant joins, makes a referral, or earns a reward β GrowSurf can send a POST request to your server with all the event details. From there, you can trigger any custom logic: update your database, send notifications, process rewards, sync to CRMs, or kick off complex workflows.
This guide is the foundational reference for working with GrowSurf webhooks. You'll learn how to set up webhook endpoints, validate incoming requests, parse event payloads, handle different event types, implement error handling and retry logic, and build a robust webhook processing architecture.
Set up your GrowSurf campaign to send webhook events to your server.
https://api.yourapp.com/webhooks/growsurf)PARTICIPANT_CREATED β new participant signed upPARTICIPANT_REFERRED β someone was referredCAMPAIGN_REFERRAL_CONVERTED β referral convertedPARTICIPANT_REACHED_REWARD β participant earned a rewardCreate a server endpoint that receives and processes GrowSurf webhook events.
Verify that incoming webhooks actually come from GrowSurf to prevent spoofing.
Handle different event types with dedicated processing functions.
event field from the request bodyPARTICIPANT_CREATED β create user record, send welcome emailPARTICIPANT_REFERRED β update CRM, send notificationsCAMPAIGN_REFERRAL_CONVERTED β process rewards, update metricsBuild resilient webhook processing that handles retries and failures gracefully.
Set up monitoring to ensure your webhook handler stays healthy.
// Complete GrowSurf Webhook Handler
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// Processed event cache (use Redis in production)
const processedEvents = new Set();
// Verify webhook signature
function verifySignature(payload, signature, secret) {
const computed = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(computed)
);
}
// Event handlers
const eventHandlers = {
PARTICIPANT_CREATED: async (data) => {
console.log(`New participant: ${data.participant.email}`);
// Add to your database, CRM, email list, etc.
},
PARTICIPANT_REFERRED: async (data) => {
console.log(`${data.participant.email} referred by ${data.referrer.email}`);
// Notify referrer, update CRM, track analytics
},
CAMPAIGN_REFERRAL_CONVERTED: async (data) => {
console.log(`Referral converted: ${data.participant.email}`);
// Process rewards, update metrics, celebrate
},
PARTICIPANT_REACHED_REWARD: async (data) => {
console.log(`Reward earned: ${data.participant.email}`);
// Fulfill reward, send confirmation
}
};
// Main webhook endpoint
app.post('/webhooks/growsurf', async (req, res) => {
// 1. Verify signature
const signature = req.headers['x-growsurf-signature'];
if (!verifySignature(req.body, signature, process.env.GROWSURF_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Check idempotency
const eventId = req.body.id || `${req.body.event}-${req.body.participant?.id}-${Date.now()}`;
if (processedEvents.has(eventId)) {
return res.status(200).json({ message: 'Already processed' });
}
// 3. Route to handler
const handler = eventHandlers[req.body.event];
if (!handler) {
console.warn(`Unknown event: ${req.body.event}`);
return res.status(200).json({ message: 'Unrecognized event' });
}
// 4. Process asynchronously
res.status(200).json({ received: true });
try {
await handler(req.body);
processedEvents.add(eventId);
} catch (error) {
console.error(`Error processing ${req.body.event}:`, error);
// Alert your monitoring system
}
});
app.listen(3000);GrowSurf expects a response within 5 seconds. If your processing logic takes longer (database queries, API calls to third parties), return 200 immediately and process the event asynchronously using a job queue or background worker. This prevents webhook timeouts and unnecessary retries.
Always use crypto.timingSafeEqual() when comparing webhook signatures. Regular string comparison is vulnerable to timing attacks. This security practice prevents attackers from incrementally guessing your webhook secret.
When event processing fails, don't just log and forget. Push failed events to a dead letter queue (DLQ) for manual review and replay. This ensures no referral events are permanently lost, even during outages or bugs in your handler code.
Use a versioned endpoint (e.g., /webhooks/growsurf/v1) so you can deploy a new version alongside the old one during migrations. This prevents downtime when you change your webhook processing logic.
GrowSurf retries failed webhooks (non-2xx responses) with exponential backoff. Typically, retries happen at 1 minute, 5 minutes, 30 minutes, and 2 hours after the initial failure. After several failed retries, the webhook is marked as failed in GrowSurf's delivery log. Always return 2xx for successfully received events to prevent unnecessary retries.
The payload includes: the event type, the participant object (email, name, referral code, referral count, metadata), the referrer object (if applicable), and campaign details. The exact fields vary by event type β for example, PARTICIPANT_REFERRED includes both participant and referrer objects, while PARTICIPANT_CREATED only includes the participant.
Yes. In GrowSurf's webhook settings, you can select which event types to receive. Only enable the events you actually process β this reduces unnecessary traffic and simplifies your handler code. You can add more event types later as your integration expands.
Trusted by marketing and product teams at fast-growing B2C, fintech, and SaaS companies
