Migrate from Apideck to Unified.to
Apideck and Unified.to are both real-time unified APIs: requests go to the source API rather than a synced copy, so your data freshness model doesn't change. The main differences are how a connection is addressed, how end users authorize, and how pagination and webhooks work.
This guide maps each Apideck concept to its Unified.to equivalent. For the overall plan and checklist, see Migrate to Unified.to.
Concepts
| Apideck | Unified.to |
|---|---|
Application (x-apideck-app-id) | Workspace |
Consumer (x-apideck-consumer-id) | Your own customer ID, stored on each connection as external_xref |
| Connection (consumer + unified API + service ID) | Connection, with its own connection ID |
Service ID (e.g. hubspot) | Integration type (e.g. hubspot) |
Unified API (crm, hris, ats, accounting, …) | Category (crm, hris, ats, accounting, …) |
| Vault | Authorization component or auth URL |
raw=true / _raw | fields=raw / raw (Custom & original fields) |
pass_through / Proxy API | Passthrough API |
| Webhooks and virtual webhooks | Native and virtual webhooks |
The biggest shift: Apideck identifies an account by a consumer ID plus a service ID, while Unified.to gives each authorized account its own connection ID. A customer who connects both HubSpot and Salesforce has two connection IDs. Store them next to your customer record, where you use the consumer ID today.
Authentication
Apideck sends four headers on each request. Unified.to needs only your API key; the connection is part of the URL.
# Apideck
GET https://unify.apideck.com/crm/contacts
Authorization: Bearer {APIDECK_API_KEY}
x-apideck-app-id: {APP_ID}
x-apideck-consumer-id: {CONSUMER_ID}
x-apideck-service-id: hubspot
# Unified.to
GET https://api.unified.to/crm/{connection_id}/contact
Authorization: Bearer {UNIFIED_API_KEY}
Use https://api-eu.unified.to or https://api-au.unified.to if your workspace is in the EU or Australia region.
Authorizing end users
With Apideck, your backend creates a Vault session (POST /vault/sessions) and your frontend opens it with @apideck/vault-js. With Unified.to, there's no session to create: embed the Authorization component, or redirect the user to the auth URL, and the user returns to your success_redirect with the new connection ID.
<!-- Before: Apideck Vault -->
<script type="module">
import { ApideckVault } from '@apideck/vault-js';
// session_token comes from POST /vault/sessions on your server
ApideckVault.open({ token: sessionToken });
</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=crm&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, and surl / furl set the success and failure redirects.
To build your own UI instead, send users to the auth URL for a specific integration:
https://api.unified.to/unified/integration/auth/{WORKSPACE_ID}/hubspot
?success_redirect=https://yourapp.com/integrations/callback
&failure_redirect=https://yourapp.com/integrations/error
&external_xref=customer_123
&scopes=crm_contact_read,crm_contact_write
&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. See Authorization for framework components and all options.
Moving existing connections
Apideck Vault holds your customers' credentials, so you have two options:
- Re-authorize each customer through the Authorization component. You can roll this out gradually: route new connections to Unified.to and prompt existing customers to reconnect.
- Import credentials you hold, such as tokens issued to your own OAuth app or API keys your customers entered, with Create connection. See Move your connections.
If you configured your own OAuth client ID and secret in Apideck for an integration, enter the same credentials in Unified.to and add Unified.to's redirect URI to that OAuth app.
Making API calls
Apideck resources are plural (/crm/contacts), while Unified.to objects are singular and scoped to a connection (/crm/{connection_id}/contact).
# Apideck
curl https://unify.apideck.com/hris/employees?limit=50 \
-H "Authorization: Bearer $APIDECK_API_KEY" \
-H "x-apideck-app-id: $APP_ID" \
-H "x-apideck-consumer-id: $CONSUMER_ID" \
-H "x-apideck-service-id: bamboohr"
# Unified.to
curl "https://api.unified.to/hris/$CONNECTION_ID/employee?limit=50" \
-H "Authorization: Bearer $UNIFIED_API_KEY"
With the SDKs:
// Apideck
import { Apideck } from '@apideck/unify';
const apideck = new Apideck({
apiKey: process.env.APIDECK_API_KEY,
appId: APP_ID,
consumerId: CONSUMER_ID,
});
const contacts = await apideck.crm.contacts.list({ serviceId: 'hubspot', limit: 50 });
// Unified.to
import { UnifiedTo } from '@unified-api/typescript-sdk';
const unified = new UnifiedTo({ security: { jwt: process.env.UNIFIED_API_KEY } });
const contacts = await unified.crm.listCrmContacts({ connectionId, limit: 50 });
Response shape
Apideck wraps results in an envelope (status_code, status, service, resource, operation, data, meta, links). Unified.to returns the data itself: a JSON array for list calls and an object for single-record calls. Errors use standard HTTP status codes.
Common objects
| Apideck | Unified.to |
|---|---|
/crm/contacts | /crm/{connection_id}/contact |
/crm/companies | /crm/{connection_id}/company |
/crm/opportunities | /crm/{connection_id}/deal |
/crm/leads | /crm/{connection_id}/lead |
/hris/employees | /hris/{connection_id}/employee |
/hris/companies | /hris/{connection_id}/company |
/ats/applicants | /ats/{connection_id}/candidate |
/ats/jobs | /ats/{connection_id}/job |
/ats/applications | /ats/{connection_id}/application |
/accounting/invoices | /accounting/{connection_id}/invoice |
/accounting/customers | /accounting/{connection_id}/contact |
Field names differ between the two models, so update your mappers object by object using the API Reference.
Pagination and filtering
| Apideck | Unified.to |
|---|---|
limit (max 200) | limit (default 100) |
cursor, with meta.cursors.next | offset; you're done when a page returns fewer than limit |
filter[updated_since] | updated_gte |
filter[email], filter[name] | query |
sort[by], sort[direction] | sort, order |
fields | fields |
// Read every employee updated since the last run
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.
Vendor-specific data
Replace raw=true with raw in the fields parameter. The original record comes back in each object's raw property:
GET /crm/{connection_id}/contact?fields=id,name,emails,raw
For custom fields that must be requested explicitly, add them with a raw. prefix, such as fields=raw,raw.leadFunnelStage. See Custom & original fields.
Passthrough and proxy
Apideck's Proxy API takes the downstream URL in x-apideck-downstream-url. Unified.to's Passthrough API takes the vendor path in the URL and uses the HTTP method you call it with:
# Apideck
curl -X GET https://unify.apideck.com/proxy \
-H "Authorization: Bearer $APIDECK_API_KEY" \
-H "x-apideck-app-id: $APP_ID" \
-H "x-apideck-consumer-id: $CONSUMER_ID" \
-H "x-apideck-service-id: hubspot" \
-H "x-apideck-downstream-url: https://api.hubapi.com/crm/v3/objects/tickets"
# Unified.to
curl "https://api.unified.to/passthrough/$CONNECTION_ID/crm/v3/objects/tickets" \
-H "Authorization: Bearer $UNIFIED_API_KEY"
Unified.to adds the vendor's base URL and authentication. If you use Apideck's pass_through body parameter to set extra vendor fields on a write, send those fields in raw on the Unified.to request instead. See Passthrough.
Webhooks
Apideck subscriptions are per application and list event types such as crm.contact.updated; the payload identifies the changed entity. Unified.to webhooks are per connection and object, and the payload carries the changed records themselves, so you don't need a follow-up read.
| Apideck event | Unified.to webhook |
|---|---|
crm.contact.created | object_type: crm_contact, event: created |
crm.contact.updated | object_type: crm_contact, event: updated |
crm.contact.deleted | object_type: crm_contact, event: deleted |
hris.employee.updated | object_type: hris_employee, event: updated |
ats.applicant.created | object_type: ats_candidate, event: created |
Create one per connection, typically right after the connection is authorized:
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": "crm_contact",
"event": "updated",
"hook_url": "https://yourapp.com/webhooks/unified",
"interval": 60
}'
Like Apideck, Unified.to uses native webhooks where the vendor has them and virtual webhooks where it doesn't; interval sets how often, in minutes, virtual webhooks check for changes.
Signature verification. Replace your x-apideck-signature check with a check of the sig256 field in the payload body: HMAC-SHA256(workspace secret, JSON of data + nonce), base64-encoded. Code samples are in Webhooks.
Deleting connections
Replace DELETE /vault/connections/{unified_api}/{service_id} with DELETE /unified/connection/{connection_id}.
Next steps
- Follow the migration plan and checklist
- Browse Supported integrations to confirm coverage
- Questions? Email hello@unified.to or join our Discord