---
title: How to Publish JSONata Manifests and Mapping Examples for API Integrations
slug: how-to-publish-jsonata-manifests-and-mapping-examples-for-api-integrations
date: 2026-05-27
author: Sidharth Verma
categories: [Engineering, Guides, By Example]
excerpt: "Replace brittle per-vendor API adapters with declarative JSONata manifests. Real code examples for mapping responses, custom objects, and error normalization."
tldr: "Hardcoded API integrations fail at scale. By using JSONata manifests, teams can map custom objects, translate queries, and normalize errors purely as data operations instead of code deployments."
canonical: https://truto.one/blog/how-to-publish-jsonata-manifests-and-mapping-examples-for-api-integrations/
---

# How to Publish JSONata Manifests and Mapping Examples for API Integrations


If you are an engineering leader or product manager at a B2B SaaS company, you already know that building integrations is a massive financial drain. If you have ever maintained a `mapHubSpotContact()` function next to a `mapSalesforceContact()` function next to twenty more, you already understand the problem. Your team likely spends weeks writing custom code to connect with third-party APIs, only to spend the rest of the year maintaining those connections when vendors deprecate endpoints, change their pagination strategies, or introduce new OAuth scopes.

Hardcoded per-vendor adapters do not scale. If your engineering team is buried under integration logic, the fastest way out is to stop writing brittle API adapters in Node.js or Python and start treating API integration as a data transformation problem, as we explore in our [developer tutorial on building JSONata mappings](https://truto.one/developer-tutorial-how-to-build-jsonata-mappings-for-api-integrations/).

This guide walks through how to write production-grade **JSONata manifests** for response transformation, custom object handling, query translation, and error normalization. A manifest is a declarative configuration that pairs a JSON or YAML description of an API with JSONata expressions that translate between your unified schema and the vendor's native format. We will provide concrete, working code samples you can lift straight into an integration pipeline.

## The Hidden Cost of Hardcoded API Adapters

Building a single production-grade integration is not a weekend project. Industry estimates from API tooling vendors like DreamFactory put the cost of a typical API build between **$10,000 and $50,000** depending on complexity, auth scheme, and edge case handling. But the initial build is just the down payment.

Independent software maintenance benchmarks consistently put ongoing annual upkeep at **15% to 20% of the initial build cost**. This factors in vendor API changes, OAuth scope drift, pagination quirks, and the steady drip of customer-reported edge cases. Multiply that across 30 integrations, and you have a permanent engineering tax that grows linearly with your connector count.

The deeper problem is architectural. Most teams start building integrations using the strategy pattern. They define a common `Connector` interface in their application and write a separate adapter class for every third-party API: `HubSpotConnector`, `SalesforceConnector`, `PipedriveConnector`.

The strategy pattern looks fine when you have three integrations. It rots once you cross ten. At that scale, you start noticing the breakdown:

- A bug fix in pagination logic only helps the specific connector you fixed it in.
- Adding a new unified field requires *N* pull requests, one per adapter.
- Customer-specific custom fields force you to fork connectors or sprinkle endless conditional logic.
- Onboarding a new engineer means teaching them *N* different codebases.

The strategy pattern organizes integration code; it does not reduce it. When you hardcode integrations, you are coupling your application's deployment lifecycle to the unpredictable whims of fifty different SaaS vendors. [Schema normalization](https://truto.one/why-schema-normalization-is-the-hardest-problem-in-saas-integrations/) becomes an exercise in endless conditional logic. To actually shrink the surface area of your integration layer, you need to push integration-specific behavior out of code entirely and into data.

## What is JSONata and Why Does it Matter for Integrations?

JSONata is a declarative, Turing-complete query and transformation language purpose-built for JSON. Think of it as XSLT for modern REST APIs, but designed for the way developers actually work with data today. 

Expressions in JSONata are pure functions: they take a JSON input and produce a JSON output, with no side effects and no state. Three properties make it the perfect tool for integration manifests:

1. **It is data, not code.** A JSONata expression is just a string. You can store it in a database column, version it, override it per customer, and [hot-swap it at runtime without triggering a code deployment](https://truto.one/hot-swappable-api-integrations-add-connectors-without-code-deploys/).
2. **It composes beautifully.** Conditionals, array transformations, string manipulation, and complex object reshaping all live inside a single, readable expression.
3. **It is being adopted by serious infrastructure.** Major cloud providers and orchestration platforms are actively adopting it to replace complex data transformation code. AWS recently added JSONata support to Step Functions to cut out intermediate data transformation states. Orchestration platforms like Kestra position it as the default "Swiss Army Knife" for declarative JSON reshaping. Stedi uses it in its Mappings API, and Epilot exposes JSONata-based mapping simulators for ERP integrations.

The trend is unambiguous: complex data transformation is moving from imperative code into declarative expression languages, and JSONata is winning that race for JSON. Instead of a `hubspot_mapper.ts` file, you store a JSONata expression in a database. A generic execution engine reads the expression and evaluates it against the incoming API payload. Adding a new integration becomes a data operation, not a code deployment.

## Publishing JSONata Manifests: The Core Architecture

A modern integration manifest has two layers. The **integration config** describes *how* to talk to the API. The **integration mapping** describes *what* to do with the data.

Here is the skeleton of an integration config written in YAML:

```yaml
base_url: https://api.hubspot.com
credentials:
  format: oauth2
authorization:
  format: bearer
  config:
    path: oauth.token.access_token
pagination:
  format: cursor
  config:
    cursor_field: paging.next.after
resources:
  contacts:
    list:
      method: get
      path: /crm/v3/objects/contacts
      response_path: results
```

Nothing in that file is procedural code. It is a description that a generic engine reads and executes. To add Salesforce, you write a different config; you do not write a different engine.

The mapping layer is where JSONata earns its keep. Let us look at how the same unified "list CRM contacts" operation works for two very different APIs—HubSpot and Salesforce—using JSONata manifests.

### Mapping HubSpot's Nested Properties

HubSpot's contacts API returns data nested inside a `properties` object. It also includes an arbitrary number of custom fields that you need to preserve. Here is a JSONata expression that normalizes a HubSpot contact into a standard schema while dynamically catching any custom fields:

```jsonata
(
  $defaultProperties := ["firstname", "lastname", "jobtitle", "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,
    "title": response.properties.jobtitle,
    "email_addresses": $append(
      response.properties.email ? [{ "email": response.properties.email, "is_primary": true }] : [],
      response.properties.hs_additional_emails ? response.properties.hs_additional_emails.$split(";").{ "email": $, "is_primary": false } : []
    ),
    "phone_numbers": $filter([
      response.properties.phone ? { "number": response.properties.phone, "type": "phone" },
      response.properties.mobilephone ? { "number": response.properties.mobilephone, "type": "mobile" }
    ], function($v) { $boolean($v.number) }),
    "created_at": response.createdAt,
    "updated_at": response.updatedAt,
    "custom_fields": response.properties.$sift(function($v, $k) { $k in $diff })
  }
)
```

**How it works:**
1. We define an array of standard fields we expect to map directly.
2. We use the `$difference` function to compare all keys in `response.properties` against our standard list. Anything left over is dynamically flagged as a custom field.
3. We map the standard fields to our unified schema, using `$append` and `$split` to cleanly handle HubSpot's semicolon-separated secondary emails.
4. We construct a `phone_numbers` array and use `$filter` to drop any null objects.
5. We use the `$sift` function to dynamically construct a `custom_fields` object containing only the non-standard properties.

### Mapping Salesforce's Flat Schema

Salesforce returns flat, PascalCase fields. Custom fields are identified by a `__c` suffix. The JSONata expression for Salesforce looks completely different from HubSpot, yet it produces the exact same unified output:

```jsonata
response.{
  "id": Id,
  "first_name": FirstName,
  "last_name": LastName,
  "name": $join($removeEmptyItems([FirstName, LastName]), " "),
  "title": Title,
  "email_addresses": [{ "email": Email, "is_primary": true }],
  "phone_numbers": $filter([
    { "number": Phone, "type": "phone" },
    { "number": MobilePhone, "type": "mobile" },
    { "number": Fax, "type": "fax" },
    { "number": HomePhone, "type": "home" }
  ], function($v) { $boolean($v.number) }),
  "created_at": CreatedDate,
  "updated_at": LastModifiedDate,
  "custom_fields": $sift($, function($v, $k) { $k ~> /__c$/i and $boolean($v) })
}
```

**How it works:**
1. We map the flat PascalCase fields directly to our snake_case unified schema.
2. We use `$join` and `$removeEmptyItems` to safely concatenate the first and last name into a full name string, even if one is missing.
3. We build the `phone_numbers` array and filter out empty values just like we did for HubSpot.
4. We use `$sift` with a regular expression (`/__c$/i`) to automatically extract any key ending in `__c` as a custom field.

[Developer guides for API mapping](https://truto.one/developer-guide-mapping-api-data-with-jsonata-code-samples/) rely heavily on these functional array and object manipulation techniques to avoid writing brittle loops in application code.

> [!TIP]
> **Architectural Note:** Keep your response mappings idempotent and side-effect free. If you find yourself wanting to make an HTTP call inside a mapping expression, that belongs in a separate pre- or post-execution step, not in the mapping itself. Mixing stateful HTTP requests into a data transformation layer turns debugging into archaeology.

## Handling Custom Objects and Enterprise Edge Cases

Standard objects like Contacts and Deals are easy. The moment you integrate with enterprise CRMs, you hit the wall of custom objects. Your enterprise customer's Salesforce instance might have an `Invoice__c` or `Project_Milestone__c` object, alongside 147 custom fields on their standard Contact object.

Fixed, hardcoded API schemas fail here. You cannot anticipate every custom object an enterprise customer might create at design time. This is the part most unified-API approaches get wrong.

By treating API mappings as JSONata configuration stored in a database, you can implement an override hierarchy. This allows you to apply different JSONata mapping manifests at different levels of your architecture:

1. **Platform Base Mapping:** The default JSONata expression that handles standard fields for 90% of use cases (like the examples above).
2. **Environment Override:** A JSONata expression applied to a specific staging or production environment to tweak field names.
3. **Account Override:** A JSONata expression tied directly to a single customer's connected account.

If a specific enterprise customer needs their highly customized `Customer_Tier__c` object mapped into a structured `tier` field on their unified response, you simply write a JSONata expression that extracts those specific fields and attach it to their account configuration. The core execution engine deep-merges this account-level JSONata over the base platform mapping at runtime.

You solve a complex enterprise requirement by [mapping custom objects](https://truto.one/step-by-step-developer-guide-mapping-custom-objects-with-jsonata/) via configuration, without touching a single line of backend source code or triggering a deployment.

## Normalizing API Errors with Declarative Expressions

Vendor error formats are an embarrassment to the industry. Some use standard HTTP status codes. Others return a `200 OK` status with an error payload hidden in the body. If your application blindly trusts HTTP status codes, these body-based errors will silently corrupt your data pipelines.

Instead of writing custom error-handling logic for every API, you can use JSONata to evaluate the raw API response and produce a structured error object containing a normalized HTTP status code and a human-readable message.

### Example: Catching Slack's 200 OK Errors

Slack's API is notorious for returning `200 OK` for failed requests, signaling the error with a payload like `{ "ok": false, "error": "invalid_auth" }`. A JSONata error manifest maps this into a structured `{ status, message }` object before it reaches your application:

```jsonata
$not(data.ok) ? {
  "status": $mapValues(data.error, {
    "invalid_cursor": 400,
    "invalid_auth": 401,
    "token_expired": 401,
    "token_revoked": 401,
    "missing_scope": 403,
    "channel_not_found": 404,
    "ratelimited": 429,
    "internal_error": 500
  }),
  "message": $mapValues(data.error, {
    "invalid_cursor": "Value passed for cursor was not valid.",
    "invalid_auth": "Authentication cannot be validated.",
    "token_expired": "Authentication token has expired.",
    "ratelimited": "The request has been rate limited."
  })
}
```

If `data.ok` is true, the expression evaluates to undefined, and the system proceeds normally. If it is false, the expression looks up the specific Slack error string in a mapping object and outputs a standardized HTTP status.

### Example: Correcting Semantically Wrong Status Codes

Sometimes APIs return the wrong HTTP status entirely. Freshdesk, for example, returns a `429 Too Many Requests` when an account's billing plan does not include API access. Highlevel returns `401 Unauthorized` for permission errors that should be `403 Forbidden`.

We can use JSONata to detect Freshdesk's fake 429 (which lacks a retry header) and remap it to a `402 Payment Required`:

```jsonata
status = 429 and $not($exists(headers.`retry-after`)) ? {
  "status": 402,
  "message": "API access is not available on the current plan."
}
```

This matters for AI agents and sync jobs especially, because they need a reliable signal to decide whether to retry, escalate, or stop. A `401` should trigger a re-authentication flow. A `429` should trigger exponential backoff. A `402` should surface to the customer. Without normalization, every consumer of your unified API has to reimplement vendor-specific error handling.

> [!WARNING]
> **A Note on Handling Rate Limits Transparently:** While JSONata handles error normalization, rate limits require specific architectural handling. A sane unified API should not silently swallow legitimate `429` responses. Truto's approach is to normalize upstream rate limit information into standardized headers (`ratelimit-limit`, `ratelimit-remaining`, `ratelimit-reset`) per the IETF specification. When an upstream API hits a limit, Truto passes that HTTP 429 error directly to the caller. We do not automatically retry or absorb rate limit errors. Retry, throttling, and exponential backoff stay in the caller's hands, where they belong, because only the caller knows the business priority of the request.

## Query Translation and Request Body Mapping

Mapping responses is only half the battle. When a user makes a request to your application—say, searching for a contact by email—you have to translate that unified search query into the highly specific syntax required by the destination API.

HubSpot uses a complex JSON payload with `filterGroups`. Salesforce uses SOQL (Salesforce Object Query Language) in the query string. JSONata can generate both dynamically based on the incoming request.

### Translating to HubSpot filterGroups

If your application sends `?email=alice@example.com`, this JSONata request body mapping converts it into HubSpot's search syntax:

```jsonata
rawQuery.{
  "filterGroups": $firstNonEmpty(email, first_name, last_name) ? [{
    "filters": [
      email ? { "propertyName": "email", "operator": "EQ", "value": email },
      first_name ? { "propertyName": "firstname", "operator": "CONTAINS_TOKEN", "value": first_name }
    ]
  }]
}
```

### Translating to Salesforce SOQL

For Salesforce, the same incoming query needs to become a SOQL `WHERE` clause. JSONata handles the string concatenation cleanly:

```jsonata
(
  $whereClause := query.email ? "Email = '" & query.email & "'" :
                  query.first_name ? "FirstName LIKE '%" & query.first_name & "%'";
  {
    "q": query.search_term 
      ? "FIND {" & query.search_term & "} RETURNING Contact(Id, FirstName, Email)",
    "where": $whereClause ? "WHERE " & $whereClause
  }
)
```

By executing these expressions at runtime, your application code never sees `filterGroups` or `SOQL`. It simply executes an HTTP request using a unified query object. When HubSpot ships a new search operator, you update the expression in the database. No deploy, no PR, no regression risk on the other 99 integrations.

## Architecting a Code-Free Integration Pipeline

What we have described here is an instance of the **interpreter pattern** applied at platform scale.

In a traditional integration architecture, every new API requires new code. In a declarative architecture, the runtime engine is a generic interpreter that executes a Domain-Specific Language (DSL) comprised of JSON configuration and JSONata expressions.

```mermaid
graph TD
    A[Unified API Request] --> B[Generic Execution Engine]
    B --> C[(Database)]
    C -.->|Integration Config| B
    C -.->|JSONata Mappings| B
    C -.->|Account Overrides| B
    B --> D[Evaluate Request JSONata]
    D --> E[Execute HTTP Fetch]
    E --> F[Evaluate Error JSONata]<br><i>Normalizes 200 OK errors</i>
    F --> G[Evaluate Response JSONata]
    G --> H[Unified Response]
```

This is exactly how Truto operates. The entire platform handles hundreds of third-party integrations without a single line of integration-specific code in its database schema or runtime logic. The same code path that handles a HubSpot CRM contact listing also handles Salesforce, Pipedrive, and Zoho.

The trade-offs are real and worth naming. JSONata has a learning curve, especially for engineers who have never written XSLT or a functional transformation language. Debugging a malformed expression is harder than debugging an imperative function with stack traces. And for genuinely exotic APIs (like SOAP, gRPC, or proprietary binary protocols), you will still need some glue code to bridge to JSON before the expressions can take over. 

However, none of that changes the underlying math: the maintenance curve of a manifest-driven system scales with the number of *unique API patterns*, not the number of integrations. That difference compounds massively.

## Where to Take This Next

If you are evaluating whether to adopt this declarative pattern, here are three concrete next steps:

1. **Pick your two most painful integrations.** Write the manifests for both by hand. Compare the line count, clarity, and flexibility against your existing adapter code.
2. **Build a thin generic runner** that reads a manifest and executes one resource. You will discover quickly which parts of your current adapters are genuinely vendor-specific and which are accidentally so.
3. **Decide on your override model.** Per-customer customization is the feature that turns this from a clever engineering choice into a sales weapon. Without it, you have just rebuilt a slightly nicer adapter pattern.

If your team is drowning in custom integration scripts, migrating to a declarative, JSONata-driven architecture will eliminate the vast majority of your maintenance burden. You stop writing integrations, and you start configuring them.

> Stop writing brittle integration code. Want to see how a manifest-driven architecture handles your specific integration list, including custom objects and per-customer overrides? Book a working session with our team and we will walk through your stack.
>
> [Talk to us](https://cal.com/truto/partner-with-truto)
