view .md
Answers · Migration

How do I migrate legacy CRM cards to UI extensions before October 2026?

TL;DR — Legacy CRM cards (the JSON-over-fetch kind) die October 31, 2026. You build a React app card on the projects framework (2025.2+), add it to the same app as the legacy card, then call the views/migrate endpoint to swap every user view at once — irreversibly, with no user action required. Tickets need a second card for helpdesk.sidebar. hs-x migrate cards generates the replacement from your existing card; hs-x migrate swap-cards runs the swap.

What is actually changing?

A legacy CRM card is configuration plus a fetch URL: HubSpot calls your endpoint, you return JSON, HubSpot renders tiles and actions in its own fixed layout. An app card (UI extension) inverts that: you ship a small React component that runs in HubSpot's sandbox, talks to your backend, and renders with HubSpot's component library. Same slot on the record page — completely different contract.

That inversion is why this is a migration and not an upgrade. Your card definition (title, object types, fetch URL, actions) doesn't carry over mechanically; it has to be re-expressed as a component plus a backend. The good news: the legacy card's shape maps almost one-to-one onto a small, well-understood app card — HubSpot's own Legacy CRM Card Converter and HS-X's migrate cards both exploit exactly that.

The deadline mechanics, per HubSpot's changelog: unsupported since June 16, 2025; sunset and auto-deleted on October 31, 2026. An app that still ships a legacy card after that date simply loses the card.

What do I need before I can start?

Two prerequisites, both from HubSpot's migration guide:

  1. Your app must be on the projects framework, platform version 2025.2 or newer. Many apps with legacy CRM cards are pre-projects public apps configured in the UI — those need the app-to-projects migration first. If you're choosing a target version today, go straight to 2026.03.
  2. The latest HubSpot CLI (or HS-X, which drives the same surfaces).

A configurable test account is strongly recommended — the swap step is one-way, so everything before it should happen against an account you can afford to break.

How do I build the replacement card?

You have two honest options:

  • Redesign from scratch if you want the card to do more than it does today — React app cards support tables, forms, images, and real data fetching that legacy cards never could.
  • Convert like-for-like if you want the fastest safe path: a card that fetches the same URL and renders tiles, expandable content, and action buttons almost identically to the legacy card. HubSpot's converter sample is the reference implementation of this shape.

This is where HS-X collapses the work to one command. hs-x migrate cards reads your legacy card definition (title, object types, fetch URL, actions), classifies what converts automatically versus what needs human review, and generates a complete, typed replacement project — the app card component, its backend handler, and the project wiring — targeting the current platform version. The generated project is a separate testbed app: you deploy and verify it against a real portal without touching the app your users have installed.

hs-x migrate cards          # classify + generate the replacement project
hs-x dev                    # run it against your test portal

How does the swap actually work?

Once the replacement card is in the same project as the legacy card (copy the cards/ folder in, hs project upload — or let HS-X's cutover step do it), the swap is one API call:

POST /crm/v3/extensions/cards-dev/{appId}/views/migrate
{ "legacyCrmCardId": "...", "appCardId": "...", "helpdeskAppCardId": "..." }

Three things to internalize about this endpoint, because they're the parts that surprise people:

  1. It is irreversible. Once the views migration starts, there's no undo. Every user view that showed the legacy card gets the app card; the legacy card is hidden and can no longer be added to views. Do your beta testing before this call.
  2. It is asynchronous and retry-safe. The endpoint returns a progress message ("X of X installs still processing"); re-calling it with the same body reports progress without duplicating work. High-install apps take longer; users may briefly see both cards mid-swap. Nobody loses access.
  3. Tickets need a second card. If your legacy card supported tickets, the replacement requires two app cards — crm.record.sidebar and helpdesk.sidebar, with different titles — and the migrate call takes both IDs. Miss this and your help-desk users lose the card.

Two more gotchas for marketplace apps: listed apps get all app cards auto-hidden for beta testing — you gate rollout with the hs-release-app-cards feature flag, and that flag (plus hs-hide-crm-cards, if you ever used it) must be deleted before the migrate endpoint will work. And if you're consolidating several legacy cards into one app card, the endpoint blocks duplicate targets by default — set allowDuplicateAppCardIds: true deliberately.

hs-x migrate swap-cards drives this call for you: pass the IDs (--app-id, --legacy-card-id, --replacement-card-id, plus --helpdesk-card-id when your card supported tickets) and it executes the swap through the HubSpot developer API.

What do I do after the swap?

Delete the legacy card (DELETE /crm/v3/extensions/cards-dev/{appId}/{cardId}). It's safe — users already see only the app card — and it clears the deprecation warnings on your app. If you skip this, HubSpot deletes it for you at sunset; deleting it yourself just makes the state explicit.

That's the full arc: replacement built and verified on a testbed → swapped into all views in one irreversible, user-invisible call → legacy card deleted. Done before October 31, 2026, your users never notice. Done after, your card is already gone.