Welcome to the Coperniq API release notes. This section highlights noteworthy changes across endpoints, schemas, and docs.

Recent highlights

  • New: Labels — GET /labels (filter by type), POST /labels, GET /labels/{labelId}. type is WORK (work orders) or ASSET. v2 mirrors these under /v2 with the standard success envelope.
  • New: Assets — GET /assets (filter by account_id), POST /assets, GET/PATCH /assets/{assetId} (set isArchived: true via PATCH to archive). v2 mirrors these under /v2 with the standard success envelope.
  • New: Invoice payments — GET /invoices/{invoiceId}/payments, GET /invoices/{invoiceId}/payments/{paymentId}, and POST /invoices/{invoiceId}/payments (optional paymentReference on create).
  • New: Quotes — full CRUD via GET/POST /quotes, GET/PATCH/DELETE /quotes/{quoteId}, GET /quotes/{quoteId}/pdf, POST /quotes/{quoteId}/send, and GET /opportunities/{opportunityId}/quotes.
  • New: Work order line items — PUT /work-orders/{workOrderId}/line-items replaces line items on service work orders (account type). GET /work-orders/{workOrderId} now includes lineItems when present.
  • New: Taxes — GET /taxes lists company tax rates with pagination and archived filtering.
  • New: Bills — full CRUD for project bills via GET/POST /bills, GET/PATCH/DELETE /bills/{billId}, and GET /projects/{projectId}/bills. Supports LINE_ITEMS and PERCENTAGE calculation methods.
  • Improvement: Nullable fields on PATCH /projects/{projectId}, PATCH /accounts/{accountId}, and PATCH /clients/{clientId} — pass null to clear a field (e.g. description, ownerId, primaryEmail) or any custom property.

Looking for a specific date? See the entries below.

Labels — list and create

Manage labels that can be applied to work orders and assets.

Endpoints

MethodPathDescription
GET/labelsList all active labels (paginated, filterable by type)
POST/labelsCreate a label
GET/labels/{labelId}Get a label by ID

v2 exposes the same routes under the /v2 prefix with the standard { status, message, data } envelope.

Query parameters (GET /labels)

ParameterTypeDefaultDescription
typeWORK | ASSETFilter by label type. Omit to return all types.
pageinteger1Page number (1-based, max 1000)
page_sizeinteger20Results per page (max 100)

Create a label

POST /labels
1{
2 "name": "Urgent",
3 "color": "#FF5733",
4 "type": "WORK"
5}
  • name — required, non-empty string.
  • type — required; WORK applies the label to work orders, ASSET to assets. Note: the public API uses WORK where the internal system uses TASK.
  • color — optional hex color code; pass null to create a label with no color.

Label object

1{
2 "id": 101,
3 "name": "Urgent",
4 "color": "#FF5733",
5 "type": "WORK",
6 "createdAt": "2026-04-30T00:00:00.000Z",
7 "updatedAt": "2026-04-30T00:00:00.000Z"
8}

Assets — list, create, read, update

Manage physical assets tied to a site and account. Reads use project-service GraphQL; creates and updates call fleet-service.

Endpoints

MethodPathDescription
GET/assetsList assets (filters, pagination, optional account_id)
POST/assetsCreate an asset
GET/assets/{assetId}Get asset by id
PATCH/assets/{assetId}Update asset fields, including archiving

v2 exposes the same routes under the /v2 prefix with the standard { status, message, data } JSON envelope where applicable.

Query parameters (GET /assets)

ParameterDescription
page, page_sizePagination (page_size max 100)
order_byasc or desc by createdAt (default desc)
account_idFilter by account
site_idFilter by site
status, typeExact match
manufacturerCase-insensitive contains
is_archivedtrue / false (default false)
updated_after, updated_beforeISO date-time filters on updatedAt

Create body highlights

  • Required: accountId, siteId, type, manufacturer, model, serialNumber.
  • Optional: status, labelIds, dates, description, image file ids, size, expectedLifetime.

