Skip to main content

Proposals

The Proposals API provides CRUD access to proposals (documents sent to a client or a lead), plus ancillary actions for status, tags and cloning.
New to the API? Start with Getting started (base URL, response envelope, errors, pagination) and Authentication (API keys). Those conventions apply to every endpoint below and are not repeated here.
Identifier note: proposals are addressed by their numeric doc_id. Wherever {id} appears below it is the doc_id.
Scope: the signature accept flow, publishing & emailing, automation, estimate generation and the rich document editor are managed in-app and are not part of this API. Client accept details are exposed read-only; a proposal can still be marked declined/accepted via the status action.

The proposal object

A proposal belongs to a client or a lead — the unused side is null.
{
  "id": 210,
  "title": "Website Proposal",
  "status": "draft",
  "body": "<p>Proposal details…</p>",
  "client": { "id": 12, "name": "Acme Inc" },
  "lead": { "id": null, "name": null },
  "category": { "id": 11, "name": "Standard" },
  "project_id": null,
  "dates": {
    "start": "2026-06-01",
    "end": "2026-12-31",
    "created": "2026-06-18T09:30:00.000000Z",
    "published": null
  },
  "signed": { "date": null, "first_name": null, "last_name": null },
  "tags": ["priority"]
}
FieldTypeNotes
idintegerProposal id. Used in all URLs.
titlestringProposal title.
statusstringdraft, new, accepted, declined, revised.
bodystring|nullProposal body (HTML).
clientobject{ id, name } (null when the proposal is for a lead).
leadobject{ id, name } (null when the proposal is for a client).
categoryobject{ id, name }.
project_idinteger|nullAttached project id (managed in-app).
datesobjectstart, end, created, published.
signedobjectRead-only client accept details: date, first_name, last_name.
tagsarrayTag titles.

List / search proposals

GET /api/proposals

Query parameters

ParameterTypeDescription
statusstringFilter by status.
client_idintegerFilter by client id.
lead_idintegerFilter by lead id.
category_idintegerFilter by category id.
searchstringFree-text search on the title.
sortstringdoc_title, doc_status, doc_date_start, doc_created.
orderstringasc or desc (default desc).
limitintegerResults per page (max 100).
pageintegerPage number.

Example request

curl -G https://your-domain/api/proposals \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d status=draft -d limit=25
<?php
$query = http_build_query(['status' => 'draft', 'limit' => 25]);
$ch = curl_init("https://your-domain/api/proposals?$query");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer YOUR_API_KEY'],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Example response — 200 OK

{
  "data": [ { "id": 210, "title": "Website Proposal", "status": "draft" } ],
  "meta": { "current_page": 1, "per_page": 25, "total": 1, "last_page": 1 },
  "message": "Proposals retrieved successfully."
}

Get a proposal

GET /api/proposals/{id}
curl https://your-domain/api/proposals/210 \
  -H "Authorization: Bearer YOUR_API_KEY"
<?php
$ch = curl_init('https://your-domain/api/proposals/210');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer YOUR_API_KEY'],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
Returns the proposal (200) with message “Proposal retrieved successfully.”

Create a proposal

POST /api/proposals
A proposal is created for a client or a lead (customer_type). Provide doc_body to set the content directly, or proposal_template to seed the body from a template. Creates a backing estimate record.

Body parameters

ParameterTypeRequiredNotes
customer_typestringyesclient or lead.
doc_client_idintegerrequired if customer_type=clientExisting client id.
doc_lead_idintegerrequired if customer_type=leadExisting lead id.
doc_titlestringyes
doc_date_startdateyesYYYY-MM-DD.
doc_categoryidintegeryesExisting (proposal) category id.
doc_date_enddatenoMust be on/after the start date.
doc_bodystring (HTML)noProposal content. Overrides proposal_template.
proposal_templateintegernoTemplate id used to seed the body when doc_body is omitted.

Example request

curl -X POST https://your-domain/api/proposals \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d customer_type=client \
  -d doc_client_id=12 \
  -d doc_title="Website Proposal" \
  -d doc_date_start=2026-06-01 \
  -d doc_categoryid=11
