Skip to main content

Enrichment Submit

The Enrichment Submit endpoint is the primary programmatic entry point for submitting enrichment jobs to the eCore Platform. It accepts either a JSON payload of records or a CSV/Excel file upload, validates the request against your subscription plan and wallet balance, creates a job, deducts the estimated credits up front, and dispatches the job to the asynchronous enrichment pipeline.

The endpoint returns a job_id immediately. Enrichment continues in the background — use the Status endpoint to poll progress and the Results endpoint to fetch enriched data once the job completes.

Overview

This endpoint supports two enrichment categories:

  • Contact enrichment — enriches person-level data (email, mobile phone, job title, LinkedIn URL, etc.).
  • Account enrichment — enriches company-level firmographics (industry, revenue, employees, NAICS/SIC, etc.). Account submissions may include an optional criteria object to expand each matched account into multiple contact rows.

Base URL

EnvironmentURL
Productionhttps://<host>/api/v1/enrichment/submit/
Staginghttps://test.ecoreservice.com/backend/api/v1/enrichment/submit/

Authentication

Authentication uses a platform-issued Bearer API token passed in the Authorization header. Tokens are validated on every request — expired or revoked tokens are rejected.

Authorization: Bearer <your_token>

Two flavors of token are supported:

  • Live tokens — authenticate as a real user account. No per-second rate limit at the endpoint level; bound by wallet balance and plan limits.
  • Test tokens — authenticate as a proxy user with a daily usage quota. Exceeding the quota returns a 429.

Inactive or deleted user accounts are rejected with 401. Tokens are issued and managed in the platform admin — do not store tokens in client-side code.

Submit Endpoint

Endpoint: /api/v1/enrichment/submit/ Method: POST Description: Validate, price, persist, and dispatch a new enrichment job. Returns a job_id and the credits deducted from the wallet.

Request Headers

HeaderValueRequired
AuthorizationBearer <token>Yes
Content-Typeapplication/json (JSON mode) or multipart/form-data (file upload mode)Yes
Acceptapplication/json (recommended)Optional

Submission Modes

There are two ways to submit data:

Mode A — JSON Body: the request body is a JSON object containing all parameters, including a data array of record objects. Recommended for programmatic integration.

Mode B — File Upload: the request body is multipart/form-data containing a file field with a CSV or Excel file plus the other parameters as form fields. The endpoint parses the file into records server-side. The data parameter must not be supplied — the parsed file replaces it.

In file-upload mode, fields_to_enrich may be sent as a single comma-separated string in a form field; the server splits it into a list and trims each entry.

Request Body Parameters

ParameterTypeRequiredAllowed Values / Notes
typestringYescontact or account
levelstringYesstandard or pro (account does not accept pro)
dataarray of objectsYes (JSON mode)1 to 500 records; each item is a dict of input fields
filefileYes (file mode)CSV (.csv) or Excel (.xlsx, .xls); ≤ 500 rows after parsing
fields_to_enricharray of stringsYesNon-empty; allowed names depend on type and criteria — see Allowed Fields
criteriaobjectOptionalAccount-only; unlocks contacts-per-account flow — see Account Criteria

Example cURL Request (JSON Mode, Contact)

curl --location --request POST 'https://test.ecoreservice.com/backend/api/v1/enrichment/submit/' \
--header 'Authorization: Bearer YOUR_API_TOKEN' \
--header 'Content-Type: application/json' \
--data-raw '{
"type": "contact",
"level": "standard",
"fields_to_enrich": ["contact_email", "contact_mobile_phone", "job_title"],
"data": [
{
"first_name": "Jane",
"last_name": "Doe",
"company_name": "Acme Corp",
"contact_linkedin_url": "https://www.linkedin.com/in/janedoe"
}
]
}'

Example cURL Request (File Upload Mode, Account)

curl --location --request POST 'https://test.ecoreservice.com/backend/api/v1/enrichment/submit/' \
--header 'Authorization: Bearer YOUR_API_TOKEN' \
--form 'file=@"/path/to/companies.csv"' \
--form 'type="account"' \
--form 'level="standard"' \
--form 'fields_to_enrich="industry,revenue_range,employee_range,company_linkedin_url"'

