# Dubsado API

> Every Dubsado endpoint on Cotlet: inputs, JSON Schema, cURL, Python and Node.js examples and an example response for each call.

Source: https://cotlet.dev/docs/api/dubsado

Docs sections: [Get started](https://cotlet.dev/docs.md) · [REST API](https://cotlet.dev/docs/api.md) · [MCP](https://cotlet.dev/docs/mcp.md) · [SDKs & CLI](https://cotlet.dev/docs/sdks.md) · [Integrations](https://cotlet.dev/docs/integrations.md)

14 endpoints for Dubsado. Each one is a POST with a JSON body, called with your API key.

- Base URL: `https://cotlet.dev/api/v1/dubsado`
- Connects with: Your own Dubsado account; team members see what their permission level allows
- Integration page: https://cotlet.dev/integrations/dubsado-api-mcp-server

## Endpoints

| Endpoint | Access | Description |
|---|---|---|
| `list_projects` | read | Projects with client, lead or job, status, tags and project date. |
| `get_project` | read | One project: client, status, dates, tags, notes, forms and invoices. |
| `list_clients` | read | Clients with name, email, phone and their projects. |
| `list_form_templates` | read | Your form templates by type, to pick one to apply to a project. |
| `list_forms` | read | Forms across projects, or in one project, with type and status. |
| `get_form_responses` | read | Answers from a completed form or lead capture form, as question and answer pairs. |
| `list_invoices` | read | Invoices with project, total, amount paid, balance and due date. |
| `create_project` | write | New project for a new or existing client. Nothing is sent to the client. |
| `update_project_status` | write | Move a project to another of your statuses. |
| `apply_form_template` | write | Add a project copy of a proposal, contract, questionnaire or sub-agreement. Not sent yet. |
| `send_form` | write | Email a proposal, contract, questionnaire or sub-agreement to the client. |
| `create_invoice` | write | Draft an invoice in a project from your packages or line items. Not emailed yet. |
| `send_invoice` | write | Email the invoice to the client with a link to it. |
| `record_manual_payment` | write | Log a payment received outside Dubsado against an invoice. |

## List projects

`POST https://cotlet.dev/api/v1/dubsado/list_projects`

Access: read.

Lists projects newest first. Category is lead or job and follows the status: Dubsado statuses are either lead or job statuses. Filter by status name, category or creation date to see what came in since your last check. Pages hold up to 100 projects; pass next_cursor back as cursor for the next page.

### Parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `category` | enum | no | lead, job or all. Default: all. |
| `status` | string | no | Your project status name, e.g. "Proposal Sent". |
| `created_since` | date | no | Only projects created on or after this date. |
| `limit` | integer | no | 1 to 100. Default: 25. |
| `cursor` | string | no | Next page cursor from the previous call. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/dubsado/list_projects \
  -H "Authorization: Bearer $COTLET_API_KEY"
```

**Python**

```python
result = client.call("dubsado", "list_projects")
```

**Node.js**

```js
const result = await cotlet.call('dubsado', 'list_projects');
```

**CLI**

```bash
cotlet call dubsado list_projects
```

### Example response

```json
{
  "projects": [
    {
      "project_id": "prj_3d81b07e",
      "title": "Spring brand shoot",
      "client": {
        "client_id": "cli_41a7e2",
        "name": "Maya Chen",
        "email": "maya@example.com"
      },
      "category": "lead",
      "status": "Proposal Sent",
      "tags": [
        "Brand"
      ],
      "project_date": "2027-04-18",
      "created_at": "2026-10-07T13:12:00Z"
    }
  ],
  "next_cursor": null
}
```

## Get project

`POST https://cotlet.dev/api/v1/dubsado/get_project`

Access: read.

Returns one project with its client, status, tags and notes, the forms in it with their status, and its invoices with balances. Use it to read a whole project in one call before you act on it.

### Parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project ID from list_projects. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/dubsado/get_project \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"project_id":"project_8c41d2"}'
```

**Python**

```python
result = client.call("dubsado", "get_project", {"project_id":"project_8c41d2"})
```

**Node.js**

```js
const result = await cotlet.call('dubsado', 'get_project', {"project_id":"project_8c41d2"});
```

**CLI**

```bash
cotlet call dubsado get_project --data '{"project_id":"project_8c41d2"}'
```

### Example response

```json
{
  "project_id": "prj_3d81b07e",
  "title": "Spring brand shoot",
  "client": {
    "client_id": "cli_41a7e2",
    "name": "Maya Chen",
    "email": "maya@example.com"
  },
  "category": "lead",
  "status": "Proposal Sent",
  "tags": [
    "Brand"
  ],
  "project_date": "2027-04-18",
  "notes": "Referred by a past client.",
  "forms": [
    {
      "form_id": "frm_6f2a91c4",
      "type": "proposal",
      "name": "Brand Photography proposal",
      "status": "sent"
    }
  ],
  "invoices": []
}
```

## List clients

`POST https://cotlet.dev/api/v1/dubsado/list_clients`

Access: read.

Lists clients, the people and companies you work with. Search by name or email. Each client lists its project IDs, which you pass to get_project. Pages hold up to 100 clients.

### Parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `query` | string | no | Search by name or email. |
| `limit` | integer | no | 1 to 100. Default: 25. |
| `cursor` | string | no | Next page cursor from the previous call. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/dubsado/list_clients \
  -H "Authorization: Bearer $COTLET_API_KEY"
```

**Python**

```python
result = client.call("dubsado", "list_clients")
```

**Node.js**

```js
const result = await cotlet.call('dubsado', 'list_clients');
```

**CLI**

```bash
cotlet call dubsado list_clients
```

### Example response

```json
{
  "clients": [
    {
      "client_id": "cli_41a7e2",
      "name": "Maya Chen",
      "email": "maya@example.com",
      "phone": "+1 555 0100",
      "projects": [
        "prj_3d81b07e"
      ]
    }
  ],
  "next_cursor": null
}
```

## List form templates

`POST https://cotlet.dev/api/v1/dubsado/list_form_templates`

Access: read.

Returns the templates you built in Dubsado: proposals, contracts, questionnaires, sub-agreements and lead capture forms. Pass template_id to apply_form_template. Templates are read only here.

### Parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `type` | enum | no | proposal, contract, questionnaire, sub_agreement, lead_capture or all. Default: all. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/dubsado/list_form_templates \
  -H "Authorization: Bearer $COTLET_API_KEY"
```

**Python**

```python
result = client.call("dubsado", "list_form_templates")
```

**Node.js**

```js
const result = await cotlet.call('dubsado', 'list_form_templates');
```

**CLI**

```bash
cotlet call dubsado list_form_templates
```

### Example response

```json
{
  "templates": [
    {
      "template_id": "tpl_5c20aa",
      "name": "Brand Photography proposal",
      "type": "proposal"
    },
    {
      "template_id": "tpl_81d9f3",
      "name": "Photography contract",
      "type": "contract"
    },
    {
      "template_id": "tpl_2e7b60",
      "name": "Shoot questionnaire",
      "type": "questionnaire"
    }
  ]
}
```

## List forms

`POST https://cotlet.dev/api/v1/dubsado/list_forms`

Access: read.

Lists forms sent from projects, the same forms as Forms, All forms in Dubsado. Status is sent, viewed, action_required or completed. Action required means a contract waits for your countersignature. Filter by type, status or project. Pass form_id to get_form_responses.

### Parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | no | Only forms in this project. |
| `type` | enum | no | proposal, contract, questionnaire, sub_agreement, lead_capture or all. Default: all. |
| `status` | string[] | no | Any of sent, viewed, action_required, completed. Default: all statuses. |
| `limit` | integer | no | 1 to 100. Default: 25. |
| `cursor` | string | no | Next page cursor from the previous call. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/dubsado/list_forms \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"contract","status":["sent","viewed","action_required"]}'
```

**Python**

```python
result = client.call("dubsado", "list_forms", {"type":"contract","status":["sent","viewed","action_required"]})
```

**Node.js**

```js
const result = await cotlet.call('dubsado', 'list_forms', {"type":"contract","status":["sent","viewed","action_required"]});
```

**CLI**

```bash
cotlet call dubsado list_forms --data '{"type":"contract","status":["sent","viewed","action_required"]}'
```

### Example response

```json
{
  "forms": [
    {
      "form_id": "frm_a4410c7e",
      "project_id": "prj_8e15c3a2",
      "project_title": "Acme rebrand",
      "type": "contract",
      "name": "Design services contract",
      "status": "viewed"
    }
  ],
  "next_cursor": null
}
```

## Get form responses

`POST https://cotlet.dev/api/v1/dubsado/get_form_responses`

