Custom export builder — map an enriched batch into your own CSV columns

Use when the user has an enriched file and needs it in their shape: a dialer import, a CRM template, a client deliverable. Typical asks: "put the best decision-maker phone in number1 and the next one in number2", "match our import spec", "we have 300 columns to fill".

The schema is yours. Nothing about your column names, their order, or their meaning is built into this API — you define them in a profile and the mapper reads that profile at runtime. Never ask the user to conform to a fixed layout.

Tools

What Route Price
List / create profiles GET POST /api/v1/export/profiles free
Derive columns from a pasted sample POST /api/v1/export/profiles/derive free
Save a profile version POST /api/v1/export/profiles/{profile_id}/versions free
Quote a run GET /api/v1/export/runs/estimate free
Preview a few rows POST /api/v1/export/runs/preview paid, per cache-miss row
Start a run POST /api/v1/export/runs paid, per billable row
Poll / list / inspect rows GET /api/v1/export/runs… free
Download the CSV GET /api/v1/export/runs/{run_id}/download.csv free
Cell overrides (fix a value) GET POST /api/v1/export/overrides, DELETE …/{override_id} free

Only the two rows marked paid ever cost money. Everything else — including the estimate, the download, and every correction you make — is free.

Billing order. A paid call debits your virtual USD balance first. Wallet x402 settlement happens only once that balance is exhausted; your wallet is never charged while virtual credit remains. A 402 here carries error: "virtual_balance_insufficient", virtual_balance_usd, bill_up_to_usd, shortfall_usd, and topup_url — show the user the topup_url as a link before you mention on-chain settlement.

Price per row depends on how wide your schema is, not on how much the model thinks: $0.015 per row up to 40 columns, $0.025 above 40, $0.040 above 80. Because it is banded, the estimate is exact rather than a guess.

Workflow

1. Define the destination schema

Paste a sample of the target file and let the API name the columns:

curl -sS -X POST https://411data.io/api/v1/export/profiles/derive \
  -H 'Authorization: Bearer 411_…' -H 'Content-Type: application/json' \
  -d '{"sample": {"CONTACT ID": "44268264", "number1": "", "number2": "", "COMPANY": ""}}'

sample takes a single record, an array of records, or a bare header array ({"headers": ["CONTACT ID", "number1"]}). It returns columns, column_count, and max_columns.

Create the profile, then save a version with the columns plus your mapping instructions:

curl -sS -X POST https://411data.io/api/v1/export/profiles \
  -H 'Authorization: Bearer 411_…' -H 'Content-Type: application/json' \
  -d '{"name": "Five9 dialer import"}'

curl -sS -X POST https://411data.io/api/v1/export/profiles/42/versions \
  -H 'Authorization: Bearer 411_…' -H 'Content-Type: application/json' \
  -d '{
    "columns": [
      {"name": "CONTACT ID", "hint": "pass through the uploaded ID", "source": "source"},
      {"name": "number1", "hint": "phone most likely to reach the decision maker", "source": "leb"},
      {"name": "number2", "hint": "next-closest contact", "source": "leb"}
    ],
    "instructions": "Rank phones by decision-maker proximity. Leave a column empty rather than guessing.",
    "settings": {"hints": ["dm_phone_rank"], "delimiter": ",", "include_header": true}
  }'

hint is free text per column — it is how you say what a column means without the API needing to know. source is leb (enriched graph), source (the uploaded row, for passthrough), or either.

Enabling the dm_phone_rank hint provider gives the model a deterministically ranked phone list per lead (scored on labels, phone type, contact role, source, and user feedback). It supplies ranked candidates; your instructions and column hints decide which candidate lands in which column.

2. Quote before you spend

curl -sS -G https://411data.io/api/v1/export/runs/estimate \
  -H 'Authorization: Bearer 411_…' \
  --data-urlencode 'batch_id=be_…' --data-urlencode 'profile_id=42'

