What Duplicate Protection Actually Checks in QuickBooks

Duplicate protection tracks what the sync tool wrote into QuickBooks, not every record in your file. The mechanism, the boundary, and which duplicates it...

Acodei Content Team · 8/10/2026 · 13 min read

Two sales receipts exist in QuickBooks for one Stripe charge. Or the opposite problem: the January entries someone keyed in by hand are still sitting next to the synced ones, and nothing caught them. Both situations produce the same conclusion, and it is the wrong one. Duplicate protection is not broken. It is scoped, and almost nobody explains what the scope is.

The short version: duplicate protection tracks what the sync tool itself put into QuickBooks. It does not audit your QuickBooks file. Those are very different promises, and the gap between them is where most "the sync created duplicates" tickets actually live.

That distinction decides something practical. If a duplicate falls inside the boundary, it is a product problem and you should expect the tool to prevent it. If it falls outside, no setting will fix it and you need a process change instead. Knowing which one you are looking at saves the afternoon you would otherwise spend toggling settings.

Start a free trial of Acodei if you want the sync side handled. The rest of this post is about the mechanism, because understanding it is what tells you which duplicates are yours to prevent.

Duplicates are structural, not a defect

Before looking at what protects against duplicates, it helps to know why the risk exists at all. It is not sloppiness on anyone's part. It is the delivery model.

Stripe sends webhook events with an at-least-once guarantee, and says so plainly: "Webhook endpoints might occasionally receive the same event more than once." Their webhooks documentation recommends guarding against it by "logging the event IDs you've processed, and then not processing already-logged events."

Redelivery is normal and has several ordinary causes. If your endpoint times out, returns a 5xx, or even returns a redirect (Stripe counts any 3xx response as a failure), the event is retried. Stripe "attempts to deliver events to your destination for up to three days with an exponential back off in live mode." Someone can also resend manually: the Dashboard allows it for up to 15 days after the event, and the CLI for up to 30. Worth knowing if you have ever clicked Resend while debugging, manually resending "doesn't dismiss Stripe's automatic retry behavior, even if it results in a 2xx status code." You can end up with both.

There is a subtler case that catches integrations built by people who read only the first paragraph. Stripe notes: "In some cases, two separate Event objects are generated and sent. To identify these duplicates, use the ID of the object in data.object along with the event.type."

Read that again, because it undermines the obvious fix. If two distinct Event objects describe the same charge, then deduplicating on event ID does not help. The event IDs differ. The only stable identity is the underlying object plus what happened to it. Any duplicate protection worth the name has to key on the thing, not on the message about the thing.

Add that Stripe "doesn't guarantee the delivery of events in the order that they're generated," and the shape of the problem is clear. A sync tool receives an unordered stream that may repeat itself, and has to produce exactly one QuickBooks record per real-world event. Every serious tool solves this with an idempotency key. The interesting question is which field it uses and what that choice costs you.

The key is a QuickBooks field you can see

Here is where Acodei's approach gets specific, and where the boundary comes from.

With Duplicate Protection on, Acodei sets the QuickBooks DocNumber on the record it creates to the Acodei transaction id. A re-sent webhook for that same transaction therefore tries to write the same document number, and collides instead of creating a second record. With Duplicate Protection off, DocNumber is left empty.

That is the whole mechanism, and its elegance is that it stores the idempotency key inside QuickBooks rather than only in the tool's own database. The check does not depend on the tool remembering anything. The evidence that a transaction was already written lives on the written record itself.

It is worth sitting with what field that is. DocNumber is not a hidden system column. It is the reference number that appears on the sales form, the one QuickBooks shows as the invoice or sales receipt number. Intuit exposes it through a setting: "Use the Custom transaction numbers option to set your own numbering preference. This adds an Invoice number field where you can enter any number or letter format you want." Their instructions are to go to Settings, select Account and settings, select Sales, then open the Sales form content section and turn Custom transaction numbers on or off.

So a visible, user-facing, business-meaningful field is doing double duty as a technical uniqueness key. That is a real trade-off and it deserves an honest accounting rather than a shrug.

What you gain is durability. The key cannot drift out of sync with reality, because it is written on the same record it protects.

