bottleneckcrm/docs/integrations/python-sample.md
Ashwin d6bb754673 Add documentation for self-hosting, production deployment, requirements, security hardening, and troubleshooting
- Created production-deploy.md to outline production deployment steps and configurations.
- Added requirements.md detailing environment prerequisites for self-hosting.
- Introduced security-hardening.md as a checklist for securing multi-tenant deployments.
- Developed troubleshooting.md to address common issues in RLS-backed deployments.
- Configured mkdocs.yml for documentation structure and navigation.
2026-08-02 20:08:12 +05:30

6.5 KiB

Python sample

What this does

A complete, runnable script that talks to the BottleCRM REST API from plain Python: it obtains an access token, lists the org's leads, and creates one.

"Obtains a token" here means reading a personal access token (bcrm_pat_…) from the environment, rather than driving one of the interactive sign-in flows on Authentication. A PAT is what BottleCRM issues specifically so a script doesn't need a browser, an OAuth client secret, or a magic-link email round trip just to make API calls — see Tokens and API keys for how it authenticates and what it inherits. There are two ways to mint one, and which applies depends on your role: an admin can do it from the CRM's Settings → API tokens page (/settings/api-tokens) — that page is admin-only (it lists every token in the org, not just your own), so a non-admin who opens it sees "Admins only" and never reaches the create form. Any role can instead call POST /api/profile/tokens/ directly — the same self-service endpoint the settings page's own create form posts to — which is the option to use if you aren't an admin, or you just want a token without the UI. For a local backend you're running yourself, the fastest way is the Django shell:

cd backend
uv run python manage.py shell -c "
from common.models import Profile, PersonalAccessToken
p = Profile.objects.filter(role='ADMIN', is_active=True).first()
print(PersonalAccessToken.generate(p, 'python-sample')[0])
"

This prints the raw bcrm_pat_… token once — copy it, there's no way to retrieve it again later.

The script

#!/usr/bin/env python3
"""List an org's leads and create one, using a BottleCRM personal access token.

Usage:
    BCRM_BASE_URL=http://localhost:8000 BCRM_TOKEN=bcrm_pat_... python leads_sample.py

See docs/api/tokens-and-api-keys.md for how to mint BCRM_TOKEN, and
docs/api/leads.md for the shape of the /api/leads/ responses this script reads.
"""

import os
import sys
import time

import requests

BASE_URL = os.environ.get("BCRM_BASE_URL", "http://localhost:8000").rstrip("/")
TOKEN = os.environ.get("BCRM_TOKEN")

if not TOKEN:
    sys.exit(
        "Set BCRM_TOKEN to a bcrm_pat_... personal access token "
        "(see docs/api/tokens-and-api-keys.md)."
    )

session = requests.Session()
session.headers.update(
    {
        "Authorization": f"Bearer {TOKEN}",
        "Accept": "application/json",
    }
)


def list_leads():
    """GET /api/leads/ — the response splits leads into open/closed sections
    rather than one flat list; see docs/api/leads.md#list-leads."""
    resp = session.get(f"{BASE_URL}/api/leads/")
    resp.raise_for_status()
    data = resp.json()
    open_section = data["open_leads"]
    leads = open_section["open_leads"]
    print(
        f"{len(leads)} open lead(s) on this page "
        f"(of {open_section['leads_count']} total open):"
    )
    for lead in leads:
        name = f"{lead.get('first_name') or ''} {lead.get('last_name') or ''}".strip()
        print(
            f"  - {name or '(no name)'} <{lead.get('email') or 'no email'}> "
            f"[{lead.get('status')}]"
        )
    return data


def create_lead():
    """POST /api/leads/ — a 200 with {"error": false, ...} is success; the
    response does not echo the created record, so we look it up afterward."""
    email = f"sample.lead+{int(time.time())}@example.com"
    payload = {
        "first_name": "Sample",
        "last_name": "Lead",
        "email": email,
        "status": "assigned",
        "source": "other",
    }
    resp = session.post(f"{BASE_URL}/api/leads/", json=payload)
    body = resp.json()
    if resp.status_code >= 400 or body.get("error"):
        sys.exit(f"Create failed ({resp.status_code}): {body}")
    print(f"Create response: {body}")
    return email


def find_by_email(email):
    """GET /api/leads/?email=... — `email` is a case-insensitive contains
    filter (docs/api/conventions.md#filtering-and-search), which is fine here
    since the address we just created is unique in this org."""
    resp = session.get(f"{BASE_URL}/api/leads/", params={"email": email})
    resp.raise_for_status()
    matches = resp.json()["open_leads"]["open_leads"]
    if matches:
        print(f"Found the new lead: id={matches[0]['id']}")
    else:
        print("New lead not on this page (increase limit/offset to find it).")


if __name__ == "__main__":
    print("Listing existing leads...")
    list_leads()
    print("\nCreating a lead...")
    new_email = create_lead()
    print("\nConfirming it was created...")
    find_by_email(new_email)

Running it

Save the script as leads_sample.py. You need a reachable BottleCRM backend (local or self-hosted) and a personal access token for a profile in the org you want to query, minted as described above.

requests isn't a dependency of this documentation site or of the CRM's own frontend — the simplest way to run the script without installing anything into a project is uv run with an inline dependency:

BCRM_BASE_URL=http://localhost:8000 \
BCRM_TOKEN=bcrm_pat_... \
  uv run --with requests python leads_sample.py

Or, with a regular virtualenv: pip install requests and then python leads_sample.py with the same two environment variables set.

A note on verification: this script was checked by static means — every URL and field against Leads and Conventions, and the whole script for valid Python syntax — rather than executed against a live database, to avoid writing a row into a shared instance this documentation didn't provision. The example output below is illustrative, not a captured run:

Listing existing leads...
3 open lead(s) on this page (of 3 total open):
  - Jordan Blake <jordan.blake@example.com> [assigned]
  - Priya Shah <priya.shah@example.com> [in process]
  - (no name) <no email> [assigned]

Creating a lead...
Create response: {'error': False, 'message': 'Lead Created Successfully'}

Confirming it was created...
Found the new lead: id=3f2a1c9e-....

A validation failure — for example, running the script twice in the same second so the generated email collides — comes back as a 400 and the script exits with the error body BottleCRM returned, per Errors:

Create failed (400): {'error': True, 'errors': {'email': ['Another lead in this organisation already uses that email address.']}}