Mackinac SDK
A shared TypeScript client for the Mackinac API.
Generated from the Mackinac OpenAPI specification, this SDK provides strongly typed models and a unified fetch-based client for web and mobile applications. It ensures consistent API usage, reduces duplication, and keeps all consumers aligned with the latest API contract.
Installation
npm install @cycomdatasystems/mackinac-sdk
# or
yarn add @cycomdatasystems/mackinac-sdk
# or
pnpm add @cycomdatasystems/mackinac-sdk
Usage
Configure the SDK once at application startup
import { configureMackinac } from '@cycomdatasystems/mackinac-sdk';
configureMackinac({
baseUrl: process.env.MACKINAC_API_URL!,
});
For applications that need to attach authorization headers or other request-specific headers to SDK calls, you can provide a headersProvider function:
import { configureMackinac } from '@cycomdatasystems/mackinac-sdk';
configureMackinac({
baseUrl: process.env.MACKINAC_API_URL!,
headersProvider: () => ({
'Authorization': `Bearer ${getToken()}`,
}),
});
The headersProvider can also be async to support token retrieval from async sources:
configureMackinac({
baseUrl: process.env.MACKINAC_API_URL!,
headersProvider: async () => {
const token = await getAccessToken();
return {
'Authorization': `Bearer ${token}`,
};
},
});
Authentication
Using Headers Provider (Recommended)
The SDK provides a headersProvider function for request-scoped authentication. This is the recommended approach for applications that need to attach Authorization headers or other dynamic headers to each SDK call.
Benefits:
- Centralized header management for all SDK calls
- Support for async token retrieval
- Automatically merges with per-call headers
- Per-request invocation prevents token leakage across requests
Example: Next.js Route Handler with Auth0
// app/api/finance/payments/route.ts
import { getSession } from '@auth0/nextjs-auth0';
import { configureMackinac } from '@cycomdatasystems/mackinac-sdk';
import { getApiV1MattersMatterIdFinancePayments } from '@cycomdatasystems/mackinac-sdk';
export async function GET(request: Request) {
const session = await getSession();
// Configure SDK with token provider for this request
configureMackinac({
baseUrl: process.env.MACKINAC_API_URL!,
headersProvider: () => ({
'Authorization': `Bearer ${session.accessToken}`,
}),
});
// All SDK calls now include Authorization header
const payments = await getApiV1MattersMatterIdFinancePayments(123);
return Response.json(payments);
}
Or, if you need async token retrieval:
// src/server/mackinac/config.ts
import { configureMackinac } from '@cycomdatasystems/mackinac-sdk';
export function configureMackinacForRequest(getToken: () => Promise<string>) {
configureMackinac({
baseUrl: process.env.MACKINAC_API_URL!,
headersProvider: async () => {
const token = await getToken();
return {
'Authorization': `Bearer ${token}`,
};
},
});
}
// app/api/finance/payments/route.ts
import { configureMackinacForRequest } from '@/server/mackinac/config';
import { getApiV1MattersMatterIdFinancePayments } from '@cycomdatasystems/mackinac-sdk';
export async function GET(request: Request) {
// Configure SDK with async token provider for this request
configureMackinacForRequest(async () => {
// Simulate async token retrieval
return await fetchTokenFromAuthService();
});
// All SDK calls now include Authorization header
const payments = await getApiV1MattersMatterIdFinancePayments(123);
return Response.json(payments);
}
Example: Synchronous Headers Provider
import { configureMackinac } from '@cycomdatasystems/mackinac-sdk';
configureMackinac({
baseUrl: process.env.MACKINAC_API_URL!,
headersProvider: () => ({
'Authorization': `Bearer ${process.env.API_TOKEN}`,
}),
});
Header Merge Priority
Headers are merged in the following order (later overrides earlier):
- Default headers (
Content-Type: application/json) - Headers from
headersProvider() - Per-call headers passed to individual endpoint functions
// Example of per-call override
const payments = await getApiV1MattersMatterIdFinancePayments(
123,
{
headers: {
'X-Request-Id': 'custom-id', // Merges with provider headers
'Authorization': 'Bearer override-token', // Overrides provider's Authorization
}
}
);
Middleware-Based Authentication (Legacy)
This SDK does not handle authentication internally.
If your application uses Auth0 (or another IdP), and access tokens are added to outbound requests by middleware or framework-level fetch interception, you should not supply tokens to this SDK directly.
The SDK simply calls fetch(). Any middleware or runtime environment that injects the Authorization header will continue to work as expected.
Example
import { getApiV1MattersMyOpen } from '@cycomdatasystems/mackinac-sdk';
// Fetch "My Open Matters" for the current user
const matters = await getApiV1MattersMyOpen({ moduleCode: 'Claims' });
console.log('Open matters:', matters);
All generated functions return response bodies directly (not wrapped objects), following the configured Orval output.
WRP Matter auto-assignee (since 13.9.0)
NG User Admins can configure an optional auto-assignee for Matters created
by WRP in the current tenant. These endpoints require users:update and an
admitted, enabled NG caller whose UserAccount supplies the linked StaffId.
Configure the SDK and authentication headers
before calling them; use the application's current tenant authentication context.
There is no tenant ID in the URL or request body, and the SDK does not select a
tenant independently of the authenticated request.
Mackinac resolves NG's authenticated org_id to the tenant and checks the
namespaced Entra tid against that tenant's GraphTenantID. Do not send X-Org-Id
or another custom tenant header. WRP's existing M2M flow resolves its tenant from
azp; these SDK calls do not change that flow.
NG caller linkage uses the admitted UserAccount and signed Entra oid, not a
token StaffId or Auth0 subject. Eligible users and linked Staff must share the
resolved integer TenantID; legacy STAFF.ORG_ID is not the tenant key.
import {
getWrpMatterAutoAssignee,
setWrpMatterAutoAssignee,
getEligibleWrpMatterAutoAssignees,
} from '@cycomdatasystems/mackinac-sdk';
const current = await getWrpMatterAutoAssignee();
const choices = await getEligibleWrpMatterAutoAssignees();
// After the administrator selects a choice, save its userId, not its staffId:
const saved = await setWrpMatterAutoAssignee({ userId: 123 });
const disabled = await setWrpMatterAutoAssignee({ userId: null });
getWrpMatterAutoAssignee(options?: RequestInit)andsetWrpMatterAutoAssignee(body: SetWrpMatterAutoAssigneeDto, options?: RequestInit)returnPromise<WrpMatterAutoAssigneeDto>:{ userId: number | null, staffId: string | null }. Both fields are null when disabled. The PUT body is{ userId: number | null }; send null explicitly to disable, not an omitted field.getEligibleWrpMatterAutoAssignees(options?: RequestInit)returnsPromise<WrpMatterAutoAssigneeUserDto[]>: a bare array of{ userId: number, staffId: string, displayName: string }, or[]when no users qualify, ordered by userId and limited to the caller's organization. It requires neitherusers:readnor Auth0 access.displayNamecomes from trimmed Staff.Name, falling back to StaffId; there is no cursor or wrapper.- Mackinac validates that the target is an Active, unblocked NG user with a bound
EntraOid and linked active Staff in the current tenant. The StaffId must be
nonblank and at most seven characters; no AS/module flag is required.
Invalid selections reject with
Mackinac 400:followed by ValidationProblemDetails witherrors.userId. Authentication, authorization, missing organization context, and storage failures reject withMackinac 401:,Mackinac 403:,Mackinac 503:, andMackinac 500:respectively, using the SDK's existingErrorbehavior rather than typed error return values. JWT challenges and existing policy denials may have no response body; tenant middleware can also reject a request before these endpoints execute. - The selected user's StaffId becomes Primary Staff only for WRP
assignment-matterandcreate-as-matteractions. No WRP submission payload change is needed. The setting is tenant-scoped, not shared across tenants and not per user or per form.
Attachment folders (since 13.15.0)
A matter's attachment folders live in SQL (MATTERFOLDER), not as SharePoint Embedded paths. listMatterAttachments is the single read for the Attachments tab: it returns the selected attachments plus the matter's whole flat folder list, so the Table and Tree views build from one response. Only SPE-backed rows (with a drive item) are listed or counted.
import {
listMatterAttachments, createAttachmentFolder, patchAttachmentFolder,
deleteAttachmentFolder, moveMatterAttachments, patchMatterAttachment,
} from '@cycomdatasystems/mackinac-sdk';
// view: 'all' (default) | 'unfiled' | 'recent'; folderId cannot be combined with a view other than 'all'.
const { items, folders, unfiledCount, totalCount } = await listMatterAttachments(matterId, { folderId: 42, q: 'brief' });
const folder = await createAttachmentFolder(matterId, { name: 'Pleadings', parentId: null });
await patchAttachmentFolder(folder.id, [{ op: 'replace', path: '/name', value: 'Pleadings 2026' }]);
await moveMatterAttachments(matterId, { attachmentIds: [101, 102], folderIds: [], targetFolderId: folder.id });
await patchMatterAttachment(matterId, 101, [{ op: 'replace', path: '/category', value: 'PLD' }], { moveToCategoryFolder: true });
const { deletedFolderCount, movedAttachmentCount } = await deleteAttachmentFolder(folder.id); // contents move to Unfiled
listAttachmentFolders(matterId)andgetAttachmentFolder(folderId)returnAttachmentFolderDtos. The list is flat: build the tree fromparentId(null = a root folder).totalAttachmentCountincludes subfolders;categoryCodeis set on category folders.patchAttachmentFolderandpatchMatterAttachmenttake an RFC 6902 JSON Patch array, which the SDK sends asapplication/json-patch+json, and resolve toundefined(204). On a folder,replace /namerenames andreplace /parentIdmoves (null orremove= the matter root). On an attachment,replaceorremoveapplies to/category(a DOCCAT code) and/description.moveMatterAttachmentsis all or nothing (at most 1000 ids).targetFolderId: nullmoves attachments to Unfiled and folders to the matter root.- Folder names are trimmed and unique among siblings, case-insensitively; a clash rejects with
Mackinac 409:(attachment-folder-name-conflict). Nesting deeper than 6 levels, or moving a folder into itself or a descendant, rejects withMackinac 400:. - Document register (
postApiV1MattersMatterIdDocumentsItemsItemIdRegister) acceptsfolderId, orfileInCategoryFolder: truewith acategory(that category's root folder, created if missing). Upload to the container root;DocumentRegistrationDto.folderIdreports where the document was filed.
Billing (since 13.5.0)
The Billing reference-data endpoints are named *Billing*: listBillingPeriods, getBillingPeriod, createBillingPeriod, patchBillingPeriod, deleteBillingPeriod, the same five operations for ...BillingRevenueCode(s), and getBillingProfile / patchBillingProfile. The PATCH operations take an RFC 6902 JSON Patch array, which the SDK sends as application/json-patch+json:
import { listBillingPeriods, patchBillingProfile } from '@cycomdatasystems/mackinac-sdk';
const periods = await listBillingPeriods({ page: 1, pageSize: 50, sortBy: 'beginDate', sortDirection: 'desc' });
const profile = await patchBillingProfile([
{ op: 'replace', path: '/billTo', value: 'Department' }, // BillingBillTo: 'Account' | 'Department'
{ op: 'replace', path: '/billGeneralExpenses', value: true },
]);
deleteBillingPeriod and deleteBillingRevenueCode reject with an Error whose message starts Mackinac 409: (the ProblemDetails body follows) when invoices or receipts still reference the item. Revenue codes are code-table rows (code, description, isDefault, isInactive), not rate classes.
Invoices, receipts and receivables (since 13.7.0)
The read model is listBillingInvoices, getBillingInvoice, listBillingInvoiceTransactions, listBillingReceipts and listBillingReceivables. Each search takes one params object, and array params are sent as repeated keys:
import {
listBillingInvoices, getBillingInvoice, listBillingInvoiceTransactions,
listBillingReceipts, listBillingReceivables,
} from '@cycomdatasystems/mackinac-sdk';
const page = await listBillingInvoices({
statuses: ['Pending', 'Billed'], // BillingInvoiceStatus; any of them
accountIds: [101], departmentIds: [202], // unioned
billedDateFrom: '2026-01-01', billedDateTo: '2026-01-31', // inclusive whole days
page: 1, pageSize: 50, sortBy: 'invoiceDate', sortDirection: 'desc',
});
const invoice = await getBillingInvoice(page.hits[0].id);
const lines = await listBillingInvoiceTransactions(invoice.id, { pageSize: 100 });
const receipts = await listBillingReceipts({ revenueCodes: ['FEE'], checkNumber: '10' });
const outstanding = await listBillingReceivables({ sortBy: 'balance', sortDirection: 'desc' });
statusisPending,Billed,Paid,Cancelled, orUnknown.Unknownappears only in responses: it covers legacy or NULL values, and passing it as a filter isMackinac 400:.transactionTypeis aBillingTransactionType.invoiceNumber,fileNumberandcheckNumberare literal prefix matches.- A date before 1753-01-01, or a
...Tothat is earlier than its...From, isMackinac 400:. getBillingInvoiceandlistBillingInvoiceTransactionsreject withMackinac 404:for an invoice you may not see. They never return an empty page.InvoiceDtocarries the components of the total:amount = attorneyAmount + staffAmount + expenseAmount + creditCardExpenseAmount + settlementAmount + adjustmentAmount - collectionAmount.- Receivables are Billed, unpaid invoices with a positive amount. Each one has a
balance.
Billing runs (since 13.16.0)
A billing run is a background job. createBillingRun queues the run and resolves with a BillingRunDto whose status is Queued (HTTP 202). Poll getBillingRun until the status is Succeeded, Failed or Cancelled, then list the invoices it produced with listBillingRunInvoices. previewBillingRun is synchronous and changes nothing.
import {
previewBillingRun, createBillingRun, getBillingRun, listBillingRunInvoices,
cancelBillingRun, listBillingRuns,
} from '@cycomdatasystems/mackinac-sdk';
import type { BillingRunCriteriaDto, BillingRunDto } from '@cycomdatasystems/mackinac-sdk';
const criteria: BillingRunCriteriaDto = { periodId: 42 }; // fromDate / toDate default to the period's dates
const preview = await previewBillingRun(criteria);
// preview.billTos: the accounts (or departments, per preview.billTo) that would get an invoice
// preview.pendingInvoiceCount: Pending invoices the run would delete and regenerate
let run: BillingRunDto = await createBillingRun(criteria);
while (run.status === 'Queued' || run.status === 'Running') { // BillingRunStatus
await new Promise((resolve) => setTimeout(resolve, 3000));
run = await getBillingRun(run.id); // progress: run.billTosProcessed of run.billToCount
}
if (run.status === 'Failed') {
console.error(run.errorDetail);
}
const invoices = await listBillingRunInvoices(run.id, { pageSize: 100 }); // InvoiceSummaryDto rows
- Filters choose what gets billed; they don't limit the work billed.
departmentIds,accountIdsandfileNumberselect which accounts (or departments) are billed. Each selected one is then billed for all of its unbilled work in the date range. - The run ANDs
departmentIdsandaccountIds. The invoice and receipt searches union them. Label both differences in the UI. - One run per tenant at a time.
createBillingRunrejects withMackinac 409:while another run isQueuedorRunning. The ProblemDetails body that follows carries the active run's id inactiveRunId. To find the active run, calllistBillingRuns({ statuses: ['Queued', 'Running'] }). - Cancelling.
cancelBillingRunresolves with the run (HTTP 202).- A
Queuedrun isCancelledat once. - A
Runningrun comes back withcancelRequested: trueand stops before its next account or department. Invoices it already produced stay Pending and stay listed under the run. - Once the run has ended, the call is
Mackinac 409:.
- A
- Reruns are safe. Re-running a period replaces only its Pending invoices. Billed, Paid and Cancelled invoices are never touched.
Invoice lifecycle and receipts (since 13.16.0)
Manual invoices, invoice lines, status changes ("Finalize" is Billed) and receipts. Every mutation needs the billing:write permission and a caller linked to a staff record (Mackinac 403: otherwise).
import {
createBillingInvoice, createBillingInvoiceTransaction, patchBillingInvoiceTransaction,
deleteBillingInvoiceTransaction, deleteBillingInvoice, updateBillingInvoiceStatus,
updateBillingInvoiceStatuses, createBillingReceipt, getBillingReceipt, patchBillingReceipt,
deleteBillingReceipt,
} from '@cycomdatasystems/mackinac-sdk';
// A manual invoice: Pending, numbered by the server, with one adjustment line for `amount`.
const invoice = await createBillingInvoice({
invoiceDate: '2026-11-05', periodId: 42, billToId: 7, amount: 150, description: 'Manual fee',
});
// Lines can change only while the invoice is Pending; totals are recomputed by the server.
const line = await createBillingInvoiceTransaction(invoice.id, { transactionDate: '2026-11-05', amount: 25 });
await patchBillingInvoiceTransaction(invoice.id, line.id, [{ op: 'replace', path: '/amount', value: 30 }]);
await deleteBillingInvoiceTransaction(invoice.id, line.id);
// Finalize: marks the invoice's time, time adjustments and payments billed.
const billed = await updateBillingInvoiceStatus(invoice.id, { status: 'Billed' });
// Record a payment. paidAmount becomes the sum of the receipts; the invoice is Paid once they cover it.
const receipt = await createBillingReceipt({ invoiceId: billed.id, receiptDate: '2026-11-20', amount: 180 });
await patchBillingReceipt(receipt.id, [{ op: 'replace', path: '/checkNumber', value: '1001' }]);
await getBillingReceipt(receipt.id);
await deleteBillingReceipt(receipt.id); // the invoice goes back to Billed
// "Change Status (All)": a partial-success summary, never an all-or-nothing error.
const result = await updateBillingInvoiceStatuses({ invoiceIds: [101, 102, 103], status: 'Cancelled' });
for (const failure of result.failures) {
console.warn(failure.invoiceId, failure.reason, failure.detail); // reason: 'NotFound' | 'Conflict'
}
await deleteBillingInvoice(invoice.id); // only a Pending invoice without receipts
- Payment state is derived, never set.
paidAmountis the sum of the invoice's receipts. A Billed or Paid invoice isPaid(isPaid: true) exactly when its receipts coveramount; an overpayment counts as paid. Editing a receipt down or deleting one moves the invoice back toBilled. - Status rules (
Mackinac 409:when broken, nothing changes):- an invoice with receipts can't move to
PendingorCancelled; Paidneeds a Billed or Paid invoice that its receipts cover (record payments withcreateBillingReceipt);Pendingun-bills the invoice's time, adjustments and payments, the exact inverse ofBilled.Unknownis part ofBillingInvoiceStatusfor responses only; sending it isMackinac 400:.
- an invoice with receipts can't move to
- Pending only. Only a Pending invoice can be deleted (and only without receipts) or have lines added, edited or deleted. Manual lines are always the
GeneralExpensetype (code 6). - Receipts go only on a Billed or Paid invoice. The payer is the invoice's bill-to; the invoice and payer can't be patched.
amountmust not be zero, andrevenueCodemust be a revenue code (listBillingRevenueCodes).receiptTypeis a raw number with no known value set. - ⚠️ While a billing run is queued or running, invoice create/delete, line changes and both status calls reject with
Mackinac 409:and the ProblemDetails carriesactiveBillingRunId. Receipts are not affected. Show the active run (getBillingRun) and retry after it ends. - Bulk status.
invoiceIdsholds 1 to 500 distinct ids. Invisible ids come back asNotFoundand ineligible ones asConflict; neither is touched, and the rest are updated together. AMackinac 409:means the batch was refused as a whole (an active run, or an invoice that became ineligible mid-request); nothing was updated, so retry. - Patch bodies use the generic
Operation[], likepatchBillingPeriod. Line paths:/transactionDate,/amount,/hours. Receipt paths:/receiptDate,/amount,/checkNumber,/revenueCode,/receiptType,/notes. Any other path isMackinac 400:. - Another tenant's invoice, line or receipt is
Mackinac 404:, never 403.
Contract Management (since 13.6.0)
The Contract Management (module CM) reads are searchContracts / getContractSearchHits (search and paging, the same MatterSearchHitDtoPaginatedResultDto as searchMatters), getContract and listContractOrders:
import { searchContracts, getContractSearchHits, getContract, listContractOrders } from '@cycomdatasystems/mackinac-sdk';
const first = await searchContracts({ searchCriteria: { contType: 'CO', title: 'Paving*' }, pageSize: 25 });
const next = first.hasMoreHits ? await getContractSearchHits(2, { pageSize: 25 }) : undefined;
const contract = await getContract(1234);
const step = contract.currentApprovalStep?.description; // currentApprovalStep and master may be null
const { orders, contractsTotal, purchaseOrdersTotal } = await listContractOrders(1234);
- The search requires Contract Management access (V16) and rejects with
Mackinac 403:without it.titleandnumberare prefix matches (*is a wildcard), limited to ASCII and Latin-1 characters; anything else isMackinac 400:. getContract/listContractOrdersreject withMackinac 404:for a matter you may not see, that is not module CM, or has no contract record. They reject withMackinac 409:when the matter has more than one contract record; the body is aContractProblemDetailswitherrorCode: 'DuplicateContractRecord'.- In
orders, change orders (contType: 'CH') carrynullfordepartment,account,expiration,authAmount,totalPaidandopenBalance.contractsTotal/purchaseOrdersTotalcover every direct child, including ones you may not see.
Certificates and approval steps (since 13.16.0)
A contract's insurance certificates and its approval steps, plus the tenant's approval profiles (read-only):
import {
listContractCertificates, createContractCertificate, patchContractCertificate, deleteContractCertificate,
listContractApprovalSteps, createContractApprovalStep, patchContractApprovalStep, deleteContractApprovalStep,
importContractApprovalProfile, listApprovalProfiles,
} from '@cycomdatasystems/mackinac-sdk';
const certificates = await listContractCertificates(1234); // latest expiration first, none last
const cert = await createContractCertificate(1234, { type: 'GL', number: 'N-1', expirationDate: '2027-01-01' });
await patchContractCertificate(1234, cert.id, [{ op: 'replace', path: '/remarks', value: 'Renewed' }]);
const { items, currentStepId } = await listContractApprovalSteps(1234);
await patchContractApprovalStep(1234, currentStepId!, [{ op: 'replace', path: '/completedDate', value: '2026-11-02' }]);
const profiles = await listApprovalProfiles();
const seeded = await importContractApprovalProfile(1234, { profileId: profiles[0].id }); // only into a contract with no steps
- PATCH bodies are typed per path (
PatchContractCertificateOperation,PatchContractApprovalStepOperation): only the listed paths andadd/replace/removeare accepted./stageOrdermay not end up null. A code, department or staff id is checked only when its value changes, so legacy rows stay editable. - Completing a step marks the next step (by
stageOrder) received on that date, and the contract'sapprovalStepfollows the current step. InvalidategetContractafter a step write. - Rejections:
Mackinac 400:for an invalid body (aValidationProblemDetailskeyed by field);Mackinac 404:for the contract (asgetContract), a certificate or step on another contract, or an unknown department, staff id or profile;Mackinac 409:witherrorCodeDuplicateContractRecord, orApprovalStepsExistwhen importing into a contract that already has steps.
Create, edit and delete contracts, POs and change orders (since 13.16.0)
previewContractOrder, createContract, patchContract and deleteContract write contracts (CO), purchase orders (PO) and change orders (CH):
import { previewContractOrder, createContract, patchContract, deleteContract } from '@cycomdatasystems/mackinac-sdk';
// Would this PO under contract 1234 need an automatic change order? Writes and allocates nothing.
const preview = await previewContractOrder({ masterMatterId: 1234, contType: 'PO', authAmount: 700 });
const { contract, autoChangeOrder } = await createContract({
contType: 'PO', masterMatterId: 1234, title: 'Paving phase 2', authAmount: 700,
confirmAutoChangeOrder: preview.wouldCreateChangeOrder, // only after the user has confirmed
});
await patchContract(contract.matterId, [
{ op: 'replace', path: '/authAmount', value: 900 },
{ op: 'replace', path: '/description', value: 'Revised scope' },
], { confirmAutoChangeOrder: false });
await deleteContract(contract.matterId, { deleteAttachments: false });
createContract:contTypeisCO,POorCH. A CO has no master, a PO's master is optional (a CO or PO), and a CH needs one (a CO or PO). A nullfileNumberis allocated from the CTYLAW mask. A child order with nonumbertakes the file number whenCM/AUTOFILLCHANGEis on. The server setsnetAuthAmount = authAmount,totalPaid = 0andapprovalStep = null.- Automatic change order (R6). When a PO under a CO would push the CO's open balance below zero, the create or the
authAmountpatch rejects withMackinac 409:anderrorCode: 'ChangeOrderConfirmationRequired'; the body carrieschangeOrderAmountandmasterOpenBalanceAfter. After the user confirms, resend withconfirmAutoChangeOrder: true(in the body forcreateContract, in the query forpatchContract). The created change order is returned asautoChangeOrder(null when none was created). InvalidategetContract/listContractOrdersfor the master too. patchContracttakes typed operations (PatchContractOperation): only the listed paths are accepted, and/contType,/masterMatterId,/netAuthAmount,/totalPaidand/approvalStepare rejected withMackinac 400:. Changing/authAmountalso setsnetAuthAmount. Changing/assocMatterIdalso sets it on the direct child orders and moves the order's payments. An empty/descriptiondeletes the description.deleteContractrequiresdeleteAttachments. It rejects withMackinac 409:anderrorCode: 'ContractHasDependents'while child orders, payments, approval steps or certificates exist; the body carrieschildOrders,payments,approvalStepsandcertificates.deleteMatteron a Contract Management matter behaves the same way.- Other rejections:
Mackinac 400:for an invalid body (aValidationProblemDetailskeyed by field), including dates before 1753 and amounts with more than 4 decimal places;Mackinac 403:frompreviewContractOrder/createContractwithout Contract Management access;Mackinac 404:for a contract you may not see, or amasterMatterId,assocMatterIdorvendorIdthat is not in your tenant;Mackinac 409:witherrorCodeDuplicateFileNumberorDuplicateContractRecord;Mackinac 503:when another request holds the numbering lock (retry).
Contract payments and spending limits (since 13.16.0)
A contract's or purchase order's Payments tab, the spending-limit preview, and "Enter payment":
import {
listContractPayments, checkContractPaymentLimit, createContractPayment,
patchContractPayment, deleteContractPayment,
} from '@cycomdatasystems/mackinac-sdk';
const { items, paidTotal } = await listContractPayments(1234); // newest first; no paging
// Before saving: would this amount warn or be blocked? Writes nothing.
const preview = await checkContractPaymentLimit(1234, { amount: 250 });
if (preview.warningLevel > 0) { /* show preview.warningMessage */ }
const { payment, limit } = await createContractPayment(1234, {
date: '2026-11-04', objectCode: 'PROF', amount: 250, // vendorId defaults to the contract's vendor
});
await patchContractPayment(1234, payment.id, [{ op: 'replace', path: '/amount', value: 300 }]);
await deleteContractPayment(1234, payment.id);
- Which payments. A PO lists payments with
poId= the PO; any other type listscontractId= the matter, so a contract also lists PO payments that carry its id. Every row counts, whateverpaidsays.paidTotalsums the rows marked paid. - Server-set fields. On create the server sets
contractId/poId(a PO payment also carries the PO's master contract),matterId(the contract's associated matter, else the request'smatterId), the vendor's payee and address (default: the contract's vendor),payType(from the object code) andorigin(1). The response never includes the payee's tax id. - Spending limits. Warnings (
warningLevel1/2 withwarningMessage) never block. A payment that would leave its order's open balance below zero is refused withMackinac 422:anderrorCode: 'ContractLimitExceeded'; the body (ContractPaymentProblemDetails) carriesopenBalanceBefore,openBalanceAfter,amountandtargetMatterId. Limits apply to the increase in amount, against the payment's own order (its PO when it has one).checkContractPaymentLimitreturns 200 even whenblockedis true; passpaymentIdto preview a new amount for an existing payment. TOTAL_PAID(ContractDto.totalPaid,ContractOrderSummaryDto.totalPaid) is now maintained by the server on every payment write, including the matter finance endpoints. InvalidategetContract/listContractOrders(and the master's, for a PO) after a payment write.patchContractPaymenttakes typed operations (PatchContractPaymentOperation): only the listed paths are accepted, and links, payee and address are not patchable. Sending/vendorIdre-copies that vendor; leaving it out keeps the stored vendor. An empty/notesdeletes the notes.- Rejections:
Mackinac 400:for an invalid body (aContractValidationProblemDetailskeyed by field), including an inactive or reserve object code, an amount of 0, with more than 2 decimal places or outside the money range, dates before 1753, anderrorCode: 'PayeeRequired'when neither the request nor the contract names a vendor;Mackinac 404:for a contract you may not see, a payment of another order, or avendorId,requestorormatterIdnot in your tenant;Mackinac 409:witherrorCode: 'DuplicateContractRecord';Mackinac 422:witherrorCode: 'ContractTypeNotPayable'when creating a payment on a change order (CH).
Code Generation
This SDK is generated from the Mackinac OpenAPI spec using Orval.
Regenerate the SDK
npm run orval
Ensure the latest swagger.json from the Mackinac API is located in the /contracts directory or at the configured URL.
Bundled OpenAPI spec
The exact OpenAPI document used to generate this SDK ships with the package as dist/openapi.json, exposed via the ./openapi.json subpath:
import spec from '@cycomdatasystems/mackinac-sdk/openapi.json'; // typed as OpenAPIV3.Document
console.log(spec.info.title, spec.info.version);
For non-import tooling (codegen, docs, Postman, etc.), resolve the file path:
const path = require.resolve('@cycomdatasystems/mackinac-sdk/openapi.json');
Note: the package uses
exports, so only the root barrel (@cycomdatasystems/mackinac-sdk) and./openapi.jsonare importable. Deep imports intodist/are not supported.
Development
- Requires Node.js 18+
- Install dependencies:
npm install
- Run codegen + build:
npm run orval && npm run build
- Run the WRP auto-assignee runtime and public type-contract checks:
npm test
Versioning
This package follows semantic versioning:
- PATCH: Regenerated SDK with no API contract changes
- MINOR: New endpoints or non-breaking additions
- MAJOR: Breaking API contract changes
License
This package is intended for internal use within Cycom.
If distributed externally, add an appropriate open-source license (e.g., MIT).