🟒HubSpot Developer Practical Textbook β€” 2026 Edition Developer Edition
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.

Webhook API v3
Signature verification required
Time required: Approximately 75 minutes
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 toexplanation
contact.creationCreate new contact
contact.deletionDelete contact
contact.propertyChangeChange contact properties
company.creationCreate new company
deal.creationCreate new opportunity
deal.propertyChangeChange properties of opportunity (change stage, etc.)
ticket.creationCreate new ticket
contact.mergeMerging 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(); // Keep raw body (requires JSON parsing to be used for signature verification) 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' }); } // Reject if the timestamp is older than 5 minutes (replay attack prevention) 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' }); } // recalculate the signature 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'); // Constant time comparison to prevent timing attacks const isValid = crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); if (!isValid) { return res.status(401).json({ error: 'Invalid signature' }); } next(); } // Apply signature verification middleware to the webhook endpoint 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) { // Each event in HubSpot has a unique eventId const events = req.body; // Webhooks arrive as an array // Immediately return 200 OK (5 seconds timeout countermeasure) res.status(200).json({ received: true }); // Processing is done asynchronously for (const event of events) { const idempotencyKey = `webhook:${event.eventId}`; // Check if it has been processed by Redis (TTL: 24 hours) const alreadyProcessed = await redis.set( idempotencyKey, '1', { NX: true, EX: 86400 } // NX: set only if not present ); if (!alreadyProcessed) { console.log(`Duplicate event skipped: ${event.eventId}`); continue; } // Processing body 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 }); // Get opportunity details and related contacts 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); // Update contact lifecycle to "customer" await client.crm.contacts.batchApi.update({ inputs: contactIds.map(id => ({ id, properties: { lifecyclestage: 'customer' } })), }); // Notification of contract on internal Slack await notifySlack({ channel: '#sales-wins', text: `πŸŽ‰ Deal: ${deal.properties.dealname} β€” Β₯${Number(deal.properties.amount).toLocaleString()}`, }); // Link order data to ERP system 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.