Skip to main content
PUT
/
api
/
lists
/
{id}
/
import
Confirm a previewed CSV import
curl --request PUT \
  --url https://app.puffle.ai/api/lists/{id}/import \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "csv_text": "<string>",
  "mapping": {},
  "skipExistingCustomers": true
}
'
{ "imported": 4503599627370495, "updated": 4503599627370495, "skipped": 4503599627370495, "crossListSkipped": 4503599627370495, "total": 4503599627370495, "rowIds": [ "3c90c3cc-0d44-4b50-8888-8dd25736052a" ], "companiesCreated": 4503599627370495, "companiesLinked": 4503599627370495, "blockedSkipped": 4503599627370495, "enrichmentTriggered": 4503599627370495, "warnings": [ "<string>" ] }

Overview

Two-step flow for importing into an existing list:
  1. POST /api/lists/{id}/import — upload the CSV/XLSX, parse it, and get back an auto-detected mapping plus the normalized csv_text. No rows are written.
  2. PUT /api/lists/{id}/import (this endpoint) — replay csv_text with a confirmed mapping to actually insert + merge rows.
The confirm step dedupes incoming rows against the list in four cascading passes — LinkedIn URL, then full name + company, then full name + location, then full name only. Matched rows are merged (new data fills in gaps; existing non-empty values win). New rows are inserted. Company firmographics are extracted by domain, creating or linking list_company_rows as needed. Rows whose company matches the caller’s blocked_companies list are skipped. After row writes, the server dispatches sheet-ai-enrich Trigger.dev runs for every AI-powered column on the list. Custom columns created during this import enrich every row in the list; pre-existing AI columns enrich only the freshly affected rows.

AI agent notes

Typical sequence.
  1. POST /api/lists/{id}/import (multipart/form-data with file) to preview.
  2. Let the user confirm / tweak the returned mapping.
  3. PUT /api/lists/{id}/import with csv_text, the final mapping, and skipExistingCustomers (default true).
Mapping keys. Built-ins are linkedin_url, first_name, last_name, full_name, headline, company, location, domain. Custom columns use column:<Name> (map into an existing custom column) or new:<Name> (create a new column at import time — the server infers source and response_type via LLM heuristics).Credit model. This endpoint does not check or deduct credits — that’s done by confirmCreateWithImport. Free to call on an existing list.Counter semantics. imported counts new rows; updated counts merged duplicates; skipped is the union of all skip reasons; crossListSkipped and blockedSkipped break skipped down. rowIds contains every affected row id (insert + update) — use this as the seed for any follow-up operations (e.g. triggering a specific enrichment).AI column enrichment. enrichmentTriggered is the count of sheet-ai-enrich Trigger.dev runs dispatched. Poll the column’s cells (GET /api/lists/{id}/columns/{colId}/preview-cells or listen via Supabase Realtime) to observe completion.Warnings are non-fatal. If warnings is present, inspect it — the import still succeeded for all rows the server could process, but individual columns or enrichment dispatches failed.

Authorizations

Authorization
string
header
required

Bearer authentication header of the form Bearer <token>, where <token> is your auth token.

Path Parameters

id
string<uuid>
required

List UUID

Pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$

Body

application/json

JSON body for the confirm step. This DOES accept JSON (the preview step was multipart).

csv_text
string
required

The exact csv_text returned by previewListImport. The server re-parses it; do not alter whitespace or quoting.

Minimum string length: 1
mapping
object
required

Finalized column mapping. Built-in field keys: linkedin_url, first_name, last_name, full_name, headline, company, location, domain. Custom columns use column:<Name> (map into an existing custom column) or new:<Name> (create a new custom column at import time). Values are the CSV column name, or null to skip.

skipExistingCustomers
boolean

Default true. When on, rows already present in any of the caller's other lists (cross-list dedup) and rows matching the blocked_companies list are skipped.

Response

Import completed. See imported / updated / skipped.

imported
integer
required

Number of new list_rows inserted.

Required range: 0 <= x <= 9007199254740991
updated
integer
required

Number of existing rows merged with incoming data (dedup matched on LinkedIn URL → name+company → name+location → name).

Required range: 0 <= x <= 9007199254740991
skipped
integer
required

Total rows skipped — union of cross-list dedup and other skip reasons.

Required range: 0 <= x <= 9007199254740991
crossListSkipped
integer
required

Rows skipped because the person is already in another list.

Required range: 0 <= x <= 9007199254740991
total
integer
required

Total rows in the parsed CSV.

Required range: 0 <= x <= 9007199254740991
rowIds
string<uuid>[]
required

All list_rows affected (inserted + updated).

Pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
companiesCreated
integer
required

New company rows created and attached to this list.

Required range: 0 <= x <= 9007199254740991
companiesLinked
integer
required

Rows that were successfully linked to a company.

Required range: 0 <= x <= 9007199254740991
blockedSkipped
integer
required

Rows skipped because the associated company is on the user's blocked_companies list.

Required range: 0 <= x <= 9007199254740991
enrichmentTriggered
integer
required

Number of sheet-ai-enrich Trigger.dev runs dispatched for AI columns.

Required range: 0 <= x <= 9007199254740991
warnings
string[]

Non-fatal issues (column creation errors, enrichment dispatch failures). Present only when there is at least one.