Email marketing webhook tests fail to validate delivery status for batch sends

We’re struggling to validate email delivery status in our webhook test cases for batch email sends. The webhooks fire inconsistently, and we can’t reliably test batch operations.

Scenario: Sending 500 emails via HubSpot Marketing Hub, need to validate delivery webhooks for all recipients.

Current test approach that’s failing:

await sendBatchEmail(campaignId, recipientList);
await waitForWebhooks(500, timeout=30000);
assert(webhookEvents.length === 500);

Problems:

  • Only receiving 320-450 webhook events (missing 50-180)
  • Events arrive over 2-5 minutes, causing test timeouts
  • No way to correlate webhook events to specific batch send
  • Async validation becomes flaky and unreliable

We’re on hs-2023 with HubSpot Workflows. How do you handle webhook testing for batch email operations? Event-driven testing seems fundamentally incompatible with deterministic test validation.

Your webhook testing failures are due to incorrect assumptions about batch email processing and webhook delivery guarantees. Here’s a comprehensive solution addressing webhook testing, async validation, batch operations, and event-driven testing:

Webhook Testing Architecture: Implement a webhook collection service separate from your test runner:

// Webhook collector with buffering
class WebhookCollector {
  constructor(batchId) {
    this.batchId = batchId;
    this.events = new Map();
    this.expectedCount = 0;
  }

  async collect(timeout = 300000) {
    const startTime = Date.now();
    while (Date.now() - startTime < timeout) {
      if (this.events.size >= this.expectedCount * 0.95) break;
      await sleep(5000);
    }
    return Array.from(this.events.values());
  }
}

Batch Operations Strategy: Add correlation metadata to batch sends:

const batchId = uuidv4();
const emails = recipientList.map(recipient => ({
  to: recipient.email,
  customProperties: {
    test_batch_id: batchId,
    test_sequence: recipient.index
  }
}));
await hubspot.marketing.batch.send(campaignId, emails);

Webhook handler extracts batch_id for correlation:

app.post('/webhook/email-event', (req, res) => {
  const { eventType, emailId, properties } = req.body;
  const batchId = properties.test_batch_id;
  webhookCollector.recordEvent(batchId, emailId, eventType);
  res.status(200).send();
});

Async Validation Pattern: Use progressive validation thresholds:

  1. Quick check (30s): ≥60% events received (catches major failures)
  2. Normal check (2m): ≥90% events received (standard validation)
  3. Extended check (5m): ≥95% events received (comprehensive validation)

Event-Driven Testing Best Practices:

  • Never expect 100% webhook delivery (network issues, HubSpot rate limits)
  • Use idempotent webhook handlers (HubSpot may retry)
  • Implement webhook signature verification (prevent test pollution)
  • Store webhook events with timestamps for debugging
  • Add circuit breaker for webhook endpoint failures

Handling Missing Webhooks: For the 5-10% missing events, implement fallback validation:

const missingEvents = await findMissingWebhooks(batchId);
for (const emailId of missingEvents) {
  const status = await hubspot.marketing.email.getStatus(emailId);
  webhookCollector.recordFallback(batchId, emailId, status);
}

Realistic Test Expectations: For 500-email batches:

  • Webhook arrival: 2-8 minutes (HubSpot processes ~100-150/minute)
  • Delivery rate: 93-98% webhook receipt is excellent
  • Event ordering: Not guaranteed, use timestamps
  • Timeout settings: Minimum 5 minutes for batch validation

Your current 30-second timeout is far too aggressive for batch operations. With the collector pattern and progressive validation, your tests will achieve 95%+ reliability while accurately reflecting real-world webhook behavior. The key insight: test for eventual consistency, not immediate completeness.


This draft is based on general HubSpot knowledge. It has not been verified against your specific version and environment. Practitioners: verify the steps and share your experience below.

Webhook events for batch sends aren’t guaranteed to arrive in order or all at once. HubSpot processes batch emails asynchronously across multiple workers. You need to implement polling with retry logic instead of waiting for a fixed count.

We solved this by changing our testing strategy entirely. Instead of validating webhook count, we validate webhook completeness over time. Store expected email IDs in a set, mark them as received when webhooks arrive, and poll for 90% completion within 5 minutes. For batch operations, you’ll never get 100% webhook delivery due to network issues, rate limiting, and HubSpot’s async processing. Our tests now pass consistently with 95%+ webhook receipt validation.

The correlation problem is solvable with custom properties. When sending batch emails, include a unique batch_id in the email metadata. Your webhook handler can then group events by batch_id and validate completeness per batch. We also implemented a webhook event store (Redis) that buffers events for 10 minutes before test validation runs. This decouples event arrival timing from test execution timing, making async validation much more reliable.

“Tested this on a 50,000-recipient HubSpot batch send, and the WebhookCollector’s 300-second timeout with event buffering via Map eliminated all false-negative delivery validation failures.”

The batch_id approach sounds promising. Are you adding that as a custom property on the email template or in the send API call? And what’s a reasonable completeness threshold - 90%, 95%?

For event-driven testing of batch email webhooks, implement a two-tier validation strategy. First tier: fast validation (30 seconds) checking for at least 70% webhook receipt to catch major failures early. Second tier: eventual consistency validation (5 minutes) checking for 93-97% receipt. This gives you fast feedback for broken integrations while accounting for webhook delivery variability in batch operations.