How to Customize Unified API Data Models Per Customer Without Code
Stop losing enterprise deals to rigid unified APIs. Learn the declarative 3-level override architecture that customizes data models per customer without code.
To customize unified API data models per customer without writing custom code, engineering teams must abandon hardcoded integration scripts and adopt a declarative mapping architecture. Instead of maintaining separate code branches for bespoke enterprise schemas, integration behavior is defined entirely as data using JSONata expressions stored in a three-level override hierarchy (platform, environment, and account).
Your unified API just torpedoed a six-figure enterprise deal because it cannot read a prospect's custom Salesforce fields.
The technical evaluation went perfectly. The demo was flawless. The prospect's VP of Sales is ready to sign. Then their systems administrator drops their organization's schema in Slack: 147 custom fields on the Contact object, a Deal_Registration__c object driving their entire partner motion, and a Forecast_Override__c rollup field that the CFO checks every Monday morning.
Your integration page says you support Salesforce, but your application's unified API only maps first_name, last_name, and email. The deal stalls. Engineering says "six weeks minimum" to add the rest. Sales loses the quarter.
This is not an isolated incident. It is the exact failure mode that kills mid-market and enterprise contracts, and it represents a structural failure in how modern B2B SaaS companies approach API integrations. The fix is not a bigger common data model. It is not a custom code branch per tenant. It is a declarative, three-level API mapping architecture where every schema variation lives as configuration data—JSON and JSONata expressions in a database—and never as integration-specific code.
The Enterprise Integration Trap: Why Standard APIs Fail
Unified API platforms exist to solve a painful problem: if you want to integrate with Salesforce, HubSpot, Pipedrive, and 50 other CRMs, you would normally build and maintain 50 separate integrations. Each has its own field names, authentication flows, pagination strategies, query languages, and response shapes. A unified API gives you one interface that works across all of them.
But standard unified APIs solve this problem with brute force. They abstract away the differences between third-party systems by forcing disparate data structures into a single, canonical schema. They flatten Salesforce, HubSpot, and Pipedrive into one canonical contacts resource.
This works perfectly for small businesses. It completely falls apart upmarket.
Enterprise organizations do not use standard schemas. They mutate their CRMs, HRIS platforms, and ERPs to fit their highly specific business processes. Per-customer API mapping is the ability to change how a unified API translates between your canonical schema and a specific customer's third-party account, without touching source code or shipping a deploy. Traditional unified APIs cannot do this. When a standard unified API encounters these mutations, it simply drops the custom objects entirely to maintain a consistent schema. For a deeper look at this limitation, read our guide on unified APIs that don't force standardized data models.
Here is what actually breaks in a rigid unified API when a real enterprise Salesforce org walks in:
- Custom fields are silently dropped. Anything not in the canonical schema disappears from the response.
- Custom objects vanish entirely.
Deal_Registration__c,Territory_Assignment__c, and other bespoke objects have no home in a fixed data model. - Rollup and formula fields lose meaning. A
Forecast_Override__cfield that aggregates seven child objects becomes an unmapped string, if it appears at all. - Field types get coerced. A picklist becomes a string. A multi-currency amount becomes a number without the currency code.
The economics of failing to support these schemas are brutal. Buyers are doing independent research and setting requirements early. According to industry data from 6Sense, 83% of B2B buyers have already defined their purchase requirements before they engage with a sales representative. If your documentation cannot prove you handle bespoke schemas, you are disqualified silently before the first call.
Integrations are a primary buyer consideration. Roughly 90% of B2B buyers say a vendor's ability to integrate with their existing technology significantly influences their shortlist. Failing to support a prospect's custom tech stack means losing the deal. When standard unified APIs drop custom objects, the resulting poor integration experience directly drives churn and vendor replacement, with 51% of B2B buyers citing poor integration with their existing tech stack as a reason to explore new vendors.
The Problem with Passthrough Endpoints and Custom Code
When a standard unified API drops a critical custom field, engineering teams are forced into a corner. To get the data the prospect needs, they have to bypass the unified abstraction entirely.
The surface-level answer most legacy platforms give is "use our passthrough endpoint." That answer is worse than it sounds. Most legacy integration platforms handle this in one of three highly inefficient ways:
1. Authenticated Passthrough Requests
Platforms force developers to use raw passthrough endpoints or webhooks for custom objects that fall outside their common data model. Your engineering team is now hand-writing raw SOQL (Salesforce Object Query Language), raw HubSpot filterGroups, or raw NetSuite SuiteQL against a proxy endpoint. You lose normalization, pagination handling, retry semantics, and every other reason you bought integration infrastructure in the first place. This completely defeats the point of the unified abstraction.
2. Custom TypeScript Branches
Code-first platforms require engineers to write and maintain custom TypeScript scripts for each integration and schema variation. That model works fine for one integration. It collapses when you have thirty integrations and every enterprise customer wants a different subset of custom fields. Adding a new integration means writing new code, deploying it, and hoping it does not break the 50 integrations already running.
// The anti-pattern: Hardcoded integration logic
async function getContacts(provider: string, accountId: string) {
if (provider === 'hubspot') {
// 50 lines of HubSpot specific logic
} else if (provider === 'salesforce') {
if (accountId === 'enterprise_client_A') {
// Custom logic for Client A's bespoke schema
return db.query('SELECT Id, Custom_Field__c FROM Contact');
}
// Standard Salesforce logic
}
}Every time an API changes, this code must be updated. Adding a new integration requires going through code review, CI/CD, and deployment. Every enterprise deal becomes a sprint planning ticket.
3. Rigid Schema Polling
Some systems expose a second endpoint that returns a customer's custom field metadata, which you then have to join against the primary response in your own application. This doubles your API calls, adds severe latency to the integration workflow, and pushes the mapping logic back into your product code.
All three approaches share the same root cause: integration behavior is expressed as code, so any variation requires more code. If you have 100 enterprise customers, you cannot maintain 100 separate code branches or a monolithic switch statement of doom. When evaluating what integration platform lets you configure field mappings per customer without forking code, the solution requires abandoning imperative scripts entirely.
Architecting Per-Customer Data Model Customization Without Code
The architectural fix is to invert this paradigm. Move the mapping logic out of code entirely and store it as data.
The pattern is straightforward to state and hard to build: the runtime engine contains zero integration-specific logic. All provider behavior is defined as declarative configuration, and all data transformations are defined as expressions stored in a database.
This requires shifting from a strategy pattern (where each integration is a separate class or module) to an interpreter pattern at platform scale.
Concretely, the generic runtime engine needs three inputs to serve any unified API request:
- An integration config: A JSON blob describing how to talk to a provider's API (base URL, auth scheme, endpoints, pagination strategy, error shape).
- An integration mapping: A set of expressions that translate between your unified schema and the provider's native format.
- The connected account context: Credentials, subdomain, instance URL, and any per-account overrides.
flowchart TD
A["Unified API Request<br>GET /crm/contacts"] --> B["Generic Execution Engine"]
subgraph Configuration Data
C["Integration Config<br>(Auth, Pagination, Base URL)"]
D["Integration Mapping<br>(JSONata Expressions)"]
end
B --> C
B --> D
D --> E["Translate Request<br>via JSONata"]
E --> F["Proxy Layer<br>Executes HTTP Call"]
F --> G["Translate Response<br>via JSONata"]
G --> H["Normalized Unified Response"]There is zero integration-specific code in the runtime logic. No if (hubspot). No switch (provider). No provider-specific tables. No hardcoded field names in the runtime. The same code path that lists HubSpot contacts lists Salesforce contacts, Pipedrive contacts, and Zoho contacts. Everything provider-specific lives in the config and mapping.
flowchart LR
A["Unified request<br/>GET /crm/contacts"] --> B[Generic engine]
B --> C[Load integration config]
B --> D[Load mapping expressions]
B --> E[Load account overrides]
C --> F[Build provider request]
D --> F
E --> F
F --> G[Call third-party API]
G --> H[Evaluate response mapping]
D --> H
E --> H
H --> I[Unified response]In this architecture, each integration is a program written in a DSL of JSON config plus expressions. Adding a new integration is writing a new program, not extending the interpreter. Modifying a mapping is a data operation, eliminating the need for CI/CD pipelines. For a deep dive into this concept, see our guide on shipping API connectors as data-only operations.
The 3-Level Override Hierarchy: Platform, Environment, and Account
A generic engine plus declarative mappings gets you to a working unified API. But defining integrations as data solves only the problem of adding new providers. How do you solve the problem of per-customer schema mutations? How do you map the 147-custom-field Salesforce org for Enterprise Client A without affecting the schema for Enterprise Client B? The key to how to map standard fields to custom fields dynamically in a unified API is that because mappings are just JSON configuration data, they can be deep-merged at runtime. The answer is a 3-level override hierarchy that lets you stack customizations on top of a base template.
Each level is deep-merged on top of the previous, from broadest to narrowest scope:
| Level | Scope | Stored As | Typical Use |
|---|---|---|---|
| Platform base | All customers | Default mapping row per resource per integration | The 80% mapping that works for most standard Salesforce orgs. |
| Environment override | One tenant / workspace | Environment-scoped mapping override | Your application 'AcmeCorp' needs specific custom fields across all accounts they connect. |
| Account override | One connected account | Override on the integrated account record | One specific enterprise customer's Salesforce instance has a Territory__c object no one else uses. |
flowchart TD
A["Platform Base Mapping<br>(Default Schema)"] --> D["Deep Merge Engine"]
B["Environment Override<br>(App-wide Customizations)"] --> D
C["Account Override<br>(Per-Customer Bespoke Fields)"] --> D
D --> E["Final Execution Mapping<br>Applied at Runtime"]At request time, the engine resolves the effective mapping by loading the integrated account credentials, the integration config, and the integration mapping. It then deep-merges the three layers into a single execution mapping.
// Runtime resolution of the effective mapping for a request
const platformMapping = await loadPlatformMapping(integration, resource, method);
const envMapping = await loadEnvironmentMapping(environmentId, integration, resource, method);
const accountMapping = get(integratedAccount, [
'unified_model_override', unifiedModel, resource, method
]) ?? {};
// Conceptual representation of the deep merge process
const effectiveMapping = deepMerge(
deepMerge(platformMapping, envMapping),
accountMapping
);
// The generic engine executes the final mapping
const response = await mapResponse({
obj: providerPayload,
mapping: effectiveMapping.response_mapping,
context: { ...integratedAccount.context, ...baseContext },
});Every field of the mapping can be overridden at any level: response_mapping, query_mapping, request_body_mapping, resource (which endpoint to hit), method (which HTTP verb), and before / after steps for pre- and post-request enrichment.
When AcmeCorp's Salesforce admin adds a new custom object next quarter, you patch the account override with an expression that maps it. No deploy. No downtime. No blast radius beyond that one account. Every other customer sees the platform default. Zero code changed. Read more about this pattern in our breakdown of per-customer data model customization without code.
Design rule: Never let the override hierarchy leak into your runtime code. The runtime should ask "give me the effective mapping for this request" and get a single merged config back. The merge logic is a library function, not a business rule scattered across services.
Using JSONata as the Universal Transformation Engine
To make this architecture work, you need a way to describe complex data transformations as simple strings of data. You cannot store JavaScript functions in a database securely or efficiently. Why JSONata specifically, and not JSONPath, JMESPath, Jsonnet, Lua, or a homegrown DSL?
JSONata is a functional query and transformation language specifically designed for JSON. It is the perfect universal transformation engine for API mappings because it possesses several critical properties:
- Declarative and side-effect free: Mappings describe what the output should look like, not how to produce it. They cannot mutate state, call external services, or exfiltrate data. This is a security property, not just an aesthetic one—mappings can be stored as untrusted-ish configuration data without opening a shell exec risk.
- Turing-complete for reshaping JSON: It supports conditionals, string manipulation, regex, array transforms, custom functions, date formatting, and recursive expressions. A single expression can handle transformations that would take dozens of lines in imperative code.
- Storable as a plain string: An expression is just a string in a database column. It can be versioned, diffed, hot-swapped, and overridden per environment or per account without restarting the application.
- Compact: Complex logic—like flattening nested properties or detecting custom fields via regex—fits in a single database field.
Comparing Data-Driven Mappings
To understand the power of JSONata in a generic execution engine, look at how the exact same unified request is handled for two very different APIs using purely data-driven configurations.
When a user requests GET /unified/crm/contacts, the caller does not know or care whether the underlying account is HubSpot or Salesforce.
For HubSpot, the API returns data nested inside a properties object. The JSONata response mapping stored in the database looks like this:
response_mapping: >-
(
$defaultProperties := ["firstname", "lastname", "email"];
$diff := $difference($keys(response.properties), $defaultProperties);
{
"id": response.id.$string(),
"first_name": response.properties.firstname,
"last_name": response.properties.lastname,
"email_addresses": [
response.properties.email ? { "email": response.properties.email, "is_primary": true }
],
"custom_fields": response.properties.$sift(function($v, $k) { $k in $diff })
}
)For Salesforce, the API returns flat PascalCase fields. The JSONata mapping for Salesforce is completely different, but it is executed by the exact same generic engine. Here is a sophisticated Salesforce contact response mapping that handles flat fields, splices six phone number types into a unified array, detects custom fields dynamically via regex, and generates dynamic Lightning URLs:
response_mapping: >-
response.{
"id": Id,
"first_name": FirstName,
"last_name": LastName,
"name": $join($removeEmptyItems([FirstName, LastName]), " "),
"email_addresses": [{ "email": Email }],
"phone_numbers": $filter([
{ "number": Phone, "type": "phone" },
{ "number": Fax, "type": "fax" },
{ "number": MobilePhone, "type": "mobile" },
{ "number": HomePhone, "type": "home" },
{ "number": OtherPhone, "type": "other" },
{ "number": AssistantPhone, "type": "assistant" }
], function($v) { $v.number }),
"custom_fields": $sift($, function($v, $k) { $k ~> /__c$/i and $boolean($v) }),
"urls": [{
"url": "https://" & %.context.subdomain & ".lightning.force.com/lightning/r/Contact/" & Id & "/view",
"type": "self"
}]
}Notice the Salesforce custom_fields logic: $sift($, function($v, $k) { $k ~> /__c$/i and $boolean($v) }). This single line of JSONata dynamically captures every custom field ending in __c and maps it into the unified response. No hardcoded field names. No custom code.
If an enterprise customer has a highly specific custom object—like Territory__c and Deal_Registration__c—you simply write a JSONata expression for that specific account and store it in the account override table:
response_mapping: >-
response.{
"territory": { "id": Territory__c, "name": Territory__r.Name },
"deal_registration": {
"id": Deal_Registration__c,
"partner": Deal_Registration__r.Partner__c,
"stage": Deal_Registration__r.Stage__c
}
}The deep-merge fires. That specific enterprise customer's response now includes id, first_name, last_name, email_addresses, custom_fields, plus territory and deal_registration. Every other Salesforce customer sees the platform default. For a deeper technical dive into handling Salesforce's unique quirks, review why unified data models break on custom Salesforce objects.
Radical Honesty: What This Architecture Actually Costs
This pattern is incredibly powerful, but it is not free. When evaluating this architecture, engineering teams must weigh several real-world tradeoffs:
- Learning curve: JSONata is a new language for most engineers. Complex mappings require a mental model shift away from imperative loops and toward functional data transformations.
- Debugging is different: When a mapping produces the wrong output, you debug an expression, not a stack trace. Good tooling—like expression playgrounds, request/response logs, and dry-run modes—matters enormously.
- Not every problem is a mapping problem: Long-running exports, complex OAuth dances with device-code flows, and provider-specific webhook signature schemes still need engineering attention at the platform level. The declarative model handles the request/response shape, not every operational concern.
- Governance: With per-account overrides, you need review workflows for who can edit an override, an audit trail, and a rollback path. A bad JSONata expression on a production account can break that customer's sync until it is reverted.
These are real costs. However, they are dramatically lower than the alternative of maintaining a monolithic codebase with a custom branch for every enterprise customer.
Stop Losing Enterprise Deals to Schema Mismatches
The test for whether your integration architecture is enterprise-ready is simple: When a prospect sends over a schema with 147 custom fields and three custom objects, can your team support it before the next standup, or before the next quarter?
If the answer is "next quarter," you are shipping code for every deal. That model does not scale, it does not survive a real enterprise pipeline, and it will keep torching six-figure deals until the architecture changes. Enterprise integration is not about finding the perfect common denominator. It is about building an architecture flexible enough to handle infinite variations without collapsing under technical debt.
The alternative is to treat integrations as data:
- The runtime is a generic interpreter that never knows which provider it is talking to.
- Provider APIs are described by JSON configs. Field-level translations are JSONata expressions.
- Per-customer variations are overrides layered on top of platform defaults, resolved at request time via deep-merge.
- Adding a new integration is a config change. Customizing for an enterprise customer is a config change. Neither requires a deploy.
Stop telling enterprise buyers to change their business processes to fit your application's standardized data model. Architect your integrations to adapt to them.
FAQ
- What does it mean to customize a unified API data model per customer without code?
- It means storing integration mappings as declarative configuration (JSON plus expressions like JSONata) in a database, rather than writing custom integration scripts. An override hierarchy lets you change how one specific customer's data is mapped without deploying new code, while a generic runtime engine reads the merged config and applies it dynamically.
- Why do standard unified APIs fail for enterprise Salesforce customers?
- Standard unified APIs flatten all provider data into a fixed, canonical schema. Because enterprise Salesforce organizations typically have 100+ custom fields and multiple bespoke custom objects, a rigid schema drops these unique data points or forces developers to use unnormalized passthrough endpoints.
- How does a 3-level override hierarchy work in practice?
- The 'platform base' defines the default mapping for an API provider. The 'environment' level lets an application override that base for all its connected accounts. The 'account' level lets you override mappings for a single connected account. At request time, the three layers are deep-merged into one effective runtime mapping.
- Why use JSONata instead of writing TypeScript mapping code?
- JSONata is a declarative, Turing-complete, and side-effect free language that can be stored as a plain string in a database. This allows complex data transformations to be versioned, overridden per customer, and hot-swapped instantly without the build, review, and CI/CD deployment pipelines required by TypeScript.
- What are the trade-offs of a fully declarative integration architecture?
- Engineering teams take on a JSONata learning curve, need expression-aware debugging tools, and require governance around who can edit per-account overrides in production. However, in return, you eliminate integration-specific code from your runtime and can support completely bespoke enterprise schemas in hours instead of sprints.