On a whiteboard, an integration is one arrow.
You draw a box for your system, a box for Airbnb, and an arrow between them labeled “bookings.” Then another arrow to Booking.com. Then another to WhatsApp. It looks done. It looks like an afternoon of work.
In production, each of those arrows hides a hundred small ways to fail. The other system is slow. It’s down. It sends the same event twice. It sends events in the wrong order. It changes a field name. It limits how often you can ask. It describes the same guest three different ways.
At Santara, almost everything depends on these arrows. The product turns booking data and guest messages from several platforms into one morning briefing for short-term rental hosts. If the data coming in is wrong, late or duplicated, the briefing is wrong, and a wrong briefing is worse than none.
This post collects the lessons that matter most when you build a system on top of other people’s APIs. I won’t go into anything confidential about how Santara works, and none of this is specific to rentals. If you integrate payment providers, shipping companies, CRMs or messaging apps, you’ll recognize every problem here.
Lesson 1: You don’t control the other side
This sounds obvious, and it’s the root of everything else.
When you call your own database, you know how it behaves. When you call someone else’s API, you’re a guest in their house. They decide:
- how you get access. Some platforms offer open APIs. Others only work with approved partners, or through intermediaries like channel managers. Sometimes the only practical option is a simple calendar feed (iCal) with limited information.
- what data you get. Often less than you’d like, in a shape that fits their system, not yours.
- how often you can ask. Rate limits are everywhere.
- when things change. Fields get deprecated, versions get retired, behavior shifts quietly.
- whether it works today. Every external service has bad days.
So the first design rule is a mindset: assume every external call can be slow, wrong, duplicated or missing, and design so that none of those breaks your product.
Lesson 2: Keep integrations at the edge
In Clean Architecture in Practice, I wrote that dependencies should point inward, toward the business logic. Integrations are where that rule pays for itself.
The core of your system should speak your language, not Airbnb’s or WhatsApp’s. For a rental product, that might be a few simple concepts:
- a Property;
- a Reservation, with a guest, dates and a property;
- a Message, with a sender, a time and a conversation;
- a Task, like a cleaning.
Each platform gets its own adapter at the edge of the system. The adapter’s only job is to translate: take whatever shape the platform sends and turn it into your Reservation or your Message. The rest of the system never sees a platform-specific field.
This has big practical benefits:
- Adding a new platform means writing one new adapter, not touching the core.
- When a platform changes its API, the damage stays inside one adapter.
- Testing the core is easy, because you can feed it clean Reservations without calling anything external.
- Each adapter can be tested against saved real examples of what the platform sends.
If you take only one lesson from this post, take this one. Every hour spent keeping platform details out of the core saves ten hours later.
Lesson 3: Webhooks, polling, or both
There are two basic ways to learn that something changed on another platform.
Polling: you ask regularly. “Any new bookings since 10:05?” Every few minutes, forever.
Webhooks: the platform tells you. When something happens, it sends a request to an address you provide. WhatsApp’s business messaging works this way: new messages arrive as webhook events.
Webhooks feel better: faster, less wasted work. But they have a weakness that bites everyone eventually: if you miss one, you may never know. Your server was restarting. Their retry gave up. A network blip lost the request. The event is gone, and nothing tells you.
Polling is slower and more expensive, but it’s self-healing: if one poll fails, the next one catches up.
The pattern that works best in practice is both:
- use webhooks for speed, when they’re available;
- run a regular reconciliation job that compares your data with the source and fixes any differences.
The webhook makes the product feel fast. The reconciliation makes it correct. For a morning briefing, correct matters more than fast. A host would rather see the right list at 7 a.m. than a wrong one at 6:59.
Lesson 4: Receive fast, process later
When a webhook arrives, it’s tempting to do everything right there: parse it, update the database, call the AI, send a notification.
Don’t. Most platforms expect a quick response. If you take too long, they assume you failed and send the event again, or eventually stop sending events at all.
A safer pattern:
- Receive the webhook.
- Verify it’s genuine (most platforms sign their requests, check the signature).
- Store the raw event somewhere durable, like a database table or a queue.
- Respond immediately with success.
- Process it separately, in a background worker.
This gives you a lot for free. If processing fails, the raw event is still saved and you can retry. If you find a bug in your processing, you can fix it and replay old events. And when something strange happens, you have the exact original data to look at, which is priceless when debugging.
Always keep the raw data. Your parsed version reflects your understanding of the platform at that moment. Your understanding will improve. The raw data lets you go back.
Lesson 5: Everything happens twice
In distributed systems, “exactly once” is mostly a dream. Webhooks get retried. Polling jobs overlap. A worker crashes after doing the work but before marking it done, so the work gets done again.
So design every step to be idempotent: doing it twice has the same result as doing it once.
In practice, that means:
- Use the platform’s own IDs. Every reservation and message from an external platform has an ID. Store it, and make it unique together with the platform name. If the same booking arrives twice, the second insert becomes an update, not a duplicate.
- Prefer “set this to X” over “add one.” “The reservation status is cancelled” can safely run twice. “Decrease available rooms by one” can’t.
- Remember what you’ve already processed. Keep a record of handled event IDs, so a retried event is recognized and skipped.
- Make side effects safe too. If processing a message triggers a notification, make sure a retry doesn’t notify the host twice.
Idempotency isn’t exciting. It’s the difference between a system that’s correct most of the time and one that’s correct all of the time.
Lesson 6: Events arrive in the wrong order
Here’s a classic bug. A guest books, then cancels a minute later. Two events are sent: “created” and “cancelled.” Because of network timing or retries, you receive “cancelled” first, then “created.” If you process them in arrival order, the booking ends up active. A guest who cancelled now appears in tomorrow’s briefing.
Ways to defend against this:
- Use timestamps or version numbers from the source, when they exist. Only apply an update if it’s newer than what you already have.
- Fetch the current state instead of trusting the event. Treat the webhook as a hint, “something changed on reservation 123,” and then ask the platform for the latest version of reservation 123.
- Let reconciliation fix the rest. The regular comparison job from Lesson 3 catches anything that slipped through.
The second approach is often the simplest and most robust. The event tells you what to look at. The source tells you what’s true.
Lesson 7: Respect rate limits before they respect you
Every API limits how often you can call it. Hit the limit and you get errors, slowdowns, or in the worst case a blocked account.
Some habits that help:
- Don’t fetch what hasn’t changed. Ask for changes since your last sync instead of everything.
- Spread work out. If you sync many accounts, don’t sync them all at exactly 6:00.
- Retry with backoff. When a call fails, wait a little before trying again, then a little longer, with some randomness so many retries don’t hit the server at the same moment.
- Read the limit headers. Many APIs tell you how many calls you have left. Use that information instead of guessing.
- Separate urgent and non-urgent work. A message that a guest just sent matters more than refreshing an old booking.
Lesson 8: The same guest has three names
This is the problem that surprised me most, and it’s where integrations stop being a pure engineering problem.
The same person can appear differently on every platform:
- “Nguyễn Văn An” on one platform, “An Nguyen” on another, “Anh An 🙂” as a WhatsApp display name;
- a phone number with a country code in one place and without it in another;
- a booking made by one person for a group, where a different member of the group sends the messages.
To build a useful briefing, you need to know that the WhatsApp message “we’ll arrive late” belongs to this reservation in this apartment. Getting that wrong is worse than not matching at all, because it puts the wrong information next to the wrong guest.
Lessons here:
- Normalize what you can. Phone numbers in one standard format. Names with accents and spaces handled consistently.
- Match on several signals, not one. Phone number, name, dates, the property mentioned, the conversation history. One signal alone is rarely enough.
- Keep a confidence level. A strong match can be used automatically. A weak match should be shown as uncertain, or left for the host to confirm.
- Remember confirmed matches. Once a host confirms that a WhatsApp number belongs to a guest, store that link so you never have to ask again.
- When unsure, don’t guess silently. This is the same rule as in the briefing itself: it’s better to say “I’m not sure which booking this message is about” than to attach it to the wrong one.
This is also a place where AI helps carefully: a language model can read a message like “we’re the couple arriving Friday at the river apartment” and suggest a match. But the suggestion is checked against real booking data, not trusted blindly.
Lesson 9: Time zones, dates and other small disasters
Short-term rentals live and die by dates, and dates are full of traps.
- Check-in on the 14th means a calendar date in the property’s local time, not a moment in UTC. Store it as a date in a known time zone, not as midnight in whatever zone your server runs.
- “Tomorrow” in a guest’s message depends on when they sent it and where they are.
- Daylight saving time exists in many guests’ home countries, even if not where the property is.
- Different platforms may represent the same check-in date in different formats.
The rule I follow: store moments in UTC, store calendar dates as dates, and always know which time zone a date belongs to. Then convert as late as possible, when showing it to a person.
Lesson 10: Make failure visible
The scariest integration failures are silent. A token expires, a platform changes something, and your sync quietly returns zero new bookings. Nothing crashes. Everything looks fine. And the host’s briefing is missing half the guests.
So you need to watch for absence, not just errors:
- Track when each connection last synced successfully. If it’s been too long, raise an alert.
- Notice unusual silence. If an account usually receives twenty messages a day and today it received none, something may be wrong.
- Tell the user honestly. If a connection is broken, the briefing should say so: “We couldn’t reach Booking.com since 3 a.m., information from it may be out of date.” A host can handle that. A host can’t handle a briefing that looks complete but isn’t.
- Make reconnecting easy. Expired permissions are normal. Fixing them should take the host one tap, not a support ticket.
Lesson 11: Test with real mess
Unit tests with clean, made-up data will pass forever and prove almost nothing. Integration bugs live in the weird cases.
- Save real examples (with personal data removed) of what each platform actually sends, including the strange ones, and use them as test fixtures.
- Test the failure paths: duplicated events, out-of-order events, missing fields, timeouts, rate limit errors.
- Replay history. Because raw events are stored (Lesson 4), you can run a new version of your processing over last week’s real events and compare the result with what you produced before.
Every bug you find in production should become a test, so it never comes back.
The bigger lesson
When I was a team leader, integrations were often treated as a small task. “Just connect to their API.” It took building a product that depends on them completely to understand how much of real software is exactly this work: careful, patient, defensive plumbing between systems that were never designed to work together.
It’s not the part anyone demos. It’s the part that decides whether the demo still works on day forty.
If I had to put everything here in one sentence, it would be this: trust nothing from outside, keep the outside at the edge, and design every step so it’s safe to run again.
Do that, and the arrows on the whiteboard start to behave like arrows.
Related: Clean Architecture in Practice, Writing Better Code with SOLID Principles, and How to Tell If an AI Product Is Real or Just a Demo.



