Better Proposals is a platform for creating interactive web-based proposals (not PDFs): the prospect opens a link in their browser, sees a beautifully formatted document, can sign electronically and pay directly on the page. Unlike PandaDoc or DocuSign, Better Proposals specializes specifically in the proposal stage of the pipeline — with detailed view tracking, heat maps, and A/B template tests. Without the Kommo integration: every proposal is created manually, view data does not reach the CRM, and the manager learns about signing from email.
Better Proposals vs PandaDoc vs Qwilr
| Parameter | Better Proposals | PandaDoc | Qwilr |
|---|---|---|---|
| Format | Web page | Web + PDF | Web page |
| View tracking | Detailed (scroll, time per section) | Basic | Basic |
| Electronic signature | Yes | Yes (eSign) | Yes |
| Built-in payment | Yes (Stripe) | No | Yes |
| Templates | 200+ | 750+ | 60+ |
| Price | from $19/month | from $35/month | from $35/month |
Better Proposals is chosen by agencies and consulting firms where the visual quality of a proposal affects the close rate, and tracking “when the client was reading” is critical for follow-up timing.
What gets synchronized
Kommo -> Better Proposals: — Won (or custom stage) -> create proposal from template, populate contact and deal data — Change of amount in Kommo -> update price in the proposal draft
Better Proposals -> Kommo:
— proposal.opened -> Note: “Proposal opened by client” + timestamp
— proposal.signed -> Note: “Signed” + deal status or pipeline stage change
— proposal.paid -> Note: “Payment received via proposal page”
— proposal.expired -> Task: “Proposal expired — follow-up”
Better Proposals API: creating a proposal
Base URL: https://api.betterproposals.io/v1. Authentication: Authorization: Bearer {api_token} header (token from Better Proposals -> Settings -> Integrations -> API).
import requests
BP_TOKEN = "your_bearer_token"
BP_BASE = "https://api.betterproposals.io/v1"
BP_HEADERS = {
"Authorization": f"Bearer {BP_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
}
def create_proposal(template_id: int, contact_name: str, contact_email: str,
company: str, deal_value: float, currency: str = "USD") -> dict:
# template_id from Better Proposals -> Templates -> Edit -> URL
payload = {
"template_id": template_id,
"name": f"Proposal for {company}",
"contacts": [
{
"name": contact_name,
"email": contact_email,
"company": company,
}
],
"variables": {
"company_name": company,
"deal_value": f"{deal_value:,.2f} {currency}",
},
"expiry_date": None,
"currency": currency,
"price": deal_value,
}
resp = requests.post(f"{BP_BASE}/proposals", headers=BP_HEADERS, json=payload)
resp.raise_for_status()
return resp.json()
def get_proposal_link(proposal_id: int) -> str:
resp = requests.get(f"{BP_BASE}/proposals/{proposal_id}", headers=BP_HEADERS)
resp.raise_for_status()
data = resp.json()
return data.get("url", "")
# Template mapping: Kommo deal type -> Better Proposals template
TEMPLATE_MAP = {
"consulting": 12345,
"saas": 12346,
"retainer": 12347,
}
def on_deal_reached_proposal_stage(lead: dict, contact: dict):
# Called when deal moves to "Proposal" stage
company = get_custom_field(lead, COMPANY_FIELD_ID) or contact.get("name", "")
email = get_contact_email(contact)
deal_type = get_custom_field(lead, DEAL_TYPE_FIELD_ID) or "saas"
template_id = TEMPLATE_MAP.get(deal_type.lower(), TEMPLATE_MAP["saas"])
proposal = create_proposal(
template_id = template_id,
contact_name = contact.get("name", ""),
contact_email = email,
company = company,
deal_value = lead.get("price", 0),
)
proposal_id = proposal.get("id")
proposal_url = get_proposal_link(proposal_id)
save_to_kommo_deal(lead["id"], {
"bp_proposal_id": proposal_id,
"bp_proposal_url": proposal_url,
})
create_kommo_note(
lead["id"],
f"Better Proposals: proposal created -> {proposal_url}",
)
Webhooks: Better Proposals -> Kommo
Better Proposals supports webhooks: Settings -> Webhooks -> Add Webhook. The payload is signed via HMAC-SHA256 with the X-BP-Signature header.
import hmac, hashlib
BP_WEBHOOK_SECRET = "your_webhook_secret"
@app.route("/webhooks/better-proposals", methods=["POST"])
def bp_webhook():
sig = request.headers.get("X-BP-Signature", "")
expected = hmac.new(
BP_WEBHOOK_SECRET.encode(),
request.data,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(sig, expected):
return "", 401
payload = request.json
event = payload.get("event")
proposal_id = payload.get("proposal", {}).get("id")
lead_id = find_kommo_deal_by_custom_field("bp_proposal_id", str(proposal_id))
if not lead_id:
return "", 200
if event == "proposal.opened":
opened_at = payload.get("proposal", {}).get("opened_at", "")
create_kommo_note(lead_id, f"Better Proposals: proposal opened by client ({opened_at})")
elif event == "proposal.signed":
signed_at = payload.get("proposal", {}).get("signed_at", "")
create_kommo_note(lead_id,
f"Better Proposals: proposal signed ({signed_at}) - awaiting payment")
move_kommo_deal_to_stage(lead_id, SIGNED_STAGE_ID)
elif event == "proposal.paid":
amount = payload.get("proposal", {}).get("price", 0)
create_kommo_note(lead_id,
f"Better Proposals: payment received - ${amount:.2f}")
move_kommo_deal_to_stage(lead_id, WON_STAGE_ID)
elif event == "proposal.expired":
create_kommo_note(lead_id, "Better Proposals: proposal has expired")
create_kommo_task(lead_id, "Better Proposals: follow-up - send updated proposal")
return "", 200
View tracking: how to use the data
Better Proposals records: when each section was opened, how many seconds the client spent on each section, and which device was used. This data can be retrieved via the API:
def get_proposal_analytics(proposal_id: int) -> dict:
resp = requests.get(
f"{BP_BASE}/proposals/{proposal_id}/analytics",
headers=BP_HEADERS,
)
resp.raise_for_status()
return resp.json()
def sync_proposal_analytics_to_kommo(lead_id: int, proposal_id: int):
analytics = get_proposal_analytics(proposal_id)
views = analytics.get("views", 0)
avg_time = analytics.get("average_time_spent", 0)
note = (
f"Better Proposals analytics: views {views}, "
f"average time on proposal {avg_time} sec."
)
create_kommo_note(lead_id, note)
This data helps the manager choose the right moment for a follow-up: if the client spent 3 minutes on the pricing page — it is the right time to call.
Real case
Digital agency (EU, 25 people, Kommo + Better Proposals):
- Before: the manager manually copied data from Kommo into Better Proposals. 15–20 minutes per proposal. Signing notifications came by email, which sometimes went unnoticed. The Proposal stage -> Won took 2–3 days.
- After: moving to the “Proposal” stage in the pipeline -> proposal automatically created from template (based on deal type) -> link saved in Kommo.
proposal.opened-> Note.proposal.signed-> automatic stage change to “Won”. - Result: time from “Proposal” to “Won” was reduced from 4.2 days to 1.8 days — managers saw in real time when clients were reviewing the proposal and called at the right moment.
Who should use this
- Agencies (digital, consulting, marketing) where the proposal is a key pipeline stage
- B2B with several standard proposal types (templates per service line)
- Companies where follow-up timing matters — view tracking provides the right moment
- Teams where the proposal stage takes more than 3 days — automation accelerates the cycle
Frequently asked questions
Does Better Proposals support variables for auto-populating deal data?
Yes. In a Better Proposals template you can create variables ({company_name}, {deal_value}, {manager_name}) — they are populated when the proposal is created via the API through the variables field. Standard contact fields (name, email, company) are auto-populated from the contacts object in the payload.
Can you update the price in a proposal that has already been sent?
Yes, via PATCH /proposals/{id} with a new price — but only if the proposal has not been signed. After signing, the proposal is locked. If the amount changed after sending — a new proposal is created (POST /proposals) with “Revised” in the name, and the old one is closed via DELETE /proposals/{id}.
How do you set up different templates for different deal types?
In Kommo, add a custom field “Service type” (select). When moving to the Proposal stage — the webhook passes the field value -> the Python handler looks up TEMPLATE_MAP -> selects the appropriate Better Proposals template_id. Templates are created in Better Proposals once by the team and then used automatically via the API.
Better Proposals vs Qwilr — what is the difference for Kommo integration?
The API level is similar: Bearer token, CRUD proposals, webhooks. The main difference: Better Proposals provides more detailed view analytics (section by section), Qwilr offers more flexible design. For Kommo integration the architecture is identical. The platform choice is a question of UX for managers and proposal design, not the technical side.
Summary
- API: Bearer token,
Authorization: Bearer {token}, base URLhttps://api.betterproposals.io/v1 - Flow: “Proposal” stage in Kommo ->
POST /proposalswith template_id + contact data - Webhook:
proposal.opened/signed/paid/expired-> Note/Task/stage change in Kommo - View analytics:
GET /proposals/{id}/analytics-> Note with engagement data - The trigger does not have to be Won — any pipeline stage where proposal work begins will do
If you have an agency or B2B with a proposal stage in the pipeline and want to automate proposal creation from Kommo — describe your template structure and deal types. Exceltic.dev will set up the two-way integration with tracking.