Chronly Developer API

Build on your quotes, invoices, jobs, scheduling and checkouts.

One REST API over the same records your team works in every day — quotes, invoices, checkouts, jobs and their scheduled visits, customers, products and discounts.

Download llms.txthttps://chronly.ca/api
REST over HTTPS, JSON in and out·Bearer token auth·Scoped by companyId

Getting started

1

Create an API user

In the portal, open Settings → Users and add an API user with a nickname and a role. Each API user has its own per-module permissions, so a token can be limited to exactly what the integration needs. API users are available on Seller and above, or on an Enterprise plan with API access enabled.

2

Store the token

The token is shown once, at creation, and stored hashed after that. Keep it server-side — it carries account-level access.

3

Send your company ID

Every request must say which company it acts on. Pass companyId in the query string on GET and DELETE, and usually in the body on POST, PUT and PATCH — some write endpoints need it in the query too, so check each endpoint’s parameter table. Find it under Settings → API.

Authentication

Send the token as a bearer credential on every request. There is no separate handshake and no refresh step.

Request headers
Authorization: Bearer chronly_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

Token handling

Tokens are hashed on the server and cannot be recovered. Losing one means deleting the API user and issuing a replacement, which immediately revokes the old token.

Scope

A token belongs to one account, and every request names the company it acts on with companyId. Requests are authorised against the API user's per-module permissions, so a token can read invoices without being able to delete them. Last use is recorded on the API user.

Errors

StatusMeaning
400A required field is missing or fails validation, or companyId was not supplied.
401No session cookie and no valid bearer token, or the token is not in chronly_api_ form.
403The API user lacks permission on the module being called, or the account's plan does not include it.
404The record exists in no company the token can reach, the id is wrong, or companyId is not a company this token owns.
409The write collides with an existing record - a duplicate externalId or customer email.
500Unhandled server error. Safe to retry idempotent reads.
Code samples shown as

Invoices

Create, send, schedule, update and delete invoices, record payments against them, and read their email and view activity. Invoice numbering, dates and every money field are computed server-side.

GET/invoicesList invoicesTry it ↗

Returns every invoice for the company, with recurring templates appended to the same array. Tell them apart by the presence of recurringInvoiceId.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X GET \
  "https://chronly.ca/api/invoices?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "invoices": [
    {
      "invoiceId": "a1b2c3d4e5f6",
      "invoiceNumber": 1042,
      "title": "Invoice",
      "customer": { "value": "byr_8f3k", "label": "Northline Roofing" },
      "status": "sent",
      "invoiceDate": "2026-08-16",
      "paymentDue": "2026-09-15",
      "subTotal": 2400,
      "totalTax": 120,
      "totalAmount": 2520,
      "totalPaid": 0,
      "remainingBalance": 2520
    }
  ]
}
POST/invoicesCreate an invoiceTry it ↗

Creates a draft invoice. Send the customer as a buyerId and the line items as name/quantity/price — the invoice number, dates, title, line totals and every money field are derived server-side. The invoice is created in draft; use POST /invoices/{invoiceId}/send to email it.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to create the record in.
customerstring · requiredThe buyerId of an existing customer in this company. Returns 404 if the buyer belongs to another company. The legacy { label, value } object is still accepted.
customer.contactIdstringAddress the invoice to one of the customer's secondary contacts instead of the primary. Only valid on the object form. Must be a contact on that customer, or the call returns 400. The contact's name and email are snapshotted onto the invoice, so a contact deleted later does not redirect this invoice's receipt to the primary.
itemsarray · requiredLine items. Each needs name and price; quantity defaults to 1. Note the field is items, not lineItems.
items[].namestring · requiredWhat the line is for. description is accepted as an alias.
items[].pricenumber · requiredUnit price. rate is accepted as an alias.
items[].quantitynumberDefaults to 1.
items[].taxIdsarray of stringTax rates to charge on this line, by taxId from GET /taxes. Each tax is charged only on the lines that carry it, so a mixed taxable and non-taxable invoice is taxed correctly. Unknown ids return 400.
items[].productIdstringLinks the line to a product.
paymentTermsstringUsed to derive paymentDue from invoiceDate. Defaults to the company's invoice default.One ofon-receiptnet-7net-14net-30net-45net-60net-90
paymentDuestring (YYYY-MM-DD)Overrides paymentTerms. Derived when omitted.
invoiceDatestring (YYYY-MM-DD)Defaults to today.
invoiceNumbernumberDefaults to the company's highest invoice number plus one.
titlestringDefaults to the company's configured invoice title.
summarystringShown under the line items.
invoiceNotesstringFree-text notes on the invoice.
jobIdstringAttaches the invoice to a job on creation and writes the link to the job activity log.
subTotal / totalTax / totalAmountnumberComputed from items and discounts. Supply all three to override; supplying only some is ignored, so a partly-specified invoice can never disagree with its own lines.
enableRemindersbooleanDefaults to whether the company has any enabled invoice reminder templates.
cURL
curl -X POST \
  "https://chronly.ca/api/invoices" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "customer": "byr_8f3k",
    "paymentTerms": "net-30",
    "items": [
      { "name": "Site prep", "quantity": 2, "price": 1200, "taxIds": ["tax_gst"] },
      { "name": "Permit fee", "quantity": 1, "price": 100 }
    ]
  }'
Response 201
{
  "invoiceId": "a1b2c3d4e5f6",
  "invoiceNumber": 1043,
  "title": "Invoice",
  "status": "draft",
  "customer": { "label": "Northline Roofing", "value": "byr_8f3k" },
  "invoiceDate": "2026-08-20",
  "paymentDue": "2026-09-19",
  "subTotal": 2500,
  "totalTax": 120,
  "totalAmount": 2620
}

Errors

  • 400companyId is missing, or an items[].taxIds value is not a tax rate in this company.
  • 404The customer buyerId does not exist in this company.

createdBy and createdByUserId come from the authenticated identity and are ignored if sent.

GET/invoices/{invoiceId}Retrieve an invoiceTry it ↗

Returns the invoice together with the buyer record, the company it belongs to, and any purchase records against it. If no invoice matches, the id is retried as a recurring invoice.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
invoiceIdpath · requiredAn invoiceId or a recurringInvoiceId.
companyIdquery · requiredThe company the records belong to.
cURL
curl -X GET \
  "https://chronly.ca/api/invoices/{invoiceId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "invoice": { "invoiceId": "a1b2c3d4e5f6", "status": "paid" },
  "customer": { "buyerId": "byr_8f3k", "emailAddress": "[email protected]" },
  "company": { "companyId": "cmp_4k9x2m", "companyName": "Aurora Contracting" },
  "purchases": []
}

Errors

  • 404No invoice or recurring invoice with that id in the company.
PUT/invoices/{invoiceId}Update an invoiceTry it ↗

Updates a whitelisted set of fields. Anything financial that is derived from payment activity — status, payments, totalPaid, paid, paidAt — is dropped. Sending items without totals recomputes subTotal, totalTax and totalAmount from the new lines, and remainingBalance follows totalAmount.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
invoiceIdpath · requiredThe invoice to update.
companyIdquery · requiredThe company the records belong to.

Request body

customerstringA buyerId, or the legacy { label, value } object.
customer.contactIdstringRe-address the invoice to one of that customer's secondary contacts. Send an empty string to go back to the primary. Must be a contact on the customer named, or the call returns 400.
itemsarrayReplaces the line items. Same shape as create, including taxIds. Totals are recomputed unless you send them.
title / summary / invoiceNotesstringFree-text fields.
invoiceNumbernumberNot re-derived on update — an absent value leaves the existing number alone.
invoiceDate / paymentDuestring (YYYY-MM-DD)Not re-derived on update.
jobIdstring | nullMoving the invoice between jobs updates both jobs and logs the change on each.
attachPdfInvoicebooleanAttach a rendered PDF when this invoice is emailed.
enableRemindersbooleanWhether payment reminders run for this invoice.
cURL
curl -X PUT \
  "https://chronly.ca/api/invoices/{invoiceId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "jobId": "job_71kd", "paymentDue": "2026-10-01" }'
Response 200
{
  "invoiceId": "a1b2c3d4e5f6",
  "paymentDue": "2026-10-01",
  "jobId": "job_71kd"
}

Rejected fields are logged server-side rather than failing the request, so a 200 does not guarantee every field you sent was applied.

DELETE/invoices/{invoiceId}Delete an invoiceTry it ↗

Deletes an unpaid invoice. Recurring templates can always be deleted.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
invoiceIdpath · requiredThe invoice to delete.
companyIdquery · requiredThe company the records belong to.
cURL
curl -X DELETE \
  "https://chronly.ca/api/invoices/{invoiceId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }

Errors

  • 400The invoice is paid or partially paid — deleting it would break financial records.
POST/invoices/{invoiceId}/sendSend an invoiceTry it ↗

Emails the invoice and moves it from draft to sent, stamping sentAt and sentBy. Sending an already-sent invoice appends to its email history and leaves the status alone, so this doubles as a resend.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
invoiceIdpath · requiredThe invoice to send.

Request body

companyIdstring · requiredThe company the invoice belongs to.
emailAddressesarray of string · requiredRecipients. The first is the To address; any others are BCC'd. An empty array sends to nobody and fails at the mail provider.
messagestringOptional note included in the email body.
cURL
curl -X POST \
  "https://chronly.ca/api/invoices/{invoiceId}/send" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "emailAddresses": ["[email protected]"],
    "message": "Invoice for last week's site prep — thanks!"
  }'
Response 200
{ "success": true, "message": "Invoice sent successfully" }

Errors

  • 404No invoice with that id in the company.

Whether a PDF is attached is decided by the invoice's attachPdfInvoice flag, falling back to the company setting.

POST/invoices/{invoiceId}/schedule-sendSchedule an invoice to sendTry it ↗

Queues the invoice to be emailed at a future local date and time, and sets its status to scheduled.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
invoiceIdpath · requiredThe invoice to schedule.

Request body

companyIdstring · requiredThe company the invoice belongs to.
sendDatestring (YYYY-MM-DD) · requiredLocal date to send on.
sendTimestring (HH:mm) · requiredLocal time to send at.
timezonestring · requiredIANA zone the date and time are read in, e.g. America/Vancouver.
emailAddressesarray of string · requiredAt least one recipient. An empty array is rejected with 400.
messagestringOptional note included in the email body.
cURL
curl -X POST \
  "https://chronly.ca/api/invoices/{invoiceId}/schedule-send" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "sendDate": "2026-09-01",
    "sendTime": "09:00",
    "timezone": "America/Vancouver",
    "emailAddresses": ["[email protected]"]
  }'
Response 200
{ "success": true, "jobId": "518", "scheduledFor": "2026-09-01T16:00:00.000Z" }

Errors

  • 400sendDate, sendTime or timezone is missing, emailAddresses is empty, the date format is invalid, or the moment is in the past.
  • 404No invoice with that id in the company.
DELETE/invoices/{invoiceId}/cancel-scheduled-sendCancel a scheduled sendTry it ↗

Removes the queued job and returns the invoice to draft, clearing its scheduled metadata.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
invoiceIdpath · requiredThe invoice whose scheduled send should be cancelled.
companyIdquery · requiredThe company the invoice belongs to.
cURL
curl -X DELETE \
  "https://chronly.ca/api/invoices/{invoiceId}/cancel-scheduled-send?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true, "message": "Scheduled send cancelled" }

Errors

  • 404The invoice has no active scheduled send.
POST/invoices/{invoiceId}/update-statusUpdate invoice statusTry it ↗

Sets the status directly, validated against the schema enum. Cancelling an invoice that was converted from a quote releases any deposit applied to it back to a liability.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
invoiceIdpath · requiredThe invoice to update.

Request body

companyIdstring · requiredThe company the invoice belongs to.
statusstring · requiredThe new status.One ofdraftsentviewedpaidoverduecancelledrefundedscheduledfailed
cURL
curl -X POST \
  "https://chronly.ca/api/invoices/{invoiceId}/update-status" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "companyId": "cmp_4k9x2m", "status": "cancelled" }'
Response 200
{ "invoice": { "invoiceId": "a1b2c3d4e5f6", "status": "cancelled" } }

Errors

  • 400The status is not one of the allowed values.
  • 404No invoice with that id in the company.

This changes the stored status ONLY. Setting it to sent does NOT email anything — use POST /invoices/{invoiceId}/send for that.

POST/invoices/{invoiceId}/record-paymentRecord a manual paymentTry it ↗

Records a payment taken outside Chronly — cash, cheque, e-transfer — against the invoice, recalculates totalPaid and remainingBalance, and flips the invoice to paid once the balance settles. A matching purchase record is written so the payment appears in the books.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
invoiceIdpath · requiredThe invoice being paid.

Request body

companyIdstring · requiredThe company the invoice belongs to.
paymentTypestring · requiredHow the money arrived.One ofcashchequee-transferbank-transfercredit-card-manualother
amountnumber · requiredMust be greater than zero. Rounded to two decimals.
paymentDatestring (YYYY-MM-DD) · requiredCannot be in the future.
cardBrandstringOnly meaningful with credit-card-manual.One ofvisamastercardamexdiscover
depositToLedgerAccountIdstringThe bank or credit-card ledger account the money landed in. Omit for cash and cheques, which stay in Undeposited Funds until a deposit banks them.
referenceNumberstringCheque number, e-transfer reference, and so on.
notesstringFree-text note stored on the payment.
cURL
curl -X POST \
  "https://chronly.ca/api/invoices/{invoiceId}/record-payment" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "paymentType": "cheque",
    "amount": 2620,
    "paymentDate": "2026-08-20",
    "referenceNumber": "00418"
  }'
Response 200
{
  "success": true,
  "invoice": {
    "invoiceId": "a1b2c3d4e5f6",
    "status": "paid",
    "totalPaid": 2620,
    "remainingBalance": 0
  }
}

Errors

  • 400paymentType, amount or paymentDate is missing, the amount is not positive, the date is in the future, or depositToLedgerAccountId is not a live bank or credit-card account in this company.
  • 404No invoice with that id in the company.
GET/invoices/{invoiceId}/email-eventsList email eventsTry it ↗

Every delivery event the mail provider reported for this invoice — processed, delivered, open, click, bounce, dropped — newest first.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
invoiceIdpath · requiredThe invoice.
companyIdquery · requiredThe company the invoice belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/invoices/{invoiceId}/email-events?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": [
    { "eventType": "delivered", "email": "[email protected]", "timestamp": "2026-08-20T15:04:11.000Z" }
  ]
}
GET/invoices/{invoiceId}/email-statsEmail engagement statsTry it ↗

