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.

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.
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.
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.
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.
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://chronly.ca/aurora-contracting/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. Creation guards against duplicate email addresses within a company.

GET/buyersList customersTry it ↗

Returns the customers for the company as a plain array.

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": "byr_8f3k",
    "firstName": "Dana",
    "lastName": "Whitfield",
    "companyName": "Northline Roofing",
    "emailAddress": "[email protected]"
  }
]
POST/buyersCreate a customerTry it ↗

Creates a customer with a 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.
emailAddressstring · requiredLower-cased and checked against existing customers in the company.
firstNamestring · requiredCustomer's first name.
lastNamestring · requiredCustomer's last name.
companyNamestringBusiness name.
forceCreatebooleanSkip the duplicate check and create anyway.
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",
    "companyName": "Northline Roofing",
    "emailAddress": "[email protected]"
  }'
Response 200
{
  "buyerId": "byr_8f3k",
  "emailAddress": "[email protected]",
  "companyId": "cmp_4k9x2m"
}

Errors

  • 409error is DUPLICATE_CUSTOMER and existingBuyer holds the record already on file.
GET/buyers/{buyerId}Retrieve a customerTry it ↗

Returns one customer record.

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": "byr_8f3k", "companyName": "Northline Roofing" }
PUT/buyers/{buyerId}Update a customerTry it ↗

Updates contact details, addresses and notes.

Parameters

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

Request body

emailAddressstringNew email address.
phoneNumberstringNew phone number.
cURL
curl -X PUT \
  "https://chronly.ca/api/buyers/{buyerId}" \
  -H "Authorization: Bearer chronly_api_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "phoneNumber": "+1 250 555 0142" }'
Response 200
{ "buyerId": "byr_8f3k", "phoneNumber": "+1 250 555 0142" }
DELETE/buyers/{buyerId}Delete a customerTry it ↗

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 }

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.
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.
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.

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.

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.