What you give up is that field for your own numbering. If your business runs a sequential numbering scheme that auditors or customers rely on, synced Stripe records will carry Acodei transaction ids rather than your sequence. Turning Duplicate Protection off gives the field back, and leaves DocNumber empty on synced records. Understand what that costs before you do it, because you are removing the layer described above.

For most Stripe-first businesses this is an easy call. Sales receipts generated from card payments are not documents anyone numbers by hand, and the reference number is doing more good as a collision guard than as a counter. If you are in the minority that needs the field, that is a real constraint worth raising rather than working around silently.

One more detail that makes these records auditable: the PrivateNote on the record carries the original charge amount and the Stripe fee, so a receipt that has been netted down by a fee line can still be traced back to what Stripe actually processed.

What it does not check, stated plainly

This is the part that matters most, and the part that gets assumed wrong.

Duplicate protection tracks what Acodei synced. It does not check records that arrived in QuickBooks any other way.

The reason follows directly from the mechanism. The key is an Acodei transaction id. A record that Acodei did not create does not carry one. It is not that such records fail the check. They are not visible to it at all.

In practice that means the following are outside the boundary:

  • Entries you keyed in by hand. A sales receipt someone typed for a Stripe charge has no Acodei transaction id on it. A later sync of that same charge writes its own record and does not see yours.
  • Anything the bank feed created. Clicking Add on a downloaded payout deposit instead of Match creates a record with no relationship to synced sales detail. This is the single most common way Stripe books get double counted, and it is a bank feed workflow issue rather than a sync issue.
  • CSV imports and backfills done outside the tool. If you imported January through March yourself and then asked a tool to sync the same window, the overlap is invisible to a check keyed on the tool's own ids.
  • Records another sync tool wrote. A forgotten Zapier automation or a previous app writes records with its own identifiers, or none.

None of this is a gap to be fixed with a setting. It is the definition of the feature. A tool that genuinely audited your whole QuickBooks file for lookalike transactions would be making a very different and much riskier promise, since it would have to guess whether two similar records represent one event or two legitimately similar sales on the same day.

If your duplicates are on this list, the fix is procedural. The prevention and cleanup playbook lives in our guide to Stripe QuickBooks duplicate transactions, which covers the bank feed double count, the clearing account pattern that stops it, and how to remove duplicates already in your books without disturbing reconciled months. For the specific case of overlapping backfills, the historical import playbook covers picking a boundary date. If the bank feed is the culprit, start with bank feed reconciliation in QuickBooks.

The layers underneath the document number

Document numbering is the durable layer, but it is not the only one, and the others explain behaviour you may have noticed.

Queue guards. Acodei tracks whether a given transaction is already queued or already executing. Together with the document numbering, these guards are what prevent double records when a webhook is delivered twice.

The two mechanisms are not redundant. A queue guard is state held by the tool while work is in progress. The document number is written into QuickBooks and stays on the record afterwards, which is why it is the part that still applies to a redelivery arriving long after the original sync finished.

Refund resyncs update rather than duplicate. If you resync a refund that has already been booked, the job updates the existing entity instead of creating a second one. This matters because resyncing is the natural response to a record that looks wrong, and doing it should not be an act of faith.

Three documented behaviours, then, covering three different situations: work already in flight, a redelivery of something already written, and a deliberate re-run.

Two cases where the shape changes

The description above is for real-time accounts, where each successful Stripe charge becomes its own itemized QuickBooks sales record. Two configurations behave differently and are worth knowing about before you go looking for a per-charge record that was never supposed to exist.

Daily summary accounts do not get per-charge records. On a daily summary setup, charges are aggregated into one receipt per day rather than posting individually. If you are looking for one sales receipt per Stripe charge on such an account, you will not find one, and its absence is not a duplicate protection failure. It is the mode working as designed. Our post on why transaction counts do not match covers the related counting confusion.

Charges tied to a synced invoice book differently. When a charge belongs to a Stripe invoice that was already synced, Acodei writes a payment receipt against the existing QuickBooks invoice rather than a standalone sales receipt. This is the correct behaviour, and it is also why a naive search for a sales receipt matching an invoice payment comes up empty.

