Chapter 8 Β Β·Β Webhooks & Event-Driven Design
Webhook &
event-driven design
HubSpot webhook settings, authenticity verification by signature verification, idempotent processing, external system linkage pattern. Securely build systems that react to CRM events in real time.
8-1 How HubSpot Webhook works
When an event occurs in HubSpot, a POST request is sent to the URL you registered.
π‘ Webhook operation flow
β An event occurs in HubSpot (creating a contact, changing properties, moving a deal stage, etc.)
β‘ Send a POST request to the webhook URL registered by HubSpot
β’ The receiving server 200 OK (within 5 seconds)
β£ If it is other than 200, retry up to 3 times (exponential backoff)
β€ If it still fails after retrying, HubSpot will give up on sending (Events may be lostοΌ
| Events you can subscribe to | explanation |
contact.creation | Create new contact |
contact.deletion | Delete contact |
contact.propertyChange | Change contact properties |
company.creation | Create new company |
deal.creation | Create new opportunity |
deal.propertyChange | Change properties of opportunity (change stage, etc.) |
ticket.creation | Create new ticket |
contact.merge | Merging contacts |
8-2 Signature verification (required)
Verify that webhook requests come from the real HubSpot.
π How signature verification works
When HubSpot sends a webhook, it adds X-HubSpot-Signature-v3 will be granted.
This includes request information (HTTP method, URL, timestamp, body) and
App client secretHMAC-SHA256 signature using .
By recalculating this signature on the receiving side and making sure it matches,
Prevents spoofing and man-in-the-middle attacks.
Node.js/Express β Signature verification middleware
import crypto
from 'crypto';
import express
from 'express';
const app = express();
app.use(express.json({
verify: (req, res, buf) => { req.rawBody = buf; }
}));
function verifyHubSpotSignature(req, res, next) {
const signature = req.headers[
'x-hubspot-signature-v3'];
const timestamp = req.headers[
'x-hubspot-request-timestamp'];
if (!signature || !timestamp) {
return res.status(
401).json({ error:
'Missing signature' });
}
const MAX_AGE_MS =
5 *
60 *
1000;
if (Math.abs(Date.now() - parseInt(timestamp)) > MAX_AGE_MS) {
return res.status(
401).json({ error:
'Request too old' });
}
const method = req.method.toUpperCase();
const url =
`https://${req.hostname}${req.originalUrl}`;
const body = req.rawBody?.toString(
'utf8') ??
'';
const source = method + url + body + timestamp;
const expected = crypto
.createHmac(
'sha256', process.env.HUBSPOT_CLIENT_SECRET)
.update(source)
.digest(
'base64');
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
if (!isValid) {
return res.status(
401).json({ error:
'Invalid signature' });
}
next();
}
app.post(
'/webhook/hubspot', verifyHubSpotSignature, handleWebhook);
8-3 Idempotent processing pattern
Use an idempotent design that can safely handle even if the same webhook arrives multiple times.
π Why is idempotent processing necessary?
HubSpot supports webhook sending At-Least-Once(at least once).
If the 200 OK is not received due to network failure, the same event will be resent.
Without idempotent processing (a design in which the result does not change no matter how many times the same operation is performed),
Problems such as creating duplicate data, double billing, and sending duplicate emails may occur.
Node.js β Deduplication with idempotent keys
import { createClient }
from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
async function handleWebhook(req, res) {
const events = req.body;
res.status(
200).json({ received:
true });
for (
const event
of events) {
const idempotencyKey =
`webhook:${event.eventId}`;
const alreadyProcessed =
await redis.set(
idempotencyKey,
'1',
{ NX:
true, EX:
86400 }
);
if (!alreadyProcessed) {
console.log(
`Duplicate event skipped: ${event.eventId}`);
continue;
}
await processEvent(event);
}
}
async function processEvent(event) {
const { subscriptionType, objectId, propertyName, propertyValue } = event;
switch (subscriptionType) {
case 'contact.creation':
await onContactCreated(objectId);
break;
case 'deal.propertyChange':
if (propertyName ===
'dealstage' && propertyValue ===
'closedwon') {
await onDealClosed(objectId);
}
break;
}
}
8-4 External system linkage pattern
Learn practical external integration patterns using HubSpot webhooks.
π SFA β HubSpot sync
When a contact is updated in an external SFA (such as Salesforce), automatically update the corresponding contact in HubSpot via a webhook. In the case of two-way synchronization, it is necessary to identify the update source and prevent infinite loops.
π³ Payment service cooperation
Receive Stripe webhooks (payment success/cancellation) and update HubSpot custom objects (subscriptions). The lifecycle stage will also change automatically.
Node.js β Update external system on closed event
async function onDealClosed(dealId) {
const client =
new HubspotClient({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN });
const [deal, contacts] =
await Promise.all([
client.crm.deals.basicApi.getById(dealId, [
'dealname',
'amount',
'hubspot_owner_id']),
client.crm.associations.v4.basicApi.getPage(
'deals', dealId,
'contacts'),
]);
const contactIds = contacts.results.map(r => r.toObjectId);
await client.crm.contacts.batchApi.update({
inputs: contactIds.map(id => ({
id,
properties: { lifecyclestage:
'customer' }
})),
});
await notifySlack({
channel:
'#sales-wins',
text:
`π Deal: ${deal.properties.dealname} β Β₯${Number(deal.properties.amount).toLocaleString()}`,
});
await createErpOrder({
dealId,
amount: deal.properties.amount,
contactIds,
});
}
β Immediate 200 responses + asynchronous processing:
The webhook handler is within 5 secondsshould return 200.
Throw heavy processing (DB operations, external API collaboration) to an asynchronous queue (BullMQ, SQS, etc.)
Please separate it from the response.
8-5 Summary of this chapter
β
Chapter 8 Checklist
- Understand HubSpot webhook event types and At-Least-Once guarantees
- Signature verification using X-HubSpot-Signature-v3 can be implemented with Node.js
- Prevent replay attacks with timestamp verification (within 5 minutes)
- Duplicate processing can be eliminated with idempotent keys using Redis + NX flags
- Immediate 200 response + Understood the design pattern of asynchronous processing
- You can design external system linkages starting from contract/settlement events.
About the next chapter (Chapter 9):External collaboration designβLearn App and OAuth. Practical explanation of Public App structure, OAuth 2.0 flow, Marketplace application, and Scopes design.