The same email events rolled up into counts and rates. Rates are percentages, and are 0 when the denominator is 0.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
invoiceIdpath · requiredThe invoice.
companyIdquery · requiredThe company the invoice belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/invoices/{invoiceId}/email-stats?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": {
    "processed": 1,
    "delivered": 1,
    "opened": 1,
    "clicked": 0,
    "bounced": 0,
    "dropped": 0,
    "totalEvents": 3,
    "openRate": 100,
    "clickRate": 0,
    "deliveryRate": 100,
    "bounceRate": 0,
    "uniqueRecipients": 1
  }
}
GET/invoices/{invoiceId}/view-statsInvoice view statsTry it ↗

How often the customer-facing invoice page was viewed, from page-view tracking. Portal views are excluded, so this reflects the customer rather than your own team.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
invoiceIdpath · requiredThe invoice.
companyIdquery · requiredThe company the invoice belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/invoices/{invoiceId}/view-stats?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": { "pageViews": 4, "clicks": 1, "conversions": 0, "uniqueVisitors": 2 }
}
GET/invoices/{invoiceId}/tracking-eventsList tracking eventsTry it ↗

The raw tracking events behind the view stats, newest first. Unlike view-stats this includes portal views.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
invoiceIdpath · requiredThe invoice.
companyIdquery · requiredThe company the invoice belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/invoices/{invoiceId}/tracking-events?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": [ { "type": "page-view", "pathname": "/pdf-invoice/a1b2c3d4e5f6", "createdAt": "2026-08-20T15:31:02.000Z" } ]
}
GET/invoices/{invoiceId}/delivery-issuesList delivery issuesTry it ↗

Bounces, drops and spam reports for this invoice — the subset of email events that mean the customer did not receive it.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
invoiceIdpath · requiredThe invoice.
companyIdquery · requiredThe company the invoice belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/invoices/{invoiceId}/delivery-issues?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": [ { "eventType": "bounce", "email": "[email protected]", "reason": "550 5.1.1 unknown recipient" } ]
}
POST/invoices/recurringCreate a recurring invoiceTry it ↗

Creates a recurring invoice template. Templates live in their own collection keyed by recurringInvoiceId, and are returned by GET /invoices alongside ordinary invoices. Creating the template does not start it — set its schedule with PUT /invoices/{recurringInvoiceId}/recurring/schedule.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to create the template in.
customerstring · requiredThe buyerId of an existing customer. The legacy { label, value } object is still accepted.
itemsarray · requiredLine items copied onto each generated invoice. Same shape as POST /invoices, including taxIds.
titlestringDefaults to the company's configured invoice title.
subTotal / totalTax / totalAmountnumberComputed from items. Supply all three to override.
paymentSettingsobjectOverrides the company payment defaults, which are used when omitted.
cURL
curl -X POST \
  "https://chronly.ca/api/invoices/recurring" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "customer": "byr_8f3k",
    "items": [{ "name": "Monthly retainer", "quantity": 1, "price": 900 }]
  }'
Response 201
{
  "recurringInvoiceId": "K2mQ81xVn4Bd7Lp",
  "status": "draft",
  "companyId": "cmp_4k9x2m",
  "totalAmount": 900
}

Errors

  • 403The account does not have the recurringInvoices capability. The body is { "error": "subscription_required", "capability": "recurringInvoices" }.
  • 404The customer buyerId does not exist in this company.

invoiceNumber is never stored on a template — each generated invoice takes the next number at the moment it is created.

PUT/invoices/{invoiceId}/recurring/scheduleSet a recurring scheduleTry it ↗

Attaches a recurrence to a recurring invoice template and activates it. The path id is a recurringInvoiceId.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
invoiceIdpath · requiredThe recurringInvoiceId of the template.

Request body

companyIdstring · requiredThe company the template belongs to.
startDatestring (YYYY-MM-DD) · requiredFirst occurrence.
recurrenceTypestring · requiredHow often invoices are generated.One ofdailyweeklymonthlyyearly
timezonestring · requiredIANA zone the schedule runs in.
sendTimestring (HH:mm) · requiredLocal time of day to generate and send.
weeklyDaystring · required for weeklyDay of week, 0 = Sunday.One of0123456
monthlyDaynumber · required for monthlyDay of month, 1-31.
yearlyMonthnumber · required for yearlyMonth, 1-12. Required together with yearlyDay.
yearlyDaynumber · required for yearlyDay of month. Required together with yearlyMonth.
endDatestring (YYYY-MM-DD)Must be after startDate. Runs indefinitely when omitted.
emailAddressesarray of stringRecipients for each generated invoice. Each must be a valid address.
cURL
curl -X PUT \
  "https://chronly.ca/api/invoices/{invoiceId}/recurring/schedule" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "startDate": "2026-09-01",
    "recurrenceType": "monthly",
    "monthlyDay": 1,
    "timezone": "America/Vancouver",
    "sendTime": "09:00",
    "emailAddresses": ["[email protected]"]
  }'
Response 200
{ "success": true, "nextRun": "2026-09-01T16:00:00.000Z" }

Errors

  • 400A required field is missing, the recurrence-specific field for the chosen recurrenceType is absent, the timezone or sendTime is invalid, an email address is malformed, or endDate is not after startDate.
  • 403The account does not have the recurringInvoices capability.
  • 404No recurring invoice with that id in the company.
PATCH/invoices/{invoiceId}/recurring/schedulePause or resume a scheduleTry it ↗

Stops or restarts generation without discarding the schedule.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
invoiceIdpath · requiredThe recurringInvoiceId of the template.

Request body

companyIdstring · requiredThe company the template belongs to.
actionstring · requiredWhat to do to the schedule.One ofpauseresume
cURL
curl -X PATCH \
  "https://chronly.ca/api/invoices/{invoiceId}/recurring/schedule" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "companyId": "cmp_4k9x2m", "action": "pause" }'
Response 200
{ "success": true, "status": "paused" }

Errors

  • 400action is not pause or resume.
  • 404No recurring invoice with that id in the company.
DELETE/invoices/{invoiceId}/recurring/scheduleRemove a recurring scheduleTry it ↗

Cancels the recurrence and removes its queued jobs. The template itself survives and can be rescheduled.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
invoiceIdpath · requiredThe recurringInvoiceId of the template.
companyIdquery · requiredThe company the template belongs to.
cURL
curl -X DELETE \
  "https://chronly.ca/api/invoices/{invoiceId}/recurring/schedule?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }

Errors

  • 404No recurring invoice with that id in the company.
GET/taxes/company/{companyId}List tax ratesTry it ↗

The company's tax registry. Use the taxId values here for items[].taxIds when creating an invoice — without this endpoint that parameter is unusable, since the ids appear nowhere else. Soft-deleted rates are omitted. The response is a bare array, not an object.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdpath · requiredThe company whose tax rates to list. There is no query parameter on this endpoint — the company goes in the path.
cURL
curl -X GET \
  "https://chronly.ca/api/taxes/company/{companyId}" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
[
  { "taxId": "tax_gst", "name": "GST", "rate": 5, "taxType": "gst-hst" },
  { "taxId": "tax_pst", "name": "PST", "rate": 7, "taxType": "pst" }
]

Errors

  • 404The company is not one this token's account owns.

Needs no module grant — any token or session for the company can read it, because tax rates are shared by invoices, quotes and events.

Checkouts

Hosted payment pages. This is the module built for token access first: it accepts a bearer token and arbitrary metadata you can read back later.

GET/checkoutList checkoutsTry it ↗

Returns all checkouts for the company, newest first, including drafts and duplicates. Ad-hoc checkouts with a maxPurchases cap also carry live purchase counts.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X GET \
  "https://chronly.ca/api/checkout?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "checkouts": [
    {
      "checkoutId": "Xk92mLp01qZa",
      "name": "Deposit — 2026 season",
      "checkoutMode": "ad-hoc",
      "price": 500,
      "currency": "CAN",
      "isActive": true,
      "maxPurchases": 25,
      "currentPurchaseCount": 11,
      "cancelledPurchaseCount": 1
    }
  ]
}
POST/checkoutCreate a checkoutTry it ↗

Creates a checkout page. Payment methods fall back to the company defaults when omitted, and a checkout with an expiry and an expiry webhook schedules that webhook at creation time.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to create the record in. Required for token auth.
namestring · requiredInternal name for the checkout.
typestring · requiredCheckout type. Accepts a raw string or { value }.One ofphysicaldigitalserviceprofessional_servicetaxable_servicetax_exemptbundle
pricenumber · requiredUnit price, parsed as a float.
checkoutModestringDefaults to ad-hoc. Ad-hoc checkouts also require description.One ofad-hocproduct
currencystringDefaults to CAN.
billingTypestringDefaults to one-time; set recurring with a recurrence object.One ofone-timerecurring
externalIdstringYour own identifier. Unique per account — a repeat returns 409.
maxPurchasesnumberCap on completed and pending purchases.
checkoutExpiresbooleanWhen true, expiresSettings.expiresAt must be a valid datetime.
metadataobjectFree-form key/value data stored on the checkout and echoed in webhooks.
cURL
curl -X POST \
  "https://chronly.ca/api/checkout" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "name": "Deposit — 2026 season",
    "type": "product",
    "price": 500,
    "description": "Season booking deposit",
    "externalId": "booking-8841",
    "metadata": { "bookingId": "8841" }
  }'
Response 200
{
  "success": true,
  "checkout": { "checkoutId": "Xk92mLp01qZa", "isActive": true },
  "message": "Checkout created successfully"
}

Errors

  • 400A required field is missing, description is absent on an ad-hoc checkout, or expiresAt is not a valid datetime.
  • 409A checkout already exists with that checkout ID or external ID.

Webhooks are a Seller/Pro capability. On accounts without it, webhookEnabled and webhookUrl are stripped rather than rejected.

GET/checkout/{checkoutId}Retrieve a checkoutTry it ↗

Returns one checkout with its settings, discounts, attached forms, inventory and payment configuration.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
checkoutIdpath · requiredThe 12-character checkout id.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X GET \
  "https://chronly.ca/api/checkout/{checkoutId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true, "checkout": { "checkoutId": "Xk92mLp01qZa" } }
PUT/checkout/{checkoutId}Update a checkoutTry it ↗

Updates name, price, activity, expiry, discounts, forms and inventory on an existing checkout.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
checkoutIdpath · requiredThe checkout to update.
companyIdstring · requiredThe company to create the record in. Required for token auth.

Request body

isActivebooleanTake the page offline without deleting it.
pricenumberNew unit price.
cURL
curl -X PUT \
  "https://chronly.ca/api/checkout/{checkoutId}" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "isActive": false }'
Response 200
{ "success": true, "checkout": { "checkoutId": "Xk92mLp01qZa", "isActive": false } }
DELETE/checkout/{checkoutId}Delete a checkoutTry it ↗

Removes the checkout page. Purchases already made against it are retained.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
checkoutIdpath · requiredThe checkout to delete.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X DELETE \
  "https://chronly.ca/api/checkout/{checkoutId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }
POST/checkout/payTake a paymentTry it ↗

Processes a payment against a checkout: card, Apple Pay, Google Pay or bank, with surcharge and tax handling, authorization-only capture, and post-purchase webhooks.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to create the record in. Required for token auth.
checkoutIdstring · requiredThe checkout being paid.
quantitynumberUnits purchased, when the checkout allows quantity selection.
customerobjectContact details collected at the checkout.
paymentInstrumentobjectTokenized payment method.
cURL
curl -X POST \
  "https://chronly.ca/api/checkout/pay" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "checkoutId": "Xk92mLp01qZa",
    "quantity": 1,
    "customer": { "emailAddress": "[email protected]" }
  }'
Response 200
{ "success": true, "orderId": "ord_5m2k9x" }

This is the endpoint the hosted checkout page itself calls. Talk to us before driving it directly — surcharge, tax and receipt behaviour depend on company payment settings.

Quotes

Quotes carry line items, deposits and approval state, and convert into invoices or jobs. Send the customer as a buyerId and the lines as description/quantity/price — line amounts, totals and tax are recalculated from the items every time a quote is saved, so values you send for them are overwritten.

GET/quotesList quotesTry it ↗

Returns every quote for the company. A quote whose validUntil has passed is auto-expired the next time it is saved.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the records belong to.
cURL
curl -X GET \
  "https://chronly.ca/api/quotes?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "quotes": [
    {
      "quoteId": "quote_m9x2kap01",
      "quoteNumber": "QUO-481920",
      "title": "Quote",
      "customer": { "value": "byr_8f3k", "label": "Northline Roofing" },
      "status": "sent",
      "subtotal": 2500,
      "totalTax": 120,
      "totalAmount": 2620,
      "validUntil": "2026-09-19T00:00:00.000Z"
    }
  ]
}
POST/quotesCreate a quoteTry it ↗

Creates a draft quote. Line amounts, subtotal, totalTax and totalAmount are computed from the items on save — values you send for them are overwritten, so there is no way for a quote to disagree with its own lines.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to create the record in.
customerstring · requiredThe buyerId of an existing customer in this company. Returns 404 if the buyer belongs to another company. The legacy { label, value } object is still accepted. Name, email, phone and address are copied onto the quote from the buyer record.
customer.contactIdstringAddress the quote to one of the customer's secondary contacts instead of the primary. Only valid on the object form. Must be a contact on that customer, or the call returns 400. The contact's name and email are snapshotted onto the quote.
quoteNumberstring · requiredYour reference for the quote. NOT generated server-side — unlike an invoice number, you must supply one. The portal uses the form QUO-481920.
itemsarray · requiredLine items.
items[].descriptionstring · requiredWhat the line is for. name is accepted as an alias, so an invoice-shaped line works here too.
items[].pricenumber · requiredUnit price. rate is accepted as an alias. Defaults to 0, so omitting it silently creates a free line.
items[].quantitynumberDefaults to 1.
items[].taxIdsarray of stringTax rates to charge on this line, by taxId from GET /taxes. Each tax is charged only on the lines that carry it. Unknown ids return 400.
items[].taxRatesarrayThe long form of taxIds, as { id, name, value } where value is the percentage. Takes precedence when both are sent.
titlestringDefaults to the company's configured quote title.
quoteDatestring (YYYY-MM-DD)Defaults to today.
validUntilstring (YYYY-MM-DD)Defaults to the company's quote validity period. Past this date the quote auto-expires and can no longer be accepted.
summary / quoteNotesstringFree-text fields shown on the quote.
depositobjectDeposit required before work starts, as { type, value } where type is percent or dollarAmount.One ofpercentdollarAmount
allowLinkAcceptancebooleanLets the customer accept from the emailed link without signing in. Required for the public acceptance flow.
attachPdfQuotebooleanAttach a rendered PDF when the quote is emailed.
jobIdstringAttaches the quote to a job on creation and writes the link to the job activity log.
cURL
curl -X POST \
  "https://chronly.ca/api/quotes" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "customer": "byr_8f3k",
    "quoteNumber": "QUO-481920",
    "items": [
      { "description": "Site prep", "quantity": 2, "price": 1200, "taxIds": ["tax_gst"] },
      { "description": "Permit fee", "quantity": 1, "price": 100 }
    ]
  }'
