MindSwarm
Case study

The hard part of the integration wasn't the integration

Sergej July 26, 2026 ~9 min read

A guest checks out of one of my apartments on the 10th. My calendar showed a guest arriving that same day. No one arrived. The apartment was empty, being cleaned. The feed that told me "arrival" was not wrong about the data — it faithfully reported exactly what Booking.com had sent. It was wrong about the meaning, and the meaning is the only thing that matters when the alternative is selling an occupied room twice.

This is another case study from MindSwarm's own proving ground — three rental apartments in Lithuania I run as a live testbed before any method reaches a client. The system pulls the calendar feeds that Booking.com and Airbnb publish and turns them into one occupancy view I can trust. Reading those feeds is a first-week task any engineer can do. Trusting what they mean took months, and a class of bug worth showing you, because it is the exact shape of every enterprise integration I have ever been handed.

What the system actually does

For each apartment the system fetches two iCal feeds every few minutes — one from Booking.com, one from Airbnb — and merges them into a single calendar: which dates are occupied, who checks in today, who checks out, which day is a cleaning day. From that one view hang the things that cost real money if they are wrong: the cleaner's schedule, guest key-codes and messages, and the hard rule that an occupied date must never be offered for sale again. Get the reading wrong and the page is empty. Get the meaning wrong and the page is confidently, dangerously incorrect.

The feed carries less than the world means

Here is what nobody warns you about when you integrate someone else's system: their data model is a lossy compression of reality, and the parts they threw away are exactly the parts you need. Booking.com's calendar answers one question for its own purposes — is this date sellable or not? — and it answers with a single state: closed. But in my world a date can be closed for two completely different reasons that demand opposite handling. Either a guest has a real reservation, or the owner blocked the date by hand. The feed compresses both into the same word.

The core problem: a real booking and a manually blocked date arrive from Booking.com as the identical string — CLOSED - Not available. Nothing in the feed distinguishes "a guest is coming" from "the owner shut this date." One needs a check-in, a key-code, a cleaning. The other needs none of it. The feed cannot tell them apart — and neither can any code that only reads the feed.

One feed tells you, one doesn't

Airbnb, to its credit, keeps the distinction: its feed says Reserved for a booking and Not available for a block, and that one field is enough. Booking.com does not. And this is the trap in every multi-system integration — you learn the semantics of one source, assume the other shares them, and build on a distinction only half your inputs actually carry. The moment you treat the two feeds as interchangeable, half your data starts lying to you in a way that looks exactly like the truth.

The disambiguator lives outside the feed

If the feed cannot answer the question, the answer has to come from somewhere else. Here it comes from a second signal the feed never carries: a real reservation also produces a booking-confirmation email, which the system logs; an owner's manual closure produces no email. So the rule is not "read the state" — it is "read the state, then check whether a confirmation exists for these dates." Email present → a guest is really coming. No email → the date is occupied but no one is arriving; a human closed it. That human, incidentally, is real and out-of-band: three different people can close a date — me, the owner, or the cleaner — and none of those actions ever touches the feed.

Even the matching is not clean. You cannot join the email to the block by exact check-in date, because Booking.com shifts the start date when it re-exports a split or modified block — so the feed's "arrival" drifts off the real one. The match has to be by date overlap, not by an exact key. This is the unglamorous truth of integration work: the join key you were handed is not stable, and discovering that costs you a day and a wrong assumption before it buys you a correct heuristic.

The heuristic has a price, and honesty means stating it: if a confirmation email never arrives — spam filter, provider hiccup — a real booking will look like a manual closure, and the automated watchman stays silent about it. That is a deliberate, known failure mode, and the safety net is human: the owner's Booking Extranet and a separate bookings page. A disambiguator built on a second signal is only as reliable as that signal — and pretending otherwise is how you get quietly double-booked.

Fix it once, and it walks back in

Now the part that makes this an engineering story rather than a clever if-statement. In a single day I found three separate bugs on the calendar, all of them the same mistake — the system treating a closed date as a guest arrival — and each one entered from a different direction.

From the feed

