Migrate from Kombo to Unified.to

Kombo covers HRIS, ATS, assessment and LMS. Unified.to covers the same categories, plus more than 25 others such as CRM, accounting and ticketing, with the same connection and API model, so adding a new category later doesn't mean adding another vendor.

The architectural difference matters most. Kombo syncs data from each source system into its own database on a schedule and serves your reads from that copy. Unified.to is real-time pass-through: every request goes to the source API, and no end-customer data is stored on our side. This guide maps each Kombo concept to its Unified.to equivalent. For the overall plan and checklist, see Migrate to Unified.to.

Concepts

KomboUnified.to
Integration (X-Integration-Id)Connection, identified by a connection ID in the URL path
Tool (e.g. bamboohr, greenhouse)Integration type (e.g. bamboohr, greenhouse)
Category (hris, ats, assessment, lms)Category (hris, ats, assessment, lms, and more)
end_user_origin_idexternal_xref
Kombo ConnectAuthorization component or auth URL
Scheduled syncs and force-syncNot needed: reads are live
data-changed webhookNative and virtual webhooks per object
remote_data, custom_fields, integration_fieldsraw (Custom & original fields)
Passthrough (/passthrough/{tool}/{api})Passthrough API (/passthrough/{connection_id}/{path})

What changes without a sync

Because Kombo reads come from its database, your code may rely on sync behavior. Plan for these differences:

  • No setup wait. A Kombo integration isn't fully usable until its first sync finishes. A Unified.to connection can be read as soon as the user authorizes it.
  • No force-sync. Replace calls to POST /v1/force-sync with a direct read, or with a webhook's initial data load.
  • Writes are immediate. A Unified.to write goes to the source API in the same request, and the response reflects the source's result.
  • Rate limits are the source's. Each read calls the source API. Keep a local copy in your own database if you read the same data often, and keep it current with updated_gte or webhooks.
  • Deletions. Kombo marks deleted records with remote_deleted_at after a full sync. With Unified.to, subscribe to deleted webhooks where the integration supports them, or compare IDs in periodic full reads.

Authentication

# Kombo
GET https://api.kombo.dev/v1/hris/employees
Authorization: Bearer {KOMBO_API_KEY}
X-Integration-Id: {INTEGRATION_ID}

# Unified.to
GET https://api.unified.to/hris/{connection_id}/employee
Authorization: Bearer {UNIFIED_API_KEY}

Kombo's default host is in the EU. If you need your data processed in the EU, create your Unified.to workspace in the EU region and use https://api-eu.unified.to. See API URLs.

Authorizing end users

Kombo Connect takes three steps: your backend creates a link (POST /v1/connect/create-link), your frontend opens it with showKomboConnect(link), and your backend exchanges the returned token for an integration ID (GET /v1/connect/integration-by-token/{token}).

Unified.to takes one: embed the Authorization component, or redirect the user to the auth URL. The user comes back to your success_redirect with the connection ID already in the URL.

<!-- Before: Kombo Connect -->
<script type="module">
    import { showKomboConnect } from '@kombo-api/connect';
    const link = await createLinkOnYourServer(); // POST /v1/connect/create-link
    const token = await showKomboConnect(link);
    await activateOnYourServer(token); // GET /v1/connect/integration-by-token/{token}
</script>

<!-- After: Unified.to Authorization component -->
<script src="https://api.unified.to/docs/unified.js?wid=YOUR_WORKSPACE_ID&did=unified_widget&env=Production&cat=hris&uid=customer_123&surl=https%3A%2F%2Fyourapp.com%2Fintegrations%2Fcallback"></script>
<div id="unified_widget"></div>

In the embed script, cat limits the list to one category, uid sets external_xref (the equivalent of end_user_origin_id), and surl / furl set the success and failure redirects.

To use your own integration picker, which is the equivalent of passing integration_tool to Kombo, send users to the auth URL for that integration:

https://api.unified.to/unified/integration/auth/{WORKSPACE_ID}/bamboohr
    ?success_redirect=https://yourapp.com/integrations/callback
    &failure_redirect=https://yourapp.com/integrations/error
    &external_xref=customer_123
    &scopes=hris_employee_read,hris_group_read
    &redirect=1

On success, the callback URL receives id (the connection ID), state, nonce, type and sig. Verify the signature, then store the connection ID where you store the Kombo integration ID today.

Moving existing connections

Kombo stores your customers' credentials, so most teams re-authorize: send existing customers through the Authorization component, either all at once or the next time they visit your integrations page. Match returning users to their account with external_xref.

For integrations where you hold the credentials yourself, such as API keys your customers gave you or tokens issued to your own OAuth app, import them with Create connection so the customer doesn't have to reconnect. See Move your connections.

Making API calls

