API & integration

Several systems, one reliable synchronisation flow

Webhooks, imports and API events from booking and ticketing systems reduced to one controllable processing flow.

LaravelREST APIsOAuthWebhooksRedisQueues

The situation

Leads and conversions do not only come from a native form. They also arrive through HubSpot Meetings, Calendly, MICE, Weeztix and direct tracking APIs. Each source provides different identifiers, field names and moments at which its data becomes complete.

Where it got stuck

A webhook may be delivered twice, an order may arrive before its visitor and an external API may time out during enrichment. Separate handlers made it difficult to tell whether an event was new, updated or already processed.

The technical question

Processing had to be repeatable without duplicate leads or conversions. External identifiers such as event ID, form GUID and invitee URI had to be used correctly per source, while incomplete data still needed to be enriched later.

My approach

I reduced the sources to one internal event model and defined identification and field mapping per adapter. Upserts and idempotency keys prevent duplicate work. Temporary failures return to the queue with increasing delays; permanent failures receive a clear final state and useful logging context.

What changed

Delayed and redelivered events can now be handled predictably. For each step, an operator can see which source data arrived, which record it matched and why an external synchronisation is waiting or failed.

Under the hood

Decisions that made the difference

01

Source-specific identifiers

Matching does not rely on email alone; every integration uses its most stable external key for matching and deduplication.

02

Normalise at the edge

Adapters translate source fields into one internal format before business logic or attribution starts.

03

Purposeful retries

Rate limits and timeouts are retried; invalid configuration or data is not retried forever.

04

Traceable states

Received, matched, processed and permanently failed are separate states with useful error context.

Let’s talk

Facing something similar?

Send me the context. I can usually tell you quickly whether and how I can help.

Tell me what is going on