In development ·Join early access
Engineering guide

Designing customer-specific SaaS integrations

A practical guide to application-owned contracts, per-customer adapters, capability negotiation, fixture testing, and maintaining integration profiles.

The integration boundary becomes part of your product

A SaaS application that integrates with one customer can often call that customer’s API directly. The second customer introduces another endpoint and credentials. The third uses a different interpretation of “active,” and the fourth cannot accept traffic from your current egress addresses. Soon a shared helper is full of customer branches, and changing one mapping requires a product release.

The useful design question is where those differences should live. A good integration boundary lets application code express what it needs while a customer-specific implementation handles the destination. That boundary does not erase differences; it makes them explicit, testable, and owned. You can build it in your own codebase before considering any managed infrastructure.

Design an operation around an application need

Start with one real interaction. Suppose a fictional vendor needs to list records for a product screen. It defines records.list with resource, limit, and an optional opaque cursor. The output contains id, displayName, status, and nextCursor. These are the vendor’s choices, not an industry-wide model.

Write down the meanings before choosing field names. Is an identifier unique across a customer’s environments? Does a display name have a maximum length? Does active mean usable now, licensed, approved, or simply not deleted? If those distinctions matter to the product, the contract needs to represent them or explicitly exclude them.

Avoid exporting every field from your first customer’s response. That makes the first provider’s schema your application’s permanent dependency. Also avoid a universal object so abstract that every useful field becomes an unvalidated bag of attributes. A narrow operation with documented semantics is usually easier to maintain.

Keep capability differences visible

A shared method name does not mean every destination supports the same actions. One interface may list records but lack an upsert endpoint. Another may support filtering but only on a subset of fields. A gateway can enforce a lower page size than your product requests.

Decide whether those differences are rejected, negotiated, or represented in a capability description. Return an explicit unsupported_operation for an unavailable write. Reject an unsupported filter instead of silently returning a broader dataset. If a requested limit is capped, document the behavior so callers know to follow the cursor.

For HTTP-based adapters, separate transport semantics from your business contract. RFC 9110 defines HTTP methods, status codes, and retry-related semantics; it cannot tell you whether a particular enterprise action is safe to repeat. That requires the destination’s own contract.

Bind customer configuration to an implementation

Use an adapter template for reusable behavior and a customer profile for its concrete binding. The template handles request construction, response parsing, error classification, and pagination translation. The profile selects adapter version, endpoint, mapping version, credential reference, environment, and approved route.

For example, Client Alpha’s REST adapter maps account_name to displayName and uses an upstream cursor. Client Beta’s SOAP adapter maps AccountLabel and wraps a page number in an opaque application cursor. Both can satisfy the output shape, but their consistency and cursor-expiry rules may differ. Those limits should be documented.

Keep credentials out of configuration files. Reference a secret managed by the server environment. Separate sandbox and production references even if the same adapter code runs in both. A profile should not let a developer accidentally use a production token while testing a staging endpoint.

Validate mappings at the boundary

Validation should happen before the application relies on the normalized result. A record without an identifier is not useful just because the upstream API returned HTTP 200. A source status of archived cannot automatically become inactive if the application treats inactivity as reversible and archive as permanent.

Return a bounded error category such as mapping_validation, identify the relevant contract field, and preserve a redacted original context for authorized debugging. Do not include credentials, unrestricted payloads, or confidential customer details in ordinary application logs.

There is a tradeoff between strict validation and forward compatibility. Rejecting every unknown optional property can break an adapter when a provider adds harmless fields. Accepting unknown enum values can corrupt meaning. Be strict about fields your application uses, deliberate about unknown data, and explicit about unsupported semantics.

Test examples that can actually fail

A useful fixture suite includes valid records, an empty result, missing required fields, unexpected status values, expired credentials, rate limits, and multi-page responses. Include a destination that cannot implement one optional operation. Use synthetic data with intentionally distinct identifiers so accidental cross-profile behavior becomes obvious.

Test cursor handling beyond the first page. Confirm what happens if records change during pagination, if a cursor expires, or if an upstream page repeats. Your contract can describe best-effort traversal without pretending that every system offers a stable snapshot.

Fixtures are only one layer. A customer-approved sandbox test checks endpoint access and authorization in a real environment. A production readiness check verifies the approved route, credential scope, and operational owner. Passing a local fixture cannot establish any of those external facts.

Version changes as operational changes

Pin adapter and mapping versions. When an upstream response changes, update the affected fixtures, compare normalized outputs, and decide which profiles should move first. A seemingly small status mapping change may alter product behavior more than a large refactor of request serialization.

Keep the original version reference in run history. If an operator replays work later, either reuse that configuration or make the migration choice visible. Also retain the idempotency policy and destination outcome context; version pinning alone does not make an external write safe to repeat.

Configuration in Git helps review and CI validation. It does not require putting secrets or customer payloads in Git. Store references, synthetic fixtures, schemas, and migration notes; keep sensitive material in systems with appropriate access and retention.

A short implementation checklist

  • Define one operation’s input, output, error categories, and permission boundary.
  • Record identifier scope, enum meaning, pagination limits, and unsupported capabilities.
  • Separate reusable adapter logic from customer and environment configuration.
  • Keep credential references and approved routing explicit.
  • Test validation failures, pagination, authorization, and unsupported actions.
  • Pin versions and name the maintenance and incident owners.

This architecture is useful when customer implementations vary while your product’s needs repeat. A unique one-time migration may not justify the same infrastructure. Begin with the repeated pain, then choose how much machinery to centralize.

Invokation is in development around this operation-and-profile model. The integration profiles page illustrates the intended product boundary; the enterprise onboarding walkthrough adds approval and handover steps. For operational consequences, continue with retries, idempotency, and replay.

In development · Shape the first product

Bring a real integration problem.

Share the constraints your team keeps meeting. No payment or commitment required.

Join early access