AWRA OpsHub Search

A Hash Instead of a Lookup

The QuickBooks connector asks the remote system whether a record exists before deciding to create or update it. This one never asks. It computes the contact's address into an identifier and writes to it — and the difference between those two designs is most of what makes a sync predictable.

Integrations & Data AWRA OpsHub Team 12 min read

Every integration that pushes records somewhere has to answer the same question, and how it answers determines nearly everything about how it behaves. Is this record already over there? There are two ways to find out. You can ask, or you can arrange things so that you already know. This connector series contains a clean example of each, and they are worth putting side by side.

The identifier is computed, not looked up

A contact in a Mailchimp audience is addressed by an identifier derived from their email address — the address, lowercased, run through a fixed one-way function. The same address always produces the same identifier, from anywhere, without asking anybody.

So the sync does not search. It computes the identifier and writes the contact to it. If somebody is already there, they are updated. If not, they are created. One request, no branch, no stored remote identifier on our side.

There is no create-or-update decision, because there is nothing to decide. The address already determined where the record goes.

One detail carries the whole arrangement: the address is lowercased before the identifier is computed. A one-way function over text is exact, so two capitalisations of the same address would produce two different identifiers and two contacts. Normalising first is what makes the identifier a property of the person rather than of how somebody happened to type them.

Both designs make a new record when the key field changes. Only one of them shows you the rule.
Both designs make a new record when the key field changes. Only one of them shows you the rule.

Compared with asking

The accounting connector in this series does the other thing. It queries the remote system for a supplier with a matching name, then updates what it found or creates one, and stores the returned identifier.

Computed identifier Look it up first
Requests per record One Two, or more
Race between two runs Both write the same place Both can decide to create
Renaming the key field A new contact; the old one stays A duplicate record
Needs a stored remote id No Yes
Works from any system Yes — the rule is public Only with access to the search

The third row is the one people meet in practice, and it is the same hazard in both designs wearing different clothes. Change the field that identity is derived from and you get a second record. Whatever the key is, changing it makes a new thing — the only difference is whether you can see the rule.

And the last row is the underrated one. Because the identifier is computed from a published rule, anything can address the same contact — another tool, a script, a person with a calculator. There is no shared state that has to be kept in step.

Writing without overwriting

A write that lands on an existing contact could easily destroy work, and two decisions stop it.

The first is about subscription status. The status this sync carries applies only when the contact is new. Somebody who unsubscribed last month, or who was marked as anything else, keeps that. A sync cannot resubscribe a person who opted out, which is not a nicety — it is the difference between a list-keeping tool and a consent problem.

The second is about the fields. Names and phone numbers are sent as updates, and any that are empty are removed from the request before it goes.

That is a small thing with a large consequence. A contact whose phone number is missing in your records but present in Mailchimp — because somebody in marketing added it — keeps it. Sending the empty value would have replaced a real number with nothing, silently, and the sync would have reported success.

It is the same distinction the SMS and email connectors make in a different context: an absent field means "I have nothing to say about this" and an empty one means "make it empty". Systems that treat them alike quietly delete things.

What actually gets sent

Two record types, each opted into separately, each capped per run.

  1. Customers, if selected

    Those with an email address, most recent first. A first name where there is one, falling back to the company name — because a business customer often has the second and not the first.

  2. Vendors, if selected

    The same shape. The contact name, falling back to the company, and a phone number where there is one.

  3. Anything without an address is not considered

    Filtered in the query rather than skipped in the loop, so records with no email never become work.

  4. And each type is capped per run

    A bounded number, most recent first, so a first sync on a large workspace is several predictable runs rather than one that takes an unknown time.

The cap is the sort of decision that looks like a limitation and is a scheduling one. An unbounded sync over tens of thousands of contacts is a single job whose runtime nobody can predict and whose failure loses all of it. A bounded one is repeatable, resumable and finishes.

Two types, two guards

Each record type is synced inside its own guard, and a failure in one is logged and returns nothing rather than stopping the other.

This is the same shape as the notification fan-out, the email message loop and the SMS recipient loop, and it recurs for the same reason. The unit of failure should be the smallest thing that can independently fail — one record type, one message, one recipient — because anything larger means an unrelated problem takes work with it that would otherwise have succeeded.

The count reported is successes only. A contact the provider refused is not counted as synced, so the number on the screen is the number of contacts actually in the audience rather than the number attempted.

What is in place, layer by layer

The audience sync as it stands

Identity computed, never searched

