Skip to main content
API unification means building a single, consistent interface across multiple APIs that share the same domain — for example, syncing contacts from HubSpot, Salesforce, and Pipedrive using one unified Contact model. Nango lets you define your own unified models and implement the mapping logic in TypeScript functions. Unlike pre-built unified APIs, you control the schema and can handle API-specific edge cases without being boxed in.

When unification makes sense

Unification delivers the most value when you integrate with multiple APIs in the same category — CRMs, HR platforms, accounting systems, support tools — where customers choose between providers. Standardizing these into a single internal model means you only change the mapping logic, not your application.
Unification doesn’t have to be perfect to be useful. Even partial alignment across APIs reduces downstream complexity.
Not all APIs are good candidates. APIs with highly unique structures (like Notion’s block system) are difficult to unify with others. For those, use Nango’s proxy or functions without unification.

How to build a unified API

1

Define your unified model

Define a shared schema using zod in a models.ts file. This becomes the contract between your integration functions and your application.
models.ts
import { z } from 'zod';

export const ContactUnified = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email().nullable(),
  company: z.string().nullable(),
  createdAt: z.string().datetime(),
});

export type ContactUnified = z.infer<typeof ContactUnified>;
2

Implement a sync for each provider

Write a sync function per provider that fetches data and maps it to the unified model. Each sync uses the same output type.
import { createSync } from 'nango';
import { ContactUnified } from '../models.js';

export default createSync({
  output: ContactUnified,
  exec: async (nango) => {
    let page = 1;
    while (true) {
      const res = await nango.get({
        endpoint: '/crm/v3/objects/contacts',
        params: { limit: '100', after: String((page - 1) * 100) }
      });
      const contacts: ContactUnified[] = res.data.results.map((c: any) => ({
        id: c.id,
        name: `${c.properties.firstname} ${c.properties.lastname}`.trim(),
        email: c.properties.email ?? null,
        company: c.properties.company ?? null,
        createdAt: c.createdAt,
      }));
      await nango.batchSave(contacts, 'ContactUnified');
      if (!res.data.paging?.next) break;
      page++;
    }
  }
});
3

Implement unified actions (optional)

For write operations, implement the same action for each provider with identical input/output types.
import { createAction } from 'nango';
import { ContactUnified } from '../models.js';

export default createAction({
  input: ContactUnified.pick({ name: true, email: true, company: true }),
  output: ContactUnified,
  exec: async (nango, input) => {
    const [firstname, ...rest] = input.name.split(' ');
    const res = await nango.post({
      endpoint: '/crm/v3/objects/contacts',
      data: { properties: { firstname, lastname: rest.join(' '), email: input.email, company: input.company } }
    });
    return { id: res.data.id, name: input.name, email: input.email, company: input.company, createdAt: res.data.createdAt };
  }
});
4

Read unified data from your app

Once syncs are running, read records using the same model regardless of provider.
import { Nango } from '@nangohq/node';

const nango = new Nango({ secretKey: process.env.NANGO_SECRET_KEY! });

// Works the same for hubspot, salesforce, pipedrive — same model, same call
const { records } = await nango.listRecords<ContactUnified>({
  providerConfigKey: 'hubspot',   // or 'salesforce', 'pipedrive'
  connectionId: 'user-123',
  model: 'ContactUnified',
});

Best practices

If your application already has a data model for contacts, invoices, or tickets — use it as the unified model. This keeps your codebase consistent and avoids a separate translation layer in your app.
Not every API provides the same data. Design your unified model with nullable fields for data that may be absent from some providers, and handle missing values gracefully.
Use identical input/output types for both syncs (reads) and actions (writes). This eliminates duplicate mapping logic and makes your integration easier to maintain.
For fields unique to one provider, extend your unified model rather than polluting the shared schema. You can attach raw API responses in a separate field for maximum flexibility.
export const ContactUnified = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().nullable(),
  // Provider-specific extension
  _raw: z.record(z.unknown()).optional(),
});
Need help designing your unified schema? Ask in the Nango Slack community.

Build docs developers (and LLMs) love