# Kombo
curl "https://api.kombo.dev/v1/ats/candidates?page_size=100" \
  -H "Authorization: Bearer $KOMBO_API_KEY" \
  -H "X-Integration-Id: $INTEGRATION_ID"

# Unified.to
curl "https://api.unified.to/ats/$CONNECTION_ID/candidate?limit=100" \
  -H "Authorization: Bearer $UNIFIED_API_KEY"
import { UnifiedTo } from '@unified-api/typescript-sdk';

const unified = new UnifiedTo({ security: { jwt: process.env.UNIFIED_API_KEY } });
const candidates = await unified.ats.listAtsCandidates({ connectionId, limit: 100 });

Response shape

Kombo returns { "status": "success", "data": { "results": [...], "next": "…" } }. Unified.to returns the list itself as a JSON array, and a single object for single-record calls. Errors use standard HTTP status codes.

Common objects

KomboUnified.to
/hris/employees/hris/{connection_id}/employee
/hris/groups/hris/{connection_id}/group
/hris/absences/hris/{connection_id}/timeoff
/hris/legal-entities/hris/{connection_id}/company
/hris/locations/hris/{connection_id}/location
/hris/payslips/hris/{connection_id}/payslip
/ats/candidates/ats/{connection_id}/candidate
/ats/applications/ats/{connection_id}/application
/ats/jobs/ats/{connection_id}/job
/ats/interviews/ats/{connection_id}/interview
/ats/scorecards/ats/{connection_id}/scorecard

Field names differ between the two models, so update your mappers object by object using the API Reference.

Pagination and filtering

KomboUnified.to
page_size (max 250)limit (default 100)
cursor, with data.nextoffset; you're done when a page returns fewer than limit
updated_afterupdated_gte
idsGet the record by ID: /hris/{connection_id}/employee/{id}
include_deleteddeleted webhooks where supported
let offset = 0;
const limit = 100;
while (true) {
    const page = await unified.hris.listHrisEmployees({
        connectionId,
        limit,
        offset,
        updatedGte: lastRun,
    });
    await save(page);
    if (page.length < limit) break;
    offset += limit;
}

See Pagination, filtering & sorting.

Custom fields and raw data

Kombo exposes vendor data through custom_fields, integration_fields and, when enabled for your account, remote_data. Unified.to returns the vendor's original record in raw when you ask for it, with no account setting needed:

GET /hris/{connection_id}/employee?fields=id,name,emails,raw

To write a vendor-specific field, include it in raw on the create or update request. See Custom & original fields.

Passthrough

Kombo's passthrough endpoint names the tool and API in the URL and takes the method and path in the body. Unified.to's takes the vendor path in the URL and uses the HTTP method you call it with:

# Kombo
curl -X POST https://api.kombo.dev/v1/passthrough/bamboohr/v1 \
  -H "Authorization: Bearer $KOMBO_API_KEY" \
  -H "X-Integration-Id: $INTEGRATION_ID" \
  -H "Content-Type: application/json" \
  -d '{ "method": "GET", "path": "/meta/fields" }'

# Unified.to
curl "https://api.unified.to/passthrough/$CONNECTION_ID/meta/fields" \
  -H "Authorization: Bearer $UNIFIED_API_KEY"

Unified.to adds the vendor's base URL and authentication. See Passthrough.

Webhooks

Kombo's data-changed webhook tells you that an integration's data changed after a sync, and you then read the changes from Kombo. Unified.to webhooks are created per connection and object, and each delivery carries the changed records, so there's no follow-up read.

KomboUnified.to
data-changed (employees)Webhook with object_type: hris_employee, event: updated / created
data-changed (candidates)Webhook with object_type: ats_candidate, event: updated / created
sync-finishedNot needed; first delivery is marked INITIAL-PARTIAL / INITIAL-COMPLETE
integration-createdYour success_redirect receives the new connection ID
curl -X POST https://api.unified.to/unified/webhook \
  -H "Authorization: Bearer $UNIFIED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "connection_id": "'$CONNECTION_ID'",
    "object_type": "hris_employee",
    "event": "updated",
    "hook_url": "https://yourapp.com/webhooks/unified",
    "interval": 60
  }'

Integrations with native webhooks deliver changes as they happen. For the rest, virtual webhooks check for changes every interval minutes. A webhook can also backfill existing records, which replaces Kombo's initial sync.

Signature verification. Replace your X-Kombo-Signature check with a check of the sig256 field in the payload body: HMAC-SHA256(workspace secret, JSON of data + nonce), base64-encoded. See Webhooks.

Deleting connections

Replace DELETE /v1/integrations/{integration_id} with DELETE /unified/connection/{connection_id}. Because Unified.to holds no end-customer data, there's no retained copy to wait on after deletion.

Next steps

Are we missing anything? Let us know
Was this page helpful?