The contact identifier is derived from the lowercased email address, so a write goes to the right place without a lookup and running twice changes nothing.

Built in

The address normalised before hashing

Lowercased and trimmed first, so two capitalisations of one address are one contact rather than two.

Built in

Addresses validated before sending

A malformed address is refused with a reason rather than becoming an identifier for something that is not a person.

Built in

Subscription status set only for new contacts

Somebody who unsubscribed keeps that. A sync cannot resubscribe a person who opted out.

Built in

Empty fields removed before the write

A blank in your records does not overwrite a value already in the audience, because an absent field and an empty one are different instructions.

Built in

Two record types, chosen separately

Customers and suppliers are independent selections, so a workspace can sync one and not the other.

Built in

Each type guarded on its own

A failure syncing one type is logged and the other still runs, rather than one problem costing both.

Built in

A bounded run, most recent first

A capped number per type per run, so a large first sync is several predictable runs rather than one of unknown length.

Built in

Successes counted, not attempts

A contact the provider refused is not counted, so the reported number is what is actually in the audience.

Built in

Contacts without an email address are excluded by the query rather than skipped in the loop, so they never become work in the first place.

Three positions held on purpose

  • An empty value never overwrites a populated one. A blank field in your records means there is nothing to say about it, not that the other system should forget what it knows — and treating those alike deletes data while reporting success.
  • A sync cannot change somebody's subscription status. Setting it only for new contacts is what keeps this a list-keeping tool rather than a way to quietly resubscribe people who opted out.
  • Runs are bounded rather than exhaustive. An unbounded sync is one job of unknown duration whose failure loses everything; a capped one is repeatable, resumable, and finishes.

Five questions about pushing contacts to another system

How do you decide a contact already exists?

A good answer sounds like

A named rule.

What ours actually is

The identifier is computed from the lowercased email address, so there is nothing to look up.

Is running it twice safe?

A good answer sounds like

Yes.

What ours actually is

The same address writes to the same place. A repeat run changes nothing.

Can it overwrite data we have over there?

A good answer sounds like

Not with blanks.

What ours actually is

Empty fields are removed from the request, so a missing value does not erase a real one.

Can it resubscribe someone who opted out?

A good answer sounds like

No.

What ours actually is

Subscription status is set only for contacts that are new. An existing status is untouched.

What happens on a large first sync?

A good answer sounds like

Bounded runs.

What ours actually is

A cap per type per run, most recent first — repeatable and resumable rather than one long job.

Our take

Deriving identity instead of looking it up is one of the most useful patterns in integration work and it is available far more often than people take it. Wherever the remote system will let you address a record by something you already know — an email address, a document number, a reference you both agree on — you get idempotency for free, you halve the request count, and you delete an entire category of race condition without thinking about it. The cost is that you inherit whatever instability the key field has, which is a real cost and is usually smaller than the machinery you avoid. Ask first whether the remote system offers it, because it often does and it is rarely the first thing in the documentation.

Ask what a sync uses as its key

Whether it looks up or computes is the question that tells you how it behaves on a rename, a retry and a race. Everything else follows from it.

Talk through contact syncing

Frequently asked questions

What happens if a contact changes their email address?

A new contact appears in the audience and the old one remains, because the address is what determines identity. That is the same hazard the name-matching connectors have, made visible: whatever the key is, changing it makes a new record. Tidying the old one is done in the audience.

Will syncing overwrite details somebody added in Mailchimp?

Not with blanks. Empty values are removed from the request before it is sent, so a phone number present there and missing in your records is kept. Where you do hold a value, it is sent as an update, so your records are the source for the fields you actually fill in.

Can this resubscribe someone who unsubscribed?

No. The subscription status applies only when a contact is being created for the first time. An existing contact keeps whatever status they have, which is what keeps this a list-keeping tool rather than a consent problem.

Why does the sync only handle a limited number at a time?

Because a bounded run is repeatable, resumable and finishes. An unbounded sync over a large contact list is one job of unpredictable duration whose failure loses all of it. The cap is per record type per run, most recent first.

Does a failure syncing customers stop suppliers from syncing?

No. Each record type runs inside its own guard, so a problem with one is logged and the other still runs. The reported total counts contacts actually accepted rather than contacts attempted.

Help Center

Need a quick answer while you read?

Run inventory, procurement, assets, sales, and field work with approved AWRA guidance for setup, migration, integrations, security, pricing, and support.

Search all approved AWRA public help articles.

Open Help Center