API & webhooks

Work with Leadvect from your own code, Zapier, Make or n8n: add leads to lists, create and move CRM deals, log calls, read replies, enrich businesses — and get a signed call the moment something happens.

AuthenticationListsDealsRepliesEnrichmentWebhooksEventsMeetingsSignaturesErrors & limits

Authentication

Create a key on Integrations (workspace owners and admins). Send it with every request:

curl https://leadvect.com/v1/me \
  -H "Authorization: Bearer lv_YOUR_KEY"

Base URL https://leadvect.com/v1. Bodies are JSON (Content-Type: application/json). A key acts for its workspace, with that workspace’s plan and allowances. Keep keys on servers — never in a web page or app.

Lists

RequestWhat it does
GET/listsYour lists, with member counts.
POST/listsCreate a list. {"name": "Paris bakeries", "type": "segment"} → {"id"}
GET/lists/{id}/membersMembers, newest first. ?limit= (≤ 500) &offset=
POST/lists/{id}/membersAdd businesses (up to 10,000 per call; repeats are ignored).
curl -X POST https://leadvect.com/v1/lists/LIST_ID/members \
  -H "Authorization: Bearer lv_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"leads":[{"name":"Du Pain et des Idées","domain":"dupainetdesidees.com","city":"Paris","country":"FR","email":"contact@dupainetdesidees.com"}]}'

Lead fields: name, domain, email, phone, city, country (2 letters), category, address, website, social.

Deals (CRM)

RequestWhat it does
GET/deals?stage= ?q= (name, email, company, domain) ?updated_since= (ISO date) ?limit= (≤ 1000)
POST/dealsCreate — or return the existing deal with the same email (or phone, when there’s no email). 201 when new.
GET/deals/{id}The deal and its timeline notes.
PATCH/deals/{id}Update any field below (POST works too).
DELETE/deals/{id}Delete the deal and its notes.
POST/deals/{id}/notes{"body": "Sent the price list"}
POST/deals/{id}/activityLog a call or WhatsApp message: {"kind":"call","outcome":"call_back","note":"…","nextStep":"Call back","nextDue":"2026-11-03"}

Deal fields: company, contactName, contactEmail, phone, domain, city, country, stage, value, currency, nextStep, nextDue (YYYY-MM-DD), lostReason.

Stages: new · contacted · replied · interested · meeting · proposal · won · lost. Call outcomes: connected · no_answer · voicemail · call_back · interested · not_interested · wrong_number (a logged activity only moves a deal forward).

Replies

RequestWhat it does
GET/repliesReplies to your outreach, newest first, with the AI label. ?since= (ISO) ?label= ?limit= (≤ 200)

Labels: interested · question · meeting · not_now · not_interested · wrong_person · unsubscribe · out_of_office · other.

Enrichment

RequestWhat it does
POST/enrich{"domain":"septime.fr","name":"Septime","country":"FR"} → contacts (emails verified), tech, signals, lead score. 1 enrichment from your plan.
POST/owner{"name":"Septime","domain":"septime.fr","country":"FR"} → owners and decision makers (website, company registries). 1 enrichment.

Webhooks

Add an address on Integrations, or from code (Zapier-style REST hooks):

RequestWhat it does
GET/webhooksYour webhooks.
POST/webhooks{"url":"https://…","events":["reply.received"]} → the webhook and its signing secret (shown once).
DELETE/webhooks/{id}Remove it.

Each event is a POST of JSON to your https address. Answer with any 2xx within 10 seconds. Anything else is retried after 1 min, 5 min, 30 min, 2 h, 6 h and 24 h (7 tries in all); redirects are not followed. Answer 410 Gone to switch the webhook off. A webhook that fails 25 times in a row is paused (turn it back on from Integrations).

Events

{
  "id": "evt_6f1c…",
  "type": "deal.stage_changed",
  "created_at": "2026-10-28T14:03:11.000Z",
  "workspace_id": "…",
  "data": {
    "deal": { "id": "…", "company": "Septime", "contact_email": "direction@septime.fr", "stage": "meeting", "value": 1200, "currency": "EUR", "url": "https://leadvect.com/crm?deal=…", … },
    "from": "interested",
    "to": "meeting"
  }
}
Typedata
reply.receivedemail, name, company, subject, text, label, label_by (rules | ai), deal_id, received_at
deal.createddeal
deal.stage_changeddeal, from, to
activity.loggedkind (call | whatsapp), outcome, note, next_step, next_due, deal
meeting.bookedstatus (booked | rescheduled), email, name, start, end, title, source, deal
meeting.cancelledSame fields, status = cancelled.
contact.unsubscribedemail, source (link | reply | email), campaign_id
test.pingSent by the Test button.

The same event can arrive twice (a retry after a timeout): use id to ignore repeats. Deals imported from a CSV don’t send deal.created.

Meetings booked

Your workspace has a private address (Integrations → Meetings) that booking tools call when someone books, reschedules or cancels: POST https://leadvect.com/in/meetings/<your address>. It takes Cal.com and Calendly webhooks as they are, or plain JSON (or form fields) from Zapier, Make or n8n:

{"email": "nadia@bistro.example", "start": "2026-11-04T10:30:00Z", "title": "Intro call", "status": "booked", "ref": "s_…"}

status: booked (default) · rescheduled · cancelled. ref is optional: the utm_content that {{meeting_link}} puts on your booking page link, so a booking made with another address still reaches the right deal. The deal moves to meeting (never backwards), the meeting becomes its next step, and meeting.booked / meeting.cancelled fire.

Checking signatures

Every call carries Leadvect-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw body>" with your webhook’s signing secret. Also sent: Leadvect-Event and Leadvect-Delivery.

// Node.js
import crypto from 'node:crypto';

function fromLeadvect(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // 5 minutes
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ''));
}

Errors & limits

Questions or a missing endpoint? Write to support.