Skip to main content
Some leads reach you somewhere other than an Erdo page: a Meta lead form whose answers land in a Google Sheet, a portal your agency forwards through Zapier, a list from an event. A lead source brings them into your lead dataset, each exactly once, so they get the same lead page, the same follow-up and the same reports as every other lead. There are three kinds: 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_did is 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 or lead_source.
  • lead_source is what the person did as one of Erdo’s values: meta_lead_form, linkedin_lead_gen_form or imported_list. It is optional. The meta_lead_export format sets meta_lead_form itself.
  • format is columns (the mapping alone) or meta_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 the l: and p: prefixes Meta adds. Meta’s ad, ad set, campaign, form and placement fields are read only when columns names them, under their ad_* names (campaign_name as ad_campaign_name, never as a campaign tag); the preview lists them as not read, with those names suggested.
  • row_id is the field, or fields, that identify one lead in the source. A row is written once per id. The Meta format uses id.
  • submitted_at is the field that says when the person got in touch. The Meta format uses created_time.
  • default_country is the country of a phone written without a country code, for a lead whose own country (location_country, as an ISO code such as US) is not known. A lead’s own country wins.
  • values maps 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.
  • consent is 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 as text. When the mapping reads a consent column into consent_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. Without consent, nothing is recorded where a row has no answer, and the lead is not texted. A webhook posts consent per lead.
  • ignore lists fields the format reads by itself that this source should not read, such as Meta’s company_name. Meta’s id cannot be ignored, and email and phone_number cannot both be. A dry run lists every field it reads, with its column, as read_columns.
  • keep_other_fields (webhooks only) keeps every field your tool posts that columns does not name, under the field’s own name made into a column name (“Budget range?” becomes budget_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.
  • columns maps any other field to a lead column name. A source that asked the person for consent to calls and texts can map it to consent_given (true or false; yes and no are read too), consent_method (checkbox for a ticked opt-in box, disclosure for consent by submitting) and consent_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 in suggest_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

The answer includes 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:
Without --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-only on the CLI) refuses the save if a source with that name already exists.
  • "if_updated_at" set to the source’s updated_at, as you read it, refuses the save if anyone has saved the source since. Read it again and redo your change.
A save that does not include enabled keeps the source’s current state, so rotating a paused webhook’s secret leaves it paused.

Listing and removing

Use the source’s 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.