Example cURL Request (Account With Contacts-Per-Account)

curl --location --request POST 'https://test.ecoreservice.com/backend/api/v1/enrichment/submit/' \
--header 'Authorization: Bearer YOUR_API_TOKEN' \
--header 'Content-Type: application/json' \
--data-raw '{
"type": "account",
"level": "standard",
"fields_to_enrich": ["industry", "first_name", "last_name", "contact_email"],
"criteria": {
"limit": 5,
"job_level": ["Director", "C-level"],
"job_function": "Sales"
},
"data": [
{ "company_name": "Acme Corp", "company_website": "acme.com" }
]
}'

Successful Response

On success, the endpoint returns HTTP 200 with a JSON body. The job is queued, not finished — poll the Status endpoint to track progress.

FieldTypeDescription
statusstringAlways submitted on success
job_idUUID stringUnique identifier of the created job; used for status, results, and pause/resume
estimated_creditsintegerCredits deducted from the wallet at submission time
messagestringHuman-readable confirmation

Example (Successful Response)

{
"status": "submitted",
"job_id": "9f3e7a2b-7c1d-4f12-8c9a-1b2c3d4e5f60",
"estimated_credits": 240,
"message": "Enrichment job submitted successfully"
}

Enrichment Types: Contact vs Account

AspectContactAccount
Input representsA personA company
Lookup primary keysemail, LinkedIn URL, first+last+companycompany name, website
Allowed levelsstandard, prostandard only
criteria honoredNoYes — optional
Multiple output rows per inputNo (1 input → 1 output)Yes when criteria.limit set (1 account → N contacts)

Contact enrichment matches against DataCore, Snowflake, scraped LinkedIn data, BigQuery, and (at Pro level) external scrapers. Contact submissions may also request company-level fields — those are filled from the matched person's company record.

Account enrichment matches companies via Meilisearch and DataCore lookups and fills firmographic fields. Account is restricted to standard level. When criteria with limit is supplied, the pipeline expands each matched account into up to N contacts that satisfy the supplied filters.

Enrichment Levels: Standard vs Pro

Standard — uses internal sources only (DataCore, Snowflake, BigQuery, scraped LinkedIn snapshots). Lower per-row cost. Suitable for bulk enrichment where coverage from internal databases is sufficient.

Pro — Standard sources plus paid external sources (Netnut LinkedIn scraping, ZeroBounce / AnyMailFinder email validation, Serper Google search fallbacks). Higher per-row cost; better coverage when internal databases lack the contact.

Quick — an internal mode used by other entry points (such as the Pro-Quick contact enrichment flow). The submit endpoint does not accept quick as a level value — the only valid choices on this endpoint are standard and pro. When Quick jobs do run elsewhere on the platform, they skip name-match verification and still-in-company verification entirely; pricing follows the same per-row table as Standard.

Required Identifier Fields per Record

Every record in data must carry at least one identifier the pipeline can match on. Records that fail this check cause the entire submission to be rejected with a 400 citing the offending record number.

For type = "contact", at least one of:

  • contact_email
  • contact_linkedin_url
  • company_name
  • first_name
  • last_name

For type = "account", at least one of:

  • company_name
  • company_website

The more identifiers a record has, the higher the match success rate. A record with email + LinkedIn URL + name + company will match much more reliably than one with only an email — particularly when Pro-level fallbacks are required.

Allowed Fields in fields_to_enrich

Validation rules:

  • Must be a non-empty list of field names.
  • May be passed as a JSON array OR (in file-upload mode) a single comma-separated string.
  • Whitespace around each name is stripped.
  • Any field not in the allowed set causes a 400 listing every invalid field.

Allowed Contact fields (type = "contact") — Contact requests may also include any Account field, since contact rows expose company data:

first_name, last_name, job_title, job_level, job_function,
contact_email, contact_linkedin_url,
contact_office_street, contact_office_city, contact_office_state,
contact_office_postal_code, contact_office_country,
contact_office_phone, contact_mobile_phone, contact_direct_phone,
contact_office_country_code, contact_office_state_code