The first was the one above: a manual Booking closure rendered as a green guest check-in, complete with a phantom checkout stacked on top of a real Airbnb guest's checkout — which in turn spawned a second, non-existent cleaning in the cleaner's schedule. Fixed at the source: closures with no confirmation email are shown as occupancy, not as an arrival.

From our own record

The second came from a source I had built myself. A "cleaning day" is a special kind of block — but unlike an owner's closure, a cleaning day starts with a checkout: the guest leaves, then the cleaner comes. My first fix had muted the checkout on all blocks, which meant the cleaning-day block now hid the very checkout it was supposed to follow. The same class of error — a block mistaken for a guest movement — but this time the block was my own record, not Booking's.

From the surface I hadn't hardened

The third was the meanest. After fixing the month grid, I opened the day itself — the detail panel a click deeper — and it still announced "guest arriving today" over what was only a cleaning. The classification logic lived in two places: the server that renders the grid, and the client-side code that renders the day drawer. I had hardened one and forgotten the other. The grid told the truth; the drawer still lied. Every integration has more than one surface, and a rule enforced on some of them is a rule you do not actually have.

Hide it, and you sell it twice

There was a tempting shortcut through all of this: if a closed date is not a real arrival, just stop drawing it. Never do that. A date you hide is a date your system thinks is free — and a free date gets offered for sale, and now two guests hold the same night. The rule is the opposite of hiding: an occupied date must always show as occupied, it just must not masquerade as an arrival. When you take an entity out of one view, you owe it its own place in another. That is literally how a "Closed for cleaning" section came to exist on the calendar — the cleaning blocks needed somewhere honest to live once they were no longer allowed to pose as check-ins.

The disambiguation, drawn out

Put together, turning two lossy feeds into a calendar you can trust looks nothing like "call the API." It looks like this:

Airbnb iCal ──► "Reserved" vs "Not available" (the feed already tells you) Booking iCal ──► "CLOSED - Not available" (booking AND owner block: identical) │ ▼ disambiguate with a SECOND signal the feed never carries confirmation email logged for these dates? ├─ yes ─► a guest is arriving (check-in · key-code · cleaning) └─ no ─► occupied, but NO arrival (a human closed the date) │ ▼ match by DATE OVERLAP, not exact check-in (Booking shifts the start on re-export) enforce the SAME rule on the grid AND in the day drawer (or the drawer lies) │ ▼ never hide an occupied date ──► give it its own place ("Closed for cleaning")

Notice that the interesting work is in none of the single boxes — it is in refusing to trust the obvious field, in sourcing a second signal from outside the feed, in matching it loosely because the key is unstable, and in enforcing the same rule on every surface that renders the data. None of that shows up in an API tutorial. All of it is the difference between a calendar that looks right and one you can safely hang a cleaner, a key-code, and a for-sale flag on.

Why this is the job

The industry keeps selling integration as plumbing — connect system A to system B, read the field, done. But the field is a lossy compression of a richer world, and the gap between what the field says and what actually happened is where the money leaks. A booking platform, a CRM, an ERP, a ticketing queue — each hands you a schema that answers its questions, not yours, and the mismatch is invisible until it quietly does the wrong thing at scale. It is the same reason so many AI and automation pilots die in integration rather than in the model: the hard part was never reading the data, it was knowing what the data leaves unsaid.

My version of the problem is small — three apartments, one ambiguous word, a few hundred dates. An enterprise's version is a warehouse of feeds, each compressing reality in its own way, joined on keys that don't quite line up, rendered on surfaces that don't quite agree. The shape is identical: the read is a commodity, the meaning is the work, the failures are silent, and each one costs more the larger you scale. I learned exactly where that gap opens by living inside a small one — three bugs in a day, one root, three surfaces — on a system I own end to end and can afford to be wrong about.

MindSwarm is Sergej's independent Forward Deployed AI Engineering practice. The method is the one in this story: treat reading the data as the easy part, embed close enough to the real process to see where the feed's model and the world's meaning diverge, source the missing signal, match it honestly, and enforce the rule on every surface — then hand back an integration the team can actually trust. When everyone is selling you the connector, the work — and the risk — is in the meaning the connector throws away.

Connecting systems that don't quite agree?

The gap between reading a feed and trusting it — in one brief.

Read the brief →