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
| Rutter | Unified.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, Ads | accounting, commerce, payment, ads categories |
| Rutter Link | Authorization component or auth URL |
Initial sync, hourly sync, force_fetch, manual syncs | Not needed: reads are live |
Async writes (response_mode, /jobs/:id) | Synchronous writes: the response is the source's result |
expand=platform_data | fields=raw (Custom & original fields) |
Field-level passthrough on writes | raw on writes |
GET /connections/credentials to call the platform yourself | Passthrough 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_UPDATEfires, unless you read withforce_fetch=trueand 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_syncorbackfillcall to make. - Writes are synchronous. Rutter writes can return
202with anasync_responsethat 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_gteor 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/credentialsreturns 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.
| Rutter | Unified.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
| Rutter | Unified.to |
|---|---|
limit (default 50) | limit (default 100) |
cursor, with next_cursor | offset; 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=true | Not 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 event | Unified.to |
|---|---|
ORDER_CREATED / ORDER_UPDATED | object_type: accounting_order, event: created / updated |
INVOICE_CREATED / INVOICE_UPDATED | object_type: accounting_invoice, event: created / updated |
*_DELETED | event: deleted, where the integration supports it |
INITIAL_UPDATE, HISTORICAL_UPDATE | Not needed; the first delivery is marked INITIAL-PARTIAL / INITIAL-COMPLETE |
INCREMENTAL_SYNC_COMPLETED | Not needed; each change is delivered as it's detected |
CONNECTION_AUTHENTICATED | Your 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
- Follow the migration plan and checklist
- Browse Supported accounting integrations and commerce integrations to confirm coverage
- Questions? Email hello@unified.to or join our Discord