Why duplicates happen
Three scenarios produce duplicate webhook deliveries:
The first two are common. The third is rare but worth handling defensively.
The partner integration’s job: detect “I’ve already processed this event” before producing any side effects. The simplest reliable mechanism is a deduplication store.
The deduplication store
A dedup store records the unique identifier of every event the integration has processed. Before processing a new event, check the store. If the event is already there, skip processing. If not, add it and proceed.JavaScript
Choosing the deduplication key
The key uniquely identifies an event. The right key depends on what “the same event” means for the integration.Option 1: contextId + event type
The simplest reliable key. ThecontextId from the webhook log is the ID of the entity that triggered the event (the Gift ID, Donor ID, etc.); combined with the event type integer, it uniquely identifies the event:
JavaScript
gift created event (eventType 10, contextId 9876) as the same event regardless of how many times it’s delivered.
Strengths: simple, durable, works without depending on undocumented event payload structure.
Weakness: doesn’t distinguish multiple legitimate updates to the same resource. A gift updated event for gift 9876 today is the same key as a gift updated event for gift 9876 tomorrow — they’ll be treated as duplicates.
To handle multiple legitimate updates, see Option 3 below.
Option 2: payload hash
Compute a stable hash over the relevant fields of the event payload:JavaScript
id, modifiedDate) being stable. If Raise changes the payload shape, the key changes. Use a small, stable subset of fields rather than hashing the whole payload.
Option 3: contextId + event type + modifiedDate
A hybrid that distinguishes both the resource and the version of the resource:JavaScript
modifiedDate) but treats duplicate deliveries of the same update as the same event.
Recommended for most integrations. Distinguishes legitimate updates while still deduplicating delivery retries.
Option 4: delivery-level identifier (if available)
If Raise’s webhook payload or headers include a delivery ID (a unique identifier per delivery attempt), use that. This is the cleanest key — each delivery is uniquely identified.The Raise OpenAPI spec doesn’t document a per-delivery identifier in the webhook payload or headers. Inspect real webhook deliveries (via the log endpoints or your endpoint’s request logs) to check whether one is included. If so, use it as the dedup key.
Time-bounded vs. permanent deduplication
The dedup store needs to remember processed events. Two approaches to retention:Time-bounded retention
Keep dedup records for a bounded window (e.g., 7 days) and discard older entries. Acceptable when:- Retry attempts complete within the window (typically minutes to hours, well under 7 days).
- Storage cost matters and the integration processes high event volume.
JavaScript
Permanent retention
Keep dedup records indefinitely. Acceptable when:- Storage cost is negligible.
- Re-processing risk extends beyond the retry window (e.g., manual replays during incident recovery).
- The integration’s audit requirements include “we processed event X at time T.”
Atomicity: check-and-record as a unit
A subtle race condition: two duplicate deliveries arrive simultaneously, both check the dedup store (neither finds the key), and both proceed to process. To prevent this, the check-and-record needs to be atomic.Pattern 1: insert-then-process
Insert the dedup record first, with a unique constraint on the key. If the insert succeeds, you “won” the race — proceed to process. If the insert fails (unique constraint violation), another worker is already processing — skip.JavaScript
JavaScript
Pattern 2: transactional check-and-process
If your downstream side effects are themselves transactional, wrap the dedup check and the side effects in the same transaction:JavaScript
Idempotent side effects
The dedup store handles the “I’ve seen this before” check. But for side effects that touch external systems, idempotency at the side-effect layer adds robustness.Idempotency keys on downstream API calls
If your integration writes to a downstream system that supports idempotency keys (e.g., Stripe, many modern APIs), pass the event key as the idempotency key:JavaScript
Database upserts instead of inserts
If your integration writes records to a database, use upserts keyed by a stable identifier (the Raise resource ID, typically):JavaScript
Email and notification deduplication
Email and Slack notifications are typically not idempotent at the platform level — sending the same email twice produces two emails in the donor’s inbox. For these side effects, the dedup store check is the only defense. Make sure it runs before the notification is sent.JavaScript
emailDedup store (distinct from the main event dedup store) lets you track “we already sent this specific email” independent of the broader event processing.
Reprocessing for recovery
Sometimes you want to reprocess an event — for example, recovering from a bug in the original processing logic, or replaying events after a downstream system restoration.Selective reprocessing
For one-off recoveries, fetch the event from the webhook log and reprocess directly:JavaScript
Bulk reprocessing for a time window
For wider recoveries, query the webhook logs across a time window and reprocess each:JavaScript
Operational practices
A few practices that make idempotent processing robust in production:Monitor the duplicate rate
Track how often the dedup store rejects duplicates. A small steady rate is normal (occasional retries). A sudden spike indicates either:- Partner-side issues causing slow responses and retry storms.
- Raise-side issues causing redeliveries.
- A bug in your event processing.
Log the dedup decisions
Log each “skipping duplicate” decision with enough context to investigate later:JavaScript
originalProcessedAt and thisAttemptAt reveals the retry pattern — useful for understanding webhook delivery behavior.
Test the dedup path
Include tests that explicitly fire the same event twice and verify the second processing is skipped. This is easy to forget — the happy path of single-delivery events doesn’t exercise the dedup logic — but it’s the most important test for production reliability.JavaScript
Where to go next
Local Testing
Test the dedup logic locally by replaying captured events.
Retry Behavior
The retry pattern that makes dedup necessary.
Webhooks Overview
The webhook log endpoints used for inspection and reprocessing.
Error Recovery Patterns
The broader retry and dead-letter patterns this idempotency story fits into.