<?php
$ch = curl_init('https://your-domain/api/proposals');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer YOUR_API_KEY'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'customer_type'  => 'client',
        'doc_client_id'  => 12,
        'doc_title'      => 'Website Proposal',
        'doc_date_start' => '2026-06-01',
        'doc_categoryid' => 11,
    ]),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Example response — 201 Created

{
  "data": { "id": 210, "title": "Website Proposal", "status": "draft" },
  "message": "Proposal created successfully."
}

Update a proposal

PATCH /api/proposals/{id}
ParameterTypeRequiredNotes
doc_titlestringyes
doc_date_startdateyes
doc_categoryidintegeryesExisting category id.
doc_date_enddatenoMust be on/after the start date.
doc_bodystring (HTML)noReplaces the proposal content.
curl -X PATCH https://your-domain/api/proposals/210 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d doc_title="Website Proposal v2" \
  -d doc_date_start=2026-06-01 \
  -d doc_categoryid=11
<?php
$ch = curl_init('https://your-domain/api/proposals/210');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer YOUR_API_KEY'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'doc_title'      => 'Website Proposal v2',
        'doc_date_start' => '2026-06-01',
        'doc_categoryid' => 11,
    ]),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
Returns the proposal (200) with message “Proposal updated successfully.”

Delete a proposal

DELETE /api/proposals/{id}
curl -X DELETE https://your-domain/api/proposals/210 \
  -H "Authorization: Bearer YOUR_API_KEY"

Example response — 200 OK

{
  "data": { "id": 210 },
  "message": "Proposal deleted successfully."
}

Change status

PUT /api/proposals/{id}/status
Changing the status clears any recorded client acceptance signature.
ParameterTypeRequiredNotes
statusstringyesaccepted, declined, revised, draft, new.
curl -X PUT https://your-domain/api/proposals/210/status \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d status=accepted
<?php
$ch = curl_init('https://your-domain/api/proposals/210/status');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer YOUR_API_KEY'],
    CURLOPT_POSTFIELDS     => http_build_query(['status' => 'accepted']),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
Returns the proposal (200) with message “Proposal status updated successfully.”

Set tags

PUT /api/proposals/{id}/tags
The tags array is the full set — it replaces existing tags; an empty array clears them.
ParameterTypeRequiredNotes
tagsarray of stringsyesTag titles. Empty array clears all.
curl -X PUT https://your-domain/api/proposals/210/tags \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d "tags[]=priority"
<?php
$ch = curl_init('https://your-domain/api/proposals/210/tags');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer YOUR_API_KEY'],
    CURLOPT_POSTFIELDS     => http_build_query(['tags' => ['priority']]),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
Returns the proposal (200) with message “Proposal tags updated successfully.”

Clone a proposal

POST /api/proposals/{id}/clone
Creates a new proposal from an existing one.
ParameterTypeRequiredNotes
doc_titlestringyesTitle of the new proposal.
customer_typestringyesclient or lead.
doc_client_idintegerrequired if customer_type=client
doc_lead_idintegerrequired if customer_type=lead
doc_categoryidintegeryes
doc_date_startdateyes
doc_date_enddateno
curl -X POST https://your-domain/api/proposals/210/clone \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d doc_title="Website Proposal (copy)" \
  -d customer_type=client \
  -d doc_client_id=12 \
  -d doc_categoryid=11 \
  -d doc_date_start=2026-06-01
<?php
$ch = curl_init('https://your-domain/api/proposals/210/clone');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer YOUR_API_KEY'],
    CURLOPT_POSTFIELDS     => http_build_query([
        'doc_title'      => 'Website Proposal (copy)',
        'customer_type'  => 'client',
        'doc_client_id'  => 12,
        'doc_categoryid' => 11,
        'doc_date_start' => '2026-06-01',
    ]),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
Returns the new proposal (201) with message “Proposal cloned successfully.”

Errors

See Getting started for the shared error format. Proposal-specific:
StatusMeaning
404 Not FoundThe proposal id does not exist.
422 Unprocessable EntityValidation failed (e.g. missing customer/title/date/category, or an invalid status).
{
  "message": "The given data was invalid.",
  "errors": {
    "customer_type": ["The customer type field is required."],
    "doc_title": ["The doc title field is required."]
  }
}