New You can now talk to your Gelee agent from Claude, ChatGPT or Cursor through our MCP. Read the docs →

Reference

Adding leads

Source people however you like, then let Gelee enrich them, filter them and run the outreach. Works best with an evergreen campaign you top up continuously. Needs a Read + write key.

Last updated 22 October 2026

Push leads into a campaign

POST/campaigns/{id}/leadsread_write

Up to 100 leads a call. The only required field is the LinkedIn URL.

Each lead
linkedin_urlstringrequired
A profile URL, a bare handle, or a member id.
first_name, last_name, namestring
If you send the parts, we build the name.
company, titlestring
Used by your filters and exclusions.
email, location, headlinestring
Stored on the lead.
sourcestring
Your own label, so you can tell where a lead came from.
custom_fieldsobject
Merge fields for your messages, used as {{custom.key}}. See merge fields.
tagsarray
Your own labels, up to 30.
dataobject
Up to 20 of your own key and value pairs, kept on the lead.
Headers
Idempotency-Keystring
Tags the batch so you can check how it landed later.
202, not 200

The lead is saved at once, but enrichment and the campaign's full filters resolve later, from minutes to hours. Anything we can decide on the spot comes back in rejected right away.

Safe to retry. Leads are keyed on campaign plus LinkedIn profile, so sending a batch again returns the same lead_id values with created: false and creates nothing new.

curl -X POST "https://www.gelee.ai/api/v1/campaigns/9a2c…/leads" \
  -H "Authorization: Bearer gk_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: my-run-42" \
  -d '{
    "leads": [
      {
        "linkedin_url": "https://www.linkedin.com/in/janedoe",
        "first_name": "Jane",
        "last_name": "Doe",
        "company": "Acme",
        "title": "VP Sales",
        "source": "reddit-monitor",
        "custom_fields": {
          "report_link": "https://acme.example/r/5d70"
        },
        "tags": [
          "Series A"
        ]
      }
    ]
  }'
202 response
{ "batch_id": "my-run-42",
  "campaign_id": "9a2c…",
  "accepted": [ { "lead_id": "5d70…", "linkedin_id": "janedoe",
                  "name": "Jane Doe", "created": true } ],
  "rejected": [],
  "counts": { "received": 1, "accepted": 1, "rejected": 0, "duplicates": 0 } }

Why a lead was turned away

reasonMeaning
invalid_linkedin_urlNot a LinkedIn profile URL, handle or member id.
duplicate_in_requestThe same person appeared twice in one call.
blacklistedOn your do-not-contact list, by profile or by company.
already_in_another_campaignAlready being worked in another of your campaigns.
hard_filterExcluded by this campaign's filters. Comes with a lead_id and the exact reason.
invalid_fieldsA custom field or tag was not valid. detail names it.

Your do-not-contact list and the check for people already in another campaign always apply. No setting turns them off.

Check how a push landed

GET/campaigns/{id}/leads

The whole batch at once, instead of polling each lead. GET /leads/{id} returns one lead in exactly this shape, with the whole story.

Query
batch_idstring
Your Idempotency-Key, or the batch_id you got back.
sourcestring
all to include leads added any other way, not just pushed ones.
limit, offsetinteger
Default 50, max 200.
filter_verdictMeaning
pendingNot enriched yet, so the filters have not had the data to judge.
acceptedEnriched and cleared for outreach.
rejectedExcluded. filter_reason says why.

filter_reason is always worked out against your filters as they stand now. A rejected lead with filter_unknown_only: true failed only on data we never learned, not on a proven mismatch. To be told instead of asking, subscribe to the lead_accepted and lead_rejected webhooks: they fire once per pushed lead when the verdict lands.

curl "https://www.gelee.ai/api/v1/campaigns/9a2c…/leads?batch_id=my-run-42" \
  -H "Authorization: Bearer gk_your_key"
Response
{ "data": [
    { "lead_id": "5d70…", "campaign_id": "9a2c…", "linkedin_id": "janedoe",
      "name": "Jane Doe", "status": "active", "contacted": false,
      "enrichment_status": "done",
      "filter_verdict": "accepted", "filter_reason": null,
      "batch_id": "my-run-42", "source": "api_intake",
      "company": "Acme", "role": "VP Sales",
      "linkedin_url": "https://www.linkedin.com/in/janedoe", …,
      "created_at": "…", "updated_at": "…" }
  ],
  "pagination": { "limit": 50, "offset": 0, "total": 1 } }

No-code intake URL

If your tool can only paste a URL (Zapier, Make, Clay, a form), use a campaign's intake URL instead. No key, no headers, no body shape to match. Make it in the campaign's Settings, under Lead intake URL.

curl
curl -X POST "https://web.gelee.ai/api/webhooks/leads/YOUR_URL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"linkedin_url":"https://www.linkedin.com/in/janedoe",
       "first_name":"Jane","company":"Acme","title":"VP Sales"}'

It reads whatever your tool sends. Field names are matched loosely, ignoring case, spaces, underscores and hyphens.

We look forNames that work
The profile (required)linkedin_url, linkedin, profile_url, url, link, public_identifier, Person Linkedin Url
The personfirst_name, last_name, name, full_name, display_name
The companycompany, company_name, organization, employer, account_name
The roletitle, job_title, role, position
The restemail, location, city, country, headline, bio, source, list_name

Send one object, a bare list, { "leads": [ … ] }, { "data": [ … ] } or a nested { "body": … }. Up to 100 people a call. Extra columns are kept on the lead. Rows we could not read come back counted as unreadable, so a partial success is never mistaken for a full one. Everything after that works like the endpoint above. A GET on the URL returns a short note, so pasting it in a browser tells you it is alive.

Treat the URL like a password

Anyone holding it can add people to that one campaign. It cannot read anything and works for nothing else. Replace or revoke it in the campaign's settings and the old one stops working at once.