Read rows_billable and total_usd. rows_cached rows cost $0.00. is_rebuild: true means the entire run is free. If shortfall_usd > 0, show the user topup_url instead of starting the run.

Quote with the same file_instructions and scope you intend to submit — they feed the mapping_hash, so estimating against different text quotes a free rebuild and then charges.

3. Optionally preview a handful of rows

POST /api/v1/export/runs/preview with row_limit 1–5 maps a few rows so the user can sanity-check the instructions before paying for the whole file. Each cell comes back beside the ranked hint: hint_rank tells you where the mapped value sat in the ranking, and hint_rank: null on a filled cell means the model produced something the ranker never offered — worth showing the user.

Preview is not a free sample. It charges cache-miss rows at the run's price, banks each result to the map cache so the later full run does not re-charge those rows, and refunds degraded rows. A preview prepays part of the run.

4. Run, poll, download

curl -sS -X POST https://411data.io/api/v1/export/runs \
  -H 'Authorization: Bearer 411_…' -H 'Content-Type: application/json' \
  -d '{"batch_id": "be_…", "profile_id": 42, "scope_kind": "all"}'

Poll GET /api/v1/export/runs/{run_id} until status is completed, then download GET /api/v1/export/runs/{run_id}/download.csv. The run precharges the estimate and refunds the unused remainder when it settles or is cancelled.

Scope options: all, row_list (with scope_json: [0, 3, 7] — the way to export a single lead), and rebuild.

Re-exporting is free

A mapping decision is paid for once per (lead content × mapping semantics). mapping_hash covers the column set, the per-column hints, the instructions, the hint providers, and the model. It deliberately excludes the profile id, the version number, the column order, and CSV output settings.

So these are always $0.00:

You are charged again only when the mapping semantics genuinely change — new columns, edited hints or instructions, different hint providers, a different model — or when a lead's enriched data itself changed and you ask for a re-map.

Fixing individual values — free

Do not pay for a re-run to correct a cell. A cell override is free, durable, and applied when the CSV is assembled, so it never mutates the model's output: revoke it and the original value returns.

curl -sS -X POST https://411data.io/api/v1/export/overrides \
  -H 'Authorization: Bearer 411_…' -H 'Content-Type: application/json' \
  -d '{"overrides": [
    {"batch_id": "be_…", "profile_id": 42, "scope": "file",
     "column_name": "number1", "mode": "source_ref",
     "source_ref": "hints.ranked_phones.0.e164"}
  ]}'

Assembly precedence is a contract: row override → file override → mapped value → blank.

Prefer source_ref at file scope. It fixes every row at once and keeps working as rows change. A literal is a human-typed value, so it bypasses the no-invention validator by construction — it is tagged as human-typed and counted on the download rather than blocked, but do not use it to fill in data you do not have.

Re-running only the bad rows

Pass parent_run_id plus filters to re-map just the rows a previous run got wrong. Filters are OR-ed — a union of problems, not an intersection:

Filter Selects
degraded the map produced nothing usable
errored the row failed or was cancelled
guard_flagged a value looked suspect
low_confidence below min_confidence (default 0.7)
column_empty any column you name in empty_columns came back blank
hint_disagreement no top-ranked hint value reached the row
profile_version_lt the parent ran on an older profile version

Any filter requires parent_run_id; without one the API returns 400 PARENT_RUN_REQUIRED, because an empty scope would read as "nothing needs re-running". A filtered re-run always bills the rows it selects — they were chosen precisely because the cached result is wrong. Rows outside the scope carry forward free, so the re-run still downloads as a complete file.

Chaining onto an upload

POST /api/v1/batch/enrich/upload accepts post_enrich_profile_id, post_enrich_instructions, and post_enrich_auto, so an export run fires automatically when enrichment finishes. To bind an already enriched file, PATCH /api/v1/batch/enrich/{batch_id}/post-enrich, then quote it with GET /runs/estimate to show the user the per-lead price and the file total before anything runs.

Anti-patterns