Response 201
{
  "quoteId": "quote_m9x2kap01",
  "quoteNumber": "QUO-481920",
  "status": "draft",
  "subtotal": 2500,
  "totalTax": 120,
  "totalAmount": 2620
}

Errors

  • 400customer is missing, an items[].taxIds value is not a tax rate in this company, or a line item has no description.
  • 404The customer buyerId does not exist in this company.

quoteNumber is the one field a quote will not derive for you — an invoice takes the next number automatically, a quote does not.

GET/quotes/{quoteId}Retrieve a quoteTry it ↗

Returns the quote with its line items, deposit state and acceptance record.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
quoteIdpath · requiredThe quote to retrieve.
companyIdquery · requiredThe company the quote belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/quotes/{quoteId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "quote": {
    "quoteId": "quote_m9x2kap01",
    "status": "accepted",
    "totalAmount": 2620,
    "acceptedBy": { "name": "Sam Doyle", "acceptedAt": "2026-08-19T18:22:04.000Z" }
  }
}

Errors

  • 404No quote with that id in the company.
PUT/quotes/{quoteId}Update a quoteTry it ↗

Updates the quote and bumps its version. Totals are recalculated from the items on every save.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
quoteIdpath · requiredThe quote to update.
companyIdquery · requiredThe company the quote belongs to.

Request body

customerstringA buyerId, or the legacy { label, value } object. Reassigning re-copies the name, email, phone and address onto the quote.
customer.contactIdstringRe-address the quote to one of that customer's secondary contacts. Send an empty string to go back to the primary. Must be a contact on the customer named, or the call returns 400.
itemsarrayReplaces the line items. Same shape as create, including taxIds and the name alias. Totals follow automatically.
title / summary / quoteNotesstringFree-text fields.
validUntilstring (YYYY-MM-DD)Extends or shortens the acceptance window.
depositobjectDeposit terms, as { type, value }.
jobIdstring | nullMoving the quote between jobs updates both jobs and logs the change on each.
cURL
curl -X PUT \
  "https://chronly.ca/api/quotes/{quoteId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "validUntil": "2026-10-15"
  }'
Response 200
{ "quoteId": "quote_m9x2kap01", "version": 2, "validUntil": "2026-10-15T00:00:00.000Z" }

Errors

  • 400An items[].taxIds value is not a tax rate in this company.
  • 404No quote with that id in the company, or the customer buyerId does not exist in it.
DELETE/quotes/{quoteId}Delete a quoteTry it ↗

Deletes the quote. A quote with a paid deposit cannot be deleted, since that would orphan the payment.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
quoteIdpath · requiredThe quote to delete.
companyIdquery · requiredThe company the quote belongs to.
cURL
curl -X DELETE \
  "https://chronly.ca/api/quotes/{quoteId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }

Errors

  • 400The quote has a paid deposit or has already been converted to an invoice.
POST/quotes/{quoteId}/sendSend a quoteTry it ↗

Emails the quote and moves it to sent. Sending an already-sent quote resends it.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
quoteIdpath · requiredThe quote to send.
companyIdquery · requiredIN THE QUERY STRING, not the body — this endpoint's authorization runs before the body is read.

Request body

emailAddressesarray of string · requiredRecipients. The first is the To address; any others are BCC'd.
messagestringOptional note included in the email body.
attachPdfQuotebooleanAttach a rendered PDF, overriding the quote and company settings.
cURL
curl -X POST \
  "https://chronly.ca/api/quotes/{quoteId}/send?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "emailAddresses": ["[email protected]"],
    "message": "Here's the quote we discussed."
  }'
Response 200
{ "success": true, "message": "Quote sent successfully" }

Errors

  • 400The quote could not be sent — no recipients, or the quote no longer exists.
POST/quotes/{quoteId}/schedule-sendSchedule a quote to sendTry it ↗

Queues the quote to be emailed at a future local date and time and sets its status to scheduled.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
quoteIdpath · requiredThe quote to schedule.
companyIdquery · requiredIN THE QUERY STRING, not the body — this endpoint's authorization runs before the body is read.

Request body

sendDatestring (YYYY-MM-DD) · requiredLocal date to send on.
sendTimestring (HH:mm) · requiredLocal time to send at.
timezonestring · requiredIANA zone the date and time are read in.
emailAddressesarray of string · requiredAt least one recipient.
messagestringOptional note included in the email body.
attachPdfQuotebooleanAttach a rendered PDF.
cURL
curl -X POST \
  "https://chronly.ca/api/quotes/{quoteId}/schedule-send?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "sendDate": "2026-09-01",
    "sendTime": "09:00",
    "timezone": "America/Vancouver",
    "emailAddresses": ["[email protected]"]
  }'
Response 200
{ "success": true, "jobId": "612", "scheduledFor": "2026-09-01T16:00:00.000Z" }
DELETE/quotes/{quoteId}/cancel-scheduled-sendCancel a scheduled sendTry it ↗

Removes the queued job and returns the quote to draft.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
quoteIdpath · requiredThe quote whose scheduled send should be cancelled.
companyIdquery · requiredThe company the quote belongs to.
cURL
curl -X DELETE \
  "https://chronly.ca/api/quotes/{quoteId}/cancel-scheduled-send?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }

Errors

  • 404The quote is not scheduled for sending.
POST/quotes/{quoteId}/acceptAccept a quoteTry it ↗

Marks the quote accepted on the customer's behalf. Only a quote in sent or viewed status can be accepted, and only before validUntil passes.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
quoteIdpath · requiredThe quote to accept.

Request body

companyIdstring · requiredThe company the quote belongs to.
namestringWho accepted it. Falls back to the customer name on the quote.
cURL
curl -X POST \
  "https://chronly.ca/api/quotes/{quoteId}/accept" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "companyId": "cmp_4k9x2m", "name": "Sam Doyle" }'
Response 200
{ "success": true, "quote": { "quoteId": "quote_m9x2kap01", "status": "accepted" } }

Errors

  • 400The quote is not in sent or viewed status, or it has expired.
  • 404No quote with that id in the company.

Sending acceptanceType: "client" takes a different, PUBLIC code path used by the emailed acceptance link — it needs no token, requires the quote to have allowLinkAcceptance, and records the accepter's IP and user agent. Do not use it for server-to-server acceptance.

POST/quotes/{quoteId}/convert-to-invoiceConvert a quote to an invoiceTry it ↗

Creates an invoice from the quote, copying its line items and totals exactly so the two always agree. A paid deposit on the quote is carried across as a payment on the invoice, and the quote moves to converted.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
quoteIdpath · requiredThe quote to convert.

Request body

companyIdstring · requiredThe company the quote belongs to.
paymentTermsstringSets the new invoice's paymentDue relative to today. Defaults to net-30.One ofon-receiptnet-7net-14net-30net-45net-60net-90
cURL
curl -X POST \
  "https://chronly.ca/api/quotes/{quoteId}/convert-to-invoice" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "companyId": "cmp_4k9x2m", "paymentTerms": "net-30" }'
Response 201
{
  "invoiceId": "a1b2c3d4e5f6",
  "invoiceNumber": 1044,
  "status": "draft",
  "totalAmount": 2620
}

Errors

  • 400The quote does not exist, or it has already been converted.
  • 403The token holds quotes:write but not invoices:create.

Needs TWO grants: quotes:write AND invoices:create. Holding the quotes grant alone must not be a way to mint invoices.

POST/quotes/{quoteId}/create-jobCreate a job from a quoteTry it ↗

Creates a job from the quote and links the two, writing both the creation and the link to the job activity log.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
quoteIdpath · requiredThe quote to build the job from.

Request body

companyIdstring · requiredThe company the quote belongs to.
namestringJob name. Defaults to something derived from the quote.
startDate / endDatestring (YYYY-MM-DD)Job window.
assignedUserIdsarray of stringCrew assigned to the job.
cURL
curl -X POST \
  "https://chronly.ca/api/quotes/{quoteId}/create-job" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "companyId": "cmp_4k9x2m", "name": "Northline — roof replacement" }'
Response 201
{
  "jobId": "job_71kd",
  "job": { "jobId": "job_71kd", "name": "Northline — roof replacement", "status": "unscheduled" }
}

Errors

  • 400The quote does not exist or a job could not be created from it.
  • 403The token holds quotes:read but not jobs:create, or the account's plan does not include jobs.

Needs TWO grants: quotes:read AND jobs:create. Jobs are also plan-gated, so a free-tier account gets 403 subscription_required.

GET/quotes/{quoteId}/email-eventsList email eventsTry it ↗

Every delivery event the mail provider reported for this quote, newest first.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
quoteIdpath · requiredThe quote.
companyIdquery · requiredThe company the quote belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/quotes/{quoteId}/email-events?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": [ { "eventType": "open", "email": "[email protected]", "timestamp": "2026-08-19T18:02:00.000Z" } ]
}
GET/quotes/{quoteId}/email-statsEmail engagement statsTry it ↗

The quote's email events rolled up into counts and rates. Rates are percentages, and are 0 when the denominator is 0.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
quoteIdpath · requiredThe quote.
companyIdquery · requiredThe company the quote belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/quotes/{quoteId}/email-stats?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": { "processed": 1, "delivered": 1, "opened": 1, "clicked": 0, "openRate": 100, "deliveryRate": 100, "uniqueRecipients": 1 }
}
GET/quotes/{quoteId}/view-statsQuote view statsTry it ↗

How often the customer-facing quote page was viewed. Portal views are excluded, so this reflects the customer rather than your own team.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
quoteIdpath · requiredThe quote.
companyIdquery · requiredThe company the quote belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/quotes/{quoteId}/view-stats?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": { "pageViews": 3, "clicks": 1, "uniqueVisitors": 1 }
}
GET/quotes/{quoteId}/tracking-eventsList tracking eventsTry it ↗

The raw tracking events behind the view stats, newest first.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
quoteIdpath · requiredThe quote.
companyIdquery · requiredThe company the quote belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/quotes/{quoteId}/tracking-events?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": [ { "type": "page-view", "pathname": "/pdf-quote/quote_m9x2kap01" } ]
}
GET/quotes/{quoteId}/delivery-issuesList delivery issuesTry it ↗

Bounces, drops and spam reports for this quote — the subset of email events that mean the customer did not receive it.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
quoteIdpath · requiredThe quote.
companyIdquery · requiredThe company the quote belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/quotes/{quoteId}/delivery-issues?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": [ { "eventType": "bounce", "email": "[email protected]", "reason": "550 5.1.1 unknown recipient" } ]
}

Jobs

Jobs are the container the rest of the work hangs off: scheduled visits, assigned crew, linked quotes and invoices, documents, client shares and an activity log.

GET/jobsList jobsTry it ↗

Returns jobs newest first, each with a scheduling summary so a list view needs no follow-up calls. Visibility is the greater of the company-wide jobs permission and per-job assignment, so a token scoped below viewer sees only jobs it created or is assigned to.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X GET \
  "https://chronly.ca/api/jobs?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "jobs": [
    {
      "jobId": "job_71kd",
      "name": "Northline re-roof",
      "status": "unscheduled",
      "color": "#1c659e",
      "assignedUserIds": ["usr_2k9m"],
      "visitSummary": { "count": 3, "nextStart": "2026-09-02T15:00:00.000Z" }
    }
  ]
}
POST/jobsCreate a jobTry it ↗

Creates a job. Assigning crew at creation writes viewer-level assignment records, emails those people, and logs both the creation and the assignment on the job.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to create the record in. Required for token auth.
namestring · requiredJob name. Blank or whitespace is rejected.
statusstringDefaults to unscheduled.One ofdraftscheduledunscheduledcompletedcancelledarchivedactiveon-hold
customerobject{ value, label } — defaults to nulls.
locationobjectJob site address.
startDate / endDatestringPlanned job window.
colorstringCalendar colour. Defaults to #1c659e.
assignedUserIdsarrayUser ids to put on the job as viewers.
projectCoordinatorstringCoordinator user id.
cURL
curl -X POST \
  "https://chronly.ca/api/jobs" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "name": "Northline re-roof",
    "customer": { "value": "byr_8f3k", "label": "Northline Roofing" },
    "startDate": "2026-09-02",
    "assignedUserIds": ["usr_2k9m"]
  }'
Response 201
{
  "success": true,
  "job": { "jobId": "job_71kd", "name": "Northline re-roof", "status": "unscheduled" }
}

Errors

  • 400Job name is required.
GET/jobs/{jobId}Retrieve a jobTry it ↗

Returns the job with its linked records and scheduling detail.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
jobIdpath · requiredThe job id.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X GET \
  "https://chronly.ca/api/jobs/{jobId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true, "job": { "jobId": "job_71kd" } }
PUT/jobs/{jobId}Update a jobTry it ↗

Updates job details. Changes are written to the job activity log.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
jobIdpath · requiredThe job to update.
companyIdquery · requiredIN THE QUERY STRING. Job sub-routes authorize before reading the body, so a companyId in the body is not seen.

Request body

statusstringJob workflow state.One ofdraftscheduledunscheduledcompletedcancelledarchivedactiveon-hold
assignedUserIdsarrayReplace the assigned crew.
cURL
curl -X PUT \
  "https://chronly.ca/api/jobs/{jobId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "status": "scheduled" }'
Response 200
{ "success": true, "job": { "jobId": "job_71kd", "status": "scheduled" } }
DELETE/jobs/{jobId}Delete a jobTry it ↗

Deletes the job. Use the restore endpoint to bring one back.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
jobIdpath · requiredThe job to delete.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X DELETE \
  "https://chronly.ca/api/jobs/{jobId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }
GET/jobs/{jobId}/activityJob activity logTry it ↗

Chronological log of everything that happened on the job: creation, assignments, linked and unlinked quotes and invoices.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
jobIdpath · requiredThe job id.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X GET \
  "https://chronly.ca/api/jobs/{jobId}/activity?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true, "activity": [] }
GET/jobs/{jobId}/assignmentsList crew assignmentsTry it ↗

The people on the job and the role each holds. POST the same path to add one; PUT and DELETE /jobs/{jobId}/assignments/{assignmentId} change or remove it.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
jobIdpath · requiredThe job id.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X GET \
  "https://chronly.ca/api/jobs/{jobId}/assignments?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true, "assignments": [ { "assignmentId": "asg_3k1m", "userId": "usr_2k9m", "role": "viewer" } ] }
POST/jobs/{jobId}/invoicesLink an invoiceTry it ↗

Attaches an existing invoice to the job and logs the link. DELETE the same path to unlink. /jobs/{jobId}/quotes behaves identically for quotes.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
jobIdpath · requiredThe job id.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.

Request body

invoiceIdstring · requiredThe invoice to link.
cURL
curl -X POST \
  "https://chronly.ca/api/jobs/{jobId}/invoices?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "invoiceId": "a1b2c3d4e5f6" }'
Response 200
{ "success": true }
GET/jobs/{jobId}/statsJob totalsTry it ↗

Rolled-up figures for the job — quoted, invoiced and collected.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
jobIdpath · requiredThe job id.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X GET \
  "https://chronly.ca/api/jobs/{jobId}/stats?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true, "stats": { "quoted": 8600, "invoiced": 8600, "paid": 1720 } }
GET/jobs/{jobId}/sharesClient sharesTry it ↗

Share links that let a client view the job. POST creates one; PUT and DELETE /jobs/{jobId}/shares/{shareId} manage it; /jobs/{jobId}/share-email-stats reports on the share emails.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
jobIdpath · requiredThe job id.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X GET \
  "https://chronly.ca/api/jobs/{jobId}/shares?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true, "shares": [] }
GET/jobs/{jobId}/documentsJob documentsTry it ↗

Files attached to the job.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
jobIdpath · requiredThe job id.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X GET \
  "https://chronly.ca/api/jobs/{jobId}/documents?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true, "documents": [] }
POST/jobs/{jobId}/assignmentsAssign a user to a jobTry it ↗

Adds a crew member to the job, or updates their role if they are already on it. A genuinely new assignment also emails the person; a role change on an existing assignment does not.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
jobIdpath · requiredThe job.
companyIdquery · requiredIN THE QUERY STRING. Job sub-routes authorize before reading the body, so a companyId in the body is not seen.

Request body

userIdstring · requiredThe user to assign.
rolestringTheir role on this job. Anything else falls back to viewer.One ofviewereditoradmin
permissions.canCompleteJobbooleanLets a viewer mark the job complete. Editors and admins can already do this through their role, so it is only stored for viewers.
cURL
curl -X POST \
  "https://chronly.ca/api/jobs/{jobId}/assignments?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "userId": "usr_3k9m", "role": "editor" }'
Response 201
{
  "success": true,
  "assignment": { "jobId": "job_71kd", "userId": "usr_3k9m", "role": "editor" }
}

Errors

  • 400userId is missing.
  • 403The caller's role on this job is below admin.
PUT/jobs/{jobId}/assignments/{assignmentId}Update an assignmentTry it ↗

Changes a crew member's role or per-job permissions.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
jobIdpath · requiredThe job.
assignmentIdpath · requiredThe assignment to update.
companyIdquery · requiredIN THE QUERY STRING, not the body.

Request body

rolestringNew role. Must be valid if sent.One ofviewereditoradmin
permissions.canCompleteJobbooleanLets a viewer mark the job complete.
cURL
curl -X PUT \
  "https://chronly.ca/api/jobs/{jobId}/assignments/{assignmentId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "role": "admin" }'
Response 200
{ "success": true, "assignment": { "role": "admin" } }

Errors

  • 400Neither role nor permissions was sent, or role is not one of the allowed values.
  • 404No assignment with that id on this job.
DELETE/jobs/{jobId}/assignments/{assignmentId}Remove an assignmentTry it ↗

Takes a crew member off the job and logs it to the job activity log.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
jobIdpath · requiredThe job.
assignmentIdpath · requiredThe assignment to remove.
companyIdquery · requiredThe company the job belongs to.
cURL
curl -X DELETE \
  "https://chronly.ca/api/jobs/{jobId}/assignments/{assignmentId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }

Errors

  • 404No assignment with that id on this job.
DELETE/jobs/{jobId}/invoicesUnlink an invoice from a jobTry it ↗

Detaches the invoice from the job. The invoice itself is untouched.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
jobIdpath · requiredThe job.
invoiceIdquery · requiredThe invoice to unlink.
companyIdquery · requiredThe company the job belongs to.
cURL
curl -X DELETE \
  "https://chronly.ca/api/jobs/{jobId}/invoices?invoiceId=%3CinvoiceId%3E&companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }

Errors

  • 400The invoiceId query parameter is missing.
POST/jobs/{jobId}/quotesLink a quote to a jobTry it ↗

Attaches an existing quote to the job and writes the link to the job activity log.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
jobIdpath · requiredThe job.
companyIdquery · requiredIN THE QUERY STRING, not the body.

Request body

quoteIdstring · requiredThe quote to link. Must be in the same company.
cURL
curl -X POST \
  "https://chronly.ca/api/jobs/{jobId}/quotes?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "quoteId": "quote_m9x2kap01" }'
Response 200
{ "success": true }

Errors

  • 400quoteId is missing.
  • 404No quote with that id in the company.
DELETE/jobs/{jobId}/quotesUnlink a quote from a jobTry it ↗

Detaches the quote from the job. The quote itself is untouched.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
jobIdpath · requiredThe job.
quoteIdquery · requiredThe quote to unlink.
companyIdquery · requiredThe company the job belongs to.
cURL
curl -X DELETE \
  "https://chronly.ca/api/jobs/{jobId}/quotes?quoteId=%3CquoteId%3E&companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }

Errors

  • 400The quoteId query parameter is missing.
GET/jobs/{jobId}/restorePreview a restoreTry it ↗

Reports whether a cancelled job can be brought back and how many of its cancelled visits would come with it. Read-only — nothing changes.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
jobIdpath · requiredThe job.
companyIdquery · requiredThe company the job belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/jobs/{jobId}/restore?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true, "canRestore": true, "restorableVisitCount": 3 }
POST/jobs/{jobId}/restoreRestore a cancelled jobTry it ↗

Brings a cancelled job back. Its new status comes from its schedule — scheduled if it has visits, unscheduled if not — rather than from whatever it was before the cancellation.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
jobIdpath · requiredThe job to restore.
companyIdquery · requiredIN THE QUERY STRING, not the body.

Request body

restoreVisitsbooleanAlso restore the visits that were cancelled with the job. Defaults to false.
cURL
curl -X POST \
  "https://chronly.ca/api/jobs/{jobId}/restore?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "restoreVisits": true }'
Response 200
{ "success": true, "job": { "jobId": "job_71kd", "status": "scheduled" }, "visitsRestored": 3 }

Errors

  • 400The job is not cancelled.
POST/jobs/{jobId}/sharesCreate a client share linkTry it ↗

Creates a share link that lets a customer view the job without an account, and optionally emails it to them. The share is reached at /{company}/job/{shareCode}.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
jobIdpath · requiredThe job to share.
companyIdquery · requiredIN THE QUERY STRING, not the body.

Request body

recipientEmailstring · requiredWho the link is for.
recipientNamestringDisplay name for the recipient.
canUploadDocumentsbooleanLets the recipient upload files to the job. Defaults to false.
expiresAtstring (ISO 8601)When the link stops working. Never expires when omitted.
sendEmailbooleanEmail the link to recipientEmail on creation.
cURL
curl -X POST \
  "https://chronly.ca/api/jobs/{jobId}/shares?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "recipientEmail": "[email protected]",
    "recipientName": "Sam Doyle",
    "canUploadDocuments": true
  }'
Response 200
{
  "success": true,
  "share": { "shareId": "jsh_2k91", "shareCode": "7Qm2xL9pRt4Bd0Vs", "status": "active" },
  "shareUrl": "https://aurora-contracting.chronly.ca/job/7Qm2xL9pRt4Bd0Vs"
}

Errors

  • 400recipientEmail is missing.
  • 403The caller's role on this job is below admin.
PUT/jobs/{jobId}/shares/{shareId}Update a share linkTry it ↗

Changes what a share link allows, when it expires, or revokes it. Each change is named individually in the job activity log.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
jobIdpath · requiredThe job.
shareIdpath · requiredThe share to update.
companyIdquery · requiredIN THE QUERY STRING, not the body.

Request body

permissions.canUploadDocumentsbooleanAllow or disallow uploads through this link.
expiresAtstring (ISO 8601) | nullNew expiry, or null to remove it.
statusstringRevoke or reinstate the link. Any other value is ignored.One ofactiverevoked
cURL
curl -X PUT \
  "https://chronly.ca/api/jobs/{jobId}/shares/{shareId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "status": "revoked" }'
Response 200
{ "success": true, "share": { "shareId": "jsh_2k91", "status": "revoked" } }

Errors

  • 404No share with that id on this job.
DELETE/jobs/{jobId}/shares/{shareId}Delete a share linkTry it ↗

Removes the share link entirely. The link stops working immediately.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
jobIdpath · requiredThe job.
shareIdpath · requiredThe share to delete.
companyIdquery · requiredThe company the job belongs to.
cURL
curl -X DELETE \
  "https://chronly.ca/api/jobs/{jobId}/shares/{shareId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }

Errors

  • 404No share with that id on this job.
GET/jobs/{jobId}/share-email-statsShare email statsTry it ↗

Delivery and engagement counts for the share-link emails sent from this job, keyed by shareId.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
jobIdpath · requiredThe job.
companyIdquery · requiredThe company the job belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/jobs/{jobId}/share-email-stats?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": { "jsh_2k91": { "delivered": 1, "open": 2 } }
}
POST/jobs/{jobId}/documents/uploadUpload a job documentTry it ↗

Attaches a file to the job. This endpoint takes multipart/form-data, not JSON.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
jobIdpath · requiredThe job.
companyIdquery · requiredIN THE QUERY STRING — this is a multipart request, so there is no JSON body to carry it.
fileform-data · requiredThe file to attach.
cURL
curl -X POST \
  "https://chronly.ca/api/jobs/{jobId}/documents/upload?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true, "document": { "documentId": "jdoc_5m2p", "fileName": "permit.pdf" } }

Errors

  • 400No file was provided.

Customers

Customers — buyers, in the data model — are the records quotes, invoices and checkouts point at. Each one carries a billing address and a separate shipping address. Note that the business name is company, not companyName. Creation guards against duplicate email addresses within a company. A customer can also carry secondary contacts — other people at the same business — which an invoice or quote can be addressed to instead of the primary.

GET/buyersList customersTry it ↗

Returns the customers for the company as a plain array, newest first.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X GET \
  "https://chronly.ca/api/buyers?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
[
  {
    "buyerId": "k3n8Qp2xLm41",
    "firstName": "Dana",
    "lastName": "Whitfield",
    "emailAddress": "[email protected]",
    "phoneNumber": "250 555 0142",
    "company": "Northline Roofing",
    "address": {
      "address1": "114 Wharf St",
      "address2": "",
      "city": "Victoria",
      "province": "British Columbia",
      "postalCode": "V8W 1T3",
      "country": "Canada"
    },
    "fullName": "Dana Whitfield",
    "formattedAddress": "114 Wharf St, Victoria, British Columbia V8W 1T3, Canada"
  }
]

A bare array of every customer in the company — there are no pagination, filter or sort parameters. fullName and formattedAddress are computed on read rather than stored, and appear on every response that returns a customer.

POST/buyersCreate a customerTry it ↗

Creates a customer with a server-generated buyerId. If the email already exists in the company the call returns 409 with the existing record instead of creating a second one — send forceCreate to override.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to create the record in. Required for token auth.
firstNamestring · requiredCustomer's first name.
lastNamestring · requiredCustomer's last name.
emailAddressstring · requiredStored lower-cased, and checked against the existing customers in the company. Must be in [email protected] form.
middleNamestringIncluded in fullName when present.
phoneNumberstringDigits, spaces and the characters - + ( ) only. Stored as sent — no reformatting.
companystringBusiness name. The field is company; companyName is not recognised and is dropped without an error.
addressobjectBilling address, and the one formattedAddress is built from. Every sub-field is optional.
address.address1stringStreet address.
address.address2stringUnit, suite or floor.
address.citystringCity.
address.provincestringProvince or state. Free text — the portal writes the full name, e.g. British Columbia, not a code.
address.postalCodestringPostal or ZIP code. Stored upper-cased.
address.countrystringCountry. Free text — the portal writes the full name, e.g. Canada, not an ISO code.
shippingAddressobjectWhere goods go when that differs from billing. Same six sub-fields, stored independently — setting one address never populates the other.
shippingAddress.address1stringStreet address.
shippingAddress.address2stringUnit, suite or floor.
shippingAddress.citystringCity.
shippingAddress.provincestringProvince or state, as a full name.
shippingAddress.postalCodestringPostal or ZIP code. Stored upper-cased.
shippingAddress.countrystringCountry, as a full name.
integrations.quickbooks.displayNamestringThe name QuickBooks matches this customer on. Left blank, an export falls back to company, or to the full name when there is no company.
integrations.quickbooks.customerNumberstringYour QuickBooks customer number.
contactsarraySecondary contacts at this business. The customer's own name and email stay the primary contact and are never repeated here. Sending contacts replaces the whole list — a row you leave out is deleted.
contacts[].contactIdstringServer-generated, 12 alphanumeric characters. Omit it on a new contact; send it back unchanged when editing, or the contact is treated as a new person and any invoice or quote pointing at it falls back to its stored snapshot.
contacts[].firstNamestring · requiredContact's first name.
contacts[].lastNamestring · requiredContact's last name.
contacts[].emailAddressstring · requiredStored lower-cased. Rows with no email address are dropped.
contacts[].phoneNumberstringSame format rules as the customer's own phone number.
contacts[].jobTitlestringFree text, e.g. Accounts Payable.
contacts[].notesstringFree text note about this contact.
forceCreatebooleanSkip both duplicate-email checks and create the record anyway. A control flag — it is not stored on the customer.
cURL
curl -X POST \
  "https://chronly.ca/api/buyers" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "firstName": "Dana",
    "lastName": "Whitfield",
    "emailAddress": "[email protected]",
    "phoneNumber": "250 555 0142",
    "company": "Northline Roofing",
    "address": {
      "address1": "114 Wharf St",
      "city": "Victoria",
      "province": "British Columbia",
      "postalCode": "v8w 1t3",
      "country": "Canada"
    }
  }'
