Migrate from Rutter to Unified.to

Rutter covers accounting, commerce, payments, ads and banking. Unified.to covers the same accounting, commerce, payment and ads data, and uses the same connection and API model for HRIS, ATS, CRM and more than 20 other categories.

The biggest difference is architecture. Rutter pulls a connection's data when it's created, then syncs it on a schedule (hourly by default) and serves your reads from that synced data. Unified.to is real-time pass-through: every request goes to the source platform and returns current data, and no end-customer data is stored on our side. Writes are synchronous too, so there are no async jobs to poll.

This guide maps each Rutter concept to its Unified.to equivalent. For the overall plan and checklist, see Migrate to Unified.to.

Concepts

RutterUnified.to
Connection, selected with ?access_token=Connection, identified by a connection ID in the URL path
Platform (e.g. SHOPIFY, QUICKBOOKS)Integration type (e.g. shopify, quickbooks)
Accounting, Commerce, Payments, Adsaccounting, commerce, payment, ads categories
Rutter LinkAuthorization component or auth URL
Initial sync, hourly sync, force_fetch, manual syncsNot needed: reads are live
Async writes (response_mode, /jobs/:id)Synchronous writes: the response is the source's result
expand=platform_datafields=raw (Custom & original fields)
Field-level passthrough on writesraw on writes
GET /connections/credentials to call the platform yourselfPassthrough API: /passthrough/{connection_id}/{path}
Webhooks (INITIAL_UPDATE, ORDER_CREATED, …)Native and virtual webhooks

What changes without a sync

Code built on Rutter often assumes its sync behavior. Plan for these differences:

  • No initial sync. A Rutter connection isn't ready until INITIAL_UPDATE fires, unless you read with force_fetch=true and accept incomplete data. A Unified.to connection can be read as soon as the user authorizes it.
  • Current data on every read. Data is never up to an hour old, and there's no incremental_sync or backfill call to make.
  • Writes are synchronous. Rutter writes can return 202 with an async_response that you poll at /versioned/jobs/:id. A Unified.to write goes to the platform in the same request and returns the created or updated record, or the platform's error.
  • Reads call the platform. Accounting and commerce platforms have their own rate limits. If you read the same data often, keep a copy in your own database and keep it current with updated_gte or webhooks.

Authentication

Rutter authenticates your organization with Basic auth and selects the connection with an access_token query parameter and a version header. Unified.to uses one Bearer API key, and the connection is part of the URL.

# Rutter
GET https://production.rutterapi.com/versioned/accounting/invoices?access_token={ACCESS_TOKEN}
Authorization: Basic {base64(client_id:client_secret)}
X-Rutter-Version: 2024-08-31

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

Unified.to has no API version header. Use https://api-eu.unified.to or https://api-au.unified.to if your workspace is in the EU or Australia region, and use the Sandbox environment in place of Rutter's sandbox host.

Authorizing end users

With Rutter Link, your frontend opens Link with your public key, receives a publicToken in onSuccess, and your backend exchanges it for an access token (POST /versioned/item/public_token/exchange).

Unified.to has no token to exchange: embed the Authorization component, or redirect the user to the auth URL. The user comes back to your success_redirect with the connection ID in the URL.

// Before: Rutter Link
import { useRutterLink } from 'react-rutter-link';

const { open } = useRutterLink({
    publicKey: RUTTER_PUBLIC_KEY,
    onSuccess: (publicToken) => exchangeOnYourServer(publicToken), // POST /versioned/item/public_token/exchange
});

// After: Unified.to Authorization component
import UnifiedDirectory from '@unified-api/react-directory';

<UnifiedDirectory
    workspaceId={WORKSPACE_ID}
    categories={['accounting', 'commerce']}
    external_xref={customerId}
    success_redirect="https://yourapp.com/integrations/callback"
/>;

Or, without a framework:

<script src="https://api.unified.to/docs/unified.js?wid=YOUR_WORKSPACE_ID&did=unified_widget&env=Production&cat=accounting&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 to your customer ID, and surl / furl set the success and failure redirects.

If you call open({ platform: 'SHOPIFY' }) to skip Rutter's platform picker, use the auth URL for that integration instead. It's also the equivalent of the link_url returned by POST /versioned/connections/create:

https://api.unified.to/unified/integration/auth/{WORKSPACE_ID}/shopify
    ?success_redirect=https://yourapp.com/integrations/callback
    &failure_redirect=https://yourapp.com/integrations/error
    &external_xref=customer_123
    &scopes=accounting_order_read,commerce_item_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 Rutter access token today. Unlike an access token, a connection ID isn't a secret on its own: it only works with your workspace API key.

Moving existing connections

You have two options, and you can mix them per platform:

  • Re-authorize each customer through the Authorization component. Most teams roll this out gradually: new customers connect through Unified.to, and existing customers are asked to reconnect the next time they visit your integrations page.
  • Import credentials with Create connection, so the customer doesn't have to reconnect. This works for credentials you hold, such as API keys your customers gave you or OAuth tokens issued to your own OAuth app. Rutter's GET /versioned/connections/credentials returns a connection's platform credentials, but not refresh tokens, so an imported OAuth connection will only keep working if you can supply a refresh token. See Move your connections.

If you registered your own OAuth apps with platforms for Rutter, enter the same client ID and secret in Unified.to and add Unified.to's redirect URI to each app.

Making API calls

# Rutter
curl "https://production.rutterapi.com/versioned/accounting/invoices?access_token=$ACCESS_TOKEN&limit=50" \
  -u "$RUTTER_CLIENT_ID:$RUTTER_SECRET" \
  -H "X-Rutter-Version: 2024-08-31"

