3-Level API Mapping: Per-Customer Data Model Overrides Without Code
Learn how a 3-level API mapping architecture lets you customize unified API data models per customer with declarative overrides—no integration code or deployments required.
Your unified API just torpedoed a six-figure enterprise deal, and it has nothing to do with your core product.
The technical evaluation went perfectly. The demo was flawless. The prospect's VP of Sales was ready to sign. Then their Salesforce administrator forwards over their organization's schema: 147 custom fields on the Contact object, a highly modified Deal_Registration__c custom object with nested relationships that drives their entire partner pipeline, and a Revenue_Forecast__c rollup field powering their quarterly board decks.
If you want the contract, your software needs to read and write to all of it.
But your standard unified API maps all of this data into a flattened structure containing just first_name, last_name, and email. The custom objects vanish entirely to maintain a consistent schema. The rollup field is gone. To get the data your prospect needs, your engineering team is now hand-writing raw SOQL against a passthrough endpoint, which completely defeats the entire point of buying integration infrastructure in the first place.
You cannot build a bespoke integration for this one customer without derailing your engineering roadmap. But you also cannot tell the enterprise buyer to change their core business processes to fit your application's standardized data model. This is the exact moment where traditional integration strategies break down.
The answer is not a bigger common data model. It is a per-customer API mapping architecture built on a 3-level override hierarchy—platform base, environment override, and account override—where every customization lives as declarative configuration data, never as integration-specific code. This pattern lets you support infinite enterprise schema variations without deploying new code, hiring an integrations team, or abandoning your unified abstraction.
Why Standard API Schema Normalization Fails Upmarket
API schema normalization is the process of translating disparate data models from different third-party APIs into a single, canonical JSON format. It works perfectly for SMBs running standard out-of-the-box Salesforce or HubSpot setups. It breaks the moment you move upmarket and sell to an enterprise.
The reason is simple: enterprise SaaS deployments are heavily customized by default, not as edge cases. Salesforce allows up to 3,000 total custom objects per org, a ceiling of 800 custom fields on any single object, and a firm cap of 40 total relationships per object. Enterprise orgs routinely push these limits. One customer might have 50 custom fields on their Account object while another has 12 entirely custom objects tracking a proprietary sales process.
This domain complexity is not just a Salesforce-specific problem. NetSuite editions vary wildly—OneWorld customers have multi-subsidiary, multi-currency schemas that standard editions lack. HubSpot enterprise customers define custom objects heavily through their API. Workday tenants customize their worker data model per individual organization.
To understand why forcing competing data structures into a common shape is so difficult, look at how different platforms define a simple "Contact." In HubSpot, a Contact is a relatively flat object. In Salesforce, the schema is a sprawling web of standard and custom objects. Salesforce allows external IDs for integration matching, while HubSpot requires unique property constraints. Salesforce tracks modifications via LastModifiedDate, while HubSpot uses properties.hs_lastmodifieddate.
When your unified API maps these platforms to a rigid common schema, it drops everything that makes the enterprise buyer's setup their setup. The custom Churn_Risk_Score__c field on Contact that drives their entire retention workflow? Gone. The Deal_Registration__c custom object with nested relationships? Dropped from the unified model.
When competitors critique standard unified APIs, they correctly point out that normalized schemas often flatten or abstract away tenant-specific fields, limiting tailored enterprise experiences. Most unified API platforms build their normalization as a fixed translation layer—a static set of field mappings compiled into code. When a new enterprise customer's schema doesn't match, the options are:
- Ask the customer to change their business process (they won't).
- Write a custom adapter for this one tenant (doesn't scale).
- Fall back to a passthrough endpoint (defeats the purpose).
Why Schema Normalization is the Hardest Problem in SaaS Integrations breaks down exactly why this domain complexity leaks into your codebase when using basic mapping tools. None of the traditional options work. You need a different architectural pattern entirely.
The Flawed Alternative: Passthrough APIs and Custom Code
When a standard unified schema drops the 150 custom fields your enterprise buyer needs, the default fallback is to drop down to the raw API. You expose a "passthrough" (proxy) endpoint, hand the enterprise customer direct access to Salesforce's REST API, and let them deal with the response format themselves.
This feels practical in the short term. It is actually a trap.
The moment you write custom code against a passthrough endpoint, you have abandoned the abstraction. You lose the normalization value you were paying for. The entire point of a unified API is that your application code doesn't need to know whether it's talking to Salesforce, HubSpot, or Pipedrive. By writing per-tenant SOQL queries through a proxy, you are back to maintaining N integration code paths for N enterprise customers.
Custom building each integration takes 2 weeks to 3 months, with an average cost of $10K per integration, illustrating how quickly this becomes unsustainable as the number of integrations increases. Industry data from integration experts shows that maintaining custom integrations is highly expensive and resource-intensive, often costing engineering teams between $50,000 and $150,000 annually per integration.
Furthermore, you inherit every API quirk raw. Passthrough means your application now handles Salesforce's SOQL syntax, HubSpot's filterGroups arrays, and Pipedrive's field-based filtering—each with completely different semantics. Pagination styles differ. Error formats differ. You can't let each enterprise customer's customizations bleed into your codebase. If Customer A needs Churn_Risk_Score__c mapped, Customer B needs Partner_Tier__c, and Customer C needs a completely different custom object, writing per-tenant code for each one turns your integration layer into a maintenance disaster. One CEO was shocked to find that 20% of their engineering budget went to maintaining integrations instead of building new features—a project that was supposed to "run itself" became a money pit.
The Reality of Rate Limits on Passthrough Endpoints
When you bypass the unified mapping layer, you are also entirely responsible for rate limiting. Every upstream API communicates rate limits differently. Salesforce returns Sforce-Limit-Info. HubSpot uses X-HubSpot-RateLimit-Daily. Pipedrive has its own convention. If your passthrough endpoint just relays these headers verbatim, your application code needs per-provider logic to parse them—which is exactly the complexity you were trying to avoid.
A common anti-pattern in API gateways and integration platforms is attempting to automatically absorb, retry, or apply exponential backoff to rate limit errors (HTTP 429) on behalf of the caller. This is dangerous. If a unified API automatically retries rate-limited requests, it creates a "thundering herd" problem, exhausting connection pools and masking architectural flaws in the client application.
A better approach is to take a strictly transparent approach but normalize the chaotic rate limit information from dozens of different upstream APIs into standardized response headers, regardless of the provider. For example, based on the IETF RateLimit header fields specification, you can expose three consistent headers:
| Header | Meaning |
|---|---|
ratelimit-limit |
The maximum number of requests permitted in the current window |
ratelimit-remaining |
The number of requests remaining before throttling |
ratelimit-reset |
The number of seconds until the rate limit window resets |
When an upstream API returns a rate-limit error, the integration infrastructure should pass that error directly back to the caller—do not retry, throttle, or apply backoff automatically. By normalizing the headers rather than absorbing the errors, the caller (or your AI agent) receives consistent rate limit data and can implement their own intelligent retry and backoff logic. Your application controls its own retry behavior rather than depending on a black-box intermediary to guess the right strategy.
The Solution: A 3-Level Mapping Architecture for Unified APIs
To handle highly customized enterprise data models without writing bespoke integration code for every single tenant, you need an architecture that separates your mapping logic from your runtime code, stores it as data, and makes it overridable at multiple levels.
Per-Customer API Mappings: 3-Level Overrides for Enterprise SaaS introduces the concept of the override hierarchy. Instead of a single, rigid mapping file that applies to every customer, the unified API engine resolves the mapping configuration by deep-merging three distinct levels of configuration at runtime.
graph TD
A["Level 1: Platform Base<br>Default mapping for all customers"] --> D["Deep Merge Engine"]
B["Level 2: Environment Override<br>Per-environment customization"] --> D
C["Level 3: Account Override<br>Per-connected-account customization"] --> D
D --> E["Final Executable Mapping Configuration<br>Applied at runtime"]
style A fill:#e8f4fd,stroke:#2196f3,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style D fill:#fff2cc,stroke:#d6b656,stroke-width:2px
style E fill:#f3e5f5,stroke:#9c27b0,stroke-width:2pxLevel 1: Platform Base Mapping
This is your standard unified mapping—the default configuration that works for 80% of customers out of the box. It lives in the core database and maps the standard FirstName and LastName fields from the third-party API to the unified first_name and last_name schema. This level handles the standard fields every CRM has and ensures that out-of-the-box functionality works instantly without any configuration.
A simplified example for CRM contacts:
response.{
"id": $string(Id),
"first_name": FirstName,
"last_name": LastName,
"email": Email,
"phone": Phone,
"created_at": CreatedDate,
"updated_at": LastModifiedDate
}Level 2: Environment Override
Sometimes, a specific customer environment requires a global change to how an integration behaves. For example, a customer might have installed a managed package that adds specific fields, or their Salesforce edition includes features that change the API surface. They might have a strict security policy requiring a specific custom HTTP header on all requests, or they might map standard statuses differently than your default configuration.
The environment override allows you to modify the base mapping for an entire tenant without affecting other customers. This deep-merges on top of the platform base, so you only specify what changes:
{
"department": Department__c,
"region": Sales_Region__c
}The runtime evaluates both expressions and merges the results. The base mapping still produces id, first_name, last_name, email, etc. The override adds department and region on top. No code was changed. No deployment happened.
Level 3: Account Override
This is where enterprise deals are saved. Individual connected accounts can have their own mapping overrides attached directly to their integration record.
When one specific connected account (say, Acme Corp's Salesforce instance) has 147 custom fields that exist nowhere else, you apply an account-level override that only affects that specific account.
{
"churn_risk_score": Churn_Risk_Score__c,
"ltv_cohort": LTV_Cohort__c,
"billing_system_id": Billing_System_ID__c
}Now Acme Corp's contacts come back with all the standard unified fields plus their custom fields. The account override is deep-merged on top of the environment and platform mappings. This means a customer can add their own custom fields to the unified response at the account level without your engineering team changing any code, requiring a deployment, or even caring that Acme Corp's Salesforce admin added LTV_Cohort__c last Tuesday.
Deep Merging Mechanics The override system uses array-overwrite semantics. When the deep merge occurs, simple objects are merged recursively, but arrays from the override level replace the base arrays entirely. This prevents duplicate array entries and gives the override level complete control over lists like fallback endpoints or conditional routing paths.
What Can Be Overridden
The true power of this pattern comes from the scope of what each level can change:
| Override Target | What It Controls | Example |
|---|---|---|
response_mapping |
How response fields are mapped to the unified schema | Add custom fields, rename fields, restructure nested objects |
query_mapping |
How unified filter params translate to provider-specific queries | Support custom filter parameters, change query syntax |
request_body_mapping |
How create/update payloads are formatted | Include integration-specific fields on write operations |
resource |
Which API endpoint to call | Route to a custom object endpoint instead of the standard one |
method |
Which HTTP method to use | Use POST instead of GET for search-style operations |
Every override is stored as configuration data—a JSON blob or a declarative expression string in a database column. The runtime engine reads the base mapping, checks for an environment override, checks for an account override, deep-merges them all together, and executes the result.
This means a product manager or solutions engineer can unblock an enterprise deal by simply editing a configuration record. No pull request. No CI/CD pipeline. No deployment window. The mapping change takes effect on the next API call.
Choosing the Right Override Level
The three override levels exist for different situations. Using the wrong one creates unnecessary complexity or misses the scope of the change you actually need. Here is a decision framework.
Use Platform Base (Level 1) when the mapping applies to every customer using a given integration. This is the default. If Salesforce's Contact object always has FirstName, LastName, and Email, those belong in the base mapping. The base should cover the standard fields that exist in every out-of-the-box instance of the third-party platform. You want the base to be as complete as possible so that most customers never need an override at all.
Use Environment Override (Level 2) when an entire customer tenant needs a different behavior. Common scenarios:
- The customer installed a managed package (e.g., Gong, Outreach, or a CPQ tool) that adds fields to standard objects across their org.
- Their Salesforce edition or HubSpot tier exposes API endpoints or fields that your base mapping doesn't cover.
- They require a custom HTTP header on all requests due to security policies.
- They use a non-standard status taxonomy (e.g., mapping
Qualifiedtoactiveinstead of your defaultopen).
Environment overrides affect every connected account within that customer's environment. If a customer has three Salesforce orgs connected (production, staging, sandbox), the environment override applies to all three.
Use Account Override (Level 3) when the change is specific to one connected account. This is the most common override for enterprise customizations. Common scenarios:
- One customer's Salesforce org has custom fields (
Churn_Risk_Score__c,LTV_Cohort__c) that no other customer has. - A specific HubSpot portal uses a custom object (
Deal_Registration) that needs to be mapped. - One connected account requires a different API endpoint because of a custom Salesforce app.
- You need to add a before/after step pipeline for one account's specific data enrichment flow.
Account overrides are the safest override level. They affect exactly one connected account. If something breaks, the blast radius is a single integration.
| Scenario | Override Level | Why |
|---|---|---|
| Map standard CRM fields (name, email, phone) | Platform Base | Every instance has these fields |
| Customer installed a managed package that adds fields across their org | Environment | Affects all accounts in that customer's environment |
| Customer's Salesforce edition has features your base doesn't cover | Environment | Edition-level change, not account-specific |
| One Salesforce org has 50 custom fields on Contact | Account | Unique to that specific org |
| One HubSpot portal uses a custom object for deal registration | Account | Unique to that specific portal |
| Customer requires a custom HTTP header on all API calls | Environment | Security policy applies org-wide |
Rule of thumb: Start with an account override. If you find yourself applying the same account override to every connected account in a customer's environment, promote it to an environment override. If you find yourself applying the same environment override to every customer using a specific integration, promote it to the platform base.
Step-by-Step: Applying Per-Customer Overrides Without Code
This section walks through the actual workflow for creating overrides - both through the admin dashboard and the API. Solutions engineers, Customer Success managers, and product managers can follow these steps without filing engineering tickets.
Adding an Account Override via the API
Account-level overrides are applied by patching the integrated account record with a unified_model_override object. This is the most common path for per-customer customizations.
Step 1: Get the integrated account ID.
Find the integrated account you want to customize. You can grab this from the Truto dashboard under Integrated Accounts, or from the API:
curl 'https://api.truto.one/integrated-account?integration_name=salesforce' \
-H 'Authorization: Bearer <your_api_key>'The response includes each connected account's id and metadata. Identify the account you want to override.
Step 2: Build the override payload.
The override structure follows the path: unified_model_override > model_name > resource_name > method_name > mapping_type. You only specify what you want to add or change.
For example, to add a churn_risk_score field to the CRM contacts response for one account:
{
"unified_model_override": {
"crm": {
"contacts": {
"list": {
"response_mapping": "{ \"churn_risk_score\": response.Churn_Risk_Score__c }"
},
"get": {
"response_mapping": "{ \"churn_risk_score\": response.Churn_Risk_Score__c }"
}
}
}
}
}The response_mapping is a JSONata expression. The response variable inside the expression contains the raw third-party API response (remote_data), so you reference fields using the upstream API's native field names.
Step 3: Apply the override with a PATCH request.
curl -X PATCH 'https://api.truto.one/integrated-account/<integrated_account_id>' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <your_api_key>' \
-d '{
"unified_model_override": {
"crm": {
"contacts": {
"list": {
"response_mapping": "{ \"churn_risk_score\": response.Churn_Risk_Score__c }"
},
"get": {
"response_mapping": "{ \"churn_risk_score\": response.Churn_Risk_Score__c }"
}
}
}
}
}'Step 4: Verify the override.
Make a unified API call for that account and confirm the new field appears:
curl 'https://api.truto.one/unified/crm/contacts?integrated_account_id=<integrated_account_id>' \
-H 'Authorization: Bearer <your_api_key>'The response should now include churn_risk_score alongside the standard unified fields. Every other connected account is completely unaffected.
Adding an Environment Override via the Dashboard
Environment overrides are best managed through the Truto admin dashboard, which provides a visual mapping editor with live preview.
Step 1: Navigate to Installed Unified API in your Truto dashboard.
Step 2: Select the Mapping tab and choose the integration you want to modify (e.g., Salesforce).
Step 3: Find the specific method you want to override (e.g., list on contacts) and click the pencil icon next to it.
Step 4: A modal opens with tabs for each mapping type: Query Mapping, Request Body Mapping, Response Mapping, Error Mapping, Request Header Mapping, and Response Header Mapping. Select the tab for the mapping you want to change.
Step 5: Edit the JSONata expression. You can add new fields, remove existing ones, or change transformation logic. Toggle the Code view to edit the full YAML configuration at once if you prefer working with the raw config.
Step 6: Click Save. The override takes effect on the next API call for all integrated accounts in that environment.
Environment overrides via API: You can also apply environment overrides programmatically. First, fetch the installed unified model ID with GET /environment-unified-model?unified_model.name=crm. Then PATCH that resource with an override object that specifies the integration name, resource, method, and mapping expression. See the Overriding Unified APIs documentation for the full API reference.
Real Example: Mapping a Salesforce Custom Field for One Customer
Let's walk through a real scenario from start to finish. Acme Corp just signed a contract. Their Salesforce org has a custom field Partner_Tier__c on the Contact object that categorizes contacts by partnership level (Gold, Silver, Bronze). Your application needs this field in the unified CRM contacts response, but only for Acme Corp's account.
1. Identify the field in the upstream API.
Using the Truto Proxy API or Salesforce's own tools, confirm the field name. Salesforce custom fields always end with __c. In this case, the field is Partner_Tier__c and returns string values.
2. Write the JSONata override expression.
The override only needs to specify the new field. The base mapping continues to produce all the standard fields (id, first_name, last_name, etc.).
{ "partner_tier": response.Partner_Tier__c }That's it. One line. The response object inside the expression is the raw Salesforce API response for each contact record.
3. Apply the override to Acme Corp's integrated account.
curl -X PATCH 'https://api.truto.one/integrated-account/<acme_account_id>' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <your_api_key>' \
-d '{
"unified_model_override": {
"crm": {
"contacts": {
"list": {
"response_mapping": "{ \"partner_tier\": response.Partner_Tier__c }"
},
"get": {
"response_mapping": "{ \"partner_tier\": response.Partner_Tier__c }"
}
}
}
}
}'4. The unified response now includes the custom field.
Before the override, a contact from Acme Corp's Salesforce looked like this in the unified API:
{
"id": "003xx000004TmiP",
"first_name": "Jane",
"last_name": "Chen",
"email_addresses": [{ "email": "jane@acmecorp.com" }],
"created_at": "2024-03-15T10:30:00Z",
"remote_data": { "Partner_Tier__c": "Gold", "..." : "..." }
}After the override:
{
"id": "003xx000004TmiP",
"first_name": "Jane",
"last_name": "Chen",
"email_addresses": [{ "email": "jane@acmecorp.com" }],
"partner_tier": "Gold",
"created_at": "2024-03-15T10:30:00Z",
"remote_data": { "Partner_Tier__c": "Gold", "..." : "..." }
}The partner_tier field is now a first-class field in the unified response. Your application code can read it directly without parsing remote_data. Every other customer's contacts response is unchanged.
5. Handle more complex mappings.
If the custom field needs transformation - say, Acme stores partner tier as a numeric code (1, 2, 3) and you want human-readable labels - the JSONata expression handles it:
{
"partner_tier": response.Partner_Tier_Code__c = 1 ? "Gold"
: response.Partner_Tier_Code__c = 2 ? "Silver"
: response.Partner_Tier_Code__c = 3 ? "Bronze"
: "Unknown"
}Conditionals, string manipulation, array transforms - all within the declarative expression. No code. No deployment.
Activation and Propagation: When Override Changes Take Effect
A common question from teams applying overrides for the first time: how long until the change is live?
The answer is immediate. Overrides take effect on the next API call after the configuration is saved. There is no cache to invalidate, no deployment to wait for, no background job to complete.
Here is exactly what happens at runtime when a unified API request comes in:
- The engine loads the platform base mapping for the requested integration, resource, and method.
- It checks for an environment override for this customer's environment. If one exists, it is noted.
- It checks for an account override on the specific integrated account. If one exists, it is noted.
- The engine applies the base mapping (or environment override, if present) to the raw API response.
- If an account override exists, it is applied to the same raw API response independently.
- The results are merged: any fields from the account override that collide with the base result overwrite them, and any new fields are added.
This means:
- Save an override at 2:14 PM, and the 2:15 PM API call returns the new fields. There is no propagation delay.
- Rolling back is just as fast. Remove the override from the integrated account record, and the next API call returns the base mapping result.
- No restart or redeploy required. The mapping configuration is read at request time, not compiled into the application.
Testing before production: Because overrides are instant, test your JSONata expression before applying it to a production account. Use the mapping editor's live preview in the dashboard (which evaluates your expression against a sample response) or apply the override to a sandbox integrated account first. A malformed expression won't crash the API, but it may return unexpected field values.
Who Should Edit Mappings: Roles and Permissions
One of the biggest operational advantages of the override architecture is that it moves mapping work out of the engineering backlog. But that raises a question: who should actually be editing these configurations?
Solutions Engineers are the primary users of account-level overrides. During a proof-of-concept or enterprise onboarding, they know exactly which custom fields the customer needs. They can apply overrides via the API or dashboard without waiting for a sprint cycle.
Customer Success Managers can handle straightforward overrides - adding a single custom field to a response, adjusting a status mapping. If the JSONata expression is simple (a one-line field extraction), it doesn't require engineering expertise. The live preview in the dashboard lets them verify the result before saving.
Product Managers should own the decision of when an account override should be promoted to an environment override or folded into the platform base. If three customers all request the same custom field mapping, that is a signal to update the base mapping rather than maintaining three separate overrides.
Engineering should own the platform base mappings and handle complex override scenarios - multi-step before/after pipelines, conditional resource routing, expressions with advanced JSONata functions. They should also set up guardrails: review processes for production overrides, naming conventions for override fields, and documentation requirements.
| Role | Override Level | Typical Tasks |
|---|---|---|
| Solutions Engineer | Account, Environment | Add custom fields during POC, configure per-customer mappings during onboarding |
| Customer Success | Account | Simple field additions, status value remapping |
| Product Manager | Decision-maker | Decide when overrides should be promoted to base mappings |
| Engineering | Platform Base, Complex overrides | Write base mappings, handle multi-step pipelines, set governance policies |
Governance tip: Treat every account-level override as soft technical debt. Maintain a simple log (even a spreadsheet) that tracks which accounts have overrides, what the override does, who created it, and when it should be reviewed. If you find the same override applied to five or more accounts, it belongs in the base mapping.
Using JSONata for Declarative Data Transformation
The override architecture only works if your transformation language is powerful enough to handle the complexity of real-world API responses without falling back to procedural code. Simple key-value JSON objects cannot handle conditionals, string splitting, or array manipulation.
This is where most unified API platforms break down. They offer basic key-value field mapping but cannot handle Salesforce's custom fields that match a __c suffix pattern and need to be dynamically extracted (which is exactly why unified data models break on custom Salesforce objects), HubSpot's semicolon-separated hs_additional_emails field that needs to be split into an array, or NetSuite's SuiteQL-driven queries that require constructing SQL at runtime based on the customer's edition.
The solution is JSONata—a functional query and transformation language for JSON. It is a Turing-complete expression language purpose-built for reshaping JSON objects. JSONata is a declarative functional language for querying and transforming JSON data, inspired by the path semantics of XPath.
What makes JSONata the right tool for per-customer API mapping:
- It's declarative. JSONata's declarative approach means you can describe what you want to achieve without getting bogged down in the procedural details. You describe the shape of the output, not the steps to produce it.
- It's Turing-complete. Conditionals, string manipulation, array transforms, date formatting, recursive expressions—all within a single expression string.
- It's storable as data. A mapping expression is just a string. It can live in a database column, be versioned, overridden per customer, and hot-swapped without restarting anything.
- It's side-effect free. Expressions are pure functions. They transform input to output without mutating state, making them safe to evaluate in any context.
- It's safe: perfect for allowing end-users or product managers to write custom data-extraction scripts without risking arbitrary code injection.
Example: Mapping HubSpot vs. Salesforce Contacts
Salesforce and HubSpot both have "contacts," but their API response shapes are completely different. Using JSONata, you can handle both elegantly without procedural code.
HubSpot response mapping (nested properties object, semicolon-separated emails):
(
$defaultProperties := ["firstname", "lastname", "email", "phone", "hs_additional_emails", "mobilephone"];
$diff := $difference($keys(response.properties), $defaultProperties);
{
"id": response.id.$string(),
"first_name": response.properties.firstname,
"last_name": response.properties.lastname,
"name": response.properties.firstname & " " & response.properties.lastname,
"email_addresses": [
response.properties.email ? { "email": response.properties.email, "is_primary": true },
response.properties.hs_additional_emails
? response.properties.hs_additional_emails.$split(";").{ "email": $ }
],
"phone_numbers": [
response.properties.phone ? { "number": response.properties.phone, "type": "phone" },
response.properties.mobilephone ? { "number": response.properties.mobilephone, "type": "mobile" }
],
"custom_fields": response.properties.$sift(function($v, $k) { $k in $diff })
}
)Notice the $sift function at the end. This is a dynamic expression that automatically captures any property returned by HubSpot that is not in the $defaultProperties list and places it into a custom_fields object. This handles standard fields while gracefully capturing unknown enterprise custom fields, all without a single if (provider === 'hubspot') statement in the runtime code.
Salesforce response mapping (flat PascalCase fields, custom fields with __c suffix):
response.{
"id": $string(Id),
"first_name": FirstName,
"last_name": LastName,
"name": $join([FirstName, LastName], " "),
"email_addresses": [{ "email": Email }],
"phone_numbers": $filter([
{ "number": Phone, "type": "phone" },
{ "number": MobilePhone, "type": "mobile" },
{ "number": HomePhone, "type": "home" }
], function($v) { $v.number }),
"custom_fields": $sift($, function($v, $k) { $k ~> /__c$/i and $boolean($v) }),
"created_at": CreatedDate,
"updated_at": LastModifiedDate
}Both produce the exact same unified output shape. The Salesforce mapping dynamically extracts custom fields using a regex pattern (__c suffix). The HubSpot mapping splits semicolon-separated emails into an array. Neither requires a single line of procedural code.
Example: The Enterprise Account Override
Now, imagine your enterprise customer needs to map a highly specific custom object, Deal_Registration__c, and they want it exposed at the top level of the unified response, not buried in a generic custom_fields object.
You simply apply an Account Override with a JSONata expression that merges their specific requirement into the response:
{
"response_mapping": "$merge([$, { \"deal_registration_status\": response.Deal_Registration__c.Status }])"
}At runtime, the execution engine evaluates the Platform Base mapping, evaluates the Account Override mapping, and deep-merges the results. Acme Corp gets everything. Every other Salesforce customer is unaffected. Your engineering team wrote zero code. No deployments were triggered. The unified abstraction remains perfectly intact.
The Unified API That Doesn't Force Standardized Data Models on Custom Objects dives deeper into how this specific workflow unblocks sales teams.
The Architectural Superiority of the Interpreter Pattern
Most unified API platforms use the Strategy Pattern (or Adapter Pattern). They define a common interface and write a specific adapter class in TypeScript or Python for each provider. HubSpotAdapter.ts, SalesforceAdapter.ts, PipedriveAdapter.ts. Each implements a common interface.
This is cleaner than raw if/else branches, but it still means code-per-integration and code-per-customization. It scales linearly with pain. Every new integration adds more code to maintain. A bug in the Salesforce pagination logic does not get fixed for HubSpot. Adding support for a new custom field requires a pull request, code review, and a production deployment.
The 3-level mapping architecture uses the Interpreter Pattern at platform scale instead. The integration configuration and JSONata mapping expressions form a domain-specific language (DSL) for describing API interactions. The runtime engine is a generic execution interpreter that executes this DSL without knowing which integration it's running.
Traditional Architecture (Adapter Pattern):
Unified Interface --> HubSpotAdapter.ts (code)
--> SalesforceAdapter.ts (code)
--> PipedriveAdapter.ts (code)
3-Level Mapping Architecture (Interpreter Pattern):
Unified Interface --> Generic Engine --> Base Mapping (data)
(one code path) Environment Override (data)
Account Override (data)New integrations are just new "programs" in this DSL, not new features in the interpreter. Because the runtime code operates exclusively on abstract concepts—"evaluate this JSONata expression," "build this URL from this template," "apply this authentication scheme"—it never asks "which integration am I talking to?"
The operational implications are massive. When you fix a bug in the generic engine, every integration benefits. When you improve pagination handling, all 100+ integrations get the improvement. According to McKinsey, maintenance costs typically account for 15-25% of the initial development expense annually—and with the interpreter pattern, you maintain one engine instead of N adapters. This is why adding a new integration is a data operation, not a code operation, and it is why you can apply a 3-level override hierarchy dynamically at runtime.
Look Ma, No Code! Why Truto's Zero-Code Architecture Wins explains how this interpreter pattern completely eliminates the maintenance hell of adapter classes.
How Modern Unified APIs Implement This Architecture
Advanced unified API platforms build this 3-level override architecture as the foundation of their infrastructure. The entire system—database, runtime engine, proxy layer, sync jobs, webhooks—contains zero integration-specific code. No if (hubspot). No switch (provider). Every integration is defined entirely as data: a JSON configuration blob describing the API surface, and JSONata expressions describing the data transformations.
The override hierarchy processes requests seamlessly:
- Platform base mappings define the standard unified response for each integration and resource (e.g., Salesforce CRM contacts).
- Environment-level overrides let a customer's environment customize any aspect of the mapping—response fields, query translations, default values—without affecting other environments.
- Account-level overrides let a single connected account (one specific Salesforce org) have its own mapping behavior.
At runtime, the engine deep-merges all three levels, evaluates the resulting JSONata expression against the API response, and returns the unified output. Adding support for an enterprise customer's custom schema is a data operation: update a configuration record, and you are done. No code review, no deployment, no risk to existing customers.
This pattern extends far beyond response mapping. Query mappings (how unified filter parameters translate to provider-specific query syntax), request body mappings (how create/update payloads are formatted), and even resource routing (which API endpoint to call) are all overridable at each level.
The Trade-offs You Should Know About
No architecture is without costs. While treating integration behavior as data solves the enterprise custom object problem, here is what you must consider:
JSONata has a learning curve. It is not JavaScript. The XPath-inspired syntax is unfamiliar to most developers, and debugging complex expressions can be frustrating. Tools like the JSONata Exerciser help, but you will still need team members who are comfortable with functional, declarative expression languages.
Override sprawl is a real risk. If you are not disciplined about when to create account-level overrides vs. improving the base mapping, you can end up with hundreds of per-customer overrides that nobody understands. Treat overrides like technical debt: every one should have a documented reason and a review date.
Some transformations genuinely need code. If you need to make HTTP calls, write to a database, or execute complex multi-step logic that goes beyond data transformation, a pure JSONata expression won't cut it. The architecture should support escape hatches (like before/after step pipelines that can chain multiple API calls) for these specific cases.
You still need to understand the upstream APIs. Declarative mappings don't eliminate the need to deal with terrible vendor API docs, undocumented edge cases, and breaking changes. The mapping layer absorbs the complexity, but someone still needs to write and maintain the mappings themselves.
Ship Integrations as Data, Not Code
The enterprise custom object problem is not going away. Salesforce holds roughly 21% of the global CRM market, and 80% of Fortune 500 companies rely on it. Nearly every one of those enterprise deployments is heavily customized. And Salesforce is just one platform. Multiply this across every HRIS, ATS, ticketing, and accounting system your customers use, and you are looking at thousands of schema variations you need to support.
When you treat integration behavior as declarative configuration data rather than executable code, you fundamentally change the economics of building B2B SaaS. The 3-level mapping architecture solves the schema fragmentation problem by changing the unit of work. Adding a new integration is a data operation. Customizing the mapping for an enterprise customer is a data operation. Rolling back a mapping change is a data operation. Your runtime engine stays exactly the same.
- For engineering leaders: Your integration infrastructure doesn't scale linearly with customer count. One generic execution engine supports infinite configurations.
- For product managers: You can unblock enterprise deals instantly by applying account-level overrides to capture bespoke custom objects, rather than filing engineering tickets and waiting for the next sprint.
- For solutions engineers: You can customize the API behavior for a specific prospect during the proof-of-concept phase, before the contract is even signed.
The pattern is straightforward. Store your mappings as data. Make them overridable at multiple levels. Use a declarative transformation language that is powerful enough to handle real-world complexity. And keep your runtime code completely ignorant of which integration or customer it is serving. The 3-level mapping architecture ensures that your unified API serves your product's needs, rather than forcing your enterprise customers to conform to a rigid, lowest-common-denominator schema.
FAQ
- What is a 3-level API mapping override hierarchy?
- It's an architecture where integration mappings are defined at three levels - platform base (shared defaults), environment override (per-customer environment), and account override (per-connected account) - that deep-merge at runtime to produce the final mapping.
- How do you handle custom Salesforce fields in a unified API without writing code?
- Use a multi-level override hierarchy where per-account JSONata expressions add custom field mappings on top of the standard unified schema. The override is stored as configuration data, not code, so it can be changed without deployment.
- What is JSONata and why use it for API data transformation?
- JSONata is a declarative, functional query and transformation language for JSON. It allows complex data mapping logic to be stored as configuration data rather than executable code, enabling hot-swappable integrations that handle conditionals and array manipulations without procedural code.
- What is the difference between the Adapter Pattern and the Interpreter Pattern for integrations?
- The Adapter Pattern writes separate code modules per integration (HubSpotAdapter.ts, SalesforceAdapter.ts). The Interpreter Pattern stores integration behavior as declarative data and runs it through a single generic engine, eliminating per-integration code entirely.
- How should a unified API handle rate limits?
- A unified API should not absorb or automatically retry rate-limited requests, as this causes thundering herd problems. Instead, it should pass the HTTP 429 error back to the caller while normalizing rate limit data into standard headers (like ratelimit-limit and ratelimit-remaining).