Skip to main content
POST
/
api
/
campaigns
/
{id}
/
leads
/
import-from-list
Import prospects from a saved list
curl --request POST \
  --url https://app.puffle.ai/api/campaigns/{id}/leads/import-from-list \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "listId": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
'
{ "imported": 420, "duplicates": 30, "invalid": 50, "errors": 0, "total": 500 }

Overview

Hydrates every row of a list (from the lead-gen / lists feature) into the campaign as prospects. The server paginates through list_rows 1000 at a time and pulls:
  • linkedin_url — direct from the row
  • email — from profile_data.enrichment.work_email when present
  • Profile fields — firstName, lastName, fullName, currentCompany, currentRole, headline — from profile_data
Invalid rows (missing the required identity for the campaign channel) are skipped. Duplicates are caught by the DB unique constraint. Campaign stats are recomputed atomically after insert.

AI agent notes

Channel-specific validation. Email campaigns require profile_data.enrichment.work_email on the row. LinkedIn campaigns require a real linkedin_url — placeholder imported-* URLs (used when a list was imported from a non-LinkedIn source) do NOT count and are rejected as invalid.Response shapes.
  • 200 — import succeeded (fully or partially); imported, duplicates, invalid, errors, total all returned.
  • 206 — a page fetch failed mid-run after earlier rows had already committed. partial: true. Retry with the same listId to pick up remaining rows — already-imported prospects will be rejected as duplicates.
  • 422 — the list was readable but zero prospects could be imported: every row invalid, every row a duplicate, or a mix of the two with zero new inserts. The response body still contains the count breakdown.
  • 500 — failed to fetch any list rows, or every insert batch errored.
Partial success guard. If pagination errors after some writes, the server returns 206 with partial: true rather than rolling back. Agents should surface this as “imported so far” in UI.Dedup semantics. Same as addCampaignProspects — the unique constraint is (campaign_id, email) for email campaigns and (campaign_id, linkedin_url) for LinkedIn campaigns.Memory. Paginated fetch + batched insert (500/batch) keeps memory bounded for large lists. Safe for lists up to tens of thousands of rows.

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
listId
string<uuid>
required

UUID of a saved list (lists table) owned by the caller. Every row is imported as a prospect into the campaign.

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)$

Response

Import succeeded. Returns counts for imported, duplicate, invalid, and error rows, plus the total inspected.

imported
integer
required

Prospects actually inserted.

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

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

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

Rows missing the required identity field — for LinkedIn campaigns that's a real linkedin_url (placeholder imported-* URLs don't count), for email campaigns it's a work email in the list's enrichment data.

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

Rows that failed to insert for reasons other than duplicate key (e.g. DB errors).

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

Total list rows inspected (sum of all pages).

Required range: 0 <= x <= 9007199254740991
partial
boolean

Present on 206 responses. Indicates a page fetch failed after some rows had already been inserted — the counts reflect the committed subset.