# Unified.to
curl "https://api.unified.to/accounting/$CONNECTION_ID/invoice?limit=50" \
  -H "Authorization: Bearer $UNIFIED_API_KEY"

Unified.to also has official SDKs in seven languages:

import { UnifiedTo } from '@unified-api/typescript-sdk';

const unified = new UnifiedTo({ security: { jwt: process.env.UNIFIED_API_KEY } });
const invoices = await unified.accounting.listAccountingInvoices({ connectionId, limit: 50 });

Response shape

Rutter returns the list under a named key with a cursor and connection details, such as { "invoices": [...], "next_cursor": "…", "connection": {...} }. 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

Rutter keeps storefront orders and customers under Commerce. Unified.to returns orders and customers from storefront platforms like Shopify through the accounting category, and products through commerce.

RutterUnified.to
/accounting/accounts/accounting/{connection_id}/account
/accounting/invoices/accounting/{connection_id}/invoice
/accounting/bills/accounting/{connection_id}/bill
/accounting/accounting-customers/accounting/{connection_id}/contact?type=CUSTOMER
/accounting/vendors/accounting/{connection_id}/contact?type=SUPPLIER
/accounting/journal-entries/accounting/{connection_id}/journal
/accounting/expenses/accounting/{connection_id}/expense
/accounting/purchase-orders/accounting/{connection_id}/purchaseorder
/accounting/sales-orders/accounting/{connection_id}/salesorder
/accounting/balance-sheets/accounting/{connection_id}/balancesheet
/accounting/income-statements/accounting/{connection_id}/profitloss
/accounting/bank-feeds/accounting/{connection_id}/bankfeedaccount, bankfeedtransaction
/orders/accounting/{connection_id}/order
/customers/accounting/{connection_id}/contact?type=CUSTOMER
/products/commerce/{connection_id}/item
Payouts/payment/{connection_id}/payout
Subscriptions/payment/{connection_id}/subscription
Payment transactions/payment/{connection_id}/payment, /payment/{connection_id}/refund

Field names differ between the two models, so update your mappers object by object using the API Reference, and check each platform's Feature Support tab in the dashboard for the objects you rely on.

Pagination and filtering

RutterUnified.to
limit (default 50)limit (default 100)
cursor, with next_cursoroffset; you're done when a page returns fewer than limit
filter=updated_at >= "…", updated_at_min (Unix ms)updated_gte (ISO 8601 date)
sort (default updated_at DESC)sort, order
force_fetch=trueNot needed
let offset = 0;
const limit = 100;
while (true) {
    const page = await unified.accounting.listAccountingInvoices({
        connectionId,
        limit,
        offset,
        updatedGte: lastRun,
    });
    await save(page);
    if (page.length < limit) break;
    offset += limit;
}

See Pagination, filtering & sorting.

Platform data and passthrough

Replace expand=platform_data with raw in the fields parameter. The platform's original record comes back in each object's raw property:

GET /accounting/{connection_id}/invoice?fields=id,total_amount,status,raw

Where you send a root-level passthrough object on a Rutter write to set platform-specific fields, send those fields in raw on the Unified.to create or update request. See Custom & original fields.

Rutter has no general proxy endpoint, so to reach platform endpoints it doesn't cover you fetch the credentials and call the platform yourself. Unified.to's Passthrough API does that for you: send the platform path in the URL, with any HTTP method, and Unified.to adds the base URL and authentication.

curl "https://api.unified.to/passthrough/$CONNECTION_ID/admin/api/2024-07/shop.json" \
  -H "Authorization: Bearer $UNIFIED_API_KEY"

See Passthrough.

Writes

Rutter writes wrap the record in a named key and may complete asynchronously:

# Rutter: may return 202 with async_response; poll /versioned/jobs/:id
curl -X POST "https://production.rutterapi.com/versioned/accounting/invoices?access_token=$ACCESS_TOKEN" \
  -u "$RUTTER_CLIENT_ID:$RUTTER_SECRET" \
  -H "X-Rutter-Version: 2024-08-31" \
  -H "Content-Type: application/json" \
  -d '{ "invoice": { … }, "response_mode": "prefer_sync" }'

# Unified.to: returns the created invoice
curl -X POST "https://api.unified.to/accounting/$CONNECTION_ID/invoice" \
  -H "Authorization: Bearer $UNIFIED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "contact_id": "…", "currency": "USD", "lineitems": [ … ] }'

Remove your job polling. If you rely on the job_id in Rutter's entity webhooks to match events to your own writes, use the id in the Unified.to write response instead.

Webhooks

Rutter webhooks are configured for your organization and fire as it syncs. Unified.to webhooks are created per connection and object, and each delivery carries the changed records.

Rutter eventUnified.to
ORDER_CREATED / ORDER_UPDATEDobject_type: accounting_order, event: created / updated
INVOICE_CREATED / INVOICE_UPDATEDobject_type: accounting_invoice, event: created / updated
*_DELETEDevent: deleted, where the integration supports it
INITIAL_UPDATE, HISTORICAL_UPDATENot needed; the first delivery is marked INITIAL-PARTIAL / INITIAL-COMPLETE
INCREMENTAL_SYNC_COMPLETEDNot needed; each change is delivered as it's detected
CONNECTION_AUTHENTICATEDYour success_redirect receives the new connection ID

Create one per connection and object, 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": "accounting_invoice",
    "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 waiting for Rutter's initial sync.

Signature verification. Replace your X-Rutter-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 /versioned/connections/:id with DELETE /unified/connection/{connection_id}. After you cut over, delete each customer's Rutter connection so Rutter stops syncing their data.

Next steps

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