Allowed Account fields (type = "account") — Account requests are restricted to account fields unless criteria is supplied (in which case Contact fields are also permitted, applied to the contacts returned for each account):

company_name, company_hq_street, company_hq_city, company_hq_state,
company_hq_postal_code, company_hq_country, company_website,
industry, sub_industry, company_linkedin_url, company_hq_phone,
revenue_range, employee_range, year_founded,
company_hq_country_code, company_hq_state_code,
industry_naics_code, industry_naics_description,
industry_sic_code, industry_sic_description

Account Criteria (Contacts-Per-Account)

The optional criteria object only applies to Account submissions. When provided, the pipeline expands each matched account into multiple contact rows that satisfy the filters supplied.

Sub-fieldTypeRequiredDescription
limitinteger ≥ 1Required to activateMaximum contacts returned per account
job_levelstring or array of stringsOptionalFilter by job level (e.g., Director, C-level)
job_functionstring or array of stringsOptionalFilter by job function (e.g., Sales)
keywordstring or array of stringsOptionalFree-text keyword filter applied to contact records

Behavior notes:

  • If limit is missing, null, empty, or zero, the criteria is ignored and the submission behaves as a pure account-only enrichment.
  • List-typed sub-fields accept either a single string (treated as a one-item list for backwards compatibility) or an array of strings.
  • Whitespace-only entries are stripped; if a list becomes empty after stripping, that sub-field is dropped.
  • When criteria with limit is in effect, total credits scale by limit — the per-account cost is multiplied by limit.
  • When criteria.limit is set, Contact fields become valid choices in fields_to_enrich and are applied to the returned contact rows.

File Upload Format & Parsing Rules

When using file-upload mode, the file is parsed server-side before validation runs.

  • Accepted extensions: .csv, .xlsx, .xls. Any other extension returns a parsing error.
  • Files exceeding 500 rows are rejected with an explicit error message stating the file's row count.
  • Column headers are normalized: whitespace stripped, lowercased, internal spaces replaced with underscores. For example, First Name becomes first_name.
  • Empty cells are converted to empty strings (no nulls).
  • The parsed records are validated against the same identifier-presence rules as JSON-mode submissions.
  • fields_to_enrich may be sent as a single comma-separated string in form fields; the server splits and trims it.

Validation Order

Validation happens in this deterministic order. The first failure short-circuits and returns its specific error response:

  1. Authentication — Bearer token must be present and valid.
  2. File parsing (file mode only) — extension, row count, column normalization.
  3. Schema validationtype{contact, account}; level{standard, pro}; data is a list of dicts with length 1–500; fields_to_enrich is a non-empty list of strings; criteria (if supplied) conforms to its schema.
  4. Type/level compatibilityaccount + pro is rejected.
  5. Allowed-fields validation — every entry in fields_to_enrich must belong to the allowed set for (type, has_criteria).
  6. Identifier-presence validation — every record must contain at least one allowed identifier.
  7. Plan-limit validation — record count ≤ user's subscription enrichment_limit (default 50 if no active subscription).
  8. Wallet pre-check — wallet balance must be ≥ estimated credit cost.
  9. Job creation — a job row is created; its UUID becomes the job_id.
  10. Credit deduction — credits are deducted under a row-level lock; on failure the just-created job is deleted and a 402 is returned.
  11. Task dispatch — input data is cached and the asynchronous enrichment task is dispatched to the queue.

Plan Limits & Records-Per-File

Each subscription plan defines an enrichment_limit that caps the number of records permitted in a single submission.

  • If the user has no active subscription, a default limit of 50 records applies.
  • If the request exceeds the limit, the response is 400 with error, limit, and requested fields.
  • The hard ceiling enforced by the parsing layer is 500 records regardless of plan; plans with higher limits are still capped at 500 per submission.
  • To process larger datasets, split the data across multiple submissions.

Credit Pricing & Estimation

All enrichment costs flow through the platform's dynamic pricing configuration (cached for 5 minutes). There are no hardcoded prices.

