---
title: "Edit, add or remove records: a unit, building, lease, contact or any other"
method: POST
path: "/api/v1/portfolio/changes"
tags: ["Portfolio"]
---

# Edit, add or remove records: a unit, building, lease, contact or any other

`POST /api/v1/portfolio/changes`

## Request

- Base URL: `http://localhost:5180`
- URL: `http://localhost:5180/api/v1/portfolio/changes`
- Auth: API key in cookie `better-auth.session_token`

The write path for everything the owner edits, and the way to update, change or set any field of a record, such as a unit’s purchase price (`acquisition`), a lease’s rent or a contact’s address. To change some fields of a record that exists, send a `patch` naming the collection, the id and only those fields; read the record first if you need what it holds. An `upsert` writes a whole record, which is how a new one is added. The whole list is applied in one D1 batch, so it either all lands or none of it does, and it is applied in the order it was given. The changes go into the signed-in owner’s rows and there is no way to address anyone else’s. Every record carries a server-owned `rev`: an upsert or a remove sends the revision it was based on, the account checks that the row still stands there, and a batch with a stale change in it is refused whole with the current rows in the 409 body. A record written for the first time carries no `rev` and comes back at 1.

## Request body

- object
  - `schemaVersion` integer: The schema the client was built against, as GET /portfolio reports it (currently 10). Any other value, or none, is refused: a page from an older build must not keep writing older rows. A batch of patches alone may leave it out, because a patch writes onto the record in the shape the account holds it.
  - `changes` union[], required: Applied in the order given, in one transaction. An upsert then a remove of the same id leaves nothing behind.
    - union
      - object: A whole record. What every new record starts with, a hue or the time it was made, is filled in where it is left out.
        - `op` 'upsert', required
        - `collection` 'bankAccounts' | 'bankFeeds' | 'statementImports' | 'buildings' | 'units' | 'meters' | 'meterReadings' | 'keySets' | 'inventoryItems' | 'contacts' | 'contractors' | 'leases' | 'leaseTemplates' | 'leaseClauses' | 'deposits' | 'inspections' | 'applications' | 'viewings' | 'listings' | 'invoices' | 'payments' | 'bankTransactions' | 'reconciliationMatches' | 'arrearsCases' | 'paymentPlans' | 'expenses' | 'supplierInvoices' | 'mortgages' | 'serviceChargePeriods' | 'budgets' | 'issues' | 'workOrders' | 'quotes' | 'maintenancePlans' | 'certificates' | 'mailboxes' | 'mailAliases' | 'threads' | 'messages' | 'calls' | 'templates' | 'workflows' | 'workflowRuns' | 'approvalRequests' | 'auditEvents' | 'tasks' | 'notifications' | 'channelConnections' | 'bookings' | 'cleaningTasks' | 'pricingRules' | 'reviews' | 'documents' | 'documentFolders' | 'equipment' | 'insurancePolicies' | 'reports' | 'savedViews' | 'customSections' | 'jurisdictions' | 'jurisdictionOverrides', required: Which collection the change lands in, e.g. `leases`.
        - `entity` object, required: The whole record, not a patch: its lists are replaced whole in the same batch, so a write cannot half-apply. A field every record holds is refused when it is missing. Its `rev` is the revision the edit was based on; leave it out for a new record.
          - `id` string, required
          - `rev` number: The revision this edit was based on, as the last read or write handed it back. Left out for a brand new record.
      - object
        - `op` 'remove', required
        - `collection` 'bankAccounts' | 'bankFeeds' | 'statementImports' | 'buildings' | 'units' | 'meters' | 'meterReadings' | 'keySets' | 'inventoryItems' | 'contacts' | 'contractors' | 'leases' | 'leaseTemplates' | 'leaseClauses' | 'deposits' | 'inspections' | 'applications' | 'viewings' | 'listings' | 'invoices' | 'payments' | 'bankTransactions' | 'reconciliationMatches' | 'arrearsCases' | 'paymentPlans' | 'expenses' | 'supplierInvoices' | 'mortgages' | 'serviceChargePeriods' | 'budgets' | 'issues' | 'workOrders' | 'quotes' | 'maintenancePlans' | 'certificates' | 'mailboxes' | 'mailAliases' | 'threads' | 'messages' | 'calls' | 'templates' | 'workflows' | 'workflowRuns' | 'approvalRequests' | 'auditEvents' | 'tasks' | 'notifications' | 'channelConnections' | 'bookings' | 'cleaningTasks' | 'pricingRules' | 'reviews' | 'documents' | 'documentFolders' | 'equipment' | 'insurancePolicies' | 'reports' | 'savedViews' | 'customSections' | 'jurisdictions' | 'jurisdictionOverrides', required: Which collection the change lands in, e.g. `leases`.
        - `id` string, required: Stable record id, e.g. `lse_012`.
        - `rev` number: The revision of the record being removed, as last read. A record that has moved on since is not removed.
      - object
        - `op` 'settings', required
        - `settings` object, required: The whole settings block, in the AppSettings shape GET /settings documents. A field the spec does not list yet is kept.
      - object
        - `op` 'patch', required
        - `collection` 'bankAccounts' | 'bankFeeds' | 'statementImports' | 'buildings' | 'units' | 'meters' | 'meterReadings' | 'keySets' | 'inventoryItems' | 'contacts' | 'contractors' | 'leases' | 'leaseTemplates' | 'leaseClauses' | 'deposits' | 'inspections' | 'applications' | 'viewings' | 'listings' | 'invoices' | 'payments' | 'bankTransactions' | 'reconciliationMatches' | 'arrearsCases' | 'paymentPlans' | 'expenses' | 'supplierInvoices' | 'mortgages' | 'serviceChargePeriods' | 'budgets' | 'issues' | 'workOrders' | 'quotes' | 'maintenancePlans' | 'certificates' | 'mailboxes' | 'mailAliases' | 'threads' | 'messages' | 'calls' | 'templates' | 'workflows' | 'workflowRuns' | 'approvalRequests' | 'auditEvents' | 'tasks' | 'notifications' | 'channelConnections' | 'bookings' | 'cleaningTasks' | 'pricingRules' | 'reviews' | 'documents' | 'documentFolders' | 'equipment' | 'insurancePolicies' | 'reports' | 'savedViews' | 'customSections' | 'jurisdictions' | 'jurisdictionOverrides', required: Which collection the change lands in, e.g. `leases`.
        - `id` string, required: Stable record id, e.g. `lse_012`.
        - `fields` object, required: Only the fields to change, each replacing that field whole: `{ "acquisition": { "date": "2026-05-01", "price": 25000000, "costs": 0 } }`. Every field left out keeps what the record holds, lists included.
        - `rev` number: The revision the edit was based on, when you read one. Left out, the patch lands on the record as it stands.

