Lead identity
A lead’s contact details can change.canonical_lead_id is the permanent
reference for a lead in an enrolled dataset; email and phone are evidence used to
find that lead. Lead attributes stay in the dataset row.
Turning identity on for an existing dataset is a platform migration. Adding a
pipeline option alone does not turn it on or backfill anything.
Lead IDs and conversion IDs
These references mean different things:
Do not replace an already-reported conversion transaction ID with the permanent
lead UUID. Historical
id, lead_id and lid values keep their original
meaning. Their presence alone does not establish permanent person identity.
Matching
The platform resolves contact evidence against raw dataset rows, including rows hidden by exclusions. This stops a hidden shared phone number from looking like a unique match.- A unique, consistent email or phone match reuses the existing identity.
- Missing contact information can be filled when it does not conflict with another lead.
- When email and phone point to different leads, or contact evidence is ambiguous, the capture records the unresolved result instead of guessing or creating a third lead.
- A new eligible contact with no match receives a new permanent ID.
Capture responses
An adopted capture pipeline separates the two identities in its response:leadId remains the conversion reference for compatible existing clients.
canonicalLeadId identifies the lead. A client must honor conversionEligible
and must not report a conversion merely because capture succeeded. Historical
leads without established conversion evidence do not become newly eligible
because identity storage changed.
Only the operation that creates the acquisition may report it. A later capture
for the same lead reuses the conversion reference but is not a new acquisition,
so identity resolution alone prevents a repeat submission from double-reporting.
The receipt records the platform’s decision; it does not prove an external
analytics service received the event.
A pipeline can carry the producer contract before its dataset is migrated. It
keeps its existing response behavior until the IDs exist, so adoption is safe to
apply first. Once IDs are populated, the dataset refuses legacy writes.
Historical migration
ID backfill assigns a distinct permanent ID to each existing source row. It does not merge rows, rewrite historical conversion IDs, or replay capture pipelines. Existing contacts, timestamps and exclusions are preserved. Migration is one operator call per dataset. Start with a preview. It mutates nothing and reports the rows that would be assigned, the pipeline writers that will take the producer contract, and any consumer that blocks the migration. Apply gives every pipeline writer of the dataset the producer contract, then backfills the IDs and publishes the new dataset revision together with the identity metadata in one transaction. No new-row arrival event is emitted. Managed automations need no preparation.canonical_lead_id is reserved for
every dataset, generated scripts hide it from their row inputs and content
fingerprints whether or not the dataset is migrated, and each script is re-derived
from its declaration before it runs. Adding the column therefore cannot change
what an automation sees or make on_update: "act" replay rows whose business
content did not change.
Apply refuses while any consumer is queued, running, or cannot be re-derived from
a platform-owned declaration, because the backfill rewrites the file all of them
read. A hand-written script is the usual case: it must be adapted or retired
first. The refusal names what blocked it.
Platform operator endpoint
Migration requires a first-party Erdo session. Dataset and automation edit permissions still apply. API keys and scoped CLI tokens cannot call it.
Apply takes a stable
operation_key. Retry an uncertain apply with the same key
and the committed migration settles it, so a lost response never applies twice.
A different key on an already-migrated dataset is refused rather than migrating a
second time. Nothing else is carried between calls: everything the migration
needs is derived under the dataset write lock at apply time.
Merging two leads
Some people cannot be connected by contact evidence alone, and the platform deliberately refuses to guess:- Two leads share an email address or a phone number, so a capture resolves
ambiguous. - A submission’s email points at one lead and its phone at another, so it
resolves
conflict. - A lead forked before migration, because the old writers matched on email and phone together and one submission had no phone.
- A phone-only lead returns email-only, with nothing in common.
What a merge does
The absorbed lead stops being its own lead. In one transaction, under the same per-dataset write lock a capture takes:- The absorbed identity points at the survivor.
- Both leads’ email and phone evidence becomes the survivor’s, so a capture that would have been refused now resolves to the survivor.
- The two dataset rows become one row. An identity table saying “these are one person” while the dataset still holds two rows is worse than not merging, because every count, export and automation reads the rows.
overwrite_columns. The combined row keeps the earlier of the two arrival
times, because the absorbed row is being destroyed and the later stamp would date
the person after they actually arrived.
A merge cannot be undone
The pointer survives, but the absorbed row’s values are combined away. There is no unmerge. Read both leads first. The absorbed identity keeps existing rather than being deleted: capture receipts reference it, and they remain accurate about what happened at the time. An id you already hold — from a capture response or an outbound link — keeps working, and resolves to whatever that lead is now, following a chain of merges to its end. A merge takes no idempotency key. It is identified by the pair of leads it joins, so retrying it after an uncertain response either finds the two already one lead and answersalready_merged, or performs the merge that did not commit.
Addressing a lead
A lead is addressed by itscanonical_lead_id UUID or by a 22-character lead
reference, which is the same value in a form short enough for a link or an SMS.
Both forms are accepted everywhere a lead is named, and every response carries
both.
Endpoints
lead-candidates also returns lead_identity_enabled. An empty list means two
very different things — the platform looked and found nothing, or this dataset
has no lead identity at all — so branch on that flag rather than reporting “no
duplicates” for a dataset nothing has checked. Reading one lead on such a dataset
answers a precondition failure naming the missing migration, not a 404.
Reading takes dataset view permission; merging takes dataset edit permission.
Scoped API keys — the per-end-user credentials — cannot merge: one lead’s holder
must never be able to fold another lead into it.
From the CLI:
erdo_get_lead and
erdo_list_lead_merge_candidates. Merging deliberately is not: it cannot be
undone, and the captures it settles are exactly the ones where the evidence
contradicts itself, so a person decides.

