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

ApideckUnified.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, …)
VaultAuthorization component or auth URL
raw=true / _rawfields=raw / raw (Custom & original fields)
pass_through / Proxy APIPassthrough API
Webhooks and virtual webhooksNative 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

ApideckUnified.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

ApideckUnified.to
limit (max 200)limit (default 100)
cursor, with meta.cursors.nextoffset; 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
fieldsfields
// 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 eventUnified.to webhook
crm.contact.createdobject_type: crm_contact, event: created
crm.contact.updatedobject_type: crm_contact, event: updated
crm.contact.deletedobject_type: crm_contact, event: deleted
hris.employee.updatedobject_type: hris_employee, event: updated
ats.applicant.createdobject_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

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