Why the old handler breaks
Many webhook handlers are built around one convenient sample. They assume a property called current contains the changed object, expect related person data next to the deal, or parse a timestamp by splitting on a space. Those assumptions become fragile when the webhook version changes its own envelope or event fields. Do not infer the webhook payload solely from the REST API v2 migration guide: the webhook documentation and a captured event are the authority for the subscription you actually use.
The REST API v2 migration guide documents that API responses can omit related objects to avoid unnecessary fetching. That fact is separate from the webhook contract. Regardless of the event shape, treating a webhook as a notification and retrieving authoritative current state is safer than assuming the event contains a complete object.
Treat the webhook as a notification envelope
Your handler should answer four questions before it mutates another system:
Store a compact envelope such as event ID, object type, object ID, action, timestamp, company identifier, and a hash of the redacted body. Use the provider’s event identifier when available. If a stable event ID is not exposed in the body you receive, create an idempotency key from the event metadata and entity ID, but understand that this is an approximation.
- What event happened?
- Which entity and ID changed?
- When was the event created?
- Is this event new, duplicated, or older than the state already processed?
A defensive receiver
An Express-style receiver can follow this shape:
Do not call several slow Pipedrive endpoints before acknowledging the webhook. A timeout can make the sender retry while your first request is still running, creating duplicate work. The queue consumer can retrieve current state, apply business rules, and record completion.
app.post("/webhooks/pipedrive", async (req, res) => {
const event = validateEnvelope(req.body);
await queue.publish({
idempotencyKey: makeKey(event),
objectId: event.objectId,
objectType: event.objectType,
receivedAt: new Date().toISOString(),
raw: redact(req.body)
});
res.sendStatus(200);
});
Find the entity ID reliably
A payload can include both a numeric ID and a nested object. Prefer the documented identifier for the event type, and reject an event where the ID is missing instead of guessing from a title or email address. Titles and names are editable and are not safe primary keys.
For an update event, the event may describe the changed fields but not include a complete fresh object. After the queue receives it, request the entity by ID using the current v2 endpoint. Then calculate the downstream change from the fresh record. This prevents a late event from overwriting a newer value.
Timestamp and ordering rules
V2 timestamps use RFC 3339 unless an endpoint says otherwise. Parse them with a timezone-aware date library. Never treat a timestamp without an offset as local server time. When two events arrive out of order, compare the source update time and your stored version. A newer received time does not necessarily mean a newer business change.
In practice, keep three times: source event time, source entity update time, and receiver time. They answer different questions during debugging. A record may have been updated at 09:00, delivered at 09:02, and processed at 09:03.
Keep compatibility during the cutover
If you cannot migrate every consumer at once, place a small normalization layer between Pipedrive and downstream systems. The layer accepts the tested v1 and v2 fixtures, converts both to one internal event shape, and marks which source version produced the event. This is safer than scattering version checks through billing, marketing, and reporting code.
Give the normalized event a schema version of your own. A future Pipedrive change can then be handled by one adapter while consumers continue to receive the contract they tested. Keep the raw redacted payload beside the normalized event for a limited retention period, because debugging a webhook from only the normalized form can hide the field that changed.
During cutover, measure both deliveries and business outcomes: accepted events, duplicate events, failed fetches, successful downstream updates, and dead-lettered events. A stable delivery count with a falling successful-update count points to parsing or authorization, not sender availability.
Handling deletes and missing records
A delete event may be the last notification you get for an entity. Do not assume that a follow-up GET will succeed. Model deletion as an explicit action and keep the ID, deletion time, and any permitted audit fields. If a read returns not found after an update notification, queue a retry briefly; the record may be in a transient state or the token may lack access.
If the record is missing after the retry window, mark the event unresolved and alert an operator. Silent drops are worse than a visible dead-letter queue because they create CRM divergence that looks like a normal business result.
Testing a v2 migration
Build fixtures for create, update, delete, duplicate delivery, out-of-order delivery, missing ID, malformed timestamp, and an event that references a record the token cannot read. Test the parser independently from the Pipedrive client. Then run an integration test that receives a real redacted event, fetches current state, and writes a harmless result to a staging destination.
Compare field types, nested custom fields, timestamps, and the absence of related objects. If your code previously read a related organization from the webhook, verify that it now performs an explicit organization request when necessary.
The trap: a handler can pass a JSON-schema test built from an old fixture and still fail on real v2 events. Refresh fixtures from the current platform and keep the raw event for forensic comparison.
Build a payload-diff fixture from real events
Store redacted v1 and v2 samples for create, update, and delete, then compare the fields your consumers actually read. Mark every field as stable, renamed, optional, or removed. Do not generate the v2 fixture from a REST response; a webhook event is its own contract. Replay the fixture through the normal receiver and verify acknowledgement, queueing, fetch, and downstream reconciliation.
Where these facts come from