Migrate Gift Cards to Shopify Without Losing Balances

To migrate gift cards to Shopify, export every live card from the old platform with its code and remaining balance, then recreate each one through the Admin GraphQL giftCardCreate mutation, passing the old code so customers can keep using it. Shopify’s admin can export gift cards but has no import for them, so the API (or an app built on it) is the route.

Migrations go wrong in three places: codes Shopify will not accept, balances that move between export and cutover, and an API field that changed in the 2026-07 release. Everything below was checked against Admin GraphQL 2026-07.

What carries over when you migrate gift cards to Shopify?

The balance carries over. So does the code, provided it fits Shopify’s rules, and so does an expiry date. You can also attach the card to a customer with customerId, though that needs the write_customers scope as well as write_gift_cards, and the customer has to exist in Shopify first. If you are moving customers in the same project, our post on migrating customers without losing consent covers that half.

History does not carry over. A card created through the API starts life with an initial amount equal to whatever you pass, and its transaction list is empty. The fact that a customer spent 30 of their original 50 last spring lives in your export file, not in Shopify. Put the old platform’s reference in the card’s note field (which customers never see) so support can trace a dispute back to the source record.

Sender messages and scheduled deliveries are the part to leave behind. giftCardCreate accepts recipientAttributes, which can schedule a notification to a recipient. On a migration that is the last thing you want: a gift someone bought in 2024 arriving in an inbox again.

Will the old gift card codes work on Shopify?

Sometimes. The GiftCardCreateInput reference says a code “must be 8-20 characters long and contain only letters(a-z) and numbers(0-9)”, and that it is not case sensitive. Anything outside that is rejected, so check your export before you write a line of code.

WooCommerce is the common trap. The official Gift Cards extension only supports its own 19-character format, XXXX-XXXX-XXXX-XXXX, and those hyphens are not letters or numbers. Strip them and you get a valid 16-character code. What you cannot assume is that a customer who types the hyphenated version from their old email will be accepted at Shopify checkout. Create one stripped card on a development store, try redeeming it with the hyphens in, and decide your customer comms from what actually happens.

Bad input comes back in userErrors, not as a failed request. The GiftCardErrorCode enum lists TOO_SHORT, TOO_LONG, INVALID and TAKEN (“The input value is already taken”), and TAKEN is the one that matters for a migration. If a code that already exists on the store is refused with it, a second run of the import cannot duplicate a card the first run created. Confirm that on your development store rehearsal before you rely on it.

If a legacy code cannot be made valid (a six-character voucher, say), omit code and Shopify generates a random 16-character one. The payload returns it as giftCardCode, and you then owe that customer an email with the new code, so count those cards early.

Which balance do you import?

The remaining balance, never the original value. Shopify takes a single starting amount and treats it as the card’s balance, so importing the issued value hands every customer back what they already spent.

The WooCommerce export has both columns, Issued Value and Balance, next to each other, which is exactly how this mistake gets made at the end of a long day. Use Balance. Skip cards with a zero balance, and skip inactive ones unless the client has a reason to revive them. Magento and other platforms export something equivalent; whatever they call it, you want the amount still spendable today.

The import script

This is a plain Node script, no framework. It reads the exported CSV with csv-parse, normalises each code, and creates the cards one at a time against 2026-07. Rename the column keys to match your export headers.

// import-gift-cards.mjs
// Usage: SHOP=example.myshopify.com TOKEN=shpat_xxx CURRENCY=GBP node import-gift-cards.mjs cards.csv
import { readFile } from 'node:fs/promises';
import { setTimeout as sleep } from 'node:timers/promises';
import { parse } from 'csv-parse/sync';

const { SHOP, TOKEN, CURRENCY = 'GBP' } = process.env;
const endpoint = `https://${SHOP}/admin/api/2026-07/graphql.json`;

const mutation = `
  mutation GiftCardCreate($input: GiftCardCreateInput!) {
    giftCardCreate(input: $input) {
      giftCard { id lastCharacters balance { amount currencyCode } }
      userErrors { field code message }
    }
  }`;

async function gql(query, variables) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN },
      body: JSON.stringify({ query, variables }),
    });
    const body = await res.json();
    const throttled = body.errors?.some((e) => e.extensions?.code === 'THROTTLED');
    if (!throttled) return body;
    await sleep(2000);
  }
  throw new Error('Still throttled after 5 attempts');
}

const rows = parse(await readFile(process.argv[2]), { columns: true, skip_empty_lines: true });
const failures = [];

