Skip to main content
POST
/
api
/
campaigns
/
{id}
/
leads
/
confirm-csv
Import CSV leads with a confirmed mapping
curl --request POST \
  --url https://app.puffle.ai/api/campaigns/{id}/leads/confirm-csv \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "lead_list_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
  "mapping": {
    "email": "<string>",
    "linkedin_url": "<string>",
    "first_name": "<string>",
    "last_name": "<string>",
    "company": "<string>",
    "position": "<string>",
    "headline": "<string>"
  },
  "csv_text": "<string>"
}
'
{ "imported": 95, "duplicates": 20, "invalid": 5 }

Overview

Commits the CSV upload started by uploadCampaignCsv. Takes the lead_list_id and csv_text from step 1, along with the final mapping (either the auto-detected one verbatim or an edited version), and inserts prospects into the campaign. Unlike addCampaignProspects, this endpoint handles both invalid rows (skipped with reason) and duplicates (caught by DB unique constraint), and returns a three-way breakdown: imported, duplicates, invalid. Campaign stats are recomputed atomically on success.

AI agent notes

Required identity mapping. mapping.email is required for email campaigns; mapping.linkedin_url is required for LinkedIn campaigns. A missing identity mapping returns 400 before any writes.Row-level validation. Email campaigns drop rows where the mapped email doesn’t match a basic email regex. LinkedIn campaigns drop rows whose mapped URL doesn’t contain linkedin.com. Dropped rows land in the invalid count, not errors.Normalization. Emails are lowercased. LinkedIn URLs drop trailing slashes and query strings. name is auto-computed from first_name + last_name when both are present.Custom fields. Every source column that is not in mapping’s values is rolled into each prospect’s custom_fields. The key is normalized (lowercased, spaces → underscores, non-alphanum stripped). Rendered in templates as %custom_fields.<normalized_key>%.Body size. csv_text is capped at 10 MB to match the uploadCampaignCsv limit. Anything larger returns 400 without touching the database.Idempotency. Safe to retry — the DB unique constraint catches re-runs. Reported duplicates will absorb the re-insert attempts.

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

Campaign 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

Step 2 of the CSV import flow. Confirms the mapping produced by uploadCampaignCsv and commits the prospects.

lead_list_id
string<uuid>
required

lead_list_id returned from uploadCampaignCsv. Must belong to this campaign + caller.

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)$
mapping
object
required

Target field → source column header. Either the auto-detected mapping from uploadCampaignCsv verbatim, or an edited version. email is required for email campaigns; linkedin_url is required for LinkedIn campaigns. Unmapped source columns become entries in each prospect's custom_fields.

csv_text
string
required

The csv_text returned from uploadCampaignCsv. Maximum 10 MB. The server re-parses this with the confirmed mapping.

Maximum string length: 10485760

Response

Import succeeded (fully or partially). Returns counts for imported, duplicate, and invalid rows.

imported
integer
required

Prospects actually inserted.

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

Prospects rejected by the DB unique constraint (already in this campaign).

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

Source rows skipped because the identity value was missing or malformed (non-email string on an email campaign; non-LinkedIn URL on a LinkedIn campaign).

Required range: 0 <= x <= 9007199254740991