Response 200
{
  "buyerId": "k3n8Qp2xLm41",
  "firstName": "Dana",
  "lastName": "Whitfield",
  "emailAddress": "[email protected]",
  "company": "Northline Roofing",
  "address": {
    "address1": "114 Wharf St",
    "city": "Victoria",
    "province": "British Columbia",
    "postalCode": "V8W 1T3",
    "country": "Canada"
  },
  "companyId": "cmp_4k9x2m",
  "fullName": "Dana Whitfield",
  "formattedAddress": "114 Wharf St, Victoria, British Columbia V8W 1T3, Canada"
}

Errors

  • 409error is DUPLICATE_CUSTOMER and existingBuyer holds the record already on file.
  • 409error is DUPLICATE_CONTACT_EMAIL — the email is not another customer's own address, but it is a secondary contact on one. existingBuyer is that customer and matchedContact is the contact it matched. This is a warning, not a block: send forceCreate to create the customer anyway.

buyerId is generated by the server as 12 alphanumeric characters; buyerId, accountId and companyId are ignored if sent in the body. Keys that are not fields on the customer are dropped silently rather than rejected, so check a create round-trips before relying on it. A missing required field or a malformed email or phone number currently comes back as a 500, not a 400.

GET/buyers/{buyerId}Retrieve a customerTry it ↗

Returns one customer record, including both addresses and the computed name and address strings.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
buyerIdpath · requiredThe customer id.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X GET \
  "https://chronly.ca/api/buyers/{buyerId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "buyerId": "k3n8Qp2xLm41",
  "firstName": "Dana",
  "lastName": "Whitfield",
  "emailAddress": "[email protected]",
  "phoneNumber": "250 555 0142",
  "company": "Northline Roofing",
  "address": {
    "address1": "114 Wharf St",
    "city": "Victoria",
    "province": "British Columbia",
    "postalCode": "V8W 1T3",
    "country": "Canada"
  },
  "shippingAddress": {
    "address1": "22 Dock Rd",
    "city": "Nanaimo",
    "province": "British Columbia",
    "postalCode": "V9R 5J8",
    "country": "Canada"
  },
  "integrations": { "quickbooks": { "displayName": "Northline Roofing", "customerNumber": "1042" } },
  "fullName": "Dana Whitfield",
  "formattedAddress": "114 Wharf St, Victoria, British Columbia V8W 1T3, Canada"
}

Errors

  • 404No customer with that buyerId in this company.
PUT/buyers/{buyerId}Update a customerTry it ↗

Partial update of contact details and both addresses. Send only the fields you are changing.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
buyerIdpath · requiredThe customer to update.
companyIdstring · requiredThe company the record belongs to. Required for token auth.

Request body

firstName / lastName / middleNamestringName parts. Changing any of them changes fullName.
emailAddressstringRe-validated and lower-cased. Unlike create, this is not checked for duplicates — the same address can end up on two customers.
phoneNumberstringDigits, spaces and the characters - + ( ) only.
companystringBusiness name.
addressobjectBilling address. Merged field by field into the existing one, so sub-fields you leave out keep their current values.
address.address1 / address.address2 / address.city / address.province / address.postalCode / address.countrystringSame six sub-fields as create. postalCode is stored upper-cased; province and country are free text.
shippingAddressobjectShipping address, merged the same way and independently of the billing address.
shippingAddress.address1 / shippingAddress.address2 / shippingAddress.city / shippingAddress.province / shippingAddress.postalCode / shippingAddress.countrystringSame six sub-fields as create.
integrations.quickbooks.displayName / integrations.quickbooks.customerNumberstringQuickBooks export identifiers. Unlike the addresses these are replaced as a pair — sending one on its own clears the other.
contactsarraySecondary contacts, with the same sub-fields as create. Unlike the addresses this REPLACES the whole list rather than merging — send every contact you want to keep, each with its existing contactId, or you will delete them. Omit the key entirely to leave contacts untouched.
cURL
curl -X PUT \
  "https://chronly.ca/api/buyers/{buyerId}" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "phoneNumber": "250 555 0142",
    "address": { "city": "Nanaimo", "postalCode": "v9r 5j8" }
  }'
Response 200
{
  "buyerId": "k3n8Qp2xLm41",
  "phoneNumber": "250 555 0142",
  "address": {
    "address1": "114 Wharf St",
    "city": "Nanaimo",
    "province": "British Columbia",
    "postalCode": "V9R 5J8",
    "country": "Canada"
  }
}

Errors

  • 404No customer with that buyerId in this company.
  • 409error is DUPLICATE_CONTACT_EMAIL — the emailAddress you are moving this customer to is already a secondary contact on a different customer. A warning, not a block: send forceCreate to apply the update anyway.

buyerId, accountId and companyId are ignored if sent — a customer cannot be renumbered or moved to another company this way. Keys that are not fields on the customer are dropped without an error, so a 200 does not prove every field you sent was applied.

DELETE/buyers/{buyerId}Delete a customerTry it ↗

Permanently removes the customer record.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
buyerIdpath · requiredThe customer to delete.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X DELETE \
  "https://chronly.ca/api/buyers/{buyerId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "message": "Customer deleted successfully"
}

Errors

  • 404No customer with that buyerId in this company.

A hard delete with no cascade. Invoices, quotes and purchases keep the buyerId they were created with, so their customer lookups stop resolving.

Products

The catalogue behind checkouts and line items, with sort order, inventory tracking and file attachments.

GET/productsList productsTry it ↗

Returns products sorted by sortOrder, then newest first.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X GET \
  "https://chronly.ca/api/products?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "products": [
    {
      "productId": "prd_9m2k",
      "name": "Site prep, per day",
      "price": 1200,
      "sortOrder": 1,
      "isActive": true
    }
  ]
}
POST/productsCreate a productTry it ↗

Creates a product with a generated productId. Any inventory stock changes sent along are stamped with the creating identity and a timestamp.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to create the record in.
namestring · requiredProduct name.
typestring · requiredWhat kind of thing this is. Drives tax treatment.One ofphysicaldigitalserviceprofessional_servicetaxable_servicetax_exempt
billingTypestring · requiredWhether it is billed once or on a recurrence.One ofone-timerecurring
pricingobject · requiredPrice and currency, as { price, currency }. NOTE: there is no top-level price field — a bare price is silently discarded and the request fails validation.
pricing.pricenumber · requiredUnit price. Must be zero or greater.
pricing.currencystringDefaults to CAD.
purchaseCostnumberWhat you pay your supplier for one unit. Zero or greater, or null to leave it unset. Returned by the /products endpoints and never shown on your storefront, cart or checkout.
recurrencestringRequired in practice when billingType is recurring.One ofdailyweeklymonthlyyearly
statusstringDefaults to active.One ofactiveinternal-onlyinactivearchived
skustringYour own stock-keeping code.
descriptionstringCustomer-facing description.
taxablebooleanWhether tax applies to this product.
inventoryobjectTracking settings and opening stock changes. Use POST /products/{productId}/enable-inventory to turn tracking on afterwards.
cURL
curl -X POST \
  "https://chronly.ca/api/products" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "name": "Site prep, per day",
    "type": "service",
    "billingType": "one-time",
    "pricing": { "price": 1200, "currency": "CAD" }
  }'
Response 201
{
  "productId": "prd_9m2k",
  "name": "Site prep, per day",
  "type": "service",
  "billingType": "one-time",
  "pricing": { "price": 1200, "currency": "CAD" },
  "status": "active"
}

Errors

  • 400Missing account or company information.
  • 500A required field is absent or an enum value is invalid — this surfaces as a Mongoose validation error rather than a 400.

createdBy is taken from the authenticated identity and ignored if sent.

GET/products/{productId}Retrieve a productTry it ↗

Returns one product with its inventory state.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
productIdpath · requiredThe product id.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X GET \
  "https://chronly.ca/api/products/{productId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "productId": "prd_9m2k", "price": 1200 }
PUT/products/{productId}Update a productTry it ↗

Updates catalogue fields on a product.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
productIdpath · requiredThe product to update.
companyIdstring · requiredThe company to create the record in. Required for token auth.

Request body

pricenumberNew unit price.
purchaseCostnumberWhat you pay your supplier for one unit. Send null to clear it. Never shown on your storefront, cart or checkout.
cURL
curl -X PUT \
  "https://chronly.ca/api/products/{productId}" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "price": 1250 }'
Response 200
{ "productId": "prd_9m2k", "price": 1250 }
DELETE/products/{productId}Delete a productTry it ↗

Removes the product from the catalogue.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
productIdpath · requiredThe product to delete.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
cURL
curl -X DELETE \
  "https://chronly.ca/api/products/{productId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }
PATCH/products/{productId}/statusSet product statusTry it ↗

Changes a product's status without touching the rest of the record.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
productIdpath · requiredThe product id.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.

Request body

statusstring · requiredThe product's new status. Anything outside this set is rejected.One ofactiveinternal-onlyinactivearchived
cURL
curl -X PATCH \
  "https://chronly.ca/api/products/{productId}/status?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "inactive"
  }'
Response 200
{ "productId": "prd_6k2m", "status": "inactive" }

Errors

  • 400status is missing, or is not one of the four allowed values.
PATCH/products/{productId}/inventoryAdjust inventoryTry it ↗

Records a stock change against a product that already has inventory tracking on. POST /products/{productId}/enable-inventory turns tracking on first.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
productIdpath · requiredThe product id.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.

Request body

quantitynumber · requiredSigned stock movement. Must be a number; zero is rejected.
reasonstring · requiredWhy the stock moved. Cannot be blank.
typestringWhat kind of movement this is. Defaults to adjustment.One ofinitialpurchaseadjustmentreturndamagedtransfer
referencestringYour own reference for the movement, stored on the stock change.
cURL
curl -X PATCH \
  "https://chronly.ca/api/products/{productId}/inventory?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "quantity": -3,
    "reason": "Used on job_71kd",
    "type": "adjustment"
  }'
Response 200
{ "productId": "prd_6k2m", "inventory": { "stockStatus": "in-stock" } }

Errors

  • 400quantity is missing or not a number, or reason is missing or blank.
PATCH/products/reorderReorder the catalogueTry it ↗

Writes a new sortOrder across products in one call — the order customers see on a checkout.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to create the record in. Required for token auth.
productIdsarray · requiredProduct ids in the order you want them.
cURL
curl -X PATCH \
  "https://chronly.ca/api/products/reorder" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "productIds": ["prd_9m2k", "prd_4x8p"] }'
Response 200
{ "success": true }
POST/products/uploadUpload product mediaTry it ↗

Uploads product images or PDFs. This endpoint takes multipart/form-data, not JSON. Returns a presigned URL per file, valid for one hour.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredIN THE QUERY STRING — this is a multipart request, so there is no JSON body to carry it.
filesform-data · requiredOne to five files under the repeated field name files. JPEG, PNG, GIF, WebP or PDF, each at most 10 MB.
cURL
curl -X POST \
  "https://chronly.ca/api/products/upload?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "files": [ { "id": "cmp_4k9x2m/abc123.pdf", "url": "https://…" } ]
}

Errors

  • 400No files were provided, more than five were sent, a file is not an allowed type, or a file exceeds 10 MB.
POST/products/{productId}/enable-inventoryEnable inventory trackingTry it ↗

Turns on stock tracking for a product that was created without it, recording the opening stock as an initial stock change.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
productIdpath · requiredThe product.

Request body

companyIdstring · requiredThe company the product belongs to.
initialStocknumber · requiredOpening stock. Must be a number and zero or greater — a numeric string is rejected.
lowStockThresholdnumber · requiredStock level that triggers a low-stock alert. Must be a number and zero or greater.
cURL
curl -X POST \
  "https://chronly.ca/api/products/{productId}/enable-inventory" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "companyId": "cmp_4k9x2m", "initialStock": 40, "lowStockThreshold": 5 }'
Response 200
{
  "success": true,
  "product": { "productId": "prd_9m2k", "inventory": { "trackInventory": true, "currentStock": 40 } }
}

Errors

  • 400initialStock or lowStockThreshold is not a non-negative number, or inventory tracking is already enabled.
  • 404No product with that id in the company.
GET/products/presigned-urlGet a presigned file URLTry it ↗

Issues a short-lived URL for a stored product file. The key must sit under your own company prefix.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the file belongs to.
s3Keyquery · requiredStorage key, as returned by POST /products/upload.
cURL
curl -X GET \
  "https://chronly.ca/api/products/presigned-url?companyId=cmp_4k9x2m&s3Key=%3Cs3Key%3E" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true, "url": "https://…" }

Errors

  • 400s3Key is missing.
  • 404The key is not under this company's prefix.
GET/products/pdf-proxyProxy a product PDFTry it ↗

Streams a stored product PDF through Chronly rather than handing back a storage URL, for viewers that will not follow a redirect. Same company-prefix restriction as the presigned URL.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the file belongs to.
s3Keyquery · requiredStorage key, as returned by POST /products/upload.
cURL
curl -X GET \
  "https://chronly.ca/api/products/pdf-proxy?companyId=cmp_4k9x2m&s3Key=%3Cs3Key%3E" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
The PDF bytes, with Content-Type: application/pdf.

Errors

  • 400s3Key is missing.
  • 403The key is not under this company's prefix.

Discounts

Discount codes applied at checkout, with usage limits, expiry and validation. Stats can come back with the list.

GET/discountsList discountsTry it ↗

Returns discounts for the company, filterable, with an optional stats block.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
statusqueryRestrict to a status.One ofactiveinactiveexpiredused_up
applicableTypequeryRestrict to what the code applies to.One ofcheckoutinvoiceproduct
expiringquery · booleantrue returns only codes close to expiry.
includeStatsquery · booleantrue adds a stats object alongside discounts.
cURL
curl -X GET \
  "https://chronly.ca/api/discounts?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "discounts": [
    { "discountId": "dsc_2k9m", "code": "SPRING10", "type": "percentage", "amount": 10, "isActive": true }
  ],
  "stats": { "active": 4, "redeemed": 137 }
}
POST/discountsCreate a discountTry it ↗

