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. Build a React app card on the projects framework (2025.2 or newer), add it to the same app, then call the views/migrate endpoint to swap every user view at once, irreversibly. Tickets need a second helpdesk.sidebar card. hs-x migrate cards generates the replacement; 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 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. A legacy CRM card usually belongs to a pre-projects public app configured in the UI; that app needs the app-to-projects migration first. If you're choosing a target version today, go straight to 2026.03. Note that neither hs app migrate nor hs project migrate converts the card itself: it keeps serving on the app until the sunset, and the steps below are the replacement.
  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 two commands. hs-x migrate pull reads the live card definition (title, object types, fetch URL) from your developer account and writes it to a migration.json; hs-x migrate cards takes that file, 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 pull --app-id <appId>                       # writes migration.json from the live card (title, object types, fetch URL)
hs-x migrate cards migration.json --project ./card-hsx   # classify + generate the replacement project
cd card-hsx && bun install && hs-x dev                   # run it against your test portal

pull needs the credentials stored by hs-x connect hubspot, and the legacy cards surface usually returns nothing without a developer API key (hs-x connect hubspot --developer-api-key …). It records the first card on the app; an app with several legacy cards needs the rest handled by hand today. cards refuses to generate (exit 1) while a human-review finding such as legacy-card.helpdesk-card.required is open, so resolve those first.

How does the swap actually work?

Once the replacement card is in the same project as the legacy card (copy the cards/ folder into the app's project and hs project upload; HS-X's cutover step does not do this for you), the swap is one API call, or the same operation from the project page in HubSpot (the migration banner's Review & migrate button walks the same choices, including a checkbox for consolidating several legacy cards into one app card):

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. While the flag exists you can enable it for at most five accounts, and per HubSpot's feature-flags guide the cards must pass an app-card review before unlimited distribution: email app-card-review@hubspot.com with the production app ID, build ID, the hub IDs of your developer and test accounts, any third-party credentials, and a demo, then invite that address to the test account.
  • 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. It does not expose allowDuplicateAppCardIds; for a consolidation swap, call the endpoint directly.

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


Last updated: August 27, 2026. Sourced from HubSpot's legacy CRM card migration guide and deprecation changelog. Refreshed whenever HubSpot changes the sunset date or the migrate endpoint.