Per-row pricing categories

CategoryStandard / Quick ratePro rate
Email enrichmentstandard_email_costpro_email_cost
Phone enrichmentstandard_phone_costpro_phone_cost
Account enrichmentaccount_standard_costaccount_pro_cost (account does not accept Pro on submit)

Estimation algorithm

  1. If any phone field is in fields_to_enrich (contact_mobile_phone or contact_direct_phone), the per-row rate is the phone rate.
  2. Else if contact_email is in fields_to_enrich, the per-row rate is the email rate.
  3. Else if type is account, the per-row rate is the account rate.
  4. Else (contact without email/phone), the per-row rate defaults to the email rate.

Total = row_count × per_row_rate

When type is account AND fields are not phone/email AND criteria.limit is set, the total is multiplied by criteria.limit (you pay for each contact returned per account).

The estimate is rounded to an integer when surfaced via the API response.

Reconciliation

The upfront estimate may exceed the actual cost. The pipeline reconciles credits at the end of the job — rows that fail to enrich, fail name-match verification, or are flagged as "left the company" have their charges reversed.

Wallet Deduction & Refund Behavior

  • Credits are deducted immediately on successful submission, before the asynchronous task starts running.
  • Deduction is wrapped in a row-locked transaction (SELECT … FOR UPDATE) to prevent concurrent double-spend.
  • If deduction fails (e.g., balance dropped between pre-check and deduction), the just-created job is deleted and a 402 error is returned.
  • During enrichment, rows that fail to enrich or fail post-checks (name-match, still-in-company) have their per-row charges reversed.
  • Pause-then-resume re-estimates the remaining work and deducts only the cost for the unprocessed remainder; the original deduction is not refunded but no double-charge occurs.
  • Resuming a job whose pause checkpoint is missing or invalid is rejected (400) to prevent a second full-job charge after a crash. See Pause and Resume.

Error Responses

HTTPWhenBody Shape
400Validation failure (schema, fields, identifiers, type/level mismatch, plan limit exceeded, file parse error, unsupported file type){ "error": "..." } plus contextual fields like limit/requested for plan errors, or DRF field error dict
401Missing or invalid Bearer token, or inactive/deleted user{ "message": "Invalid or expired token" } or { "message": "User is inactive or deleted" }
402Insufficient wallet balance{ "error": "You require N credits, but only M are available." }
429Test-token daily usage cap exceeded{ "error": { "message": "...", "current_usage": N, "daily_limit": M, "resets_at": "midnight UTC" } }
500Unexpected server-side failure during job creation/dispatchStandard DRF/Django error envelope

Common 400 examples

{ "level": "Account enrichment only supports 'standard'." }
{ "fields_to_enrich": "Invalid fields: ['some_unknown_field']" }
{ "data": "Record 3: needs at least one of ['company_name', 'contact_email', 'contact_linkedin_url', 'first_name', 'last_name']" }
{ "error": "Plan limit exceeded", "limit": 100, "requested": 250 }
{ "error": "Maximum 500 records. File has 712." }
{ "error": "Unsupported format. Use CSV or Excel." }

Job Lifecycle After Submission

Once submitted, a job moves through these stages. Some are conditional (e.g., email validation only runs when contact_email is requested):

  1. Uploading / Draft — record created, awaiting task pickup.
  2. Enrichment Started — the asynchronous task has begun.
  3. Preparing Data — input snapshot stored as imported_* fields, internal UUIDs assigned.
  4. DataCore lookup — match against eCore's primary contact/account database.
  5. Scraped DB lookup — match against scraped LinkedIn snapshots.
  6. BigQuery lookup — match against the BigQuery contact dataset.
  7. Meilisearch lookup (account only) — fast company name lookup.
  8. Pro enrichment (Pro only) — Netnut, Serper, ZeroBounce / AnyMailFinder fallbacks.
  9. Email validation — runs only when contact_email is in fields_to_enrich; non-deliverable emails are dropped and the row's email charge is reversed.
  10. BigQuery phone lookup — runs only when phone fields are requested.
  11. Name-match verification — Standard/Pro contact jobs only (skipped in Quick).
  12. Still-in-company check — Standard/Pro contact jobs only (skipped in Quick).
  13. Calculating stats — per-field counts (enriched / partially_enriched / not_enriched).
  14. Reconciling credits — reversal of charges for rows that failed verification.
  15. Saving results — chunked write to Firestore.
  16. Saving row results — per-row records saved to relational store for fast resume and audit.
  17. Completed — status becomes Enrichment Completed; results are downloadable.
  18. Paused — if the user pauses mid-run, status becomes Paused with a checkpoint.
  19. Failure — unrecoverable error; pipeline writes failure metadata; no further work is done.