PATCH notes

  • siteId and accountId cannot be changed after creation; PATCH requests that include either field are rejected by request validation with 400 rather than being silently ignored.
  • Set isArchived: true to archive an asset, or isArchived: false to unarchive it.

Invoice payments

New: List, get, and create payments on an invoice

You can list payments for an invoice, fetch a single payment by id, and record manual payments. Paths are scoped under the invoice (there is no global /payments resource).

MethodPathDescription
GET/invoices/{invoiceId}/paymentsList all payment records for an invoice
GET/invoices/{invoiceId}/payments/{paymentId}Get one payment on an invoice
POST/invoices/{invoiceId}/paymentsCreate a manual payment on an invoice

v1 returns a JSON array for list, a single object for get-by-id, and the payment service payload for create (amounts in dollars on reads; create accepts dollars and converts to cents for core).

v2 (/v2/...) uses the same paths with the standard { status, message, data } envelope and dollar amounts on all responses.

Create a payment

POST /invoices/{invoiceId}/payments
1{
2 "amount": 125.5,
3 "paymentMethod": "CHECK",
4 "paymentDate": "2026-04-28T18:00:00.000Z",
5 "notes": "Deposit received",
6 "paymentReference": "CHK-1042"
7}

Notes

  • paymentReference and notes are optional.
  • paymentMethod must be one of: CASH, ACH, CREDIT_CARD, KLARNA, AFFIRM, CHECK.
  • Core does not expose GET /payment-records/:id; get-by-id resolves the payment from the invoice’s nested payment records.

Quotes — full CRUD

You can now create, read, update, delete, send, and download PDF quotes through the public API.

Endpoints

MethodPathDescription
GET/quotesList all quotes (paginated, filterable by status and date)
GET/quotes/{quoteId}Get a quote by ID
GET/quotes/{quoteId}/pdfDownload a quote as PDF
GET/opportunities/{opportunityId}/quotesList quotes for an opportunity
POST/quotesCreate a quote on an opportunity
POST/quotes/{quoteId}/sendSend a quote to the customer
PATCH/quotes/{quoteId}Update a quote
DELETE/quotes/{quoteId}Delete a quote

Create a quote with sections

opportunityId is required. Provide line items either flat or grouped into named sections.

POST /quotes
1{
2 "opportunityId": 789225,
3 "sections": [
4 {
5 "name": "Materials",
6 "lineItems": [
7 {
8 "catalogItemId": 5884,
9 "quantity": 10,
10 "unitCost": 50,
11 "unitPrice": 75,
12 "name": "Solar Panel 400W"
13 }
14 ]
15 }
16 ],
17 "taxId": 12,
18 "discountType": "PERCENTAGE",
19 "discountValue": 5
20}

Business rules

  • opportunityId is always required; workOrderId is not accepted (use PUT /work-orders/{workOrderId}/line-items for work order line items).
  • Each section must contain at least one line item (lineItems min length 1).
  • issueDate may only be provided when status is SENT or APPROVED.
  • discountType must be PERCENTAGE or FLAT when discountValue is set.

Work order line items

New: PUT /work-orders/{workOrderId}/line-items

Replace all line items on a service work order’s underlying quote. If no quote exists yet, one is created automatically.

MethodPathDescription
PUT/work-orders/{workOrderId}/line-itemsReplace line items on a work order

This endpoint is only available for service work orders on an account (i.e. the parent project type is ACCOUNT). Calling it on a non-account work order returns 400.

PUT /work-orders/{workOrderId}/line-items
1{
2 "sections": [
3 {
4 "name": "Labor",
5 "lineItems": [
6 {
7 "catalogItemId": 5884,
8 "quantity": 4,
9 "unitCost": 75,
10 "unitPrice": 100,
11 "name": "Electrician — hourly"
12 }
13 ]
14 }
15 ]
16}