Access: read.

Returns the answers a client gave, in the order of the form, the same as the Responses view on the form. Only completed forms have responses. Questionnaires can be edited after completion, so the answers can change.

### Parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `form_id` | string | yes | Form ID from list_forms or apply_form_template. |
| `include_empty` | boolean | no | Include unanswered fields. Default: false. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/dubsado/get_form_responses \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"form_id":"frm_9b42e6d1","include_empty":true}'
```

**Python**

```python
result = client.call("dubsado", "get_form_responses", {"form_id":"frm_9b42e6d1","include_empty":True})
```

**Node.js**

```js
const result = await cotlet.call('dubsado', 'get_form_responses', {"form_id":"frm_9b42e6d1","include_empty":true});
```

**CLI**

```bash
cotlet call dubsado get_form_responses --data '{"form_id":"frm_9b42e6d1","include_empty":true}'
```

### Example response

```json
{
  "form_id": "frm_9b42e6d1",
  "type": "questionnaire",
  "name": "Shoot questionnaire",
  "project_id": "prj_3d81b07e",
  "responses": [
    {
      "question": "What is the shoot for?",
      "answer": "New website and press kit."
    },
    {
      "question": "Preferred location",
      "answer": "Studio, downtown"
    }
  ]
}
```

## List invoices

`POST https://cotlet.dev/api/v1/dubsado/list_invoices`