Creates a discount code. Codes are compared upper-cased and must be unique within the company.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to create the record in.
namestring · requiredInternal name for the discount.
codestring · requiredThe code customers type. Must be unique in the company, compared case-insensitively.
discountTypestring · requiredWhether value is a percentage or a dollar amount. Send the bare string; the { label, value } object the portal uses is also accepted.One ofpercentagedollar
valuenumber · requiredThe amount off. Must be 0-100 for percentage, and positive for dollar. NOTE: this is the magnitude — there is no amount field.
applicableTypesobject · requiredWhat the code may be used on, as { checkouts, invoices, products, all } booleans. At least one must be set; an empty object is rejected.
descriptionstringCustomer-facing description.
maxUsesnumberTotal redemptions allowed. null means unlimited.
startDate / endDatestring (ISO 8601)Window the code is valid in. startDate must be before endDate.
minimumAmountnumberOrder subtotal the code needs before it applies. Defaults to 0.
maximumDiscountAmountnumberCaps a percentage discount. null means no cap.
statusstringDefaults to active.One ofactiveinactiveexpiredused_up
cURL
curl -X POST \
  "https://chronly.ca/api/discounts" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "name": "Spring promo",
    "code": "SPRING10",
    "discountType": "percentage",
    "value": 10,
    "applicableTypes": { "invoices": true, "checkouts": true },
    "maxUses": 100
  }'
Response 201
{
  "discountId": "dsc_2k9m",
  "name": "Spring promo",
  "code": "SPRING10",
  "discountType": { "label": "Percentage", "value": "percentage" },
  "value": 10,
  "status": "active"
}

Errors

  • 400discountType is not percentage or dollar.
  • 500The code already exists in this company, a percentage value is outside 0-100, a dollar value is negative, startDate is not before endDate, or applicableTypes is empty. These validations pre-date the API and throw without a status, so they surface as 500 rather than 400 — the message body still names the problem.

createdBy is taken from the authenticated identity and ignored if sent.

GET/discounts/{discountId}Retrieve a discountTry it ↗

Returns one discount with its usage counters.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the records belong to. Find it in the portal under Settings > API.
discountIdpath · requiredThe discount id.
cURL
curl -X GET \
  "https://chronly.ca/api/discounts/{discountId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "discountId": "dsc_2k9m", "usedCount": 23 }
PUT/discounts/{discountId}Update a discountTry it ↗

Changes amount, limits, expiry or active state.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
discountIdpath · requiredThe discount to update.

Request body

companyIdstring · requiredThe company the record belongs to. Find it in the portal under Settings > API.
isActivebooleanTurn the code on or off.
cURL
curl -X PUT \
  "https://chronly.ca/api/discounts/{discountId}" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "isActive": false }'
Response 200
{ "discountId": "dsc_2k9m", "isActive": false }
DELETE/discounts/{discountId}Delete a discountTry it ↗

Removes the code. Past redemptions are retained.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the records belong to. Find it in the portal under Settings > API.
discountIdpath · requiredThe discount to delete.
cURL
curl -X DELETE \
  "https://chronly.ca/api/discounts/{discountId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }
POST/discounts/validateValidate a codeTry it ↗

Checks a code against a cart before charging: active, in date, within its usage limit and applicable. /discounts/validate-public is the unauthenticated variant the hosted checkout uses.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to create the record in. Required for token auth.
codestring · requiredThe code to check.
checkoutIdstringContext the code is being applied in.
amountnumberCart subtotal the discount would apply against.
cURL
curl -X POST \
  "https://chronly.ca/api/discounts/validate" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "code": "SPRING10", "checkoutId": "Xk92mLp01qZa" }'
Response 200
{ "valid": true, "discount": { "type": "percentage", "amount": 10 } }
POST/discounts/applyApply a codeTry it ↗

Records a redemption and returns the discounted totals.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to create the record in. Required for token auth.
codestring · requiredThe code being redeemed.
checkoutIdstringThe checkout the redemption belongs to.
cURL
curl -X POST \
  "https://chronly.ca/api/discounts/apply" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "code": "SPRING10", "checkoutId": "Xk92mLp01qZa" }'
Response 200
{ "success": true, "amountOff": 50 }
POST/discounts/bulkBulk create or updateTry it ↗

Creates or updates many discount codes in one request — useful for seasonal campaigns.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to create the record in. Required for token auth.
discountsarray · requiredDiscount objects to write — each with code, type and amount.
cURL
curl -X POST \
  "https://chronly.ca/api/discounts/bulk" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "discounts": [ { "code": "SPRING10", "type": "percentage", "amount": 10 } ] }'
Response 200
{ "success": true, "created": 1, "updated": 0 }

Scheduling

Calendars and the scheduled items on them. A scheduled item can stand alone or be linked to a job, in which case it appears as a visit on that job. Availability endpoints answer "who is free between these two times".

GET/scheduling/calendarsList calendarsTry it ↗

Returns the calendars the caller can see. Visibility is per calendar (private, internal or public) and is combined with the scheduling manage_all permission.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the records belong to. Find it in the portal under Settings > API.
cURL
curl -X GET \
  "https://chronly.ca/api/scheduling/calendars?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": [
    { "calendarId": "cal_7x2k", "name": "Install crew", "timezone": "America/Vancouver", "visibility": "internal" }
  ]
}
POST/scheduling/calendarsCreate a calendarTry it ↗

Creates a calendar. Requires the scheduling manage_calendars or manage_all permission.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdquery · requiredIN THE QUERY STRING. Scheduling writes authorize before the body is read, so a companyId in the body is not seen.
namestring · requiredDisplay name.
timezonestringIANA timezone; scheduled items inherit it when they do not set their own.
visibilitystringWho can see the calendar.One ofprivateinternalpublic
typestringWhat the calendar represents. Defaults to general.One ofteamresourcepersonalbookinggeneral
membersarrayUsers with a role on the calendar, as { userId, role }. role is owner, editor or viewer.
cURL
curl -X POST \
  "https://chronly.ca/api/scheduling/calendars?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Install crew",
    "timezone": "America/Vancouver",
    "visibility": "internal"
  }'
Response 200
{ "success": true, "data": { "calendarId": "cal_7x2k" } }
GET/scheduling/calendars/{calendarId}Retrieve a calendarTry it ↗

Returns one calendar with its members and settings.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
calendarIdpath · requiredThe calendar id.
companyIdquery · requiredThe company the records belong to. Find it in the portal under Settings > API.
cURL
curl -X GET \
  "https://chronly.ca/api/scheduling/calendars/{calendarId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true, "data": { "calendarId": "cal_7x2k" } }
PUT/scheduling/calendars/{calendarId}Update a calendarTry it ↗

Updates name, colour, timezone, visibility or membership.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
calendarIdpath · requiredThe calendar to update.

Request body

companyIdquery · requiredIN THE QUERY STRING. Scheduling writes authorize before the body is read, so a companyId in the body is not seen.
namestringNew display name.
cURL
curl -X PUT \
  "https://chronly.ca/api/scheduling/calendars/{calendarId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Install crew (east)"
  }'
Response 200
{ "success": true, "data": { "calendarId": "cal_7x2k" } }
DELETE/scheduling/calendars/{calendarId}Delete a calendarTry it ↗

Removes a calendar. Requires manage_all.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
calendarIdpath · requiredThe calendar to delete.
companyIdquery · requiredThe company the records belong to. Find it in the portal under Settings > API.
cURL
curl -X DELETE \
  "https://chronly.ca/api/scheduling/calendars/{calendarId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }
GET/scheduling/itemsList scheduled itemsTry it ↗

Returns scheduled items across the calendars the caller can see, filtered by date range, calendar, assignee or linked record.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the records belong to. Find it in the portal under Settings > API.
from / toquery (ISO datetime)Restrict to items overlapping the range.
calendarIdqueryRestrict to one calendar.
assigneeIdqueryRestrict to items a given user is assigned to.
linkedType / linkedIdqueryRestrict to items linked to a record. linkedType is job or event; linkedId is that record’s id.
cURL
curl -X GET \
  "https://chronly.ca/api/scheduling/items?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": [
    {
      "scheduledItemId": "sch_9m2k",
      "calendarId": "cal_7x2k",
      "title": "Roof install - day 1",
      "start": "2026-09-14T15:00:00.000Z",
      "end": "2026-09-14T23:00:00.000Z",
      "status": "scheduled",
      "linkedType": "job",
      "linkedId": "job_71kd"
    }
  ]
}
POST/scheduling/itemsCreate a scheduled itemTry it ↗

Books time on a calendar. Linking the item to a job turns it into a visit on that job and writes the scheduling event to the job activity log.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdquery · requiredIN THE QUERY STRING. Scheduling writes authorize before the body is read, so a companyId in the body is not seen.
calendarIdstring · requiredThe calendar to book on.
titlestring · requiredWhat the booking is.
startISO datetime · requiredStart of the booking.
endISO datetime · requiredEnd of the booking. Must be after start.
timezonestringDefaults to the calendar timezone.
allDaybooleanOccupies the assignee working day rather than a fixed window.
assigneesarrayUser ids to assign. Assignment also grants those users access to a linked job.
linkedType / linkedIdstringAttach the item to a record. linkedType is job or event; linkedId is that record’s id.
cURL
curl -X POST \
  "https://chronly.ca/api/scheduling/items?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "calendarId": "cal_7x2k",
    "title": "Roof install - day 1",
    "start": "2026-09-14T15:00:00.000Z",
    "end": "2026-09-14T23:00:00.000Z",
    "linkedType": "job",
    "linkedId": "job_71kd"
  }'
Response 200
{ "success": true, "data": { "scheduledItemId": "sch_9m2k" } }

Errors

  • 400calendarId, title, start or end is missing, the dates do not parse, or end is not after start.
  • 403The caller cannot edit that calendar, or the item links to a job they do not have editor access to.

Linking to a job requires editor access on that job, which is the greater of the company jobs permission and any per-job assignment.

GET/scheduling/items/{itemId}Retrieve a scheduled itemTry it ↗

Returns one scheduled item.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
itemIdpath · requiredThe scheduledItemId.
companyIdquery · requiredThe company the records belong to. Find it in the portal under Settings > API.
cURL
curl -X GET \
  "https://chronly.ca/api/scheduling/items/{itemId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true, "data": { "scheduledItemId": "sch_9m2k" } }
PUT/scheduling/items/{itemId}Update a scheduled itemTry it ↗

Moves, retitles, reassigns or re-statuses a booking.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
itemIdpath · requiredThe item to update.

Request body

companyIdquery · requiredIN THE QUERY STRING. Scheduling writes authorize before the body is read, so a companyId in the body is not seen.
start / endISO datetimeMove the booking.
statusstringe.g. scheduled or cancelled. Cancelled visits stop occupying availability.One ofscheduledconfirmedin-progresscompletedcancelledno-show
cURL
curl -X PUT \
  "https://chronly.ca/api/scheduling/items/{itemId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "start": "2026-09-15T15:00:00.000Z",
    "end": "2026-09-15T23:00:00.000Z"
  }'
Response 200
{ "success": true, "data": { "scheduledItemId": "sch_9m2k" } }
DELETE/scheduling/items/{itemId}Delete a scheduled itemTry it ↗

Removes the booking. A job-linked visit is logged as unscheduled on the job.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
itemIdpath · requiredThe item to delete.
companyIdquery · requiredThe company the records belong to. Find it in the portal under Settings > API.
cURL
curl -X DELETE \
  "https://chronly.ca/api/scheduling/items/{itemId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }
POST/scheduling/availability/checkCheck availabilityTry it ↗

Answers whether the given users are free across a window, taking working hours and existing non-cancelled bookings into account.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdquery · requiredIN THE QUERY STRING. Scheduling writes authorize before the body is read, so a companyId in the body is not seen.
userIdsarray · requiredThe users to test.
startISO datetime · requiredWindow start.
endISO datetime · requiredWindow end.
excludeItemIdstringIgnore one existing booking, so rescheduling does not collide with itself.
cURL
curl -X POST \
  "https://chronly.ca/api/scheduling/availability/check?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "userIds": [
      "usr_3k9m"
    ],
    "start": "2026-09-14T15:00:00.000Z",
    "end": "2026-09-14T23:00:00.000Z"
  }'
Response 200
{ "success": true, "data": { "usr_3k9m": { "available": false, "conflicts": [ { "scheduledItemId": "sch_9m2k" } ] } } }

Errors

  • 400userIds is not an array, or start or end is missing.
GET/scheduling/assignable-usersList assignable usersTry it ↗

Returns the users who can be assigned to a booking. API users and users who never accepted their invite are excluded, as are users hidden from scheduling.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the records belong to. Find it in the portal under Settings > API.
cURL
curl -X GET \
  "https://chronly.ca/api/scheduling/assignable-users?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": [ { "userId": "usr_3k9m", "email": "[email protected]", "firstName": "Sam" } ]
}
GET/scheduling/availabilityGet an availability scheduleTry it ↗

Returns the working hours for one owner — a user or the company — including date overrides and extended leave. Requesting someone else's schedule needs the manage_all scheduling permission.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the schedule belongs to.
ownerTypequeryWhose schedule to read. Defaults to user.One ofusercompany
ownerIdqueryThe user whose schedule to read. Defaults to the caller. Ignored when ownerType is company.
listqueryPass all to return every schedule in the company instead of one. Requires manage_all.
cURL
curl -X GET \
  "https://chronly.ca/api/scheduling/availability?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": {
    "ownerType": "user",
    "ownerId": "usr_3k9m",
    "timezone": "America/Vancouver",
    "weeklyHours": [ { "weekday": 1, "intervals": [ { "start": "08:00", "end": "16:30" } ] } ],
    "overrides": [],
    "leave": []
  }
}

Errors

  • 403Reading another user's or the company's schedule without manage_all.
PUT/scheduling/availabilitySet an availability scheduleTry it ↗

Creates or replaces the schedule for one owner. This is a REPLACE — weeklyHours, overrides and leave are each overwritten wholesale, and an omitted array is stored as empty.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
companyIdquery · requiredIN THE QUERY STRING. Scheduling writes authorize before the body is read, so a companyId in the body is not seen.

Request body