New: lineItems on GET /work-orders/{workOrderId}

The work order detail response now includes a lineItems array when the work order is a service work order on an account and has at least one line item. The property is omitted entirely when there are no line items or the work order is not on an account.

1{
2 "id": 2025433,
3 "title": "Install main panel",
4 "lineItems": [
5 {
6 "id": 8001,
7 "name": "Solar Panel 400W",
8 "description": null,
9 "quantity": 10,
10 "unitPrice": 75.00,
11 "totalPrice": 750.00,
12 "unit": "EACH"
13 }
14 ]
15}

List taxes

New: GET /taxes

List tax rates configured for your company. Use these IDs when assigning taxes to quotes, invoices, and other financial documents.

MethodPathDescription
GET/taxesList company taxes (paginated)
GET /taxes?page=1&page_size=20&include_archived=false
1[
2 {
3 "id": 42,
4 "name": "State Sales Tax",
5 "rate": "8.25",
6 "isArchived": false,
7 "createdAt": "2025-06-01T00:00:00.000Z",
8 "updatedAt": "2025-06-01T00:00:00.000Z"
9 }
10]

Query parameters

ParameterTypeDefaultDescription
include_archivedbooleanfalseInclude archived tax rates. Accepts true/false/1/0.
pageinteger1Page number
page_sizeinteger20Results per page (max 100)

Project & opportunity title fallback

When a project or opportunity has no title of its own (empty string or whitespace-only), the API now returns the parent account’s title in the title field instead of the blank value.

This applies to all read endpoints that return project or opportunity records:

  • GET /projects, GET /projects/search, GET /projects/{projectId}
  • GET /opportunities, GET /opportunities/search, GET /opportunities/{opportunityId}

Behavior