Idempotency

The endpoint is not idempotent. Submitting the same payload twice creates two jobs and consumes credits twice.

  • Always store the returned job_id and check its status before retrying.
  • On network errors that may have succeeded server-side, query the status list endpoint and look for a recently-created job matching your record count before resubmitting.
  • The job_id is a UUID4 generated server-side; clients cannot supply their own.

Rate Limits & Concurrency

  • Live API tokens have no per-second rate limit defined at the endpoint level — bound by wallet balance and plan limits.
  • Test tokens have a daily usage cap; exceeding it returns a 429.
  • Multiple jobs can run concurrently for the same user; each job consumes its own credits and has its own job_id.
  • Server-side concurrency on credit deduction is protected with a row-level lock (SELECT … FOR UPDATE) — concurrent submissions cannot overdraw the wallet.

Common Errors & How to Handle Them

SymptomLikely CauseRecommended Fix
401 Invalid or expired tokenToken wrong, deleted, or user inactiveRe-issue token via admin; verify user is active
400 Account enrichment only supports standard.Sent type=account with level=proUse level=standard for account jobs
400 Invalid fields: [...]Field name not in allowed set for this type/criteria comboCross-check against Allowed Fields; for account+contacts, ensure criteria is provided
400 Record N: needs at least one of [...]A record has no email, LinkedIn URL, name, or companyAdd at least one identifier to that record before resubmitting
400 plan limit exceededRecords exceed plan's enrichment_limitSplit into multiple submissions or upgrade plan
400 Maximum 500 records. File has X.File too largeSplit the file into chunks of ≤ 500 rows
400 Unsupported format. Use CSV or Excel.Wrong file extensionRe-export as .csv, .xlsx, or .xls
402 You require N credits, but only M are available.Insufficient wallet balanceTop up the wallet or reduce the submission size
429 daily usage exceededTest token quota hitWait until midnight UTC or use a live token
Status stuck in-progress for hoursWorker processing or external API hangCheck status; if no progress, contact support — pause/resume may recover
Status reaches paused unexpectedlyUser or system requested pauseResume via /api/v1/enrichment-control/ with action="resume"

Operational Notes

  • Input data is cached in Redis for 1 hour after submission so the worker can pick it up without re-uploading.
  • Results are persisted in Firestore in chunks of 300 records per document.
  • Per-row results are also persisted in the relational store, keyed by the row's internal UUID, to support resume without losing previously-charged work.
  • All UUIDs used internally are stored as 32-character hex strings (no dashes); the external job_id is the standard UUID format with dashes.
  • Internal _*-prefixed fields are stripped before any persistence step that produces user-visible output.

Glossary

TermMeaning
JobA single enrichment submission, identified by job_id (UUID)
RecordOne row of input data (one person or one company)
IdentifierA field used to match an input record to data sources
Enrichment levelStandard (internal sources) vs Pro (internal + paid external sources)
Enrichment typeContact (people) vs Account (companies)
fields_to_enrichThe list of field names the pipeline should attempt to fill in
criteriaOptional contacts-per-account filters used with type=account
WalletThe user's credit balance held in the platform
CheckpointA snapshot of rows_processed / state used by pause/resume
imported_*Frozen snapshot of the caller's original input on each output row
enrichment_statusenriched, partially_enriched, or not_enriched
enrichment_sourceWhich data source produced the enriched values for the row

Try It Out

Explore the Enrichment Submit API in the API Playground (password: EluuEz0J).