A five minute test

Rather than trusting any of this, including this post, verify it on your own account. The exercise takes very little time and tells you exactly where your boundary sits.

  1. Find a recent synced Stripe charge and open the resulting record in QuickBooks. Look at the reference number field. If it holds an identifier rather than a sequence number, duplicate protection is on.
  2. In the Stripe Dashboard, find the event for that charge and click Resend. Wait, then look for a second record in QuickBooks. There should not be one.
  3. Now the boundary test, and the one that actually answers the question people arrive with. Manually create a sales receipt in QuickBooks for a Stripe charge that has not synced yet, then let the sync run. You will get a second record. That is expected, and it is the clearest possible demonstration that the check does not look at records it did not write.

Step three is the one worth doing in a sandbox rather than your live file. The point is not to break anything. The point is that once you have seen it, you will never again spend an afternoon looking for the setting that would have prevented it.

What to do with this

Sort your duplicates into two piles.

The pile of records the sync created twice should be empty, and if it is not, that is a bug worth reporting with the transaction ids attached.

The pile of records that arrived from a bank feed, an import, a hand entry, or another tool is a workflow problem. It is fixed by choosing one system of record for Stripe activity, matching rather than adding in the bank feed, and drawing a clean boundary date whenever you backfill.

Most books that fill up with duplicates are not suffering from a broken feature. They are suffering from two sources of truth writing into the same file, which no idempotency key can resolve because the records genuinely came from different places.

FAQ

Does duplicate protection check my existing QuickBooks records?

No. It tracks what Acodei synced. The mechanism sets the QuickBooks document number on records Acodei creates to the Acodei transaction id, so a re-sent webhook collides instead of writing a second record. A record you keyed in by hand, imported from a CSV, or created from the bank feed carries no such id, so it is not visible to the check at all.

Why did my sync create a duplicate of a transaction I entered manually?

Because the manual entry has no Acodei transaction id on it. The sync had no way to recognise your record as representing the same charge. This is outside what duplicate protection covers, and the fix is to pick one system of record for Stripe activity rather than entering the same transactions twice.

What happens if I turn duplicate protection off?

The QuickBooks document number is left empty on synced records, which frees the field for your own numbering. You also lose the collision guard that stops a redelivered webhook from writing a second record. Queue guards still handle deliveries that arrive close together, but the durable protection against a redelivery days later comes from the document number.

Why does Stripe send the same webhook more than once?

Stripe guarantees at-least-once delivery, not exactly-once. Timeouts, 5xx responses, and even redirects count as failed deliveries and are retried for up to three days with exponential backoff. Events can also be resent manually from the Dashboard for 15 days. Stripe additionally notes that in some cases two separate Event objects are generated for the same underlying change, which is why deduplicating on event ID alone is not sufficient.

Can I use my own invoice numbers with Stripe records synced to QuickBooks?

Not on the same records, while duplicate protection is on, because that field is holding the transaction id. QuickBooks exposes the field through the Custom transaction numbers setting under Account and settings, then Sales, then Sales form content. If your numbering scheme is a hard requirement, that constraint is worth raising directly rather than turning the protection off without weighing it.

Do daily summary accounts get duplicate protection per charge?

Daily summary accounts do not create a record per charge in the first place. Their charges are aggregated into one receipt per day, so there is no per-charge record to protect or to duplicate.

Getting the sync side right

Duplicate protection is one piece of a larger question, which is whether your Stripe activity has exactly one path into QuickBooks. When it does, duplicates stop being something you watch for. When it does not, no feature on either side will fully compensate.

Acodei turns each successful Stripe charge into an itemized QuickBooks sales record and each refund into its offsetting record, with the document numbering described above guarding redeliveries and the refund job updating in place on a resync. If you would rather spend month end reading reports than hunting for records that exist twice, start a free trial or see how the pricing works.

Share

Automate your Stripe to QuickBooks sync

Save hours every month. Acodei automatically syncs your Stripe transactions, invoices, and payouts to QuickBooks Online.

Get more operational finance guides like this one

We will only send high-value product and finance content.