Overview
The LeadGrid API lets you create and manage dossiers, update stages, attach notes, and sync data with your own tools. Every request returns JSON.
All timestamps are ISO 8601 in UTC. All amounts are integers or decimals in the smallest practical unit (e.g. euros, not cents). Pagination uses page and per_page query parameters; responses include a meta object with the total count.
Authentication
Every request must carry an API key in the Authorization header. Keys start with lg_live_ and are scoped to one organization.
- 1Go to Settings → API and click Create API key.
- 2Pick the scopes you need (see table below) and optionally set an expiry. The full key is shown once . Copy it immediately.
- 3Send the key as a bearer token on every request.
curl https://leadgrid.io/api/v1/dossiers \ -H "Authorization: Bearer lg_live_your_key_here"
| Field | Type | Description |
|---|---|---|
| dossiers:read | scope | List and retrieve dossiers. |
| dossiers:write | scope | Create, update and archive dossiers. Also required to move stages. |
| contacts:read | scope | List and retrieve contacts, and read their dossier links. |
| contacts:write | scope | Create, update and delete contacts. Required alongside dossiers:write to link or unlink contacts on a dossier. |
| flows:read | scope | List flows and read their stages. |
| notes:read | scope | Read notes on dossiers. |
| notes:write | scope | Add new notes to dossiers. |
Dossiers
Dossiers are the entities you track, candidates in recruitment or leads in sales. Each dossier belongs to exactly one flow and sits in exactly one stage.
/dossiersList dossiers in your organization. Supports filtering and pagination.
| Field | Type | Description |
|---|---|---|
| type | string | 'candidate' or 'sales'. |
| status | string | 'active', 'won', 'lost' or 'archived'. |
| stage_id | uuid | Return only dossiers currently in this stage. |
| page | integer | 1-based page number. Default: 1. |
| per_page | integer | Items per page (max 100). Default: 25. |
curl "https://leadgrid.io/api/v1/dossiers?type=sales&status=active" \ -H "Authorization: Bearer lg_live_your_key"
{
"data": [
{
"id": "6f2b…",
"type": "sales",
"name": "Rabobank, Talent rollout",
"company": "Rabobank",
"contact_person": "Mark de Vries",
"deal_size": 45000,
"deal_currency": "EUR",
"status": "active",
"current_stage_id": "c3e1…",
"assigned_to": "ab12…",
"created_at": "2026-04-10T09:21:14Z"
}
],
"meta": { "total": 34, "page": 1, "per_page": 25 }
}/dossiersCreate a new dossier. If flow_id is omitted the default flow for the given type is used; the dossier starts in that flow's first stage. Pass an optional contact_id to link the dossier to an existing Contact instead of creating a new one. Send application/json for a plain create, or multipart/form-data with a 'cv' file field to create the dossier AND attach a PDF CV in one atomic call: if the upload fails, the dossier is rolled back.
| Field | Type | Description |
|---|---|---|
| type* | string | 'candidate' or 'sales'. |
| name* | string | For candidates: the person's name. For sales: deal or account name. |
| string | Primary contact email. | |
| phone | string | Primary contact phone. |
| company | string | Candidate: current employer. Sales: target company. |
| role | string | Candidate: role they're applying for. Sales: role of the contact. |
| contact_id | uuid | Link the dossier to an existing Contact instead of creating a new one. Must belong to your organization. |
| flow_id | uuid | Override the default flow. Must belong to your organization. |
| assigned_to | uuid | User ID of the member to assign this dossier to. |
| intake_notes | string | Director/intake notes shown in the dossier drawer. |
| contact_person | string | Sales only. Named contact at the target company. |
| deal_size | number | Sales only. Expected contract value. |
| deal_currency | string | Sales only. ISO 4217 currency code (e.g. 'EUR'). |
| cv | file (pdf) | multipart/form-data only. Optional PDF CV (max 10 MB). Uploaded and attached in the same call. If the upload fails, the dossier is rolled back. |
# JSON, plain create
curl -X POST https://leadgrid.io/api/v1/dossiers \
-H "Authorization: Bearer lg_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"type": "sales",
"name": "KLM, Cabin crew hiring",
"company": "KLM",
"contact_person": "Pieter van Leeuwen",
"deal_size": 62000,
"deal_currency": "EUR"
}'
# multipart, create + attach CV in one call
curl -X POST https://leadgrid.io/api/v1/dossiers \
-H "Authorization: Bearer lg_live_your_key" \
-F "type=candidate" \
-F "name=Sophie van Dijk" \
-F "role=Senior Frontend Engineer" \
-F "email=sophie@example.com" \
-F "cv=@./resume.pdf"{
"data": {
"id": "a91c…",
"type": "candidate",
"name": "Sophie van Dijk",
"cv_url": "<org-id>/dossiers/a91c…/cv.pdf",
"status": "active",
"current_stage_id": "b77f…",
"created_at": "2026-04-15T12:03:40Z"
}
}/dossiers/:idRetrieve a single dossier by ID.
curl https://leadgrid.io/api/v1/dossiers/a91c… \ -H "Authorization: Bearer lg_live_your_key"
{
"data": {
"id": "a91c…",
"type": "sales",
"name": "KLM, Cabin crew hiring",
"deal_size": 62000,
"current_stage_id": "b77f…"
}
}/dossiers/:idUpdate any subset of fields. Setting current_stage_id moves the dossier to a new stage and emits a dossier.stage_changed webhook.
| Field | Type | Description |
|---|---|---|
| name | string | Rename the dossier. |
| string | Update primary contact email. | |
| phone | string | Update phone. |
| company | string | Update company / employer. |
| role | string | Update role. |
| contact_person | string | Sales only. |
| deal_size | number | Sales only. |
| deal_currency | string | Sales only. |
| status | string | 'active', 'won', 'lost' or 'archived'. |
| assigned_to | uuid | Reassign to another member. Null to unassign. |
| intake_notes | string | Replace intake notes. |
| current_stage_id | uuid | Move to a new stage. Must belong to the dossier's flow. |
curl -X PATCH https://leadgrid.io/api/v1/dossiers/a91c… \
-H "Authorization: Bearer lg_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "current_stage_id": "d8e2…", "status": "active" }'{
"data": {
"id": "a91c…",
"current_stage_id": "d8e2…",
"status": "active"
}
}/dossiers/:idArchives the dossier (soft delete). Sets status to 'archived' and emits dossier.deleted. Data is preserved and can be restored via PATCH.
curl -X DELETE https://leadgrid.io/api/v1/dossiers/a91c… \ -H "Authorization: Bearer lg_live_your_key"
{
"data": {
"id": "a91c…",
"status": "archived"
}
}/dossiers/:id/cvUpload a PDF CV (max 10 MB) and attach it to an existing dossier. The upload replaces any previous CV. Accepts multipart/form-data with a 'cv' field, or application/pdf with the PDF bytes as the raw body. Emits dossier.updated.
# multipart/form-data curl -X POST https://leadgrid.io/api/v1/dossiers/a91c…/cv \ -H "Authorization: Bearer lg_live_your_key" \ -F "cv=@./resume.pdf" # raw application/pdf curl -X POST https://leadgrid.io/api/v1/dossiers/a91c…/cv \ -H "Authorization: Bearer lg_live_your_key" \ -H "Content-Type: application/pdf" \ --data-binary @./resume.pdf
{
"data": {
"id": "a91c…",
"cv_url": "<org-id>/dossiers/a91c…/cv.pdf"
}
}Contacts
Contacts are the people in your network: candidates, prospects, clients, suppliers and partners. A single Contact can carry multiple kinds at once and can be linked to any number of dossiers.
/contactsList contacts in your organization. Supports filtering by kind, pool status and free-text search, plus pagination.
| Field | Type | Description |
|---|---|---|
| kind | string | Filter by kind. Repeat the parameter to combine values: 'candidate', 'prospect', 'client', 'supplier' or 'partner'. |
| pool | string | 'in_pool' for contacts currently in the talent pool, 'expired' for contacts whose pool window has passed. |
| search | string | Free-text search over full_name, email, company and role. |
| page | integer | 1-based page number. Default: 1. |
| per_page | integer | Items per page (max 100). Default: 25. |
curl "https://leadgrid.io/api/v1/contacts?kind=candidate&kind=client&search=sophie" \ -H "Authorization: Bearer lg_live_your_key"
{
"data": [
{
"id": "c12a…",
"full_name": "Sophie van Dijk",
"email": "sophie@example.com",
"phone": "+31 6 12345678",
"linkedin_url": "https://linkedin.com/in/sophievandijk",
"company": "Adyen",
"role": "Senior Frontend Engineer",
"city": "Amsterdam",
"kind": ["candidate", "client"],
"notes": null,
"dossier_count": 2,
"created_at": "2026-04-08T11:14:02Z"
}
],
"meta": { "total": 142, "page": 1, "per_page": 25 }
}/contactsCreate a new contact. full_name is the only required field. If a contact with the same email already exists in your organization (case-insensitive), the API returns 409 with a duplicate_contact error.
| Field | Type | Description |
|---|---|---|
| full_name* | string | The contact's full name. |
| string | Primary email. Must be unique per organization (case-insensitive). Returns 409 on conflict. | |
| phone | string | Primary phone number. |
| linkedin_url | string | Public LinkedIn profile URL. |
| company | string | Current employer or account. |
| role | string | Job title or role. |
| city | string | City of residence. |
| kind | string[] | Array of kinds: 'candidate', 'prospect', 'client', 'supplier', 'partner'. Defaults to an empty array. |
| notes | string | Free-form internal notes shown on the contact drawer. |
curl -X POST https://leadgrid.io/api/v1/contacts \
-H "Authorization: Bearer lg_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"full_name": "Sophie van Dijk",
"email": "sophie@example.com",
"company": "Adyen",
"role": "Senior Frontend Engineer",
"city": "Amsterdam",
"kind": ["candidate"]
}'{
"data": {
"id": "c12a…",
"full_name": "Sophie van Dijk",
"email": "sophie@example.com",
"company": "Adyen",
"role": "Senior Frontend Engineer",
"city": "Amsterdam",
"kind": ["candidate"],
"dossier_count": 0,
"created_at": "2026-04-30T08:21:14Z"
}
}/contacts/:idRetrieve a single contact by ID, including their kinds and aggregated dossier counts.
curl https://leadgrid.io/api/v1/contacts/c12a… \ -H "Authorization: Bearer lg_live_your_key"
{
"data": {
"id": "c12a…",
"full_name": "Sophie van Dijk",
"email": "sophie@example.com",
"company": "Adyen",
"role": "Senior Frontend Engineer",
"city": "Amsterdam",
"kind": ["candidate", "client"],
"dossier_count": 2
}
}/contacts/:idPartial update of any field on a contact. Send only the keys you want to change.
| Field | Type | Description |
|---|---|---|
| full_name | string | Rename the contact. |
| string | Update primary email. Still subject to the per-org uniqueness check. | |
| phone | string | Update phone number. |
| linkedin_url | string | Update LinkedIn URL. |
| company | string | Update company. |
| role | string | Update role. |
| city | string | Update city. |
| kind | string[] | Replace the full kinds array. Send the complete set of kinds you want on the contact. |
| notes | string | Replace internal notes. |
curl -X PATCH https://leadgrid.io/api/v1/contacts/c12a… \
-H "Authorization: Bearer lg_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "kind": ["candidate", "client"], "city": "Rotterdam" }'{
"data": {
"id": "c12a…",
"kind": ["candidate", "client"],
"city": "Rotterdam"
}
}/contacts/:idHard delete the contact. Cascades to dossier_contacts links, but leaves the linked dossiers themselves intact.
curl -X DELETE https://leadgrid.io/api/v1/contacts/c12a… \ -H "Authorization: Bearer lg_live_your_key"
{
"data": {
"id": "c12a…",
"deleted": true
}
}Dossier links
A dossier-contact link associates a Contact with a dossier and records the role they play on that deal or hire (primary, hiring manager, decision maker, champion, gatekeeper, introducer or other). One dossier can have many contacts; one contact can appear on many dossiers.
/dossiers/:id/contactsList the contacts linked to a dossier, including their role and whether they are the primary contact.
curl https://leadgrid.io/api/v1/dossiers/a91c…/contacts \ -H "Authorization: Bearer lg_live_your_key"
{
"data": [
{
"contact_id": "c12a…",
"full_name": "Sophie van Dijk",
"email": "sophie@example.com",
"company": "Adyen",
"role_on_dossier": "primary",
"is_primary": true
},
{
"contact_id": "c44b…",
"full_name": "Mark de Vries",
"email": "mark@rabobank.nl",
"company": "Rabobank",
"role_on_dossier": "decision_maker",
"is_primary": false
}
],
"meta": { "total": 2, "page": 1, "per_page": 25 }
}/dossiers/:id/contactsLink an existing contact to a dossier with a role. Setting is_primary to true demotes any previous primary contact on that dossier.
| Field | Type | Description |
|---|---|---|
| contact_id* | uuid | The contact to link. Must belong to your organization. |
| role_on_dossier* | string | One of 'primary', 'hiring_manager', 'decision_maker', 'champion', 'gatekeeper', 'introducer' or 'other'. |
| is_primary | boolean | Default: false. Setting true demotes the previous primary contact on this dossier. |
curl -X POST https://leadgrid.io/api/v1/dossiers/a91c…/contacts \
-H "Authorization: Bearer lg_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "c44b…",
"role_on_dossier": "decision_maker",
"is_primary": false
}'{
"data": {
"dossier_id": "a91c…",
"contact_id": "c44b…",
"role_on_dossier": "decision_maker",
"is_primary": false
}
}/dossiers/:id/contacts/:contact_idUnlink a contact from a dossier. The contact itself is preserved.
curl -X DELETE https://leadgrid.io/api/v1/dossiers/a91c…/contacts/c44b… \ -H "Authorization: Bearer lg_live_your_key"
{
"data": {
"dossier_id": "a91c…",
"contact_id": "c44b…",
"unlinked": true
}
}Flows
Flows are the pipelines dossiers move through. Each flow has ordered stages with optional deadlines and win probabilities.
/flowsList flows in your organization. Stages are nested and ordered by position.
| Field | Type | Description |
|---|---|---|
| type | string | Filter to 'candidate' or 'sales'. |
| page | integer | Page number. Default: 1. |
| per_page | integer | Items per page. Default: 25. |
curl "https://leadgrid.io/api/v1/flows?type=sales" \ -H "Authorization: Bearer lg_live_your_key"
{
"data": [
{
"id": "f01a…",
"name": "Sales Flow",
"type": "sales",
"is_default": true,
"stages": [
{
"id": "s1…",
"name": "Lead",
"position": 1,
"deadline_days": 3,
"win_probability": 14,
"color": "#FF5C35"
},
{
"id": "s2…",
"name": "Discovery",
"position": 2,
"deadline_days": 5,
"win_probability": 29,
"color": "#22C55E"
}
]
}
],
"meta": { "total": 1, "page": 1, "per_page": 25 }
}/flows/:id/stagesGet the stages for a single flow, sorted by position. Useful if you already know the flow_id and want just the stages.
curl https://leadgrid.io/api/v1/flows/f01a…/stages \ -H "Authorization: Bearer lg_live_your_key"
{
"data": [
{
"id": "s1…",
"name": "Lead",
"position": 1,
"deadline_days": 3,
"win_probability": 14
}
],
"meta": { "total": 6, "page": 1, "per_page": 6 }
}Notes
Notes are the timeline of updates attached to a dossier. They're sorted oldest-first and are always internal by default.
/dossiers/:id/notesList notes for a dossier, oldest first.
curl https://leadgrid.io/api/v1/dossiers/a91c…/notes \ -H "Authorization: Bearer lg_live_your_key"
{
"data": [
{
"id": "n1…",
"dossier_id": "a91c…",
"content": "Had a great first call, strong culture fit.",
"is_internal": true,
"created_at": "2026-04-14T09:12:30Z"
}
],
"meta": { "total": 3, "page": 1, "per_page": 25 }
}/dossiers/:id/notesAdd a new note to a dossier. Emits a note.created webhook.
| Field | Type | Description |
|---|---|---|
| content* | string | The note text. Cannot be empty. |
| is_internal | boolean | Default: true. Internal notes are not shared. |
curl -X POST https://leadgrid.io/api/v1/dossiers/a91c…/notes \
-H "Authorization: Bearer lg_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "content": "Followed up by email." }'{
"data": {
"id": "n2…",
"dossier_id": "a91c…",
"content": "Followed up by email.",
"is_internal": true,
"created_at": "2026-04-15T12:04:10Z"
}
}Webhooks
Configure a webhook endpoint in Settings → API. LeadGrid sends a POST request with a JSON body when any of these events happen.
| Field | Type | Description |
|---|---|---|
| dossier.created | event | A new dossier was created (via API, UI or email). |
| dossier.updated | event | Any field on a dossier changed. Fires alongside stage_changed when applicable. |
| dossier.stage_changed | event | current_stage_id was updated. |
| dossier.deleted | event | Dossier was archived (status set to 'archived'). |
| note.created | event | A new note was added to a dossier. |
POST https://your-app.com/webhooks/leadgrid
Content-Type: application/json
X-LeadGrid-Signature: t=1713178230,v1=3b2c4f…
{
"id": "evt_…",
"type": "dossier.stage_changed",
"created_at": "2026-04-15T12:04:10Z",
"data": {
"id": "a91c…",
"current_stage_id": "d8e2…",
"status": "active"
}
}Signature verification
Each webhook request includes an X-LeadGrid-Signature header containing a timestamp and an HMAC-SHA256 signature of `${"timestamp"}.${"body"}` signed with your endpoint's secret. Verify it before trusting the payload.
import crypto from "node:crypto";
export function verifyLeadGridSignature(
header: string,
body: string,
secret: string,
) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=")),
);
const { t, v1 } = parts as { t: string; v1: string };
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${body}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(v1),
);
}Errors & rate limits
Errors use standard HTTP status codes. The body always contains an error object with a stable code and a human-readable message.
{
"error": {
"code": "not_found",
"message": "Dossier not found."
}
}| Field | Type | Description |
|---|---|---|
| unauthorized | 401 | Missing, malformed or invalid API key. Also returned for expired keys. |
| plan_required | 402 | Your organization is on Free or Pro. API access requires Growth. |
| forbidden | 403 | The API key doesn't include the required scope for this action. |
| not_found | 404 | The resource doesn't exist, or doesn't belong to your organization. |
| invalid_body | 400 | Missing required field, unknown value or malformed JSON. |
| rate_limited | 429 | You've exceeded the rate limit for your plan. Retry after the time in the Retry-After header. |
| internal | 500 | Unexpected server error. Safe to retry. |
X-RateLimit-Limit and X-RateLimit-Remaining headers so you can back off before hitting the cap.