for (const row of rows) {
  const code = row['Code'].replace(/[^a-z0-9]/gi, '');
  const balance = Number(row['Balance']);
  if (!(balance > 0)) continue;

  const input = {
    code,
    initialAmount: { amount: balance.toFixed(2), currencyCode: CURRENCY },
    note: `Migrated from WooCommerce, legacy code ending ${code.slice(-4)}`,
  };
  if (row['Expiration date']) input.expiresOn = row['Expiration date'].slice(0, 10);

  const { data, errors } = await gql(mutation, { input });
  const userErrors = data?.giftCardCreate?.userErrors ?? [];

  if (errors?.length || userErrors.length) {
    const codes = userErrors.map((e) => e.code);
    if (codes.length === 1 && codes[0] === 'TAKEN') continue; // already imported on a previous run
    failures.push({ code, errors: errors ?? userErrors });
    continue;
  }
  console.log(`created ...${data.giftCardCreate.giftCard.lastCharacters} ${balance.toFixed(2)}`);
}

console.log(`${failures.length} failures`);
if (failures.length) console.log(JSON.stringify(failures, null, 2));

A few choices in there are deliberate. The TAKEN skip is what lets you run the script twice without doubling anyone’s money. Logging only lastCharacters keeps full codes out of your terminal history and CI logs, which matters because a gift card code is a bearer token. And the expiry handling assumes the export gives an ISO-style date; if yours does not, parse it properly instead of slicing, or a card can expire a day before the customer expects.

The sequential loop is slow on purpose. One request at a time is fine for a one-off job of a few thousand cards, and you avoid fighting the cost-based throttle described in our post on Shopify GraphQL rate limits. The retry above is the bare minimum.

On API version 2026-04 or older

The 2026-07 changelog entry replaced initialValue, a bare decimal, with initialAmount, which carries both amount and currency. If your app is pinned to an earlier version, send initialValue: "25.00" instead and the card is issued in the store’s currency. Shopify’s advice is to swap to initialAmount before upgrading to 2026-07, so do it now if the script will live on.

The same release added crossCurrencyRedemptionStrategy. Left unset, a card issued in the shop’s currency defaults to MARKET_FX conversion when spent in another currency. For a single-currency UK store that changes nothing, but read the entry if the old platform sold cards in several currencies.

How do you handle redemptions during cutover?

Customers keep spending gift cards on the old store after you export. There are two ways to deal with that.

The clean one is a freeze: switch off gift card redemption on the old platform a few hours before cutover, then export, import and launch. It costs the client a short window where cards cannot be spent, and removes the reconciliation step entirely.

When a freeze is not possible, do a second export after launch and diff the balances against the first. For any card spent in between, reduce the Shopify balance with giftCardDebit, which takes the card’s ID and a debitAmount money input and records a transaction with your note. That leaves an audit trail where editing the balance directly would not.

Then check the totals. The giftCards query accepts source:api_client, so you can page through exactly the cards your script made:

query MigratedCards($cursor: String) {
  giftCards(first: 250, after: $cursor, query: "source:api_client") {
    nodes { lastCharacters balance { amount } }
    pageInfo { hasNextPage endCursor }
  }
}

Sum the balances and compare against the export. If the two numbers match to the penny, you are done. If they do not, find out why before the old store is switched off. Unspent gift card balances are money the business owes its customers, and the finance team will want the two systems to agree.

Keep the full export somewhere access-controlled for as long as the client’s records policy says. Shopify’s own admin only ever shows the last four characters of a code, so your file is the only place a support agent can match a full code a customer reads out.

What we would do

On a WooCommerce or Magento move with a few hundred to a few thousand live cards, write the script. It is short, it keeps every code the customer already holds, and the TAKEN behaviour makes it safe to rehearse on a development store and then run for real. Plan a freeze if the client will accept one.

Reach for a migration app when the client also wants ongoing gift card features (scheduled sends, branded designs, loyalty) and is going to install that app anyway. Skip API creation entirely only when nearly every outstanding card would fail Shopify’s code rules: at that point you are emailing everyone a new code regardless, and letting Shopify generate them is simpler.

Gift cards are a small slice of the replatforming work we do on Shopify development projects, alongside customer, order and URL migration. Running them after the customer import means each card can land attached to the right account on day one.

Need this built properly?

Whoooop Ltd has spent 15+ years building and maintaining web applications in TypeScript, React, Node.js and serverless — the same ground this post covers.

Get in touch