ownerTypestringWhose schedule to write. Defaults to user.One ofusercompany
ownerIdstringThe user to write. Defaults to the caller. Ignored when ownerType is company.
timezonestringIANA zone the hours are expressed in.
weeklyHoursarrayRecurring hours, as [{ weekday, intervals: [{ start, end }] }]. weekday is 0-6 with 0 = Sunday; start and end are HH:MM, 24-hour.
overridesarrayDate-specific exceptions, as [{ date: "YYYY-MM-DD", available, intervals }]. available: false with no intervals is a day off.
leavearrayContinuous stretches away, as [{ startDate, endDate }]. A null endDate is open-ended.
cURL
curl -X PUT \
  "https://chronly.ca/api/scheduling/availability?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "ownerType": "user",
    "ownerId": "usr_3k9m",
    "timezone": "America/Vancouver",
    "weeklyHours": [
      { "weekday": 1, "intervals": [{ "start": "08:00", "end": "16:30" }] },
      { "weekday": 2, "intervals": [{ "start": "08:00", "end": "16:30" }] }
    ]
  }'
Response 200
{ "success": true, "data": { "ownerType": "user", "ownerId": "usr_3k9m" } }

Errors

  • 403Editing your own schedule without manage_availability, or anyone else's without manage_all.
GET/scheduling/availability/working-intervalsResolve working intervalsTry it ↗

Flattens weekly hours, overrides and leave into the concrete intervals each user actually works over a date range — what you want before deciding when to book.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the users belong to.
fromquery (YYYY-MM-DD) · requiredStart of the range, inclusive.
toquery (YYYY-MM-DD) · requiredEnd of the range, inclusive.
userIdsquery · csv · requiredUsers to resolve. An empty list returns an empty object rather than every user.
cURL
curl -X GET \
  "https://chronly.ca/api/scheduling/availability/working-intervals?companyId=cmp_4k9x2m&from=%3Cfrom%3E&to=%3Cto%3E&userIds=%3CuserIds%3E" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": {
    "usr_3k9m": [ { "date": "2026-09-01", "start": "08:00", "end": "16:30" } ]
  }
}

Errors

  • 400from or to is missing.
PATCH/scheduling/assignable-usersHide or show a user in schedulingTry it ↗

Controls whether a user appears in the assignable-users list. Hiding does not affect their existing bookings. Requires the manage_all scheduling permission.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
companyIdquery · requiredIN THE QUERY STRING, not the body.

Request body

userIdstring · requiredThe user to hide or show.
hiddenboolean · requiredMust be a real boolean — a missing or non-boolean value is rejected with 400.
cURL
curl -X PATCH \
  "https://chronly.ca/api/scheduling/assignable-users?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "userId": "usr_3k9m", "hidden": true }'
Response 200
{ "success": true, "data": { "userId": "usr_3k9m", "hidden": true } }

Errors

  • 400userId is missing, or hidden is not a boolean.
  • 403The caller lacks manage_all.
GET/scheduling/calendar-feedRead the calendar feedTry it ↗

Everything on the calendar between two dates, already merged and flattened for display — scheduled items plus, optionally, jobs. Callers without manage_all see only the calendars they have access to.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the calendars belong to.
fromquery (ISO 8601) · requiredStart of the window.
toquery (ISO 8601) · requiredEnd of the window.
calendarIdsquery · csvRestrict to these calendars.
assigneeIdsquery · csvRestrict to items assigned to these users.
includeJobsquery · booleanPass true to include jobs alongside scheduled items.
cURL
curl -X GET \
  "https://chronly.ca/api/scheduling/calendar-feed?companyId=cmp_4k9x2m&from=%3Cfrom%3E&to=%3Cto%3E" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "data": [
    {
      "id": "sci_4b2x",
      "title": "Roof inspection",
      "start": "2026-09-01T16:00:00.000Z",
      "end": "2026-09-01T18:00:00.000Z",
      "calendarId": "cal_7p1m"
    }
  ]
}

Errors

  • 400from or to is missing.
PUT/scheduling/items/{itemId}/visit-updateLog a visit wrap-upTry it ↗

Records how a job visit went. Deliberately separate from the item PUT: moving a visit needs editor rights on the job, but the person who actually did the work may only be a viewer on the crew, so this route authorizes on assignment instead and does NOT require the view_calendar permission.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
itemIdpath · requiredThe scheduled item. Must be a job visit.
companyIdquery · requiredIN THE QUERY STRING, not the body.

Request body

outcomestringHow the visit went. Anything unrecognised falls back to completed.One ofcompletedpartialblocked
internalNotesstringNotes for your team only. Truncated at 5000 characters.
publicNotesstringNotes the customer can see on a job share. Truncated at 5000 characters.
followUpRequiredbooleanFlags that more work is needed.
followUpNotesstringOnly stored when followUpRequired is true. Truncated at 1000 characters.
jobCompletebooleanAlso marks the parent job complete. The caller must be allowed to complete the job.
cURL
curl -X PUT \
  "https://chronly.ca/api/scheduling/items/{itemId}/visit-update?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "outcome": "partial",
    "publicNotes": "Framing done, roofing to follow.",
    "followUpRequired": true,
    "followUpNotes": "Need the shingle delivery first."
  }'
Response 200
{ "success": true, "data": { "scheduledItemId": "sci_4b2x", "visitUpdate": { "outcome": "partial" } } }

Errors

  • 400The scheduled item is not a job visit.
  • 403The caller can neither edit the job nor is assigned to the visit.

Tasks

Create and manage tasks, their comments, subtasks, labels and file attachments. Tasks require a plan that includes Jobs — Pro, or an enterprise plan with the Jobs feature enabled; calls from an account without it return 403 subscription_required. Note that companyId sits in the QUERY STRING on some of these endpoints and in the BODY on others — each parameter table below is authoritative.

COMPLETED WORK IS HIDDEN BY DEFAULT. A task is finished when its status carries a category of done or cancelled — the category, never the label, so a company that renames a stage or adds its own ("Shipped", "Not doing", "Won't fix") gets the same behaviour. GET /tasks leaves those tasks out unless you pass includeCompleted=1, and it does so even when statusIds names one of them, which means a query aimed at a closed stage comes back empty without that flag. Read the category off the statuses array that every list response carries rather than matching on names.

GET/tasksList tasksTry it ↗

Returns every task the token can see, together with the company's status registry and its people roster — both are needed to render a task and are returned here so a client does not have to make three calls before it can show a row. The first call for a company seeds the six default statuses. Filters are applied server-side.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the records belong to. Required when authenticating with a token, since a token is not bound to one company.
statusIdsqueryComma-separated statusId list. Only tasks in these stages are returned.
includeCompletedquerySet to 1 to also return tasks sitting in a done or cancelled stage. They are LEFT OUT by default, and the exclusion still applies when statusIds names one of those stages — so a query for a cancelled stage ("Not doing", "Won't fix") returns nothing without this. Whether a stage counts as completed comes from its category, not its label.
prioritiesqueryComma-separated priorities. Unknown values are ignored rather than rejected.One ofurgenthighmediumlow
assigneesqueryComma-separated userId list. The literal value unassigned matches tasks with nobody on them and can be combined with real ids.
startDatequery (YYYY-MM-DD)Only tasks due on or after this date. Tasks with no due date are excluded whenever either date bound is supplied.
endDatequery (YYYY-MM-DD)Only tasks due on or before this date.
searchqueryCase-insensitive match against the title and the labels.
cURL
curl -X GET \
  "https://chronly.ca/api/tasks?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "tasks": [
    {
      "taskId": "tsk_9f3k2m1x",
      "reference": "TSK-31",
      "title": "Pricing page overflows on mobile",
      "statusId": "tst_7a1b",
      "priority": "high",
      "assignees": [],
      "watchers": [],
      "labels": ["qa"],
      "dueDate": null,
      "subtasks": [],
      "attachmentCount": 1,
      "completedAt": null,
      "createdBy": "api_4k9x2m",
      "createdAt": "2026-08-27T14:02:11.000Z"
    }
  ],
  "statuses": [
    { "statusId": "tst_7a1b", "key": "todo", "label": "To do", "category": "open", "order": 2 },
    { "statusId": "tst_3f9c", "key": "not_doing", "label": "Not doing", "category": "cancelled", "order": 9 }
  ],
  "people": [
    { "userId": "usr_2c8d", "name": "Ayla Brooks", "email": "[email protected]", "companyRole": "member", "isApiUser": false }
  ]
}

An API user sees only the tasks it created unless it holds the see_all_tasks permission. Grant it in Settings > Users if the integration needs the whole board. An integration that checks for existing work before creating a task should pass includeCompleted=1, or it will not see anything already closed or declined and will file it again.

POST/tasksCreate a taskTry it ↗

Creates one task. Only the title is required — the status defaults to the company's first open stage, and the task number and its TSK- reference are assigned server-side.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to create the record in.
titlestring · requiredWhat needs doing. Trimmed; an empty title is a 400.
descriptionstringLonger detail.
statusIdstringA statusId from GET /tasks/statuses. Defaults to the company's first open stage.
prioritystringDefaults to medium.One ofurgenthighmediumlow
assigneesarray of stringuserIds from GET /tasks/assignable-users. An API user cannot assign itself — it is not on the roster as an assignable person.
watchersarray of stringuserIds who follow the task without owning it.
dueDatestring (YYYY-MM-DD)Stored as a date-only string.
labelsarray of stringFree strings. There is no registry — a label exists because a task carries it.
estimateMinutesnumberEstimated effort in minutes.
subtasksarrayChecklist rows to create with the task. Each entry takes a text field.
documentIdsarray of stringdocumentIds returned by POST /tasks/documents/upload that were uploaded WITHOUT a taskId. They are attached to the new task. Ids already attached elsewhere are ignored.
cURL
curl -X POST \
  "https://chronly.ca/api/tasks" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "title": "Pricing page overflows on mobile",
    "description": "Horizontal scroll at 375px on the tier table.",
    "priority": "high",
    "labels": ["qa", "mobile"],
    "documentIds": ["doc_8f3k2m"]
  }'
Response 201
{
  "success": true,
  "task": {
    "taskId": "tsk_9f3k2m1x",
    "reference": "TSK-31",
    "title": "Pricing page overflows on mobile",
    "statusId": "tst_7a1b",
    "priority": "high",
    "labels": ["qa", "mobile"],
    "createdBy": "api_4k9x2m"
  }
}

Errors

  • 400title is missing or blank.

createdBy comes from the authenticated identity and is ignored if sent.

GET/tasks/{taskId}Get a taskTry it ↗

The task plus everything needed to render it in full: the status registry, the people roster, the comment thread oldest-first, the 50 most recent activity entries, and the attached documents with freshly signed read URLs.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
taskIdpath · requiredThe task to read.
companyIdquery · requiredThe company the task belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/tasks/{taskId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "task": { "taskId": "tsk_9f3k2m1x", "reference": "TSK-31", "title": "Pricing page overflows on mobile" },
  "statuses": [],
  "people": [],
  "comments": [],
  "activity": [],
  "documents": [
    { "documentId": "doc_8f3k2m", "originalFilename": "shot.png", "mimeType": "image/png", "presignedUrl": "https://..." }
  ]
}

Errors

  • 404No task with that id exists in this company.

Document read URLs are signed for one hour.

PUT/tasks/{taskId}Update a taskTry it ↗

Patches only the fields you send; anything omitted is left alone. Moving the task into a status whose category is done stamps completedAt, and moving it back out clears it. Completing a task that carries a recurrence rule creates the next occurrence and returns it as nextOccurrence.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
taskIdpath · requiredThe task to update.

Request body

companyIdstring · requiredThe company the task belongs to.
titlestringTrimmed; sending an empty string is a 400 rather than a clear.
descriptionstringLonger detail.
statusIdstringMust be a status in this company.
prioritystringOne ofurgenthighmediumlow
assigneesarray of stringReplaces the list. Newly added people are emailed; people already on it are not re-notified.
watchersarray of stringReplaces the list.
dueDatestring (YYYY-MM-DD)Send null to clear.
labelsarray of stringReplaces the list.
estimateMinutesnumberEstimated effort in minutes.
subtasksarrayReplaces the whole checklist. Prefer the subtask endpoints for single edits — a full replace carries a stale copy of every row you did not touch.
recurrencestringRepeat rule. Setting one turns the task into a series; send null to end it, which leaves the occurrences already created intact.
recurrenceEndsOnstring (YYYY-MM-DD)Last date the series may produce an occurrence for.
cURL
curl -X PUT \
  "https://chronly.ca/api/tasks/{taskId}" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "statusId": "tst_9d2e",
    "priority": "urgent"
  }'
Response 200
{
  "success": true,
  "task": { "taskId": "tsk_9f3k2m1x", "statusId": "tst_9d2e", "priority": "urgent", "completedAt": null }
}

Errors

  • 400statusId is not a status in this company, or title was sent blank.
  • 404No task with that id exists in this company.

taskId, accountId, companyId and taskNumber are server-owned and ignored if sent in the body.

DELETE/tasks/{taskId}Delete a taskTry it ↗

Really deletes the task, along with its comments and activity trail. Attached documents are soft-deleted. Use the company's cancelled status instead if you want to keep a record of work that was dropped.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
taskIdpath · requiredThe task to delete.
companyIdquery · requiredThe company the task belongs to.
cURL
curl -X DELETE \
  "https://chronly.ca/api/tasks/{taskId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }

Errors

  • 404No task with that id exists in this company.
GET/tasks/{taskId}/activityList task activityTry it ↗

The task's audit trail, newest first, capped at the 50 most recent entries.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
taskIdpath · requiredThe task to read.
companyIdquery · requiredThe company the task belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/tasks/{taskId}/activity?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "activity": [
    {
      "activityType": "task-created",
      "description": "created this task",
      "actor": { "userId": "api_4k9x2m", "name": "QA bot", "type": "user" },
      "createdAt": "2026-08-27T14:02:11.000Z"
    }
  ]
}
GET/tasks/{taskId}/commentsList commentsTry it ↗

The comment thread, oldest first.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
taskIdpath · requiredThe task to read.
companyIdquery · requiredThe company the task belongs to.
cURL
curl -X GET \
  "https://chronly.ca/api/tasks/{taskId}/comments?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "comments": [
    { "commentId": "tcm_3k9x", "body": "Reproduced on iOS Safari.", "authorName": "QA bot", "createdAt": "2026-08-27T14:05:00.000Z" }
  ]
}
POST/tasks/{taskId}/commentsAdd a commentTry it ↗