Access: read.

Lists invoices, newest first. The due date comes from the invoice payment plan and is empty when the invoice has none. Balance is the total minus the payments logged, including those logged by hand. Pages hold up to 100 invoices.

### Parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `status` | enum | no | unpaid, paid or all. Default: unpaid. |
| `project_id` | string | no | Only invoices in this project. |
| `due_before` | date | no | Only invoices with a payment due before this date. |
| `limit` | integer | no | 1 to 100. Default: 25. |
| `cursor` | string | no | Next page cursor from the previous call. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/dubsado/list_invoices \
  -H "Authorization: Bearer $COTLET_API_KEY"
```

**Python**

```python
result = client.call("dubsado", "list_invoices")
```

**Node.js**

```js
const result = await cotlet.call('dubsado', 'list_invoices');
```

**CLI**

```bash
cotlet call dubsado list_invoices
```

### Example response

```json
{
  "invoices": [
    {
      "invoice_id": "inv_20417",
      "project_id": "prj_8e15c3a2",
      "project_title": "Acme rebrand",
      "number": "0042",
      "total": "1200.00",
      "paid": "400.00",
      "balance": "800.00",
      "due_date": "2026-10-11"
    }
  ],
  "next_cursor": null
}
```

## Create project

`POST https://cotlet.dev/api/v1/dubsado/create_project`

Access: write.

Creates a project from a title and a client email. If the email is not in Dubsado, a new client is created, with client_name if you give one. The status must be one of your project statuses and decides whether the project is a lead or a job. This call sends nothing to the client, so it asks no confirmation. You can archive or delete the project in Dubsado.

### Parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `title` | string | yes | Project title, e.g. "Brand Photography". |
| `client_email` | string | yes | Existing client, or a new one is created. |
| `client_name` | string | no | Name for a new client. |
| `status` | string | no | One of your project status names. |
| `project_date` | date | no | Project date. |
| `tags` | string[] | no | Project tags. |
| `notes` | string | no | Notes on the project. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/dubsado/create_project \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Spring brand shoot","client_email":"maya@example.com","client_name":"Maya Chen","status":"Proposal Sent","project_date":"2027-04-18"}'
```

**Python**

```python
result = client.call("dubsado", "create_project", {"title":"Spring brand shoot","client_email":"maya@example.com","client_name":"Maya Chen","status":"Proposal Sent","project_date":"2027-04-18"})
```

**Node.js**

```js
const result = await cotlet.call('dubsado', 'create_project', {"title":"Spring brand shoot","client_email":"maya@example.com","client_name":"Maya Chen","status":"Proposal Sent","project_date":"2027-04-18"});
```

**CLI**

```bash
cotlet call dubsado create_project --data '{"title":"Spring brand shoot","client_email":"maya@example.com","client_name":"Maya Chen","status":"Proposal Sent","project_date":"2027-04-18"}'
```

### Example response

```json
{
  "project_id": "prj_3d81b07e",
  "title": "Spring brand shoot",
  "client": {
    "client_id": "cli_41a7e2",
    "name": "Maya Chen",
    "email": "maya@example.com"
  },
  "category": "lead",
  "status": "Inquiry",
  "created_at": "2026-10-11T09:00:00Z"
}
```

## Update project status

`POST https://cotlet.dev/api/v1/dubsado/update_project_status`