## Response `200`

Every change was written. `revs` is what each upserted record is at now.

- object
  - `ok` unknown, required
  - `data` object, required
    - `applied` integer, required: How many changes were written. Equal to the number sent, or the request failed whole.
    - `revs` object[], required: One entry per upsert, in the order sent: the revision each record carries now.
      - `collection` 'bankAccounts' | 'bankFeeds' | 'statementImports' | 'buildings' | 'units' | 'meters' | 'meterReadings' | 'keySets' | 'inventoryItems' | 'contacts' | 'contractors' | 'leases' | 'leaseTemplates' | 'leaseClauses' | 'deposits' | 'inspections' | 'applications' | 'viewings' | 'listings' | 'invoices' | 'payments' | 'bankTransactions' | 'reconciliationMatches' | 'arrearsCases' | 'paymentPlans' | 'expenses' | 'supplierInvoices' | 'mortgages' | 'serviceChargePeriods' | 'budgets' | 'issues' | 'workOrders' | 'quotes' | 'maintenancePlans' | 'certificates' | 'mailboxes' | 'mailAliases' | 'threads' | 'messages' | 'calls' | 'templates' | 'workflows' | 'workflowRuns' | 'approvalRequests' | 'auditEvents' | 'tasks' | 'notifications' | 'channelConnections' | 'bookings' | 'cleaningTasks' | 'pricingRules' | 'reviews' | 'documents' | 'documentFolders' | 'equipment' | 'insurancePolicies' | 'reports' | 'savedViews' | 'customSections' | 'jurisdictions' | 'jurisdictionOverrides', required: Which collection the change lands in, e.g. `leases`.
      - `id` string, required: Stable record id, e.g. `lse_012`.
      - `rev` integer, required: The revision now on file. Send it back with the next edit to that record.
  - `meta` object, required
    - `requestId` string, required: Echoed in the x-request-id header. Quote it in a bug report.

## Other responses

- `400`: A malformed change, a patch whose result its collection would refuse, a record missing a field its collection requires, an unknown collection, more than 200 at a time, or a schemaVersion other than the current one (then the message is "This page is running an older version of Dardaris. Reload it to keep saving." and details carry the current schemaVersion).
- `404`: A patch names a record the account does not hold.
- `409`: At least one change was based on a copy that has since changed: nothing was written, and details.conflicts names the records and how they stand now. Or the batch removes a bank account still connected to its bank: nothing was written, details.reason is `bank_linked` and details.bankAccountIds names them, to be disconnected first.
- `422`: The batch would take the portfolio past the number of units the plan covers. Nothing was written; details carry the plan, its ceiling, and the plan that fits.
- `429`: Writing faster than the limit allows.

---

[API](https://skmtc.dev/dardaris/apis/dardaris-api.md) · [All operations](https://skmtc.dev/dardaris/apis/dardaris-api/llms.txt) · [OpenAPI document](https://skmtc.dev/dardaris/apis/dardaris-api/revisions/4aff33835c73?raw)
