How to Build a Custom SaaS API Connector Without Code
Learn how to build custom SaaS API connectors without writing imperative code using declarative JSON configurations, JSONata mappings, and unified architectures.
If you want to know how to build a custom API connector without writing imperative code, the answer lies in shifting your integration architecture from custom scripts to declarative configurations. Instead of writing bespoke Node.js or Python logic for every new third-party API, you define the authentication flow, pagination rules, and data mappings in a standardized JSON format. A generic execution engine then interprets this configuration at runtime.
This approach eliminates the maintenance tax of traditional integrations. When you write custom code to interact with a vendor API, you inherit the responsibility of maintaining that code through every upstream deprecation, schema change, and undocumented edge case. By moving to a declarative model, you abstract the mechanics of HTTP requests, token management, and payload transformation away from your core product logic.
This guide breaks down the financial realities of building in-house connectors, explains the architectural shift required to adopt a declarative integration model, and provides an end-to-end tutorial on building a custom API connector without writing integration-specific code.
The Hidden Cost of Writing Integration-Specific Code
Every new third-party API connector starts as an ad-hoc request to unblock a specific enterprise deal. Engineering looks at the vendor documentation, estimates two sprints for the build, and proceeds to spend the next three months debugging token refresh failures, undocumented schema behaviors, and opaque error codes.
Depending on complexity and developer time, building a single API integration from scratch can easily reach $10,000 or more just for the initial build phase. This figure only covers the initial deployment. The true financial drain comes from the ongoing maintenance required to keep that connection alive in production.
The Integration Maintenance Tax is the recurring engineering cost associated with monitoring, updating, and fixing custom API connections. This includes handling breaking API changes, updating OAuth scopes, migrating from deprecated endpoints, and managing infrastructure to handle webhook ingestion and retry queues.
The average company relies on hundreds of SaaS applications, creating a massive integration backlog. The 2025 State of SaaS Integration outlook reports that companies today rely on over 250 SaaS applications on average. Your customers expect your product to connect to their existing stack natively. If you rely on custom code for every new connector, scaling your integration catalog becomes mathematically impossible without linearly scaling your engineering headcount.
Product teams often try to bypass this by pushing customers to third-party tools or relying on generic visual builders. But as we discussed in our guide on how to publish end-to-end developer tutorials, enterprise buyers evaluate your platform based on the friction of your native integrations. If your product requires them to build their own middleware, you will lose the deal.
Imperative Scripts vs. Declarative API Configurations
To escape the integration maintenance trap, engineering teams must stop treating API connections as bespoke software projects and start treating them as data-only operations.
There are several approaches to reducing integration development time, but not all of them scale for embedded, customer-facing use cases. Understanding the competitive landscape clarifies why declarative architecture wins:
- Visual Workflow Builders (Make / Zapier): These platforms position themselves as general-purpose visual workflow builders. They are excellent for internal automation but lack the enterprise-grade version control, isolated environments, and code-like deployment pipelines needed for native, white-labeled SaaS integrations.
- Code-First ETLs (Hotglue): Platforms like Hotglue position themselves as code-first Python ETL platforms for B2B integrations. While powerful for heavy data pipelines, they still require developers to write and maintain transformation scripts per connector, failing to eliminate the maintenance burden.
- Visual Backends (BuildShip): Tools like BuildShip offer a low-code visual backend that lets users build custom API integrations visually. However, developers still have to manually map individual API endpoints and manage the underlying logic visually, which becomes brittle at scale.
- Internal Data Extractors (Airbyte): Airbyte offers a no-code connector builder focused primarily on extracting data for internal analytics pipelines and data warehouses, rather than powering real-time, customer-facing embedded integrations.
The alternative to these methods is a declarative unified API architecture.
Gartner estimates that 70% of new enterprise applications will use no-code or low-code by 2026. This shift is driven by hard ROI. A Forrester TEI study found that low-code integration platforms reduced development timelines for non-complex integrations by up to 70% over three years.
The Zero Integration-Specific Code Pattern
To achieve true scale, you need an architecture that operates with zero integration-specific code.
In this model, the runtime is a generic execution engine. It takes a declarative JSON configuration describing how to talk to a third-party API (the base URL, the authentication method, the pagination style) and a declarative mapping describing how to translate between unified and native formats. The engine executes both without any hardcoded awareness of which integration it is running.
Whether you are connecting to Salesforce, HubSpot, or a niche industry-specific ERP, the underlying execution code path is identical. The only variable is the JSON configuration file.
What a Declarative Unified API Looks Like
Strip away the abstraction and a declarative unified API is three JSON objects working together:
- Integration Config - describes how to talk to the third-party API (base URL, auth, pagination, endpoints)
- Integration Mapping - a JSONata expression that translates the native response into a unified format
- Customer Override (optional) - lets a specific tenant modify the mapping without touching the base config
Here is a minimal integration config for a fictional CRM:
{
"base_url": "https://api.acmecrm.com",
"credentials": { "type": "oauth2" },
"authorization": { "format": "bearer" },
"pagination": { "format": "cursor", "config": { "cursor_field": "meta.next" } },
"resources": {
"contacts": {
"list": {
"method": "get",
"path": "/v2/contacts",
"response_path": "data"
}
}
}
}And here is the JSONata mapping that normalizes the response into a unified Contact schema:
response.{
"id": $string(contact_id),
"first_name": fname,
"last_name": lname,
"email": primary_email,
"phone": phone_number ? phone_number : null,
"created_at": $fromMillis(created_ts)
}The generic engine reads the config, authenticates, fetches the data, paginates automatically, and evaluates the JSONata expression against each response item. The output is always the same unified shape - regardless of which CRM is behind it.
No adapter class. No provider-specific function. The "integration" is two pieces of data, not a code module.
Code-First vs. Declarative: A Side-by-Side Comparison
To make the difference tangible, here is how listing contacts looks in a code-first architecture vs. a declarative one.
Code-first approach - a separate adapter file per provider, plus a router that branches on provider name:
// hubspot-adapter.ts
async function listContacts(creds, params) {
const res = await fetch(
`https://api.hubspot.com/crm/v3/objects/contacts?limit=${params.limit}`,
{ headers: { Authorization: `Bearer ${creds.accessToken}` } }
);
const data = await res.json();
return data.results.map(c => ({
id: c.id,
first_name: c.properties.firstname,
last_name: c.properties.lastname,
email: c.properties.email,
}));
}
// salesforce-adapter.ts
async function listContacts(creds, params) {
const soql = `SELECT Id, FirstName, LastName, Email
FROM Contact LIMIT ${params.limit}`;
const res = await fetch(
`https://${creds.instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(soql)}`,
{ headers: { Authorization: `Bearer ${creds.accessToken}` } }
);
const data = await res.json();
return data.records.map(c => ({
id: c.Id, first_name: c.FirstName,
last_name: c.LastName, email: c.Email,
}));
}
// router.ts - grows with every new provider
switch (provider) {
case 'hubspot': return hubspotAdapter.listContacts(creds, params);
case 'salesforce': return salesforceAdapter.listContacts(creds, params);
// ... repeat for every new provider
}Every new provider means a new file, new tests, a new branch in the router, and a deployment. Thirty providers means thirty adapter files to maintain.
Declarative approach - the same generic engine handles both. The only difference is the config data stored in the database:
// HubSpot config (data, not code)
{
"path": "/crm/v3/objects/contacts",
"response_path": "results",
"response_mapping": "response.{ 'id': id, 'first_name': properties.firstname, 'last_name': properties.lastname, 'email': properties.email }"
}// Salesforce config (data, not code)
{
"path": "/services/data/v59.0/query",
"response_path": "records",
"response_mapping": "response.{ 'id': Id, 'first_name': FirstName, 'last_name': LastName, 'email': Email }"
}Both flow through one code path: resolve config, authenticate, fetch, paginate, map response, return. Adding provider #31 means inserting a config row, not writing code.
Developer Effort Comparison
| Task | Code-First Approach | Declarative Unified API |
|---|---|---|
| Add a new CRM integration | 2-4 weeks (adapter code, tests, code review, deployment) | 1-3 days (JSON config + JSONata mapping) |
| Handle a provider's API deprecation | Rewrite adapter logic, update tests, deploy | Update config fields - no code deployment |
| Support a customer's custom CRM fields | Fork the adapter or add conditional logic | Customer-level mapping override (config only) |
| Fix a pagination bug | Fix per adapter, test each, N deployments | One fix in the generic engine - all integrations benefit |
| Onboard a new engineer to integrations | Learn each adapter's structure and quirks | Learn the config schema once |
End-to-End Tutorial: Building a Custom API Connector Without Code
In this tutorial, we will walk through defining a third-party API connection using a declarative configuration approach. We will define the authentication flow, map the data using JSONata expressions, normalize pagination, and handle incoming webhooks - all without writing custom imperative scripts.
We will use the architectural concepts from Truto's generic execution engine to illustrate how this is done in a production-grade environment.
Step 1: Defining the Authentication Flow Declaratively
Authentication is the most brittle component of any API integration. Managing OAuth 2.0 authorization code flows requires storing client credentials securely, handling redirects, exchanging codes for tokens, securely storing access and refresh tokens, and writing cron jobs or middleware to refresh tokens before they expire.
In a declarative model, you define the parameters of the authentication flow, and the execution engine handles the state management.
Here is an example of how an OAuth 2.0 flow is defined declaratively:
{
"auth": {
"type": "oauth2",
"grant_type": "authorization_code",
"authorization_url": "https://provider.com/oauth/authorize",
"token_url": "https://provider.com/oauth/token",
"client_id": "{{environment.client_id}}",
"client_secret": "{{environment.client_secret}}",
"scopes": ["read:contacts", "write:contacts"],
"token_refresh_behavior": "automatic"
}
}How the engine executes this: Instead of writing custom logic to track token expiration, the platform schedules work to handle token refresh automatically ahead of expiry. When a user initiates the connection, the engine dynamically constructs the authorization URL, handles the redirect callback, extracts the authorization code, and executes the token exchange.
If the provider returns an unexpected invalid_grant error during a refresh attempt, the engine flags the connection state, allowing you to trigger a re-authentication flow without writing custom error-handling logic per provider.
For more on the complexities of token management, see our guide on handling OAuth token refresh failures.
Step 2: Mapping the Unified API with JSONata
Once authenticated, the next challenge is translating the native API payload into a unified schema that your application understands.
Writing custom parsing logic for every API leads to massive switch statements and brittle code. Instead, declarative architectures use transformation languages like JSONata to translate payloads dynamically.
JSONata is a lightweight query and transformation language for JSON data. It allows you to extract values, apply conditional logic, and reshape objects without writing imperative code.
Consider a scenario where you want to map a native HubSpot contact into a unified Contact model.
{
"resource": "contacts",
"method": "GET",
"endpoint": "/crm/v3/objects/contacts",
"response_mapping": {
"id": "id",
"first_name": "properties.firstname",
"last_name": "properties.lastname",
"email": "properties.email",
"phone": "properties.phone ? properties.phone : null",
"created_at": "$toMillis(createdAt)"
}
}The Config Override Hierarchy Enterprise SaaS integrations rarely fit perfectly into a rigid, one-size-fits-all schema. Your enterprise customers will inevitably use custom fields in their CRM instances.
To handle this without code changes, Truto utilizes a 3-level JSONata architecture known as the Config Override Hierarchy:
- Platform Level: The baseline unified model provided by the platform.
- Integration Level: Overrides applied to a specific provider (e.g., adjusting the mapping for all Salesforce connections).
- Customer Level: Overrides applied to a specific tenant's linked account, allowing you to map a customer's bespoke custom field into your unified model without touching the global configuration.
This hierarchy allows you to support complex, enterprise-specific data requirements strictly through configuration. For a deeper look at this transformation layer, review our API schema normalization tutorial.
Step 3: Normalizing Pagination and Rate Limits
Third-party APIs handle pagination in wildly different ways. Some use cursor-based pagination, others use offset/limit, and some rely on URL link headers. Writing custom while-loops to handle these variations across fifty different APIs is a massive waste of engineering capacity.
In a declarative setup, you define the pagination strategy in the configuration file. The generic execution proxy intercepts the request, reads the strategy, and automatically builds the correct paginated requests.
{
"pagination": {
"type": "cursor",
"cursor_path": "paging.next.after",
"request_param": "after",
"limit_param": "limit",
"default_limit": 100
}
}When your application requests the next page of the unified resource, the execution engine injects the stored cursor into the native API request automatically.
Handling Rate Limits (HTTP 429) A critical architectural note regarding rate limits: high-performance unified APIs do not attempt to silently retry or absorb rate limit errors on behalf of the caller. Attempting to buffer requests globally across thousands of tenants leads to unpredictable latency and distributed queuing nightmares.
When an upstream API returns an HTTP 429 Too Many Requests error, Truto passes that error directly to the caller. However, it normalizes the upstream rate limit information into standardized headers per the IETF specification:
ratelimit-limit: The maximum number of requests allowed in the current window.ratelimit-remaining: The number of requests remaining in the current window.ratelimit-reset: The time at which the rate limit window resets.
The calling application is responsible for reading these standardized headers and implementing its own retry logic with exponential backoff. This separation of concerns ensures that your core application retains full control over its scheduling and backoff behavior, while the integration layer focuses strictly on normalization.
Step 4: Handling Webhooks Without Custom Endpoints
Polling APIs for changes is inefficient and quickly exhausts rate limits. Webhooks are the preferred method for real-time data synchronization, but they present significant infrastructure challenges.
Every SaaS provider formats webhooks differently. They use different HTTP methods, different payload structures, and critically, different cryptographic signature methods for verification (HMAC SHA-256, RSA, custom headers).
If you build this in-house, you have to deploy a public-facing ingestion gateway, write custom verification middleware for every provider, and maintain a durable queue to handle traffic spikes.
With a declarative unified API, you bypass this entirely. The platform provides a centralized ingestion gateway that receives webhooks from all third-party providers.
graph TD
A[Third-Party Provider] -->|HTTP POST| B(Ingestion Gateway)
B --> C{Verify Signature}
C -->|Valid| D[JSONata Transformation]
C -->|Invalid| E[Drop Request]
D --> F[Standardize Payload]
F --> G[Sign with X-Truto-Signature]
G --> H[Deliver to Customer Endpoint]The declarative configuration defines how to verify the incoming signature and how to map the native event payload into a unified event structure.
The platform then delivers this normalized event to a single webhook endpoint hosted by your application. Every outbound webhook delivered by Truto is signed with a standardized X-Truto-Signature header.
Your engineering team only needs to write one piece of verification logic to validate the Truto signature, rather than maintaining fifty different HMAC verification scripts for fifty different providers.
Security Best Practice: Always verify webhook signatures before processing payloads to prevent replay attacks and spoofing. Relying on a unified API platform standardizes this process, reducing the surface area for cryptographic implementation errors.
Exposing Complex APIs via the Proxy Layer
One of the most powerful features of a mature declarative architecture is the ability to simplify complex vendor APIs.
For example, modern project management tools like Linear rely heavily on GraphQL. Integrating with GraphQL APIs requires constructing complex query strings and understanding deep graph relationships.
A declarative proxy layer can expose these GraphQL-backed integrations as standard RESTful CRUD resources. By defining the GraphQL query in the configuration and using placeholder-driven request building, the proxy translates a standard GET /unified/issues request into the complex GraphQL query required by the provider, extracts the relevant nodes using JSONata, and returns a flat, RESTful JSON response to the caller.
This allows your engineering team to interact with complex graph architectures using standard HTTP methods.
How Maintenance Is Reduced When APIs Change
The real cost of integrations is not the initial build - it is the years of maintenance that follow. API providers deprecate endpoints, change response schemas, and overhaul versioning systems on their own timelines. You either keep up or your customers experience broken integrations.
Here is a real-world example. HubSpot sunset their Contact Lists v1 API on April 30, 2026, requiring all consumers to migrate to the v3 Lists API. The v3 API uses different endpoint paths, different ID formats (listId instead of legacyListId), and different response structures. HubSpot also introduced an entirely new date-based versioning system in March 2026, replacing the older v1/v2/v3/v4 naming with a /YYYY-MM/ format.
This is not an isolated event. API lifecycle changes like these are a constant operational reality for SaaS products that integrate with third-party platforms. Every deprecation triggers scoping work, and every sunset deadline creates a hard cutoff your engineering team did not choose.
What this looks like with code-first integrations:
Every adapter file that touched the v1 Contact Lists API needs a rewrite. Endpoint paths change. Response parsing logic changes. ID references stored in your database may need migration. You scope the work, write the code, update the tests, review, and deploy - under a deadline set by HubSpot, not by your sprint planning. While you are migrating HubSpot Lists to v3, another provider might deprecate their own endpoints. The maintenance burden scales linearly with your integration count.
What this looks like with a declarative architecture:
The migration is a config update. You change the path field from the v1 endpoint to the v3 endpoint, adjust the response_path if the response structure changed, and update the JSONata response mapping to handle the new field names. No adapter code is rewritten. No application code is redeployed. The generic execution engine does not know or care that HubSpot changed their API - it reads the updated config and executes it.
Because every integration flows through the same engine, improvements compound in your favor instead of working against you. When the pagination handler is improved, all integrations benefit. When error handling is tightened, it applies everywhere. The maintenance burden scales with the number of unique API patterns, not the number of integrations.
When You Still Need Custom Code (and How to Isolate It)
A declarative architecture handles the vast majority of standard API interactions: REST endpoints, OAuth and API key auth, JSON and XML responses, cursor/page/offset pagination, and field-level data mapping. That covers 90%+ of SaaS integrations.
But some scenarios push past what pure configuration can express:
-
Multi-step orchestration - Some operations require calling multiple API endpoints in sequence, where the output of one call feeds into the next. For example, creating a CRM contact and then associating it with a company requires two API calls with data flowing between them. Declarative architectures handle this through before/after step pipelines - ordered sequences of calls with conditional JSONata logic - rather than custom code.
-
Non-standard protocols - A small number of enterprise APIs use SOAP, proprietary binary formats, or custom RPC protocols that fall outside a REST/GraphQL config schema. The practical approach is to wrap these behind a proxy endpoint that normalizes them into REST, then treat the proxy as the "API" for your declarative config.
-
Business logic between API calls - If your application needs to run custom validation, deduplication, or enrichment logic between the inbound API response and your database write, that logic belongs in your application layer - not in the integration layer. The integration platform delivers clean, normalized data. What you do with it afterward is your responsibility.
-
Truly unique provider behaviors - Some providers return different response formats depending on the account's subscription tier, or require fetching supplementary data before a webhook payload is usable. JSONata expressions are Turing-complete and can handle most conditional logic, but occasionally you may need a raw proxy call to work around a one-off edge case.
The key principle is isolation. Your integration layer should be declarative. Your business logic layer should be code. The two connect through a clean API boundary - your application calls the unified API, receives normalized data, and applies its own rules. When a third-party API changes, only the declarative config changes. Your application code stays untouched.
Shipping Integrations as Data, Not Code Deploys
Transitioning from imperative scripts to declarative configurations fundamentally changes how a software company operates.
When integrations are defined as JSON configurations and JSONata mappings, they cease to be engineering projects. They become data-only operations. This means Product Managers, Solutions Engineers, and Technical Account Managers can build, modify, and launch new custom API connectors without waiting for an engineering sprint or going through a full CI/CD code deployment cycle.
If a vendor updates their API schema, you update a JSONata string in the platform dashboard. If an enterprise customer needs a custom field mapped, a Solutions Engineer updates the customer-level override configuration. The core application code remains untouched.
Building custom API connectors without code is not about using visual drag-and-drop toys. It is about adopting a rigorous, generic execution architecture that separates the volatile mechanics of third-party APIs from the stable business logic of your application.
FAQ
- How much does it cost to build a custom API integration?
- Building a single custom API integration from scratch can easily cost $10,000 or more in initial engineering time, not including the ongoing financial drain of maintenance, monitoring, and infrastructure.
- What is a declarative API integration?
- A declarative API integration uses a configuration file (like JSON) to define the authentication, endpoints, and data mappings. A generic execution engine reads this file at runtime, eliminating the need to write custom imperative code for each provider.
- How do you handle API rate limits without custom code?
- A unified API platform passes HTTP 429 Too Many Requests errors directly to the caller, normalizing the upstream rate limit data into standardized headers. The calling application then applies its own exponential backoff logic.
- Can I build custom API connectors without an engineering team?
- Yes. By using a declarative architecture, Product Managers and Solutions Engineers can define API mappings and authentication flows via configuration files, allowing them to ship integrations without waiting for an engineering sprint.