---
title: "Create or upsert contacts (no campaign required)"
method: POST
path: "/contacts"
tags: ["contacts"]
---

# Create or upsert contacts (no campaign required)

`POST /contacts`

Save contacts organically without creating a campaign first. Upserts by LinkedIn URL — if the contact already exists for this user, it updates the name and optional fields. Returns full contact objects with IDs so the AI agent can immediately log activities and update lifecycle stages..

## Request body

- object
  - `contacts` object[], required — Contacts to add (single or bulk)
    - `linkedinUrl` string, required — LinkedIn profile URL, URN, or bare vanity name
    - `profileUrn` string, nullable — LinkedIn profile URN (e.g. urn:li:fsd_profile:ACoAAA...). Improves dedup when provided alongside a vanity URL.
    - `publicIdentifier` string, nullable — LinkedIn vanity slug (e.g. joshuaau). Improves dedup when provided alongside a URN.
    - `name` string, required — Profile name
    - `source` 'likes' | 'comments' | 'reposts' | 'posts' | 'company_followers' | 'search_results' | 'manual_import' | 'event_attendees' | 'group_members' | 'engagement_scraping' | 'content_search' | 'followers_mining' | 'people_search' | 'job_search' | 'company_search' | 'network_expansion' | 'bulk_visit' — How this contact was found. Optional for organic creation (defaults to manual_import), required for campaign-based addition. Unknown values default to manual_import.
    - `sourceAngle` string — Which lead-gen angle found this (e.g., 'vp-sales-france')
    - `lifecycleStage` 'contact' | 'lead' | 'qualified' | 'rejected' — Lifecycle stage. Defaults to 'contact' on creation. Omit when adding existing contacts to avoid downgrading their stage.
    - `hotScore` integer — Contact quality score (0-100)
    - `qualificationNotes` string — Agent reasoning for qualification decision
    - `leadBrief` string — Human-readable lead summary for sales prep (2-3 sentences)
    - `notes` string

## Response `201`

Contacts created or updated

- object
  - `success` true, required
  - `results` object, required
    - `created` integer, required
    - `updated` integer, required
    - `skipped` integer, required
    - `errors` string[], required
  - `contacts` object[], required — Full contact objects with IDs for immediate use
    - `id` string, required
    - `linkedinUrl` string, required
    - `profileUrn` string, nullable, required — LinkedIn profile URN (e.g. urn:li:fsd_profile:ACoAAA...)
    - `publicIdentifier` string, nullable, required — LinkedIn vanity slug (e.g. joshuaau)
    - `name` string, required
    - `lifecycleStage` string, required
    - `hotScore` integer, required
    - `qualificationNotes` string, nullable, required
    - `leadBrief` string, nullable, required
    - `notes` string, nullable, required
    - `stageChangedAt` string, nullable, required
    - `profileData` unknown
    - `profileUpdatedAt` string, nullable, required
    - `conversationData` unknown
    - `conversationUpdatedAt` string, nullable, required
    - `outreachStatus` string, required
    - `lastContactedAt` string, nullable, required
    - `lastRepliedAt` string, nullable, required
    - `nextFollowUpAt` string, nullable, required
    - `doNotContact` boolean, required
    - `tags` string[], required
    - `createdAt` string, required
    - `updatedAt` string, required
  - `creditsUsed` integer, required — Credits consumed by this call (always 0 for contacts queries).
  - `retryAfter` integer, required — Seconds to wait before next call of the same type (always 0 for contacts queries).

## Other responses

- `400` — The server cannot or will not process the request due to something that is perceived to be a client error.
- `401` — Although HTTP specifies "unauthorized", this response means "unauthenticated". Authenticate to continue. NOTE: 401 is also returned with code "linkedin_not_connected" when the caller IS authenticated but has no connected LinkedIn account — connect LinkedIn (not re-authenticate) to continue.
- `403` — The client does not have access rights to the content.
- `404` — The server cannot find the requested resource.
- `409` — The request conflicts with the current state of the server.
- `410` — The requested content has been permanently deleted from the server.
- `422` — The request was well-formed but was unable to be followed due to semantic errors.
- `429` — Rate limit exceeded. Read error.retryAfter for the wait time in seconds.
- `500` — The server encountered a situation it does not know how to handle.
- `502` — LinkedIn returned a server error or the proxy connection failed. Retry after a few seconds.
- `503` — Proxy capacity temporarily exceeded. Retry after a few seconds.

---

[API](https://skmtc.net/berea/apis/bereach-api.md) · [All operations](https://skmtc.net/berea/apis/bereach-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/berea/bereach-api/revisions/3caf12036b26/schema)
