Leads API
The Leads API is representative of every entity endpoint in BottleCRM, same shape, same conventions. Once you know this one, Contacts / Accounts / Opportunities / Tasks all behave identically.
Base URL
https://<your-host>/api/leads/
All requests require a valid bearer token (see Authentication).
List leads
GET /api/leads/
Authorization: Bearer <token>
Response:
{
"count": 142,
"next": "https://…/api/leads/?page=2",
"previous": null,
"results": [
{
"id": "ab12-cd34-…",
"title": "Acme renewal interest",
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@acme.com",
"status": "qualified",
"source": "website_form",
"assigned_to": { "id": "…", "email": "rep@yours.com" },
"custom_fields": { "industry": "saas" },
"created_at": "2026-04-01T10:14:33Z"
}
]
}
Filtering
GET /api/leads/?status=qualified&source=website_form
GET /api/leads/?search=acme
GET /api/leads/?assigned_to=<profile_id>
GET /api/leads/?cf_industry=saas # custom field filter
GET /api/leads/?ordering=-created_at
Filterable fields: status, source, assigned_to, tags, created_at__gte, created_at__lte, plus any custom field marked filterable.
Create a lead
POST /api/leads/
Content-Type: application/json
Authorization: Bearer <token>
{
"title": "Acme renewal interest",
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@acme.com",
"phone": "+1 555 1234",
"status": "new",
"source": "website_form",
"custom_fields": { "industry": "saas", "bant_score": 72 }
}
201 Created returns the full record. Server-derived fields (org, created_by, id, created_at) must not be in the request body; they are populated by the view.
Retrieve / update / delete
GET /api/leads/<id>/
PATCH /api/leads/<id>/ # partial update, only fields in the body change
PUT /api/leads/<id>/ # full update
DELETE /api/leads/<id>/ # hard delete
The detail GET response also includes custom_field_definitions so the frontend can render the right form fields without a second round-trip.
Convert a lead
There is no POST /api/leads/<id>/convert/. Conversion is a status change on the lead itself:
PUT /api/leads/<id>/
{ "status": "converted", ... }
Setting status to converted runs the conversion server-side: it creates the Account, the
Contact and, where there is an amount, the Opportunity, links them to the lead, and returns
account_id, contact_id and opportunity_id in the response so you can follow them without a
second call.
Duplicates and merging
POST /api/leads/duplicates/ {"email": ..., "phone": ..., "first_name": ..., "last_name": ..., "company_name": ...}
GET /api/leads/<id>/duplicates/
POST /api/leads/<keeper id>/merge/ {"merge_id": "<id merged away>"}
The check (a POST that writes nothing, so an email or phone never sits in a URL or an
access log; from django-crm 1.13.0 an API token needs only leads:read for it) and the record GET answer up to ten open leads you can see, each with only id, name, email,
phone, matched_on and can_delete. A company name only counts when no person name is given,
since two people at one company are two leads.
The merge needs both leads readable (either one hidden or missing is 404) and the right to delete
the one merged away (an admin or its creator, else 403). A converted lead on either side is 400.
The kept lead's values, status and board position win and its blank fields are filled from the
other; tasks, web form submissions, linked contacts, notes and files move across; the other lead is
deleted, with a lead.deleted webhook (its assigned_to lists the owners the merged lead had, even those the merge
moved to the kept one) and an audit-log entry.
CSV import and export
POST /api/leads/import/preview/
POST /api/leads/import/commit/
Content-Type: multipart/form-data
file=<leads.csv>
preview validates every row and writes nothing; commit re-validates and creates every row or
none. Both need an admin or a member with sales access. The older POST /api/leads/upload/ (file
under leads_file) is deprecated and now runs the same validation synchronously.
GET /api/leads/export/ takes the list's query string and returns the same leads as a CSV.
See CSV import and export for headers, limits and the response shapes.
Errors
| Status | Meaning |
|---|---|
400 |
Validation failed. Body: { "errors": { "field": "reason" } } |
401 |
Missing or invalid token |
403 |
You can open the lead, but your role does not allow this action |
404 |
Lead does not exist, belongs to another org, or is one you are not allowed to open |
429 |
Rate limit exceeded |
See Errors for the full structure.