SaaS Product Development
Multi-tenancy, subscriptions, onboarding and the architecture decisions that are painful to reverse later.
Integration work is rarely difficult because of the API call. It is difficult because of what happens when the call times out after the remote system already committed, or the webhook arrives twice, or a partner changes a field without telling anyone. The engineering that matters is all in the failure cases.
If you are publishing the API, the decisions are about the people integrating against it: consistent resource naming and response shapes, pagination that works on large result sets rather than offset queries that degrade, filtering and sorting that cover real needs, errors that say what went wrong and what to do, and rate limits communicated in headers rather than discovered by being blocked.
Versioning from day one, because you will need to make a breaking change and doing so without a version means breaking every consumer simultaneously. And documentation generated from the schema, since hand-written API docs drift within weeks.
Plain REST suits most cases and everybody can consume it. JSON:API adds conventions for relationships, sparse fieldsets and includes, which removes a lot of bikeshedding and is what Drupal ships in core. GraphQL suits clients that need to shape their own queries and benefits front-end teams working against many resources, at the cost of more setup and real care around caching, N+1 queries and query complexity limits.
Any of these is fine. Choosing GraphQL for a two-endpoint integration, or plain REST for a front end that needs twelve round trips per screen, is the mistake worth avoiding.
Receiving: verify the signature, respond fast and process asynchronously — providers time out and retry if you take too long doing work inline. Treat delivery as at-least-once and deduplicate on the event identifier, because you will receive duplicates.
Sending: sign your payloads, retry with backoff, expose delivery history so consumers can debug their own endpoint, and let them replay events. Webhook delivery without a replay mechanism generates support tickets you cannot resolve.
Much of this work involves an older internal system, an ERP, or a partner API documented optimistically. The practical approach is an anti-corruption layer: your application talks to a clean internal interface, and one adapter absorbs the remote system's quirks. When the partner changes something — and they will, without notice — the damage is contained to the adapter.
Alongside that: contract tests against a recorded set of real responses, so a change on their side fails in CI rather than in production, and logging of raw payloads for long enough to diagnose what actually arrived.
The same principles apply to migrations between systems: repeatable and re-runnable rather than one-shot, validated against the source, idempotent so a partial failure can be resumed instead of restarted, and reconciled at the end with counts and spot checks rather than declared complete.
Usually one of three things: a synchronous call that timed out after the remote side committed, a webhook that was never delivered or was rejected and not retried, or an error swallowed by a catch block that logged nothing useful. Start by logging raw payloads and outcomes at the boundary, then add a reconciliation job comparing both sides. The reconciliation often finds the pattern faster than reading code does.
Only if you will support it. A public API is a commitment — versioning, documentation, backwards compatibility, and support for people integrating in ways you did not anticipate. If the real requirement is one partner integration, build that first. A poorly maintained public API generates more work than the goodwill it earns.
Often, via direct database access where that is safe and permitted, file-based exchange on a schedule, or in the worst case scripted interaction with a web interface. Each degrades in different ways and I would want to be clear about which failure modes you are accepting. It is workable; it is rarely pleasant, and it should be treated as temporary where possible.
An adapter layer so the change is contained to one place, plus contract tests running against recorded real responses so a breaking change fails in CI rather than in production. Beyond that it becomes a relationship problem — but at least you find out on your schedule rather than from a customer.
REST unless you have a specific reason otherwise — it is simpler to build, cache and consume, and every client can use it. GraphQL earns its complexity when clients genuinely need to shape queries across many related resources, which is most often a front end your own team owns rather than a third-party integration.
Describe the problem in your own words — I will tell you what I would actually build, and what I would not.
Start a conversation