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
| Kombo | Unified.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_id | external_xref |
| Kombo Connect | Authorization component or auth URL |
Scheduled syncs and force-sync | Not needed: reads are live |
data-changed webhook | Native and virtual webhooks per object |
remote_data, custom_fields, integration_fields | raw (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-syncwith 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_gteor webhooks. - Deletions. Kombo marks deleted records with
remote_deleted_atafter a full sync. With Unified.to, subscribe todeletedwebhooks 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
| Kombo | Unified.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
| Kombo | Unified.to |
|---|---|
page_size (max 250) | limit (default 100) |
cursor, with data.next | offset; you're done when a page returns fewer than limit |
updated_after | updated_gte |
ids | Get the record by ID: /hris/{connection_id}/employee/{id} |
include_deleted | deleted 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.
| Kombo | Unified.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-finished | Not needed; first delivery is marked INITIAL-PARTIAL / INITIAL-COMPLETE |
integration-created | Your 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
- Follow the migration plan and checklist
- Browse Supported integrations to confirm coverage
- Questions? Email hello@unified.to or join our Discord