Appends one comment to the thread. The author name is snapshotted at write time so the thread still reads correctly after someone leaves the company.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
taskIdpath · requiredThe task to comment on.

Request body

companyIdstring · requiredThe company the task belongs to.
bodystring · requiredThe comment text. Trimmed; empty is a 400.
cURL
curl -X POST \
  "https://chronly.ca/api/tasks/{taskId}/comments" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "body": "Reproduced on iOS Safari 17, screenshot attached."
  }'
Response 201
{
  "success": true,
  "comment": { "commentId": "tcm_3k9x", "body": "Reproduced on iOS Safari 17, screenshot attached.", "authorName": "QA bot" }
}

Errors

  • 400body is missing or blank.
  • 404No task with that id exists in this company.
POST/tasks/{taskId}/subtasksAdd a subtaskTry it ↗

Appends one checklist row and returns the whole task. Adding rows one at a time rather than replacing the subtasks array is what stops two callers ticking different rows from overwriting each other.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
taskIdpath · requiredThe task to add to.

Request body

companyIdstring · requiredThe company the task belongs to.
textstring · requiredWhat the checklist row says.
cURL
curl -X POST \
  "https://chronly.ca/api/tasks/{taskId}/subtasks" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "text": "Check the tier table at 375px"
  }'
Response 201
{
  "success": true,
  "task": { "taskId": "tsk_9f3k2m1x", "subtasks": [ { "subtaskId": "sbt_1a2b", "text": "Check the tier table at 375px", "done": false } ] }
}

Errors

  • 400text is missing or blank.
  • 404No task with that id exists in this company.
PUT/tasks/{taskId}/subtasks/{subtaskId}Update a subtaskTry it ↗

Ticks a checklist row off, reopens it, or renames it. Returns the whole task.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
taskIdpath · requiredThe task the subtask belongs to.
subtaskIdpath · requiredThe subtask to update.

Request body

companyIdstring · requiredThe company the task belongs to.
textstringRename the row. Blank is a 400.
donebooleanTicking it stamps completedAt; unticking clears it.
cURL
curl -X PUT \
  "https://chronly.ca/api/tasks/{taskId}/subtasks/{subtaskId}" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "done": true
  }'
Response 200
{
  "success": true,
  "task": { "taskId": "tsk_9f3k2m1x", "subtasks": [ { "subtaskId": "sbt_1a2b", "done": true } ] }
}

Errors

  • 400text was sent blank.
  • 404The task or the subtask does not exist.
DELETE/tasks/{taskId}/subtasks/{subtaskId}Delete a subtaskTry it ↗

Removes one checklist row and returns the whole task.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
taskIdpath · requiredThe task the subtask belongs to.
subtaskIdpath · requiredThe subtask to remove.
companyIdquery · requiredThe company the task belongs to.
cURL
curl -X DELETE \
  "https://chronly.ca/api/tasks/{taskId}/subtasks/{subtaskId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true, "task": { "taskId": "tsk_9f3k2m1x", "subtasks": [] } }

Errors

  • 404The task or the subtask does not exist.
GET/tasks/statusesList statusesTry it ↗

The company's status registry, in display order. The first call for a company seeds the six defaults (backlog, to do, in progress, in review, done, cancelled), so this never returns an empty list.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the statuses belong to.
cURL
curl -X GET \
  "https://chronly.ca/api/tasks/statuses?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "statuses": [
    { "statusId": "tst_7a1b", "key": "todo", "label": "To do", "category": "open", "bg": "rgba(82,82,91,0.10)", "fg": "#3f3f46", "order": 2 }
  ]
}

category is the load-bearing field, not label. Grouping, the open/closed counts and whether a task counts as finished all key off it, so renaming a stage never changes how it behaves.

POST/tasks/statusesCreate a statusTry it ↗

Adds a stage to the registry. The slug is derived from the label and de-duplicated, and the new stage is appended to the end of the order.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company to add the stage to.
labelstring · requiredWhat the stage is called. Trimmed; empty is a 400.
categorystringDefaults to open. Decides whether tasks in this stage count as outstanding, finished or dropped.One ofopendonecancelled
bgstringPill background colour. Any CSS colour.
fgstringPill text colour. Any CSS colour.
cURL
curl -X POST \
  "https://chronly.ca/api/tasks/statuses" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "label": "Awaiting QA",
    "category": "open"
  }'
Response 201
{
  "success": true,
  "status": { "statusId": "tst_5c7d", "key": "awaiting_qa", "label": "Awaiting QA", "category": "open", "order": 7 }
}

Errors

  • 400label is blank, or category is not one of the three allowed values.
PUT/tasks/statusesReorder statusesTry it ↗

Takes the full ordered list of statusIds and rewrites every order value to match. Send the whole list, not just the stage that moved.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company the statuses belong to.
statusIdsarray of string · requiredEvery statusId in the order you want them. Ids belonging to another company are silently skipped.
cURL
curl -X PUT \
  "https://chronly.ca/api/tasks/statuses" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "statusIds": ["tst_1a", "tst_7a1b", "tst_9d2e"]
  }'
Response 200
{ "success": true, "statuses": [] }

Errors

  • 400statusIds is missing or empty.
PUT/tasks/statuses/{statusId}Rename or restyle a statusTry it ↗

Changes the label or the pill colours. key and category are deliberately not editable — flipping a stage's category would move work between open and done without anybody touching a task. There is no DELETE, because removing a stage that tasks sit in would strand them.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json
statusIdpath · requiredThe stage to edit.

Request body

companyIdstring · requiredThe company the stage belongs to.
labelstringThe new name. Blank is a 400.
bgstringPill background colour.
fgstringPill text colour.
cURL
curl -X PUT \
  "https://chronly.ca/api/tasks/statuses/{statusId}" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "label": "With QA"
  }'
Response 200
{
  "success": true,
  "status": { "statusId": "tst_5c7d", "label": "With QA", "category": "open" }
}

Errors

  • 400label was sent blank.
  • 404No status with that id exists in this company.
GET/tasks/labelsList labelsTry it ↗

Every label in use across the company's tasks, alphabetically, with how many tasks carry it. Labels have no registry — the list is derived from the tasks themselves, so a label with no tasks simply stops existing.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the tasks belong to.
cURL
curl -X GET \
  "https://chronly.ca/api/tasks/labels?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "labels": [
    { "name": "mobile", "count": 4 },
    { "name": "qa", "count": 12 }
  ]
}
PUT/tasks/labelsRename or merge labelsTry it ↗

Moves every task off the labels in from and onto to. A rename is a merge with a single source, so both are this one operation. A task that already carried the target does not end up with it twice.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company the tasks belong to.
tostring · requiredThe label to end up with.
fromarray of string · requiredThe labels to move off. A bare string is accepted for a single rename. Any entry equal to `to` is ignored.
cURL
curl -X PUT \
  "https://chronly.ca/api/tasks/labels" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "to": "qa",
    "from": ["QA", "quality-assurance"]
  }'
Response 200
{ "success": true, "matched": 12, "modified": 12, "to": "qa" }

Errors

  • 400to is blank, or from is empty once entries equal to `to` are dropped.
  • 403The caller does not hold the tasks manage_labels permission.

Needs the manage_labels permission on top of write access, because it rewrites other people's tasks.

DELETE/tasks/labelsDelete a labelTry it ↗

Takes one label off every task that carries it.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company the tasks belong to.
labelstring · requiredThe label to remove everywhere.
cURL
curl -X DELETE \
  "https://chronly.ca/api/tasks/labels" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "label": "obsolete"
  }'
Response 200
{ "success": true, "modified": 3 }

Errors

  • 400label is missing or blank.
  • 403The caller does not hold the tasks manage_labels permission.

This DELETE reads a JSON BODY, unlike every other DELETE in this API. companyId goes in the body here, not the query string.

GET/tasks/preferencesGet task preferencesTry it ↗

The authenticated identity's own Tasks notification settings. Always scoped to the caller — there is no way to read another user's. A user who has never saved any gets the defaults rather than a 404.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company the preferences apply to.
cURL
curl -X GET \
  "https://chronly.ca/api/tasks/preferences?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "preferences": { "remindOnAssign": true, "dailyDigest": false, "overdueNudge": true, "showSummary": true }
}
PUT/tasks/preferencesUpdate task preferencesTry it ↗

Upserts the caller's own settings, so the first save creates the record. Only the four fields below can be changed; anything else in the body is ignored.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
Content-Typeheader · requiredapplication/json

Request body

companyIdstring · requiredThe company the preferences apply to.
remindOnAssignbooleanEmail when a task is assigned to you.
dailyDigestbooleanOne daily summary of your tasks.
overdueNudgebooleanEmail when one of your tasks goes overdue.
showSummarybooleanShow the summary tiles above the board.
cURL
curl -X PUT \
  "https://chronly.ca/api/tasks/preferences" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "cmp_4k9x2m",
    "dailyDigest": true
  }'
Response 200
{
  "success": true,
  "preferences": { "remindOnAssign": true, "dailyDigest": true, "overdueNudge": true, "showSummary": true }
}

Errors

  • 400None of the four editable fields were supplied.
GET/tasks/assignable-usersList the people rosterTry it ↗

Who a task can be assigned to or watched by. The same array is embedded in GET /tasks; this exists for a picker opened on its own.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company whose roster to read.
cURL
curl -X GET \
  "https://chronly.ca/api/tasks/assignable-users?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{
  "success": true,
  "people": [
    { "userId": "usr_2c8d", "name": "Ayla Brooks", "email": "[email protected]", "companyRole": "member", "isApiUser": false },
    { "userId": "api_4k9x2m", "name": "QA bot", "email": null, "companyRole": "api", "isApiUser": true }
  ]
}

Entries flagged isApiUser are included so a task created over the API resolves to a name, but they can never be an assignee or a watcher. Filter them out before offering the list as a choice.

POST/tasks/documents/uploadUpload a file to a taskTry it ↗

Uploads one file as multipart/form-data and records it. taskId is OPTIONAL: omit it to stage the file, then pass the returned documentId in documentIds on POST /tasks. Images are downscaled and HEIC is transcoded to JPEG on the way in, so the returned filename, mimeType and fileSize describe the stored object rather than what you sent. Maximum 25MB; images, HEIC, PDF, Word, Excel, CSV and ZIP are accepted.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
companyIdquery · requiredThe company to store the file under. It goes in the QUERY STRING because this route reads a multipart body.
fileform-data · requiredThe file itself. The field must be named exactly file.
taskIdform-dataAttach straight to this task. Omit to stage the file unattached.
cURL
curl -X POST \
  "https://chronly.ca/api/tasks/documents/upload?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 201
{
  "success": true,
  "document": {
    "documentId": "doc_8f3k2m",
    "originalFilename": "shot.png",
    "mimeType": "image/png",
    "fileSize": 148223,
    "attachedTo": { "type": "task", "id": "tsk_9f3k2m1x" },
    "presignedUrl": "https://..."
  }
}

Errors

  • 400No file field was sent, the file is over 25MB, or its type is not allowed.
  • 404A taskId was supplied that does not exist in this company.

Send no Content-Type header of your own — your HTTP client must set it, because multipart needs the boundary it generates. Read URLs are signed for one hour.

DELETE/tasks/documents/{documentId}Delete a task fileTry it ↗

Soft-deletes the record so the file stops being findable. Works on a file attached to a task and on a staged file that was never attached; a file belonging to another module is a 403.

Parameters

Authorizationheader · requiredBearer chronly_api_… - the token issued to the API user.
documentIdpath · requiredThe document to remove.
companyIdquery · requiredThe company the document belongs to.
cURL
curl -X DELETE \
  "https://chronly.ca/api/tasks/documents/{documentId}?companyId=cmp_4k9x2m" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx"
Response 200
{ "success": true }

Errors

  • 403The document is attached to something that is not a task.
  • 404No document with that id exists in this company.

Not part of the token API

These endpoints exist and are listed for completeness, but they are not reachable with an API token.

Public payment and acceptance pages

Reached by your customer from an emailed link, so they take no token. They are listed for completeness; do not call them server-to-server.

  • POST /checkout/payPays a hosted checkout.
  • POST /checkout/abandon-webhookFires the abandoned-cart webhook when a customer leaves without paying.
  • POST /invoices/{invoiceId}/payPays an invoice from its public link.
  • POST /invoices/{invoiceId}/receiptEmails a payment receipt.
  • POST /quotes/{quoteId}/accept-with-depositAccepts a quote and takes its deposit in one step.
  • POST /quotes/{quoteId}/receiptEmails a deposit receipt.
  • POST /discounts/validate-publicChecks a discount code from a storefront, without revealing the rest of the discount.

Agent runs (internal preview)

The Claude Code agent-run pipeline. Gated on an account flag plus deployment-level GitHub configuration, so an unflagged account gets a 404 from every one of them. The brief and callback routes are authenticated by a per-run token rather than an API key.

  • GET /tasks/agent-runsLists agent runs.
  • POST /tasks/agent-runsStarts an agent run.
  • POST /tasks/agent-runs/{agentRunId}/answersAnswers a question the run asked mid-flight.
  • POST /tasks/agent-runs/{agentRunId}/approveApproves the plan a run proposed.
  • POST /tasks/agent-runs/{agentRunId}/reviseSends a proposed plan back with comments and queues another planning run carrying both.
  • POST /tasks/agent-runs/{agentRunId}/cancelCancels a run.
  • GET /tasks/agent-runs/{agentRunId}/briefThe runner-facing brief. Authenticated by the run's own token.
  • POST /tasks/agent-runs/{agentRunId}/callbackThe runner reporting back. Authenticated by the run's own token.
  • GET /tasks/agent/repo-treeReads the cached repository tree.
  • POST /tasks/agent/repo-treeRefreshes the cached repository tree.
  • POST /tasks/{taskId}/planAsks the agent to plan a task.
  • PUT /tasks/{taskId}/agentEdits the agent brief on a task.
  • POST /tasks/{taskId}/agent/resetDiscards what the agent produced (its questions, answers and clarify count) and cancels any active run, leaving the brief you wrote intact.

Portal-session only

Guarded by a separate session gate that a bearer token cannot satisfy. There is no token equivalent today.

  • POST /invoices/export/sageRuns a Sage 50 .IMP export.
  • POST /invoices/export/sage/previewPreviews what the export would contain.
  • POST /invoices/export/sage/confirmMarks the previewed invoices exported.