Access: write.

Moves one project to a status you already have. A project can move from a lead status to a job status; Dubsado does not turn a status from one kind into the other. The call sends nothing to the client and you undo it with one more call, so it asks no confirmation. A Flow that starts on a status change still runs.

### Parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project ID from list_projects. |
| `status` | string | yes | One of your project status names. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/dubsado/update_project_status \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"project_id":"project_8c41d2","status":"status"}'
```

**Python**

```python
result = client.call("dubsado", "update_project_status", {"project_id":"project_8c41d2","status":"status"})
```

**Node.js**

```js
const result = await cotlet.call('dubsado', 'update_project_status', {"project_id":"project_8c41d2","status":"status"});
```

**CLI**

```bash
cotlet call dubsado update_project_status --data '{"project_id":"project_8c41d2","status":"status"}'
```

### Example response

```json
{
  "project_id": "prj_3d81b07e",
  "previous_status": "Proposal Sent",
  "status": "Booked",
  "category": "job"
}
```

## Apply form template

`POST https://cotlet.dev/api/v1/dubsado/apply_form_template`

Access: write.

Adds a copy of a template to one project and returns its form_id. Changes to the copy do not touch your template. Nothing is sent and the form is not added to the client portal, so this call asks no confirmation. Send it with send_form, or remove the copy in Dubsado.

### Parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project ID from list_projects. |
| `template_id` | string | yes | Template ID from list_form_templates. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/dubsado/apply_form_template \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"project_id":"project_8c41d2","template_id":"template_8c41d2"}'
```

**Python**

```python
result = client.call("dubsado", "apply_form_template", {"project_id":"project_8c41d2","template_id":"template_8c41d2"})
```

**Node.js**

```js
const result = await cotlet.call('dubsado', 'apply_form_template', {"project_id":"project_8c41d2","template_id":"template_8c41d2"});
```

**CLI**

```bash
cotlet call dubsado apply_form_template --data '{"project_id":"project_8c41d2","template_id":"template_8c41d2"}'
```

### Example response

```json
{
  "form_id": "frm_6f2a91c4",
  "project_id": "prj_3d81b07e",
  "type": "proposal",
  "name": "Brand Photography proposal",
  "status": "not_sent"
}
```

## Send form

`POST https://cotlet.dev/api/v1/dubsado/send_form`

Access: write. Confirm: waits for the user's OK before it runs.

Emails the form to the client with a link to it. Without subject and message it uses your default email template for the form. After the client completes it, the form is locked, except questionnaires. By default you confirm each call; you can switch that off for this tool.

### Parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `form_id` | string | yes | Form ID from list_forms or apply_form_template. |
| `subject` | string | no | Email subject. Default: from your email template. |
| `message` | string | no | Email body. Default: from your email template. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/dubsado/send_form \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"form_id":"frm_6f2a91c4","subject":"Your proposal for Spring brand shoot"}'
```

**Python**

```python
result = client.call("dubsado", "send_form", {"form_id":"frm_6f2a91c4","subject":"Your proposal for Spring brand shoot"})
```

**Node.js**

```js
const result = await cotlet.call('dubsado', 'send_form', {"form_id":"frm_6f2a91c4","subject":"Your proposal for Spring brand shoot"});
```

**CLI**

```bash
cotlet call dubsado send_form --data '{"form_id":"frm_6f2a91c4","subject":"Your proposal for Spring brand shoot"}'
```

### Example response

```json
{
  "form_id": "frm_6f2a91c4",
  "project_id": "prj_3d81b07e",
  "type": "proposal",
  "status": "sent",
  "sent_to": "maya@example.com",
  "sent_at": "2026-10-11T09:02:00Z"
}
```

## Create invoice

`POST https://cotlet.dev/api/v1/dubsado/create_invoice`

Access: write. Confirm: waits for the user's OK before it runs.

Creates an invoice in a project from saved packages, line items, or both. Dubsado numbers it automatically. The invoice date is the issue date; due_date adds a payment plan with one payment on that date. An invoice worth more than $0 appears in the client portal before it is emailed, so this call needs your OK. Email it with send_invoice. By default you confirm each call; you can switch that off for this tool.

### Parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project ID from list_projects. |
| `package_ids` | string[] | no | IDs of your saved packages to add. |
| `line_items` | object[] | no | List of { name, quantity, price, description }. |
| `due_date` | date | no | Adds a payment plan with one payment due on this date. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/dubsado/create_invoice \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"project_id":"project_8c41d2"}'
```

**Python**

```python
result = client.call("dubsado", "create_invoice", {"project_id":"project_8c41d2"})
```

**Node.js**

```js
const result = await cotlet.call('dubsado', 'create_invoice', {"project_id":"project_8c41d2"});
```

**CLI**

```bash
cotlet call dubsado create_invoice --data '{"project_id":"project_8c41d2"}'
```

### Example response

```json
{
  "invoice_id": "inv_20417",
  "project_id": "prj_8e15c3a2",
  "number": "0042",
  "status": "not_sent",
  "line_items": [
    {
      "name": "Logo package",
      "quantity": 1,
      "price": "1200.00"
    }
  ],
  "total": "1200.00",
  "balance": "1200.00",
  "due_date": "2026-10-25"
}
```

## Send invoice

`POST https://cotlet.dev/api/v1/dubsado/send_invoice`

Access: write. Confirm: waits for the user's OK before it runs.

Emails the invoice to the client with the invoice link. Without a message it uses your default email template. After it is sent, the automatic payment reminders of Dubsado follow the payment plan. By default you confirm each call; you can switch that off for this tool.

### Parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `invoice_id` | string | yes | Invoice ID from list_invoices or create_invoice. |
| `message` | string | no | Email body. Default: from your email template. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/dubsado/send_invoice \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"invoice_id":"invoice_8c41d2"}'
```

**Python**

```python
result = client.call("dubsado", "send_invoice", {"invoice_id":"invoice_8c41d2"})
```

**Node.js**

```js
const result = await cotlet.call('dubsado', 'send_invoice', {"invoice_id":"invoice_8c41d2"});
```

**CLI**

```bash
cotlet call dubsado send_invoice --data '{"invoice_id":"invoice_8c41d2"}'
```

### Example response

```json
{
  "invoice_id": "inv_20417",
  "status": "sent",
  "sent_to": "client@example.com",
  "sent_at": "2026-10-11T09:05:00Z"
}
```

## Record manual payment

`POST https://cotlet.dev/api/v1/dubsado/record_manual_payment`

Access: write. Confirm: waits for the user's OK before it runs.

Logs a payment you received outside Dubsado, such as cash or a check, on one invoice. The amount cannot be more than the remaining balance, and a payment for the full balance moves the invoice to Paid. It does not cancel autopay. Only manually logged payments can be edited or deleted, in Dubsado. By default you confirm each call; you can switch that off for this tool.

### Parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `invoice_id` | string | yes | Invoice ID from list_invoices or create_invoice. |
| `amount` | string | yes | Amount, e.g. "400.00". Not more than the remaining balance. |
| `paid_on` | date | yes | Date received. |
| `method` | enum | yes | cash, check, credit, e_transfer, paypal, stripe, square, authorize_net or other. |
| `description` | string | no | Optional note on the payment. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/dubsado/record_manual_payment \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"invoice_id":"inv_20417","amount":"400.00","paid_on":"2026-10-09","method":"e_transfer","description":"E-transfer ref 7731"}'
```

**Python**

```python
result = client.call("dubsado", "record_manual_payment", {"invoice_id":"inv_20417","amount":"400.00","paid_on":"2026-10-09","method":"e_transfer","description":"E-transfer ref 7731"})
```

**Node.js**

```js
const result = await cotlet.call('dubsado', 'record_manual_payment', {"invoice_id":"inv_20417","amount":"400.00","paid_on":"2026-10-09","method":"e_transfer","description":"E-transfer ref 7731"});
```

**CLI**

```bash
cotlet call dubsado record_manual_payment --data '{"invoice_id":"inv_20417","amount":"400.00","paid_on":"2026-10-09","method":"e_transfer","description":"E-transfer ref 7731"}'
```

### Example response

```json
{
  "invoice_id": "inv_20417",
  "amount": "400.00",
  "method": "check",
  "paid_on": "2026-10-12",
  "balance": "800.00",
  "invoice_status": "unpaid"
}
```
