Proposals

Proposals are documents that pitch work to a client, with the fee packages they can accept or choose between.

A proposal is made of ordered sections. A section is either freeform text, stored as Quill delta operations, or a fees section: one or more priced packages the client sees and, when the section allows a choice, picks from. Any section may require the client's signature.

Get Proposals

GET /api/v2/proposals

Accepts project_id, for the proposals on one project, status (draft, sent, viewed, signed, voided), from_date, and to_date, by proposal date.

A proposal's project is the project it belongs to: one made from it, or one it was converted to in the app. It is null otherwise, and it can't be set through the API.

Templates are not included; only proposals issued to clients. Each row carries its sections, as in the single-proposal response below.

{
  "total_count": 3,
  "page_count": 1,
  "page_size": 20,
  "proposals": [
    {
      "id": 1,
      "subject": "Lawn refresh agreement",
      "status": "draft",
      "date": "2026-09-11",
      "client_id": 12,
      "project": { "id": 7, "name": "Lawn refresh" },
      "created_at": "2026-09-11T16:10:59Z",
      "updated_at": "2026-09-11T16:10:59Z"
    },
    ...
  ]
}
GET /api/v2/clients/:client_id/proposals

Proposals are also a nested resource for Clients

GET /api/v2/proposals?q=lawn

Search proposals by title or client name.

Get A Proposal

GET /api/v2/proposals/:id

url is the proposal in the app. client_view_url is the client's own view, no login needed, and pdf_url is its PDF. Both keyed links can be shared as they stand.

{
  "id": 1,
  "subject": "Lawn refresh agreement",
  "status": "draft",
  "date": "2026-09-11",
  "client_id": 12,
  "url": "https://yourdomain.clientary.com/proposals/1",
  "client_view_url": "https://yourdomain.clientary.com/proposals/1/client_view/k-abc123",
  "pdf_url": "https://yourdomain.clientary.com/proposals/1/pdf/k-abc123.pdf",
  "contract_sections": [
    {
      "id": 10,
      "position": 0,
      "section_type": "freeform",
      "content": [
        { "insert": "Scope" },
        { "insert": "\n", "attributes": { "header": 2 } },
        { "insert": "Remove old sod and lay new turf.\n" }
      ]
    },
    {
      "id": 11,
      "position": 1,
      "section_type": "fees",
      "content": {
        "type": "static",
        "title": "Fee",
        "instructions": [ { "insert": "\n" } ],
        "options": [
          {
            "title": "",
            "description": "",
            "line_items": [
              { "item_type": "service", "title": "Lawn refresh", "quantity": 1, "price": 5000, "taxable": false }
            ]
          }
        ]
      }
    }
  ],
  "author_signature": null,
  "fully_signed": false,
  "signers": [
    { "id": 3, "position": 0, "party": "staff", "user_id": 7, "name": "Pat Partner", "open": false, "signed": true, "signed_at": "2026-09-28T17:02:11Z", "signature": null },
    { "id": 4, "position": 1, "party": "client", "user_id": 21, "name": "Casey Client", "open": false, "signed": false, "signed_at": null, "signature": null }
  ]
}
Signing

signers lists who signs, the team first. party is staff or client. A client signer with no user_id is open: any contact the proposal is sent to can sign it. signed says whether each has signed and signed_at when. fully_signed becomes true when the last signature is in, which is when the status becomes signed, and no signature image appears until then.
With no signers, the author signs for the account and any one contact signs for the client. A blank author_signature is the author's default signature, their name in a signature style, until they draw their own. It is "false" when there is no company signature.

Create A New Proposal

POST /api/v2/proposals
{
  "proposal": {
    "client_id": 12,
    "subject": "Lawn refresh agreement",
    "date": "2026-09-11",
    "contract_sections_attributes": [
      {
        "position": 0,
        "section_type": "freeform",
        "content": [ { "insert": "Remove old sod and lay new turf.\n" } ]
      },
      {
        "position": 1,
        "section_type": "fees",
        "content": {
          "type": "static",
          "title": "Fee",
          "instructions": [ { "insert": "\n" } ],
          "options": [
            {
              "line_items": [
                { "item_type": "service", "title": "Lawn refresh", "quantity": 1, "price": 5000, "taxable": false }
              ]
            }
          ]
        }
      }
    ]
  }
}

