Skip to content

Proposals API

All authenticated endpoints require a valid Cloudflare Access JWT. Public endpoints use a share token instead.

Base paths:

  • Authenticated: /api/proposals
  • Public (no auth): /api/proposals/public

List active proposal templates.

Query paramscategory (optional filter)

Response { "templates": ProposalTemplate[] }


Create a new template.

Body

FieldTypeDescription
namestringTemplate name
descriptionstring?Optional description
categorystring?Template category
content_richstring?Rich content JSON
brand_configstring?Brand configuration JSON
variables_schemastring?Variable definitions JSON
speaker_notesstring?Speaker notes JSON
contract_template_idstring?Linked contract template

Response { "id": "string" }


Get a single template including content and configuration.


Update template metadata and/or content.

Body — same fields as POST (all optional)

Response { "ok": true }


Soft-deletes the template (is_active = 0). Existing proposals are not affected.

Response { "ok": true }


List proposals. Scoped by access level.

Query params

  • status — filter by status
  • deal_id — filter by deal
  • owner_id — filter by owner
  • company_id — filter by company

Response { "proposals": Proposal[] }


Create a new proposal. If template_id is provided, content is cloned from the template with variables resolved.

Body

FieldTypeDescription
titlestringProposal title
template_idstring?Template to create from
deal_idstring?Deal to pull data from
company_idstring?Client company
contact_idstring?Primary contact
valid_untilstring?Expiry date (ISO)
currencystring?Currency code (default: AUD)

Response { "id": "string", "page_id": "string" }


Get full proposal detail including joined company, contact, deal, and owner names.

Response { "proposal": Proposal }


Update proposal metadata.

Bodytitle, deal_id, company_id, contact_id, valid_until, currency, speaker_notes, brand_config (all optional)

Response { "ok": true }


Delete a proposal and its associated page.

Response { "ok": true }


Create a copy of the proposal including page content and pricing sections. The copy is created in “draft” status.

Response { "proposal": { "id": "string" } }


Submit the proposal for internal approval. Transitions status from draft to review.

Response { "ok": true }


Approve the proposal. When all approvers have approved, status transitions to approved.

Body { "comments": "optional string" }

Response { "ok": true }


Request changes on a proposal under review. Transitions status to changes_requested.

Body { "comments": "required string" }

Response { "ok": true }


Mark the proposal as sent to the client. Transitions status to sent.

Response { "ok": true }


Configure sharing settings.

Body

{
"share_enabled": true,
"share_password": "optional string"
}

Response { "share_token": "string" }


Get view analytics for the proposal.

Response { "analytics": ProposalAnalytics[] }


Get all pricing sections and their line items.

Response { "sections": ProposalPricingSection[] }


Create or replace pricing sections and items.

Body

{
"sections": [
{
"title": "Development",
"position": 0,
"items": [
{
"description": "Frontend build",
"quantity": 1,
"unit_price": 5000,
"amount": 5000,
"notes": "React SPA"
}
]
}
]
}

Response { "ok": true }


DELETE /api/proposals/:id/pricing/:sectionId

Section titled “DELETE /api/proposals/:id/pricing/:sectionId”

Delete a pricing section and its items.

Response { "ok": true }


No authentication required. These endpoints use a share token.

Returns the proposal content for the public viewer. If the proposal is password-protected and no valid session exists, returns { "password_required": true }.


POST /api/proposals/public/:shareToken/verify

Section titled “POST /api/proposals/public/:shareToken/verify”

Verify the password for a protected proposal.

Body { "password": "string" }

Response { "ok": true } or { "error": "Invalid password" }


POST /api/proposals/public/:shareToken/track

Section titled “POST /api/proposals/public/:shareToken/track”

Track a view event for analytics.

Body

{
"session_id": "string",
"slide_index": 0,
"duration_delta": 5
}

Response { "ok": true }


POST /api/proposals/public/:shareToken/accept

Section titled “POST /api/proposals/public/:shareToken/accept”

Accept the proposal.

Body

{
"name": "string",
"email": "string"
}

Response { "success": true }

On success: transitions proposal to accepted, triggers onboarding automation (project, budget, invoice, contract).


POST /api/proposals/public/:shareToken/decline

Section titled “POST /api/proposals/public/:shareToken/decline”

Decline the proposal.

Body

{
"name": "string",
"email": "string",
"reason": "optional string"
}

Response { "success": true }


interface Proposal {
id: string;
title: string;
deal_id: string | null;
company_id: string | null;
contact_id: string | null;
template_id: string | null;
page_id: string;
status: ProposalStatus;
version: number;
currency: string;
total_amount: number | null;
valid_until: string | null;
speaker_notes: string;
brand_config: string;
share_token: string | null;
share_password: string | null;
share_enabled: number;
owner_id: string;
sent_at: string | null;
viewed_at: string | null;
accepted_at: string | null;
declined_at: string | null;
decline_reason: string | null;
contract_id: string | null;
project_id: string | null;
created_by: string;
created_at: string;
updated_at: string;
}
draft | review | changes_requested | approved | sent | viewed | accepted | declined | expired
interface ProposalPricingSection {
id: string;
proposal_id: string;
title: string;
position: number;
items: ProposalPricingItem[];
}
interface ProposalPricingItem {
id: string;
section_id: string;
description: string;
service_type_id: string | null;
quantity: number;
unit_price: number;
amount: number;
notes: string | null;
position: number;
}