Zero Downtime Migration Between Unified APIs: The Token Export & Import Playbook
A complete architectural playbook for migrating between unified API providers without forcing enterprise customers to re-authenticate. Learn how to export OAuth tokens, handle schema drift, and execute a zero-downtime cutover.
Zero downtime migration between unified APIs is possible only if you can export OAuth tokens from your legacy provider, re-encrypt them into the new provider's credential store, and preserve response shapes through declarative mappings so your frontend never notices. If any of those three conditions fail, you are back to emailing IT admins asking for reconnects—and that is exactly where migrations die.
When your product roadmap demands integrations beyond what your current unified API can support, you face a significant architectural bottleneck. Many engineering teams delay migrating to a more capable platform because they fear the "re-authentication cliff": the painful, high-friction process of emailing hundreds of enterprise customers and asking them to log back in and re-authorize OAuth permissions.
You can bypass this churn event entirely. By treating a unified API migration as a data portability operation rather than a code rewrite, you can hot-swap your integration infrastructure without your end users ever noticing.
This playbook covers the exact architectural sequence required to achieve this: verifying OAuth app ownership, securely extracting access and refresh tokens, replaying schema drift through declarative transforms, handling rate limits and webhooks post-cutover, and flipping traffic without dropping a single request. It is written for the engineering leaders who have already decided to switch and now need to survive the execution.
The Hidden Cost of the Re-Authentication Cliff
When you switch integration providers, the technical work is often the easy part. The hard part is the business fallout. Forcing 400 enterprise customers to click "Reconnect" actively destroys net revenue retention (NRR) and introduces massive churn risk. Every reconnect ticket is a churn signal, a forced security review, and a renewal conversation you did not want to have.
Migrations look like an engineering project on paper, but they are ultimately a retention exercise. Integration readiness is a primary driver of enterprise software adoption. When you break a working integration, you force your customer to spend political capital internally to fix it.
The base rates on data and infrastructure migrations are unforgiving. Gartner research consistently shows that roughly 83% of data migration projects fail outright or exceed their budgets and schedules due to complexity and poor planning. Bloor Group analysis of failed migrations pegs the average cost overrun at 30% and time overrun at 41%. Furthermore, a McKinsey CIO survey found that 75% of cloud migrations ran over budget, with only 15% finishing on time and within original estimates.
Integration migrations inherit all of these failure modes and add one more critical vulnerability: user-visible authentication breakage. Your customers are already dealing with massive software sprawl. According to BetterCloud, the average enterprise organization uses roughly 106 different SaaS applications, which means your customers are already fatigued by permission prompts, OAuth consent screens, and access reviews. Adding one more forced reconnect is not a neutral event. If you force them to re-authenticate, a percentage of your user base simply will not comply, resulting in silent churn.
To avoid this, engineering leaders must architect a zero-downtime migration. For a broader look at the business-side impact of a provider switch, read our guide on what happens when you switch unified API providers.
The Prerequisite: OAuth App Ownership
Zero-downtime migration is not a feature you can buy after the fact. It is a decision you made (or failed to make) the day you registered your OAuth apps.
Before you plan a token migration, you must audit your OAuth application architecture. There are exactly two states in the unified API ecosystem:
- You own the OAuth client credentials (Bring Your Own App): The
client_idandclient_secretfor Salesforce, HubSpot, Google Workspace, and every other upstream provider are registered under your company's developer accounts. End users see your company's logo during the authorization flow. You own the credentials, and you simply pass them to the unified API provider to handle the runtime execution. Access tokens and refresh tokens issued against those credentials are legally and technically yours to export. - Your legacy vendor owns the OAuth app: The unified API provider registers the OAuth application with the upstream SaaS. Tokens were issued against the vendor's
client_id. Even if the vendor exports the tokens to you, they will not authenticate against your application. Refresh calls will fail because the upstream provider expects the originalclient_idandclient_secretto match the token. You are structurally locked in.
If you are in the second state, you cannot execute a zero-downtime migration. Full stop. The only way out is the re-authentication cliff. This is the single most consequential architectural decision in the unified API space, and most teams do not realize it until they try to leave.
Truto supports Bring Your Own (BYO) OAuth apps by default, ensuring you own your credentials and can export them at any time without vendor lock-in. For a deeper dive into this architectural requirement, see our full breakdown in OAuth app ownership explained.
Before you plan a migration, audit every integration and confirm which client_id is registered with the upstream provider. If it is not yours, negotiate a BYO OAuth arrangement with your current vendor first—or accept that you are running a reconnect campaign.
Assuming you own the apps, the rest of the playbook is fully executable.
Phase 1: The Token Export and Import Playbook
To port an integration, you must securely extract the active access token, the refresh token, the upstream resource ID, and the expiration timestamp from the legacy provider.
The migration begins by moving the durable state of your integrations. This state is represented by the OAuth tokens. The token migration is a three-step data operation: export from the legacy provider, transform into the target credential schema, and import into the new provider's encrypted credential store.
Step 1: Exporting the State
Most legacy unified API providers do not offer a self-serve API endpoint for bulk token export, as it goes against their retention model. You will typically need to open a support ticket requesting an encrypted export of your token data. Providers that support BYO apps will generally comply with this request.
You should request a secure JSON payload containing the following fields for every active connection:
connection_id(The legacy provider's internal ID)upstream_tenant_id(The actual ID of the upstream SaaS workspace, e.g., Salesforceinstance_urlor HubSpothub_id)access_tokenrefresh_tokenexpires_at(Absolute timestamp, not a TTL, because clocks drift)scopes(The granted permission string as returned by the upstream provider)
Export this payload into a hardened staging environment. Treat the export file the same way you would treat a database dump containing production credentials: use envelope encryption at rest, short-lived access, full audit logging, and maintain a documented destruction timeline.
Never transmit unencrypted token payloads over email or Slack during the export process. Always use a secure, ephemeral file transfer service or an encrypted S3 bucket with strict IAM policies.
Step 2: Transform into the Target Credential Schema
Each unified API represents credentials slightly differently. You will typically need to build a small adapter script that maps legacy fields to the target provider's account creation payload. A minimal transform looks like this:
interface LegacyCredential {
connection_id: string;
provider: string; // 'salesforce', 'hubspot', ...
access_token: string;
refresh_token: string;
expires_at: string; // ISO 8601
scope: string;
metadata: Record<string, unknown>;
}
function toTargetPayload(c: LegacyCredential) {
return {
integration: c.provider,
tenant_id: c.metadata.customer_id,
credentials: {
access_token: c.access_token,
refresh_token: c.refresh_token,
expires_at: c.expires_at,
scope: c.scope.split(' '),
},
upstream_account: {
external_id: c.metadata.upstream_account_id,
instance_url: c.metadata.instance_url ?? null,
},
};
}Step 3: Import and Verify
Import tokens through the new provider's credential ingestion API. Truto, for example, accepts pre-existing access and refresh tokens for BYO OAuth apps and immediately begins managing the refresh lifecycle. Truto proactively refreshes OAuth tokens shortly before they expire, scheduling work ahead of token expiry to ensure zero disruption. That proactive refresh matters because you do not want your first post-cutover call to be a token refresh against a stale credential.
After import, you must run a dry-run verification pass against every migrated account:
- Issue a low-cost read (e.g.,
GET /users/meor the provider's equivalent identity endpoint). - Confirm the response is a 200 OK with the expected upstream account ID.
- Flag any account that returns 401, 403, or a token refresh failure for manual reconnect.
Realistically, expect 2-5% of accounts to fail verification due to stale scopes, revoked apps, offboarded users, or upstream provider policy changes. Handling that long tail before cutover is the difference between a clean migration and a support fire drill.
sequenceDiagram
participant Legacy as "Legacy Unified API"
participant Staging as "Migration Staging"
participant Target as "Truto Infrastructure"
participant Upstream as "Upstream Provider (e.g., Salesforce)"
Legacy->>Staging: Export tokens (access, refresh, scope, expiry)
Staging->>Staging: Transform to target schema
Staging->>Target: Import credentials via ingestion API
Target->>Upstream: Dry-run identity call with imported token
Upstream-->>Target: 200 OK
Target-->>Staging: Verified account list
Staging->>Staging: Flag failed accounts for reconnectIf you are migrating away from a niche provider, you can see a concrete example of this process in our guide on how to migrate from Finch to a multi-category provider. Similarly, if you are moving away from rigid standardized schemas, see our walkthrough on how to migrate from Merge.dev without re-authenticating customers.
Phase 2: Handling Schema Drift with Declarative Mappings
Standardized data models vary wildly between providers. You must use declarative schema mapping to mimic the legacy provider's response shapes, preventing frontend code rewrites.
Here is where most migrations quietly explode. Every unified API provider has an opinionated view of what a "Contact" or a "Ticket" should look like. Even if two providers both offer a "unified contact" object, the field names, nested structures, null semantics, and enum values will differ. If you cut over and your frontend suddenly receives first_name instead of firstName, or owner: {id} instead of owner_id, your UI and backend ETL pipelines will break immediately. You will spend the next quarter chasing regressions.
The wrong solution is to rewrite your application code to support the new provider's schema. This is a massive waste of engineering resources. The right solution is to keep your frontend contract stable and translate at the boundary using declarative mappings.
A declarative mapping is a JSON or JSONata document that describes how to transform an upstream response into your canonical shape. It is data, not code, which means you can version it, diff it, and swap it per integration without redeploying your API layer.
Truto's architecture uses zero integration-specific code in the execution path, relying on a generic execution pipeline and declarative mappings that can perfectly mimic legacy provider schemas.
Example 1: Flat to Nested Translation
Assume your legacy provider returned a flat first_name and last_name, but your new provider returns a nested name object. You can write a JSONata transform to bridge the gap:
{
"id": upstream_id,
"first_name": name.given_name,
"last_name": name.family_name,
"email": emails[type='work'].address[0],
"legacy_connection_id": $lookup_translation_table(upstream_id)
}Example 2: Complex Upstream Mapping (HubSpot)
If you need to mimic a legacy provider's Contact shape directly from a raw HubSpot response using JSONata, it might look like this:
{
"id": "properties.hs_object_id",
"firstName": "properties.firstname",
"lastName": "properties.lastname",
"email": "properties.email",
"phone": "properties.phone",
"owner": {
"id": "properties.hubspot_owner_id",
"email": "$lookup($, 'properties.hubspot_owner_email')"
},
"createdAt": "$fromMillis($number(properties.createdate))",
"customFields": "$sift(properties, function($v, $k) { $substring($k, 0, 2) = 'cf_' })"
}Practically, your migration mapping work requires you to:
- Snapshot the legacy provider's response for each unified resource using recorded fixtures.
- Author a declarative mapping in the new provider that produces byte-identical (or near-identical) output.
- Diff the two responses in CI against your fixtures. Fail the migration if drift exceeds a defined tolerance.
By deploying these mappings within your new unified API infrastructure, your core application remains completely unaware that the underlying integration provider has changed.
Phase 3: Managing Rate Limits and Webhooks Post-Migration
Operational logic like rate limiting and webhook signature verification will change post-migration. You must implement an ID translation layer and respect standardized IETF rate limit headers.
Moving tokens and mapping data schemas solves the read/write problem, but you must also address asynchronous events and API throttling before you can safely cut over.
Webhook Re-Routing, Dual-Writing, and ID Translation
Your application currently stores the legacy provider's connection_id in your database. When a webhook arrives from the new provider, it will carry a completely different connection_id. To solve this, you must build an ID Translation Table during the token import phase mapping the legacy ID to the new provider's ID.
For webhook-driven integrations, do not swing subscriptions in a single deploy. Instead:
- Subscribe the new provider's webhook endpoints to the upstream systems.
- Run both endpoints in parallel for 24-72 hours. Log all events on both sides with a shared correlation ID (the upstream event ID).
- Reconcile: confirm the new provider is receiving every event the legacy provider received.
- Deduplicate downstream using an idempotency key on the upstream event ID. When a webhook arrives, your middleware intercepts it, looks up the legacy ID, mutates the payload to include the legacy ID, and forwards it to your existing handlers.
- Only after reconciliation looks clean, unsubscribe the legacy webhook endpoints.
Handling API Rate Limits: Normalize, Do Not Hide
Different upstream providers expose rate limit state in fundamentally different ways: X-RateLimit-Remaining, X-Rate-Limit-Reset-Millis, Retry-After, or custom JSON error bodies. Some unified APIs attempt to absorb 429 Too Many Requests errors and silently retry them in the background.
While silent retries sound convenient, they create three massive problems:
- Amplification: A misconfigured client that retries on top of a platform that retries produces exponential upstream load.
- Loss of control: You cannot express business logic like "never retry a PATCH on an Invoice—it may double-post" if the platform is retrying underneath you.
- Debugging opacity: When a request takes 12 seconds and eventually succeeds, you have no idea whether it was one call or nine, leading to silent queue build-ups.
Truto takes a deterministic approach. A migration is a chance to standardize on the IETF RateLimit header fields spec. Truto normalizes upstream rate limit info into standardized headers (ratelimit-limit, ratelimit-remaining, ratelimit-reset). When an upstream API returns a 429 error, Truto passes that exact error directly to the caller with the headers attached.
Post-migration, you must implement per-integration retry policies in your own service layer. This architectural choice ensures that your application has precise control over task scheduling:
async function callWithBackoff(req: Request, maxAttempts = 5) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const res = await fetch(req);
if (res.status !== 429) return res;
const reset = Number(res.headers.get('ratelimit-reset') ?? '1');
const jitter = Math.random() * 250;
// Pause background worker based on exact reset timestamp
await new Promise(r => setTimeout(r, reset * 1000 + jitter));
}
throw new Error('Rate limit exceeded after retries');
}Update your HTTP clients to read the ratelimit-reset header. Use this timestamp to pause your background workers rather than relying on blind exponential backoff, which wastes CPU cycles and database connections.
Phase 4: Executing the Zero-Downtime Cutover
The final cutover requires running parallel syncs, verifying data integrity, and executing a DNS or traffic routing hot-swap without dropping requests.
You have exported the tokens, mapped the schemas, and prepared your webhook translation layer. The cutover itself is a routing exercise. You have already done the hard work; now you just need to move traffic.
- Parallel Sync Window: Do not turn off the legacy provider immediately. Run both providers in shadow mode for a bounded window (typically 7-14 days). Your service reads/writes go to the legacy provider, but every read is also issued against the new provider and diffed asynchronously. Diffs are logged, not surfaced. This produces the exact evidence you need to sign off on the cutover.
- ID Translation: Ensure your service layer looks up
legacy_connection_id -> new_connection_idon every request via the translation table. Retire the table only after the legacy provider is fully decommissioned. - The Hot Swap: The actual flip is a feature flag or config change at your API gateway, not a deploy.
flowchart LR
A[Your API Layer] --> B{Provider Flag}
B -->|legacy_pct=100| C[Legacy Unified API]
B -->|legacy_pct=0| D[New Unified API]
C --> E[Upstream SaaS]
D --> EStart at 1% traffic on the new provider. Watch error rates, p95 latency, and 401/403 rates for 30 minutes. Move to 10%, then 50%, then 100%. If any threshold trips, flip back instantly. Because both providers hold valid tokens for the same OAuth app, the rollback is symmetric—there is no penalty for hedging.
Once stability is confirmed for 48 hours, revoke the legacy provider's access to your OAuth application and shut down the legacy infrastructure.
Where to Go From Here
Migrating between unified APIs does not have to be a multi-month engineering slog that burns customer goodwill. Zero-downtime migration is not magic; it is a disciplined execution of four phases: verify OAuth app ownership, export and import tokens with dry-run verification, replay schema drift through declarative mappings, and cut over behind a feature flag with parallel syncs.
The architectural pattern to internalize is this: treat integrations as portable data, not vendor-coupled code. If your credentials are yours, your schemas are declarative, and your operational logic (retries, webhooks, ID translation) lives in your service layer rather than the provider's, you can switch providers on a quarterly basis without your customers noticing. That is the actual definition of avoiding vendor lock-in.
FAQ
- Can I migrate between unified API providers without forcing customers to re-authenticate?
- Yes, but only if you own the underlying OAuth client credentials (a BYO OAuth app). If the legacy provider owns the OAuth app, tokens issued against their client_id will not authenticate against a new provider, and you must force reconnects.
- How do I export OAuth tokens from a legacy unified API provider?
- Most unified API providers that support BYO OAuth expose a credential export endpoint (sometimes gated behind a support ticket). Export access tokens, refresh tokens, absolute expiry timestamps, granted scopes, and upstream account identifiers into a hardened staging environment with envelope encryption.
- How do I keep my frontend from breaking when I switch unified API providers?
- Use declarative mappings (JSON or JSONata documents) to translate the new provider's response shape into the exact schema your frontend already consumes. This keeps the integration contract stable and avoids rewriting UI code to accommodate schema drift.
- How should I handle rate limits during and after a unified API migration?
- Standardize on the IETF ratelimit-limit, ratelimit-remaining, and ratelimit-reset headers, and implement retry/backoff logic in your own service layer rather than relying on the platform to absorb 429s silently. This gives you precise control over which operations retry and prevents amplification bugs.
- What is the safest way to execute a zero-downtime cutover between unified API providers?
- Run both providers in shadow mode for 7-14 days, diff responses asynchronously, then cut over behind a feature flag by gradually moving traffic (1% to 10% to 50% to 100%) while watching error rates and latency. Because both providers hold valid tokens, rollback is symmetric.