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:
- downloading the same run again;
- re-exporting the same leads with the same mapping semantics
(
scope_kind: "rebuild"); - reordering columns, or changing delimiter, quoting, header, or encoding;
- every cell override you apply.
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"}
]}'
scope:row(needsrow_index) orfile(every row).mode:source_ref(pull fromlead.…,source.…, orhints.…),literal(a value you type), orclear(force blank).
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
- Do not assume a schema. There is no built-in dialer or CRM layout. Ask the user for their columns, or derive them from a pasted sample.
- Do not skip the estimate. It is free and exact. Quoting after the fact, or with different instructions than you submit, misprices the run.
- Do not treat preview as a free sample. It charges cache-miss rows.
- Do not pay to fix a cell. Use a free override.
- Do not re-run to reorder columns or change the delimiter. Those are not part
of
mapping_hash; re-export instead, which is$0.00. - Do not invent data. Leave a column blank. A blank cell is a correct answer; a fabricated phone in a dialer is a call to a stranger.
- Do not use a
literaloverride to manufacture contact data. It bypasses the no-invention check by design. - Do not mention wallet x402 while virtual balance remains. Virtual USD is debited first, always.
Related
- batch_csv_enrich — enrich the file before you export it
- bill_on_match — why no-match rows are refunded
- anti_invent — the no-invention rule in full
- prepay_deposits — topping up virtual USD
- connect_wizards —
?action=export-profilesbuilds a profile in the UI - /developers — full REST contract