Contracts

Contracts are documents a client reads and signs, such as service agreements and terms of engagement.

A contract 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 Contracts

GET /api/v2/contracts

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

A contract'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 contracts issued to clients. Each row carries its sections, as in the single-contract response below.

{
  "total_count": 3,
  "page_count": 1,
  "page_size": 20,
  "contracts": [
    {
      "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/contracts

Contracts are also a nested resource for Clients

GET /api/v2/contracts?q=lawn

Search contracts by title or client name.

Get A Contract

GET /api/v2/contracts/:id

url is the contract 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/contracts/1",
  "client_view_url": "https://yourdomain.clientary.com/contracts/1/client_view/k-abc123",
  "pdf_url": "https://yourdomain.clientary.com/contracts/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 contract 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 Contract

POST /api/v2/contracts
{
  "contract": {
    "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.

{
  "contract": {
    "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 contract 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 Contract

PUT /api/v2/contracts/: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 contract 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 contract cannot be voided.

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

Delete A Contract

DELETE /api/v2/contracts/:id

Deletions are permanent and not reversible.

Send A Contract

POST /api/v2/contracts/:id/messages
{
  "recipients": ["[email protected]", "[email protected]"],
  "subject": "Subject for the contract",
  "message": "Body message for the contract",
  "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 contract sent.

Each named client signer is sent the contract 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 contract goes to someone at the client. When the last signature is in, everyone who signed is emailed the signed copy.