Kommo + Basecamp: creating projects and tasks from won deals

Kommo + Basecamp: creating projects and tasks from won deals

Basecamp is a project management tool popular among agencies, design studios, and digital consulting firms: To-do lists, Message Boards, Schedules, Docs & Files, Campfire (chat) — all in one project. Simpler than Jira and Asana, without Gantt charts and task dependencies — and that is precisely what appeals to teams that need order, not features for the sake of features. Without the Kommo integration, a manager creates a project manually on every Won. With the integration, Won -> project with tasks in seconds, without manual effort.

Basecamp vs Asana vs Trello for delivery integration

ParameterBasecampAsanaTrello
PhilosophySimplicity + communicationPowerful workflowsKanban
Project templatesYes (Project Templates)YesNo (via Power-Up)
WebhookYesYesYes
APIREST v2 (OAuth 2.0)RESTREST + API Key
Best forAgencies, consultingSaaS, ITSmall teams

Basecamp is chosen by agencies with long-term clients: one Basecamp project = one client, with the full history of communications, tasks, and documents.

What gets synchronized

Kommo -> Basecamp: — Won -> create Project with client name — Won -> create To-do list with onboarding template tasks — Won -> add Description with deal data (plan, amount, manager) — Won -> assign responsible parties to tasks (manager -> Basecamp user mapping)

Basecamp -> Kommo: — To-do item completed -> Note: “Basecamp: task ‘{title}’ completed” — Project archived (via polling) -> Note: “Project completed”

Basecamp API: key requests

Base URL: https://3.basecampapi.com/{account_id}. Account ID: from the Basecamp Dashboard URL (app.basecamp.com/{account_id}/...). Authentication: OAuth 2.0 (recommended) or Personal Access Token. User-Agent is required: Basecamp blocks requests without a valid UA.

import requests

BASECAMP_TOKEN = "your_personal_access_token"  # or OAuth access token
BASECAMP_ACCOUNT_ID = "your_account_id"
BASECAMP_BASE_URL = f"https://3.basecampapi.com/{BASECAMP_ACCOUNT_ID}"

HEADERS = {
    "Authorization": f"Bearer {BASECAMP_TOKEN}",
    "Content-Type": "application/json",
    "User-Agent": "YourApp (yourname@company.com)",  # required!
}

def get_project_templates() -> list:
    # Get list of project templates
    resp = requests.get(
        f"{BASECAMP_BASE_URL}/templates.json",
        headers=HEADERS
    )
    resp.raise_for_status()
    return resp.json()

def create_project_from_template(template_id: int, name: str,
                                  description: str = "") -> dict:
    # Create a project from a template
    resp = requests.post(
        f"{BASECAMP_BASE_URL}/templates/{template_id}/project_constructions.json",
        headers=HEADERS,
        json={"project": {"name": name, "description": description}}
    )
    resp.raise_for_status()
    return resp.json()

def create_project(name: str, description: str = "") -> dict:
    # Create a project from scratch (without a template)
    resp = requests.post(
        f"{BASECAMP_BASE_URL}/projects.json",
        headers=HEADERS,
        json={"name": name, "description": description}
    )
    resp.raise_for_status()
    return resp.json()

def get_todoset(project_id: int) -> dict:
    # Get the todo-list container for a project
    resp = requests.get(
        f"{BASECAMP_BASE_URL}/buckets/{project_id}/todosets.json",
        headers=HEADERS
    )
    resp.raise_for_status()
    return resp.json()[0]  # one todoset per project

def create_todolist(project_id: int, todoset_id: int, name: str,
                    description: str = "") -> dict:
    resp = requests.post(
        f"{BASECAMP_BASE_URL}/buckets/{project_id}/todosets/{todoset_id}/todolists.json",
        headers=HEADERS,
        json={"name": name, "description": description}
    )
    resp.raise_for_status()
    return resp.json()

def create_todo(project_id: int, todolist_id: int, content: str,
                assignee_ids: list = None, due_on: str = None) -> dict:
    payload: dict = {"content": content}
    if assignee_ids:
        payload["assignee_ids"] = assignee_ids
    if due_on:
        payload["due_on"] = due_on  # "2026-06-15"
    resp = requests.post(
        f"{BASECAMP_BASE_URL}/buckets/{project_id}/todolists/{todolist_id}/todos.json",
        headers=HEADERS,
        json=payload
    )
    resp.raise_for_status()
    return resp.json()

ONBOARDING_TODOS = [
    "Kick-off call with client team",
    "Share access and materials",
    "Agree on project roadmap",
    "Kick-off meeting",
    "First milestone review",
]

MANAGER_TO_BASECAMP = {
    "alice@company.com": 1234567,  # Basecamp person ID
    "bob@company.com":   7654321,
}

