DEV Community

Cover image for How to Fix Shopify POS and EDC Terminal Reconciliation Mismatches published
Lucy
Lucy

Posted on

How to Fix Shopify POS and EDC Terminal Reconciliation Mismatches published

The problem: two ledgers, one sale

A cashier taps "Pay by card" in Shopify POS. The customer pays on the EDC terminal. Shopify records a sale. The terminal records a charge. Later, the bank settles the money.

That is three records for one sale. If any one of them is off, your end-of-day numbers do not match.

We see this a lot in multi-store retail projects. The totals are off by a few hundred rupees, or a few dollars. Nobody knows which order caused it. Someone then spends hours with two spreadsheets.

Why the numbers drift

Most mismatches come from four cases. Knowing them makes the fix easier to design.

  • Partial payments.A customer pays half by card and half by cash. The terminal shows only the card part. Shopify shows one order with two payments.
  • Refunds and voids. A refund is started on the terminal but not recorded in Shopify, or the other way around.
  • Settlement delays. The terminal batch closes at night. The bank pays out one to three days later. A daily report compares two days that do not line up.
  • Offline or retried transactions. The network drops. The cashier retries. The terminal charges once, but Shopify gets two attempts, or none.

Notice what these have in common. Each one breaks a match that relies on amount and time alone. Two sales of the same amount in the same minute look identical.

The approach: match on a shared reference

So the first rule is simple. Never match by amount. Match by a reference that both systems can see.

Most EDC terminals print a retrieval reference number (RRN) and an auth code on every receipt. Those values are unique per transaction. If you save them on the Shopify order, you get a key that joins all three records.

Here is the plan in four parts:

  1. Capture the RRN and auth code at the time of payment.
  2. Save them on the Shopify order.
  3. Pull terminal and bank reports on a schedule.
  4. Match by reference and flag what is left. Let's build each part.

Step 1: Capture the reference at payment time

The cleanest option is to have the cashier flow pass the terminal response back to your app. If your terminal vendor has an SDK or API, use it to read the RRN and auth code right after approval.

If your terminal cannot talk to your app, fall back to a short manual field. Ask the cashier to type the last digits of the RRN. This is slower and has typos, so treat it as a second choice.

Either way, the goal is the same: one reference per card payment, attached to the order.

Step 2: Save the reference on the Shopify order

Store the values as order metafields so they are easy to query later. Here is a minimal example using the Admin GraphQL API.

const mutation = `
  mutation SetEdcRef($metafields: [MetafieldsSetInput!]!) {
    metafieldsSet(metafields: $metafields) {
      metafields { id key value }
      userErrors { field message }
    }
  }
`;

async function saveEdcReference(client, orderGid, rrn, authCode) {
  const variables = {
    metafields: [
      { ownerId: orderGid, namespace: "edc", key: "rrn", type: "single_line_text_field", value: rrn },
      { ownerId: orderGid, namespace: "edc", key: "auth_code", type: "single_line_text_field", value: authCode },
    ],
  };
  const res = await client.request(mutation, { variables });
  const errors = res.data.metafieldsSet.userErrors;
  if (errors.length) throw new Error(JSON.stringify(errors));
}
Enter fullscreen mode Exit fullscreen mode

Always check userErrors. A silent failure here means an unmatched order later.

Also listen for the webhooks that change the money on an order. At minimum, subscribe to orders/create and refunds/create. When a webhook arrives, verify the HMAC signature before you trust it.

import crypto from "crypto";

function isValidShopifyWebhook(rawBody, hmacHeader, secret) {
  const digest = crypto
    .createHmac("sha256", secret)
    .update(rawBody, "utf8")
    .digest("base64");
  return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(hmacHeader));
}
Enter fullscreen mode Exit fullscreen mode

Keep secrets in environment variables, never in source code.

Step 3: Pull terminal and bank data on a schedule

Most acquirers and terminal vendors offer a daily settlement file or an API. Pull it once a day. Save each row with its RRN, amount, status, and settlement date.

Two tips that save pain later:

  • Store raw files first. Keep the original file before you parse it. When a row looks wrong, you can go back to the source.
  • Make the import safe to run twice. Use the RRN as a unique key so a rerun does not create duplicates.

Step 4: Match by reference and sort the results

Now the matching job is small. For each terminal row, look for a Shopify order with the same RRN. Then compare the amount.

function reconcile(terminalRows, shopifyOrders) {
  const byRrn = new Map(shopifyOrders.map((o) => [o.rrn, o]));
  const result = { matched: [], amountMismatch: [], missingInShopify: [] };

  for (const row of terminalRows) {
    const order = byRrn.get(row.rrn);
    if (!order) {
      result.missingInShopify.push(row);
    } else if (order.cardAmount !== row.amount) {
      result.amountMismatch.push({ row, order });
    } else {
      result.matched.push({ row, order });
      byRrn.delete(row.rrn);
    }
  }

  const missingOnTerminal = [...byRrn.values()];
  return { ...result, missingOnTerminal };
}
Enter fullscreen mode Exit fullscreen mode

This gives you four clear buckets:

Humans only look at the last three. On a normal day that is a short list, not a full spreadsheet.

Handle the tricky cases on purpose

A few rules make the job much more reliable.

  • Use a settlement window. A sale from Monday may settle on Wednesday. Do not flag it as missing on Tuesday. Wait for the expected settlement date, then flag it.
  • Compare card amounts only. For split payments, compare the card portion of the Shopify order, not the order total.
  • Link refunds to the original sale. Save the original RRN on the refund record. Then a refund is a second line on the same transaction, not a new mystery.
  • Log every change. Keep an audit trail of who fixed what and when. Finance teams will ask for it.
  • Protect customer data. Never store full card numbers. The RRN and auth code are enough for matching. Keep personal data out of logs, and use fake data in test environments.

Make it safe to ship

Roll this out in steps. Run the matching job in read-only mode first and compare its output with your current manual process for a week. Test on a staging store before you touch production. Use a feature flag so you can turn the sync off fast if something looks wrong.

If your payment flow involves custom terminals or several stores, a proper payment gateway integration design up front saves a lot of rework. We have built these kinds of flows at Lucent Innovation, and the pattern above is the one that has held up best.

For background on the Shopify side of POS data, see the related post Shopify POS + EDC Terminal Integration.

You can also check the Shopify Admin API docs for the exact fields on orders, transactions, and metafields.

FAQ

1. Why do my Shopify POS and EDC totals not match?
Because they are two separate records of the same sale. They drift when a partial payment, refund, delayed settlement, or offline retry is recorded on only one side.

2. What is the best key for matching card payments?
The RRN from the terminal. It is unique per transaction, and it appears on both the terminal report and the receipt. The auth code is a good second check.

3. Can I reconcile without a terminal API?
Yes. Use the daily settlement file from your acquirer and save the RRN on each order, even by manual entry. It is slower, but it still beats matching by amount.

4. How often should I run reconciliation?
Daily. Run it after the settlement file arrives, and use a short window so late settlements are not flagged too early.

5. How do I handle split payments?
Compare only the card portion of the Shopify order with the terminal amount. Cash and other methods should be reconciled separately.

Summary

Reconciliation breaks when you match by amount and time. It works when every card payment carries a shared reference.

To close the gap:

Capture the RRN and auth code at payment.
Save them on the Shopify order as metafields.
Pull terminal and settlement data daily.
Match by reference and review only the exceptions.
For brands running many stores, a Shopify Plus development setup can make this easier to scale, since every store follows the same pattern.

Start small, run it read-only for a week, and let the numbers show you where your gaps really are.

Top comments (0)