Required Fields: client_id, date, and at least one section or a template_id (HTTP 422 on failure)
From a template: pass template_id beside the document to start from one of the account's templates or a built-in default. See Templates.
Optional Fields: settings_auto_create_invoice_after_sign - when the client signs, an invoice for what they chose is created and sent to them automatically
Who signs: contract_signers_attributes, each entry a party (staff or client), a user_id (a member of staff, or a contact of the client, with an email address), and a position. A client entry may name someone new by signer_name and signer_email, who becomes a contact. The only client entry may instead leave both out, to be open to any contact. Named team members sign in place of the author. Set author_signature to "false" for no company signature. A list that names only team members keeps one open client signer, since the client always signs.

{
  "proposal": {
    "client_id": 12,
    "date": "2026-09-28",
    "contract_signers_attributes": [
      { "party": "staff", "user_id": 7, "position": 0 },
      { "party": "client", "user_id": 21, "position": 1 },
      { "party": "client", "signer_name": "Sam Client", "signer_email": "[email protected]", "position": 2 }
    ],
    "contract_sections_attributes": [ { "content": [ { "insert": "Terms.\n" } ] } ]
  }
}

Signatures are never given through the API. Each person signs in the app, from their own link or their own session. A request that tries to sign is refused (HTTP 422): one with a signature image in author_signature, or with account_signature_id, which would apply the company signature saved in the account's settings. A proposal made from a template leaves the template's signature behind, and carries its author's default signature until they draw their own in the app.

Freeform sections

content is an array of Quill delta operations. Plain text is one insert ending in a newline. A wrapped form, { "ops": [...] }, is accepted as well.

Fees sections

content is an object:

type: static shows the packages. single lets the client choose one package; multi lets them choose several. Both choice types need at least two options, each with a title.
title: Heading shown above the packages.
instructions: Quill delta operations shown above the packages; [ { "insert": "\n" } ] for none.
options: The packages. Each has an optional title and description and a required line_items array.
line_items[].item_type: service for a priced line, header for a label with no amount.
line_items[].title: Required.
line_items[].quantity, price: Required for a service line.
line_items[].taxable: Required, true or false.

A package may also carry currency_code, tax, tax_label, tax2, tax2_label, tax2_enabled, and compound_tax.

Update A Proposal

PUT /api/v2/proposals/:id

You may provide a partial list of fields to update. Sections are updated through contract_sections_attributes: include an id to change an existing section, omit it to add one, or send "_destroy": true with an id to remove one. Who signs is given whole through contract_signers_attributes: the signers listed, and no others. Include a signer's id to keep them, and leave someone off to remove them.

Only a draft changes. Once a proposal has been sent, neither its sections nor its signers change: void it ("status": "voided"), which takes back any signatures collected on it, then reopen it ("status": "draft"). A signed proposal cannot be voided.

{
  "proposal": {
    "subject": "Lawn refresh agreement, revised",
    "contract_sections_attributes": [
      { "id": 10, "_destroy": true },
      { "position": 0, "section_type": "freeform", "content": [ { "insert": "Revised scope.\n" } ] }
    ]
  }
}

Delete A Proposal

DELETE /api/v2/proposals/:id

Deletions are permanent and not reversible.

Send A Proposal

POST /api/v2/proposals/:id/messages
{
  "recipients": ["[email protected]", "[email protected]"],
  "subject": "Subject for the proposal",
  "message": "Body message for the proposal",
  "send_copy": 1,
  "attach_pdf": 1
}

Required Fields: recipients - an array of email addresses. (HTTP 422 on failure)
Encouraged Fields: subject, message are strongly encouraged, since this is what displays in the email, but it is not required.
Optional Fields: send_copy optionally allows the sending user (dependent on API Token used) to receive a copy of the email, attach_pdf allows an optional PDF to be attached.

An address that is not yet a contact of the client becomes one. Each recipient gets their own link to view and sign it. Sending marks the proposal sent.

Each named client signer is sent the proposal whether or not they are listed, with a link that signs as them. Each named team member who has not signed is emailed their own link, once the proposal goes to someone at the client. When the last signature is in, everyone who signed is emailed the signed copy.