Record titleParent account titleResponse title
"Roof replacement""Acme Co""Roof replacement"
"" (empty)"Acme Co""Acme Co"
" " (whitespace)"Acme Co""Acme Co"
"""" or no parent""

The underlying stored record is not modified — this is a read-time fallback only. Write endpoints (POST, PATCH) continue to accept and persist titles exactly as provided.

Why

Many integrations rely on title being a meaningful, human-readable label (list views, search results, downstream syncs). Projects that were created without a title were surfacing as blank rows, which was confusing. Falling back to the parent account’s title gives consumers a sensible default without changing the source of truth.


Bills

New: Bills endpoints

You can now create, read, update, and delete bills on projects via the public API.

MethodPathDescription
GET/billsList all bills (paginated)
POST/billsCreate a bill
GET/bills/{billId}Get a bill by ID
PATCH/bills/{billId}Update a bill
DELETE/bills/{billId}Delete a bill
GET/projects/{projectId}/billsList bills for a project

Bills support two calculation methods:

  • LINE_ITEMS — itemised bill with one or more line items. Each line item requires catalogItemId, quantity, unitCost (must be > 0), and unitPrice (may be negative for discounts).
  • PERCENTAGE — percentage of the project’s base amount (0–100).

Create a LINE_ITEMS bill

POST /bills
1{
2 "recordId": 789225,
3 "calculationMethod": "LINE_ITEMS",
4 "lineItems": [
5 {
6 "catalogItemId": 5884,
7 "quantity": 2,
8 "unitCost": 100,
9 "unitPrice": 150
10 }
11 ],
12 "dueDate": "2026-05-01T00:00:00.000Z",
13 "description": "Materials and labor"
14}

Create a PERCENTAGE bill

POST /bills
1{
2 "recordId": 789225,
3 "calculationMethod": "PERCENTAGE",
4 "percentage": 25,
5 "dueDate": "2026-05-01T00:00:00.000Z"
6}

Business rules

  • calculationMethod cannot be changed after creation.
  • PERCENTAGE bills cannot have issueDate or status.
  • LINE_ITEMS bills: when issueDate is provided, status must also be provided and must not be DRAFT.
  • unitCost must be greater than 0 for all line items.
  • percentage must be between 0 and 100.

Nullable fields on project, account, and client updates

Improvement: Clear fields by passing null

You can now explicitly clear nullable fields on projects, accounts, and clients by passing null in a PATCH request. Previously, null values were ignored and the existing value was left unchanged.

This applies to the following endpoints:

  • PATCH /projects/{projectId}
  • PATCH /accounts/{accountId}
  • PATCH /clients/{clientId}

Clearable fields

ResourceFields
Projectdescription, primaryEmail, primaryPhone, value, size, ownerId, salesRepId, projectManagerId
Accountdescription, primaryEmail, primaryPhone, ownerId
Clientdescription, primaryEmail, primaryPhone, ownerId

Custom properties (via the custom object) can also be cleared by passing null for any key.

Example

PATCH /projects/{projectId}
1{
2 "description": null,
3 "ownerId": null,
4 "custom": {
5 "pud_incentive": null
6 }
7}

This will clear description, unassign the owner, and clear the pud_incentive custom property on the project.

Omitting a field entirely still leaves the existing value unchanged.


Work order notes and work items

New: Work order notes endpoints

You can now create and list notes directly on work orders.

MethodPathDescription
POST/work-orders/{workOrderId}/notesAdd a note to a work order
GET/work-orders/{workOrderId}/notesList all notes on a work order

Create a note

POST /work-orders/{workOrderId}/notes
1{
2 "body": "Panels installed. Awaiting inspection sign-off.",
3 "fileIds": []
4}

Response:

1{
2 "id": 4029970,
3 "note": "Panels installed. Awaiting inspection sign-off.",
4 "createdAt": "2026-04-15T22:41:03.548Z",
5 "updatedAt": "2026-04-15T22:41:03.548Z"
6}

List notes

GET /work-orders/{workOrderId}/notes?page=1&page_size=20
1{
2 "notes": [
3 {
4 "id": 4029970,
5 "note": "Panels installed. Awaiting inspection sign-off.",
6 "createdAt": "2026-04-15T22:41:03.548Z",
7 "createdByUser": {
8 "id": 42,
9 "firstName": "Jane",
10 "lastName": "Doe",
11 "email": "jane@example.com"
12 }
13 }
14 ],
15 "totalCount": 1
16}

New: workItems on work order responses

GET /work-orders/{workOrderId} now includes a workItems array containing Blueprint workflow items linked to the work order. Two types are supported:

  • FORM — a form instance (e.g. site survey, roof mount checklist)
  • FILE_REQUEST — a document upload request (e.g. shade report)
1{
2 "id": 2025433,
3 "title": "[BP] Tech site audit (post-sale)",
4 "workItems": [
5 {
6 "type": "FORM",
7 "id": 1236647,
8 "name": "[BP] Site Survey - Roof Mount",
9 "templateId": 50,
10 "status": "OPEN",
11 "isCompleted": false,
12 "completedAt": null,
13 "dueDate": null
14 },
15 {
16 "type": "FILE_REQUEST",
17 "id": 2556,
18 "name": "Shade Report",
19 "status": "IN_PROGRESS"
20 }
21 ]
22}

If no work items are linked, workItems is an empty array.


Sites endpoints

Full CRUD support for site records via the public API.

Sites are address/location records that belong to an account. A single account may have multiple sites.

Endpoints

MethodPathDescription
GET/sitesList all sites. Optionally filter by accountId.
POST/sitesCreate a new site.
GET/sites/{siteId}Get a site by ID.
PATCH/sites/{siteId}Update a site.
DELETE/sites/{siteId}Delete a site.
POST/sites/{siteId}/moveMove a site to a different account.

Create a site

accountId is required. Either fullAddress or geoLocation must also be provided. The backend automatically geocodes fullAddress and checks for duplicate addresses — if a site with the same address already exists for the same account, the existing site is returned (200) instead of creating a duplicate.

POST /sites
1{
2 "accountId": 714570,
3 "fullAddress": "123 Main St, San Francisco, CA 94105",
4 "title": "Main Office",
5 "timezone": "America/Los_Angeles"
6}

List sites by account

GET /sites?accountId=714570

Update a site

All fields are optional. Updating fullAddress triggers re-geocoding. To move a site to a different account, use POST /sites/{siteId}/move.

PATCH /sites/9876
1{
2 "title": "Updated Office Name",
3 "fullAddress": "456 Oak Ave, San Francisco, CA 94107"
4}

Move a site to a different account

Moves the site and all associated records to a different account. The original site is permanently deleted and a new site is created on the target account. ⚠️ This operation is irreversible.

POST /sites/9876/move
1{
2 "accountId": 789224
3}

Properties endpoint naming updates

GET /properties — new account and opportunity keys

The /properties response now includes account and opportunity keys in addition to the existing client and request keys:

New keyReplaces
accountclient
opportunityrequest

The record_type query parameter now accepts account and opportunity as values:

GET /properties?record_type=account
GET /properties?record_type=opportunity
GET /properties?record_type=project

Note: client and request are still accepted as record_type values and are still returned in the response for backwards compatibility. They will continue to work until all users have migrated to the new names.


Form template update endpoint

Update a form template

A new endpoint lets you update an existing form template:

  • PATCH /form-templates/{formTemplateId} — Update a form template’s name, property layout, assignee, collaborators, labels, and due date configuration.

All fields are optional — only provided fields are updated. formLayouts is optional — when provided, it fully replaces all layout groups and their fields; omit it to leave the existing layout untouched. Include existing groups with their id to preserve them, or omit the id for new groups. Groups not included are removed. Array order determines position on the form.

Fields within each group are set via the properties array, where each item specifies a type (TEXT, NUMERIC, DROPDOWN, or FILE) or references an existing project property via projectPropertyKey.

Note: Layout id values (for groups and properties) are not stable — they change after every update. Always re-fetch the template via GET /form-templates/{formTemplateId} to get current IDs before referencing them in a subsequent update.

PATCH /form-templates/{formTemplateId}
1{
2 "name": "Site Inspection Checklist",
3 "formLayouts": [
4 {
5 "name": "Site Details",
6 "properties": [
7 { "projectPropertyKey": "site_address" },
8 { "type": "TEXT", "name": "Inspector Notes", "isRequired": false }
9 ]
10 }
11 ],
12 "assigneeProperty": "OWNER",
13 "dueDateXDaysAfter": 7
14}

Naming updates & new features

Object renames

Coperniq’s API terminology now matches the UI:

Old nameNew nameEndpoints
ClientsAccounts/accounts, /accounts/{accountId}
RequestsOpportunities/opportunities, /opportunities/{opportunityId}
CommentsNotes/{resource}/{id}/comments (unchanged); new PATCH /notes/{noteId}

Existing /{resource}/{id}/comments paths are unchanged — no updates to existing integrations are required. A new unified PATCH /notes/{noteId} endpoint is also available. The renamed terms appear in operation names, tags, and SDK method names going forward.

Retrieve archived records

All GET list and get-by-ID endpoints for projects, opportunities, accounts, and workflows now support an include_archived query parameter.

GET /projects?include_archived=true
GET /opportunities?include_archived=true
GET /accounts?include_archived=true
GET /workflows?include_archived=true

By default (include_archived=false) only active records are returned — existing behavior is unchanged. Set include_archived=true to include archived records in the response.

Form template endpoints

Two new read-only endpoints let you retrieve form templates and their structure:

  • GET /form-templates — List all workflow form templates for your company.
  • GET /form-templates/{formTemplateId} — Get a specific form template by ID, including its field definitions.

These are useful for dynamically building form submissions or inspecting available form structures before creating a form instance.