LeadMagic — Bulk jobs & uploaders
Async enrichment for lists. Bills per successful row at the same rate as the matching single-request product; failed rows are free. Any list ≥ 50 rows belongs here — never a loop of single calls.
Workflow
- Preflight:
GET /v1/credits + POST /v1/batch/preview-cost (both free).
- Validate mapping (free):
POST /bulk/validate.
- Save the exact ordered submitted rows and their original record IDs, then submit:
POST /bulk/submit. Bind the saved manifest to the returned job ID.
- Poll
GET /bulk/jobs/{jobId} ≥45s apart, or set a callback webhook, or stream GET /bulk/jobs/{jobId}/stream.
- Fetch:
GET /bulk/jobs/{jobId}/download (file) or /results / /rows (paged). Failed rows: /errors (free) — fix and resubmit only the misses.
- Validate identity for the entire merge before writing. Follow the join contract below.
Result identity and safe joins
- For ordinary one-input/one-result enrichment,
row_index is zero-based within that job's submitted data. CSV headers and blank records are excluded. It is not the original spreadsheet row number or a result's position on a page.
- Keep an immutable manifest:
{job_id, product, rows} with rows in exact submission order. Keep original record IDs alongside it, especially after filtering, sorting, batching, or retries. Each new job starts its own indices.
- Fetch all required pages from the manifest's job ID.
/results returns {rows, limit, offset}. Status filters leave gaps in source indices. Never zip the returned array onto a CSV or use offset + array_position as the source index.
- Verify every
lm_input identity against manifest.rows[result.row_index] before applying output. For profiles, also compare the returned profile identity when present. A changed profile URL may be an alias; quarantine it for review rather than silently treating it as the same person.
- Stop the merge on any mismatch, missing identity, invalid index, or ambiguous duplicate. Report counts and job ID; preserve the input file. Never guess an offset or overwrite another person's fields.
- A single source row can appear more than once in multi-product jobs. Local Leads is a fan-out product with display indices. Neither supports the simple one-result-per-source join below.
For a single-product profile_search job, the bundled checker validates all supplied pages before creating a new JSON file. It rejects duplicate indices, wrong-job manifests, and mismatched profile identities. It does not modify the input or contact the API:
python3 scripts/check-profile-results.py --manifest manifest.json \
--job-id "$JOB_ID_USED_TO_FETCH" --results page-0.json page-1.json \
--output checked-results.json
Run from this skill's directory (or use its absolute script path). --job-id must be captured from the actual fetch URL, not copied from an unrelated manifest. Each page is the unchanged /results JSON response. The output contains {source, result} pairs, with the source taken from the saved submission, and reports whether all input rows are present. A partial result set must not be represented as a complete job. Use the retained original record IDs to merge into a master file only after validation.
Submit shapes
| Variant |
Body |
When |
POST /bulk/submit |
rows | csv | fileUrl |
Auto-detect (recommended) |
POST /bulk/json |
rows |
JSON array |
POST /bulk/csv |
csv |
Inline CSV string |
POST /bulk/url |
fileUrl |
Remote CSV/JSON/JSONL |
POST /bulk/file |
multipart |
After POST /bulk/upload-session |
curl -sS -X POST "https://api.leadmagic.io/bulk/submit" \
-H "X-API-Key: $LEADMAGIC_API_KEY" -H "Content-Type: application/json" \
-d '{"product":"email_finder","rows":[{"first_name":"Jane","last_name":"Doe","domain":"acme.com"}]}'
Product keys
email_finder · email_validation · personal_email_finder · mobile_finder · b2b_profile_to_email · email_to_b2b_profile · profile_search · role_finder · company_finder · company_funding · job_change_detector — same per-row cost and input columns as the single-request product (column aliases normalize the same way).
Lifecycle & ops
GET /bulk/jobs?status=&product=&limit=&offset= — list jobs.
POST /bulk/jobs/{jobId}/pause · /resume · /cancel · /restart.
- Diagnostics:
GET /bulk/jobs/{jobId}/events, /logs, /metrics.
- Out of credits mid-job: the job pauses rather than failing — top up, then
/resume. Report rows done vs remaining.
Rules
- Always preview cost and state projected spend before submitting; get user confirmation for large jobs.
- Poll ≥45s; prefer
callback for jobs over a few thousand rows.
- Match rate matters: since misses are free, a conservative budget = rows × cost × expected match rate; a hard ceiling = rows × cost.
- Synchronous mini-batch for small arrays (no job overhead):
POST /v1/{product}/batch or POST /v1/batch (mixed products). Suppression lists: /v1/batch/suppression-lists.
- MCP:
submit_bulk_job, submit_detected_bulk_job, process_attached_csv (chat CSV uploads), get_bulk_job_status, get_bulk_job_rows, get_bulk_job_errors, list_bulk_jobs.
1---2name: bulk-jobs3description: LeadMagic bulk enrichment jobs, CSV uploaders, and synchronous mini-batches. Use when enriching any list of 50+ rows, submitting POST /bulk/submit, uploading a CSV, polling job status, pausing or resuming a job, pulling error rows, configuring callbacks, or budgeting per-row credits for any product.4license: MIT5---67# LeadMagic — Bulk jobs & uploaders89Async enrichment for lists. Bills **per successful row** at the same rate as the matching single-request product; failed rows are free. **Any list ≥ 50 rows belongs here — never a loop of single calls.**1011## Workflow12131. Preflight: `GET /v1/credits` + `POST /v1/batch/preview-cost` (both free).142. Validate mapping (free): `POST /bulk/validate`.153. Save the exact ordered submitted rows and their original record IDs, then submit: `POST /bulk/submit`. Bind the saved manifest to the returned job ID.164. Poll `GET /bulk/jobs/{jobId}` **≥45s apart**, or set a `callback` webhook, or stream `GET /bulk/jobs/{jobId}/stream`.175. Fetch: `GET /bulk/jobs/{jobId}/download` (file) or `/results` / `/rows` (paged). Failed rows: `/errors` (free) — fix and resubmit only the misses.186. Validate identity for the entire merge before writing. Follow the join contract below.1920## Result identity and safe joins2122- For ordinary one-input/one-result enrichment, `row_index` is **zero-based within that job's submitted data**. CSV headers and blank records are excluded. It is not the original spreadsheet row number or a result's position on a page.23- Keep an immutable manifest: `{job_id, product, rows}` with rows in exact submission order. Keep original record IDs alongside it, especially after filtering, sorting, batching, or retries. Each new job starts its own indices.24- Fetch all required pages from the manifest's job ID. `/results` returns `{rows, limit, offset}`. Status filters leave gaps in source indices. Never zip the returned array onto a CSV or use `offset + array_position` as the source index.25- Verify **every** `lm_input` identity against `manifest.rows[result.row_index]` before applying output. For profiles, also compare the returned profile identity when present. A changed profile URL may be an alias; quarantine it for review rather than silently treating it as the same person.26- Stop the merge on any mismatch, missing identity, invalid index, or ambiguous duplicate. Report counts and job ID; preserve the input file. Never guess an offset or overwrite another person's fields.27- A single source row can appear more than once in multi-product jobs. Local Leads is a fan-out product with display indices. Neither supports the simple one-result-per-source join below.2829For a **single-product `profile_search`** job, the bundled checker validates all supplied pages before creating a new JSON file. It rejects duplicate indices, wrong-job manifests, and mismatched profile identities. It does not modify the input or contact the API:3031```bash32python3 scripts/check-profile-results.py --manifest manifest.json \33 --job-id "$JOB_ID_USED_TO_FETCH" --results page-0.json page-1.json \34 --output checked-results.json35```3637Run from this skill's directory (or use its absolute script path). `--job-id` must be captured from the actual fetch URL, not copied from an unrelated manifest. Each page is the unchanged `/results` JSON response. The output contains `{source, result}` pairs, with the source taken from the saved submission, and reports whether all input rows are present. A partial result set must not be represented as a complete job. Use the retained original record IDs to merge into a master file only after validation.3839## Submit shapes4041| Variant | Body | When |42|---------|------|------|43| `POST /bulk/submit` | `rows` \| `csv` \| `fileUrl` | Auto-detect (recommended) |44| `POST /bulk/json` | `rows` | JSON array |45| `POST /bulk/csv` | `csv` | Inline CSV string |46| `POST /bulk/url` | `fileUrl` | Remote CSV/JSON/JSONL |47| `POST /bulk/file` | multipart | After `POST /bulk/upload-session` |4849```bash50curl -sS -X POST "https://api.leadmagic.io/bulk/submit" \51 -H "X-API-Key: $LEADMAGIC_API_KEY" -H "Content-Type: application/json" \52 -d '{"product":"email_finder","rows":[{"first_name":"Jane","last_name":"Doe","domain":"acme.com"}]}'53```5455## Product keys5657`email_finder` · `email_validation` · `personal_email_finder` · `mobile_finder` · `b2b_profile_to_email` · `email_to_b2b_profile` · `profile_search` · `role_finder` · `company_finder` · `company_funding` · `job_change_detector` — same per-row cost and input columns as the single-request product (column aliases normalize the same way).5859## Lifecycle & ops6061- `GET /bulk/jobs?status=&product=&limit=&offset=` — list jobs.62- `POST /bulk/jobs/{jobId}/pause` · `/resume` · `/cancel` · `/restart`.63- Diagnostics: `GET /bulk/jobs/{jobId}/events`, `/logs`, `/metrics`.64- **Out of credits mid-job:** the job pauses rather than failing — top up, then `/resume`. Report rows done vs remaining.6566## Rules6768- Always preview cost and state projected spend before submitting; get user confirmation for large jobs.69- Poll ≥45s; prefer `callback` for jobs over a few thousand rows.70- Match rate matters: since misses are free, a conservative budget = rows × cost × expected match rate; a hard ceiling = rows × cost.71- Synchronous mini-batch for small arrays (no job overhead): `POST /v1/{product}/batch` or `POST /v1/batch` (mixed products). Suppression lists: `/v1/batch/suppression-lists`.72- MCP: `submit_bulk_job`, `submit_detected_bulk_job`, `process_attached_csv` (chat CSV uploads), `get_bulk_job_status`, `get_bulk_job_rows`, `get_bulk_job_errors`, `list_bulk_jobs`.