Skip to main content

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.
Matching does not remove exclusions or authorize communications. Publicly supplied identifiers are not authorization to read or update another lead.

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.
Merging is how a person settles those. It is an operator action: nothing merges automatically.

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.
Rows combine the same way a merge-mode write does. The survivor’s row is the base: a value the absorbed row carries fills a blank in the survivor’s row, and never replaces a value the survivor already holds unless you name that column in 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 answers already_merged, or performs the merge that did not commit.

Addressing a lead

A lead is addressed by its canonical_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:
Reading a lead is also available to MCP clients as 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.