Ordering and idempotency
One rule makes duplicates, retries and out-of-order arrivals harmless. This page explains the rule and why nothing else is needed.
Two guarantees to build on
Every event carries a sequence — a gap-free, strictly increasing
integer, unique per integration. Events are delivered in sequence order, one at a time.
Every payload is full state for its scope, never a delta. An
availability.changed for a property and a date window contains the complete
availability of that property across that window, as it stands at the moment the event was built.
It does not describe what changed.
The rule
The only rule you must implement
sequence is greater than
the last sequence you applied for that property and type. Otherwise ignore it.key = (event["property"]["doorloom_id"], event["type"])
if event["sequence"] <= last_applied.get(key, 0):
return # duplicate, retry, or arrived late — safe to drop
apply(event["data"])
last_applied[key] = event["sequence"]
Persist last_applied, and separately persist the highest sequence you have seen
overall. The latter is your cursor for
replay.
Why that is enough
- A duplicate carries a sequence you have already applied, so it is dropped.
- A retry is a duplicate.
- An out-of-order arrival carries a lower sequence than what you hold, so it is dropped — and dropping it is correct, because the state you already hold is newer.
- A stale event can never overwrite newer state, because newer state always arrived with a higher sequence.
The second guarantee is what makes the first one sufficient. Because Doorloom builds every payload from the live database at the moment the event is created — not from a diff recorded earlier — a later sequence always describes a later reality. There is no scenario where applying only the highest sequence loses an intermediate change, because there are no intermediate changes to lose. There is only current state, stamped.
Coalescing and supersession
Two things reduce the volume you receive, and both are safe for the same reason.
Bursts are coalesced. Several changes to one property within about 10 seconds become one event per type, carrying the state after all of them.
Undelivered events are superseded. If an event is still waiting to be
delivered when a newer event of the same type fully covers its date window, the older one is
dropped and marked superseded. You receive the newest state rather than the history.
This is what shrinks the backlog while your endpoint is down.
Sequences stay gap-free
Windows
Date-scoped events carry a window with a from and to,
both inclusive. A window never spans more than one calendar month: a change across a longer range
arrives as several events, each with its own sequence.
Applying an event means replacing your stored state for that property across that
window, not merging into it. Every night in the window is present in
data.dates, so a replace is well defined.
Three event types are windowed: availability.changed, rates.changed
and restrictions.changed. The rest describe a property or a booking and carry
window: null.
Redelivery is a new event
Doorloom never re-sends a sequence number. When staff redeliver a dead-lettered event, a fresh event is created — new id, new sequence, payload rebuilt from current data. Your rule then applies it, as it should.
This is also why there is no "resend event 1042" endpoint. To recover missed events, use your
cursor with GET /events.
What about ordering across properties
The sequence is per integration, not per property, so it also gives you a global order if you
want one. But you do not need it: state for one property never depends on state for another. Keying
your rule on (property, type) is both sufficient and more forgiving.
The exception is booking.changed, which is keyed by booking rather than by date.
Its data.revision increments on every change and is the natural comparison there,
though the sequence works equally well.