The dataset must have lead identity turned on, so a person
who reaches you two ways stays one lead. Lead sources are behind a feature flag
that is off by default. For LinkedIn lead forms read straight from LinkedIn,
see LinkedIn lead form sync.
The mapping
Every sheet, webhook and import source carries a mapping: which field is which lead column. Only the fields you map are read, so a sheet’s own notes (a status column your team fills in, who called whom) never reach the lead.what_people_didis your own sentence for what someone did before the source brought them in, such as “Filled in the contact form on our partner portal”. The follow-up reads it to know how the person got in touch, and each lead the source creates keeps it (import_source_description). A webhook always needs it; a sheet or an import needs it orlead_source.lead_sourceis what the person did as one of Erdo’s values:meta_lead_form,linkedin_lead_gen_formorimported_list. It is optional. Themeta_lead_exportformat setsmeta_lead_formitself.formatiscolumns(the mapping alone) ormeta_lead_export, for Meta’s own lead export. The Meta format reads who the person is (id, created_time, name, email, phone, company, job title and location) and removes thel:andp:prefixes Meta adds. Meta’s ad, ad set, campaign, form and placement fields are read only whencolumnsnames them, under theirad_*names (campaign_nameasad_campaign_name, never as a campaign tag); the preview lists them as not read, with those names suggested.row_idis the field, or fields, that identify one lead in the source. A row is written once per id. The Meta format usesid.submitted_atis the field that says when the person got in touch. The Meta format usescreated_time.default_countryis the country of a phone written without a country code, for a lead whose own country (location_country, as an ISO code such asUS) is not known. A lead’s own country wins.valuesmaps a column’s answers to the values the column holds, by column then answer as the source writes it:{"is_broker": {"Yes, I'm an agent": "true", "No": "false"}}. The lookup ignores case and surrounding spaces. An answer the map does not name is stored as written, and a sheet keeps an alert open naming it until the source is saved with it mapped.consentis how a sheet’s or import’s leads agreed to texts:{"method": "checkbox"}for a box they ticked,{"method": "disclosure"}for a notice beside the submit button, optionally with the wording astext. When the mapping reads a consent column intoconsent_given, each yes is recorded with this method, and a no or a blank stays as it is. When it reads none, every lead is recorded as having agreed this way. Withoutconsent, nothing is recorded where a row has no answer, and the lead is not texted. A webhook posts consent per lead.ignorelists fields the format reads by itself that this source should not read, such as Meta’scompany_name. Meta’sidcannot be ignored, andemailandphone_numbercannot both be. A dry run lists every field it reads, with its column, asread_columns.keep_other_fields(webhooks only) keeps every field your tool posts thatcolumnsdoes not name, under the field’s own name made into a column name (“Budget range?” becomesbudget_range). A field that would replace a column Erdo writes itself is skipped, and at most 50 are kept per lead. A sheet or a file never keeps unnamed fields, because they often carry the sales team’s own notes.columnsmaps any other field to a lead column name. A source that asked the person for consent to calls and texts can map it toconsent_given(trueorfalse;yesandnoare read too),consent_method(checkboxfor a ticked opt-in box,disclosurefor consent by submitting) andconsent_text(the sentence they agreed to), the way a landing page records it. Only a ticked box replaces an answer already on the lead.
Suggested columns and values
A sheet’s dry run matches each field it does not read yet to one of your lead columns: the ones you list insuggest_columns (each with a short
description), or, without that list, the columns your leads dataset already
has. are_you_a_broker? reads as is_broker, “What’s your budget?” as
budget. A field that matches none of them gets no suggestion: it is left out
rather than given a new column. For a matched field holding a small fixed set
of answers, the dry run also suggests the value each answer is stored as
(suggested_values).
The suggestions come from a model that sees the field names, the columns and
their descriptions, and the answers only of fields whose few answers repeat,
so free text such as names, emails or notes is never sent. Nothing is read
until you include it in columns and values and save.
Which import a lead came from
Every lead a sheet, webhook or import brings in is marked as imported, with the source’s id, kind and name (import_source_id, import_source_kind,
import_source_name) and its what_people_did (import_source_description), so you
can see which import it came from. Only the import that
creates the lead marks it: a lead you already captured yourself stays yours
when a sheet or import later carries the same person. LinkedIn lead forms are
read from your own connected LinkedIn account and are not marked as imported.
Leads that got in touch earlier
When a sheet source is first saved, its existing rows are people who got in touch before Erdo knew about them. They, and every imported row, are written as history: dated by when they got in touch, never sent as a new-lead alert, never counted as new, and not followed up until somebody starts them from the lead page or with--start-follow-up. Rows that arrive after the source is
saved are new leads like any other.
A Google Sheet
Open the sheet to anyone with the link (Share > General access > Anyone with the link, Viewer), then copy its address from the browser with the right tab open. The#gid= at the end names the tab.
--dry-run reads the sheet and shows what saving would write: how many rows
are history and how many new, which fields are not read (with a suggested
column for each), rows that would be skipped and why, and the first rows as
leads. Nothing is saved. Run it again without --dry-run to save.
If the sheet stops being open to anyone with the link, Erdo cannot read it.
The source’s last_error says so, and after three failed reads it appears in
your attention feed. A sheet that reads fine but stops getting new rows for
three days, after usually getting them, also appears there: whatever fills it
may have stopped.
A row Erdo cannot read (no id, or a date it cannot understand) is named in
your attention feed by its row number. Fix it in the sheet and the next read
writes it; the item resolves after a read with none left.
A webhook
webhook_url and webhook_secret. The secret is shown
once. Your tool posts each lead to webhook_url as one JSON object, with the
secret in the X-Erdo-Lead-Source-Secret header:
Preview a mapping with
"dry_run": true and a "sample" body. Rotate the
secret with --rotate-secret; the old one stops working at once. A webhook that
usually delivers and goes three days without a new lead appears in your
attention feed: the tool sending it may have stopped.
An import
Declare an import source once, then send files to it:--commit the import is a preview. --default-submitted-at dates rows
the file does not date, including rows whose date cell is blank; a date Erdo
cannot read is skipped, never replaced. Importing the same file again writes
nothing new. --start-follow-up (with --commit) also starts follow-up on each
lead the file’s rows created, including ones an earlier attempt created, so an
import that stopped part-way can simply be run again. Export Excel files as CSV.
The REST equivalent is POST /v1/provider-lead-sources/import with name,
header and up to 1,000 rows.
From an agent over MCP
The same sources can be declared from Claude or any MCP client connected to Erdo. Each tool takes the same fields as the REST body above.
The dataset is named by its slug and a sheet, webhook or import source by its
name.
mapping is the same object as above; a key Erdo does not know is
refused, so a typo cannot quietly map nothing. A new webhook, or one saved with
rotate_secret, returns webhook_secret once in the tool’s answer, as it
does over REST. The tools never start follow-up on imported leads: the import
returns created_leads, and a person starts them from the lead page or with
--start-follow-up.
Saving without overwriting
Saving a source again under its name replaces it. Two fields stop that from overwriting somebody else’s work:"create_only": true(--create-onlyon the CLI) refuses the save if a source with that name already exists."if_updated_at"set to the source’supdated_at, as you read it, refuses the save if anyone has saved the source since. Read it again and redo your change.
enabled keeps the source’s current state, so
rotating a paused webhook’s secret leaves it paused.
Listing and removing
updated_at from list with --if-updated-at when removing
it. The delete is refused if somebody edited or replaced that source after the
list, so read it again before retrying. The REST equivalent is the optional
if_updated_at query parameter.
Removing a source stops it reading. Leads already written stay.