def on_deal_won(lead: dict, contact: dict):
    client_name = contact["name"]
    plan = get_custom_field(lead, PLAN_FIELD_ID) or "Growth"
    amount = lead.get("price", 0)
    manager_email = get_manager_email(lead)

    description = (
        f"Client: {client_name}\n"
        f"Plan: {plan}\n"
        f"Amount: {amount}\n"
        f"Kommo deal ID: {lead['id']}"
    )

    # Create project (from template or from scratch)
    if BASECAMP_TEMPLATE_ID:
        construction = create_project_from_template(
            template_id=BASECAMP_TEMPLATE_ID,
            name=f"Project: {client_name}",
            description=description,
        )
        # project_construction: status pending, poll until completed
        project_id = poll_construction_until_done(construction["id"])
    else:
        project = create_project(
            name=f"Project: {client_name}",
            description=description,
        )
        project_id = project["id"]

        # Create To-do list manually
        todoset = get_todoset(project_id)
        todoset_id = todoset["id"]
        todolist = create_todolist(
            project_id, todoset_id,
            name="Onboarding",
            description="Project launch tasks",
        )
        assignee_id = MANAGER_TO_BASECAMP.get(manager_email)
        for task in ONBOARDING_TODOS:
            create_todo(
                project_id, todolist["id"], task,
                assignee_ids=[assignee_id] if assignee_id else None,
            )

    update_kommo_deal(lead["id"], {"basecamp_project_id": str(project_id)})
    create_kommo_note(lead["id"],
        f"Basecamp: project 'Project: {client_name}' created (ID: {project_id})")

Handling Basecamp Webhooks:

@app.route("/webhooks/basecamp", methods=["POST"])
def basecamp_webhook():
    payload = request.json
    kind = payload.get("kind")          # "todo_completed", "todo_uncompleted" etc.
    recording = payload.get("recording", {})
    bucket = payload.get("bucket", {})

    project_id = str(bucket.get("id", ""))
    deal_id = find_deal_by_field("basecamp_project_id", project_id)
    if not deal_id:
        return "", 200

    if kind == "todo_completed":
        title = recording.get("title", "")
        creator = payload.get("creator", {}).get("name", "")
        create_kommo_note(deal_id,
            f"Basecamp: task '{title}' completed ({creator})")

    elif kind == "message_created":
        # Message on Message Board -> Note in deal (optional)
        subject = recording.get("subject", "")
        create_kommo_note(deal_id,
            f"Basecamp: new message in project - '{subject}'")

    return "", 200

Registering a Webhook in Basecamp via API:

def register_webhook(project_id: int, payload_url: str, types: list) -> dict:
    resp = requests.post(
        f"{BASECAMP_BASE_URL}/buckets/{project_id}/webhooks.json",
        headers=HEADERS,
        json={"payload_url": payload_url, "types": types}
    )
    resp.raise_for_status()
    return resp.json()

# Register for a new project:
# register_webhook(project_id, "https://yourapp.com/webhooks/basecamp",
#                  ["Todo", "Message"])

Project Templates: creating from a template

Basecamp supports Project Templates — a pre-populated project with To-do lists, documents, and a schedule. POST /templates/{id}/project_constructions.json creates the project asynchronously: you need to poll GET /project_constructions/{id}.json until status != "completed".

Templates: Basecamp -> Templates (left menu) -> create new. Template ID is in the template URL.

Real case

Design agency (EU, 8–12 new projects per month, Kommo + Basecamp):

  • Before: The PM manually created a project in Basecamp, added a standard To-do list, and invited the client. It took 15–20 minutes per Won. Sometimes the Basecamp project was forgotten — the client waited for access.
  • After: Won -> project from template in 20 seconds. All standard tasks, schedule, and client access — from the Template. The PM receives a notification that the project was created, rather than creating it themselves.
  • Additionally: todo_completed for the “Acceptance and signing” task -> Note in Kommo + stage change to “Project completed” -> trigger NPS survey.

Who should use this

  • Agencies and consulting firms with Basecamp as their primary PM tool
  • Teams with 5–20 parallel projects — at lower volumes manual work is still manageable
  • Design studios and digital agencies: client-oriented projects, not development
  • Companies where the client is a Basecamp project participant (client access features)

Frequently asked questions

Basecamp Personal Access Token vs OAuth — which to choose?

Personal Access Token (PAT) is simpler for server-side integration: it does not expire and does not require an OAuth flow. Create it: Basecamp -> My Profile -> Access Tokens. OAuth is only needed for multi-user applications (SaaS integration for different Basecamp accounts).

Basecamp project_construction — why is the status pending?

Creating a project from a template is an asynchronous process. Basecamp copies all template structures (todos, schedules, docs). You need to poll GET /project_constructions/{id}.json every 2–3 seconds until status == "completed". This typically takes 5–15 seconds.

How do you invite a client to a Basecamp project via API?

POST /projects/{id}/people/users.json with an array {"grant": [{"id": person_id}]}. If the client is not in Basecamp — create them first via POST /people.json. For client-access (Basecamp Clientside): there is a separate endpoint for external users.

Basecamp webhook — how to verify the request?

Basecamp signs webhooks via X-Basecamp-Signature: HMAC-SHA256 with the secret specified at registration. Verification is analogous to other platforms: hmac.new(secret, body, sha256).hexdigest().

Summary

  • Basecamp API: Bearer {token} + required User-Agent
  • Create from template: POST /templates/{id}/project_constructions.json -> async, polling required
  • Create todo: POST /buckets/{project_id}/todolists/{todolist_id}/todos.json
  • Webhook: HMAC-SHA256 via X-Basecamp-Signature, registered via API
  • Project Templates: more powerful than manual creation — everything in one API call

If you use Basecamp and Kommo and want to automate project creation on Won — describe the structure of your template. Exceltic.dev will set up the integration with template_construction and webhook handling.

More articles

All →