Contractors
The finance side of contractors: directory, timesheets, payouts and recruiting.
144 actions
Give an AI assistant only these actions with the contractors MCP toolset.
https://app.getoatmilk.com/api/mcp?toolset=contractorscontractorOps.activity
contractorOps.agreements
- POST
contractorOps.agreements.addContractorAdd the other party of a signed agreement or NDA that has no contractor as a new contractor and file the document with them: envelopeId, contractor (displayName, legalName, email, optional otherEmails, country, region, jobRoleId, title, rate, startDate, endDate) and idempotencyKey. Refused when an address already belongs to a contractor (use contractorOps.agreements.linkContractor), or when someone has the same or a close name unless notDuplicateOf lists them. An agreement's role, rate and dates become its terms when its title has a matching jobRoleId; otherwise the terms wait for review and role selection. With no terms given, the document is read for its terms to check. An NDA stays apart from the agreement. No invitation is sent. A document is never filed twice, whoever asks. - GET
contractorOps.agreements.contractorDraftPrefill for filing a signed agreement or NDA that has no contractor (envelopeId, or intakeItemId of the filed upload): the other party's name, email addresses, role and the job role and level it matches, rate, start and end dates and where they live, as the document states them with the words each came from, plus existing contractors it may be (any address Oatmilk knows for them, then the same name, then a close name) with which of the document's addresses each uses and whether the document is newer than their terms. Pass the name and email being added (name, email) to look those up too. Nothing is saved. - GET
contractorOps.agreements.endedList agreements that have ended for active contractors with no decision yet (not renewed, no renewal waiting for signatures, and no end decision recorded): the end date, what happened since (minutes logged, timesheet entries, pay periods, payouts and payments after the end date), a one-line summary, and the renewal to offer (the same terms from the day after it ended). An ended agreement blocks nothing. Contractors who kept working come first. Filter by contractorId. Renew with contractorOps.terms.propose, or record a decision with contractorOps.terms.endDecision (continuing keeps them working without renewing, ending lets it end); either decision removes it from this list. - POST
contractorOps.agreements.importExecutedMark an already-signed agreement as this contractor's executed agreement using a fileId from the signing upload flow, a title, the execution date, and idempotencyKey. - POST
contractorOps.agreements.keepAsHistoryKeep an agreement filed with a contractor as history instead of reading its terms (contractorId, envelopeId, idempotencyKey), such as one that couldn't be read, isn't a contractor agreement, or whose terms were entered by hand. Its terms are never read or applied, and it leaves the checklist. Refused while its terms are being read. - POST
contractorOps.agreements.linkContractorFile a signed agreement or NDA that has no contractor with an existing contractor (envelopeId, contractorId, idempotencyKey). An agreement newer than their terms is read for its terms to check and replaces them once confirmed (a draft read from an older agreement is replaced; while an update is out for signature it waits); an older or undated one is kept as history and never changes newer terms. An NDA stays apart from the agreement. - GET
contractorOps.agreements.listList contractor agreements from the versions of each contractor's terms, for a view: all, active (in effect), ending (in effect with an end date within 45 days), expired (the one in effect has passed its end date) or waiting (an update waiting for a signature or a check), with counts for every view. Each has the contractor, role, rate, start and end dates, days left, state (active, ending, expired, upcoming, replaced or waiting), finance's end decision for an expired one (continuing, ending, or null when undecided), when a renewal starts, whether new terms are waiting (renewalPending), for an undecided expired one whether they kept working and what happened since (the same as contractorOps.agreements.ended), and href, the contractor's Agreement tab. Each version also says where its signed copy is (copy: a document in Oatmilk, or a link when it was signed elsewhere). Without contractorId it covers the organization, lists active contractors with no agreement recorded, and lists signed agreements imported with the dates not stated (undated), which aren't in the views by date. With contractorId it covers that contractor and adds their documents on file from contractor agreements, signing envelopes and imports (items, each an agreement or an nda, with signedElsewhere for a copy kept at its link and datesNotStated for an undated one), whether an agreement is on file (status on_file, signed_elsewhere, terms_on_file, requested, draft or agreement_missing), and whether a signed NDA is. - POST
contractorOps.agreements.sendTemplateCreate a contractor agreement envelope from the contractor agreement template, prefilled from the profile (legal name, address, role, rate, cadence, term), optionally with a company signer who is an administrator or finance member (found by email), and send it for signature. values overrides template fields by their keys, like contractor.estimatedEngagement. Prefer contractorOps.terms.propose, which also records the terms Oatmilk enforces. - GET
contractorOps.agreements.unlinkedList signed agreements and NDAs that aren't filed with any contractor or other record, newest first, with the other party as recorded and a link to each.
contractorOps.directory
contractorOps.encryption
- POST
contractorOps.encryption.resetFingerprintKeyOnly while the organization's fingerprint key can't be read any more (the encryption key it was sealed with is lost), replace it with a new one, with a reason, so tax numbers and payment details can be saved and paid out again. Fingerprints are worked out again for every tax number and payment email that can still be read; ones that can't be read lose theirs and are flagged until they're entered again, and saved Wise recipients are created again on their next payout. Refused while the fingerprint key can still be read. Audited with counts and the reason only. Only an administrator in the dashboard can; not available to API keys or MCP clients. - GET
contractorOps.encryption.statusRead how contractor tax numbers and payment details are encrypted here: whether the encryption key and the previous key are set and usable (never their values), how many values the scheduled run still has to encrypt or re-encrypt, how many can't be read with the keys set now, recorded failures by error code, when a value was last tried, whether the previous key is safe to remove (only once nothing in any organization uses it), whether the organization's fingerprint key can be read (fingerprintKey) and can be reset (canResetFingerprintKey), and warnings. Counts only.
contractorOps.forms
- GET
contractorOps.forms.listList the custom hours forms contractors fill in for each time entry, including which one is the default. - POST
contractorOps.forms.saveCreate or update a custom hours form with name, ordered fields (key, label, type text/textarea/number/select/multiselect/date/checkbox/url, required, options, help), optional isDefault or archived, expectedRevision for updates, and idempotencyKey.
contractorOps.history
- GET
contractorOps.history.allocations.getWhat one contractor's payments and imported timesheets look like for sharing a payment across timesheets (contractorId; transactionId or payoutId to include that payment even when it isn't linked to them yet). payments lists each outgoing bank payment linked to them and each payment recorded in Oatmilk with no pay period, newest first, with its date, amount, kind (wise, bank or recorded), what it already gave which timesheets (allocations), and the timesheets an earlier Mark paid tied to it without amounts (legacyHistoryIds). timesheets lists their imported timesheets with the period, hours, amount (null for a fixed fee with no amount), what payments gave each, and whether it's unpaid, partly paid or paid and how. Nothing changes. - POST
contractorOps.history.allocations.saveShare one payment across several imported timesheets (weeks, two-week periods or parts of a month), or pay one timesheet from several payments. Send contractorId and either transactionId (an outgoing bank payment) or payoutId (a payment recorded in Oatmilk with no pay period) with the allocations of that payment, or historyId with the allocations of that timesheet. Each allocation is historyId, transactionId or payoutId, amountMinor, and closes (true counts a small shortfall, such as a fee, as paid in full). The list replaces what that payment (or timesheet) had; an empty list removes it, which is how a match is undone. A payment never gives more than it paid, a timesheet never takes more than it pays, and one payment pays one contractor. A timesheet its allocations cover (any allocation, when it has no amount) is marked paid on the latest payment's date; one no longer covered goes back to unpaid. Timesheets paid in their table or marked paid by hand keep that. Automatic matching leaves the payment alone afterwards. Optional note and idempotencyKey. Audited. - GET
contractorOps.history.listList every contractor timesheet as history: rows imported from a Notion database or CSV file, Oatmilk pay periods with hours or a payout, and milestone payouts. Each has the contractor, period, hours, rate, payout in minor units (the imported amount, or hours × rate plus GST/HST), GST/HST, status paid or unpaid with how (paid in the imported table, marked paid in Oatmilk with the date and bank payment, or the payout's state), when it was submitted and how many days late, its source (imported or oatmilk), and historyId for imported rows. An imported row whose dates also have hours logged in Oatmilk is marked alsoInOatmilk and left out of totals so nothing counts twice. Filter by contractorId, status (all, paid, unpaid), source (all, imported, oatmilk), and from and to (periods overlapping those dates). Unpaid is sorted by contractor, oldest first, with each contractor's totals in groups; the other views are newest first. Totals cover every match, by currency. - POST
contractorOps.history.markPaidRecord imported timesheets as paid, for example once the payroll that settled them went out: ids (the historyId of each imported row, up to 500), paidOn (today when omitted; never in the future), an optional transactionId of the bank payment that paid them (money out of an account, and then every row must be the same contractor's; it counts once as money sent even if it's linked to the contractor later), an optional note, and idempotencyKey. Rows already paid are left as they are and counted in alreadyPaid. Oatmilk pay periods aren't marked here; they're paid through their payouts. Importing the table again never marks them unpaid. Audited for each contractor. - POST
contractorOps.history.paymentAsks.answerAnswer a payment question (id, decision, idempotencyKey). paid marks the question's timesheets that are still unpaid paid, on the payment's date and with the payment (audited like contractorOps.history.markPaid); other records that the payment was for something else, and it is never matched to timesheets or asked about again. - GET
contractorOps.history.paymentAsks.listThe open questions about a bank payment to a contractor that is close to, or covers some of, their unpaid imported timesheets but isn't an exact match: who was paid, the payment (date, amount, Wise or bank), the timesheets it would settle with their periods, hours and amounts, and why Oatmilk isn't sure. Answer each with contractorOps.history.paymentAsks.answer. - POST
contractorOps.history.rateBackfill.applySave agreement-derived hourly rate snapshots for explicitly selected imported timesheets after reviewing the preview. Supply each history row id and expectedRevision plus idempotencyKey. Rechecks the agreement and source row atomically; a changed, split-rate or ambiguous week fails without saving any row. Does not mark paid, prepare, approve or send a payout. - GET
contractorOps.history.rateBackfill.previewPreview imported timesheets missing a source amount and rate, with the active hourly agreement covering each complete period. Shows which weeks are ready to save and which need review because an agreement changes, is missing, or has a currency or rate conflict. No data changes and no payout is prepared.
contractorOps.hours
contractorOps.import
- POST
contractorOps.import.agreementsBring agreements and NDAs in from a contracts table such as the Notion Contractor Contracts database, or the NDAs from a documents table such as Contractors Documents (source notion with databaseId, the database's link or ID, or csv with rows). mapping names the column for any fields whose column names don't match; the rest are matched by name. Fields: contractor (or email), title, kind (a type column: NDA, Contract…), signed (a signed column), role, rate (hourly), currency, startDate, endDate, file (a Notion files column with the signed copy), fileUrl (a link to a copy kept elsewhere) and nda (an NDA link or file on a contract row). A signed agreement with a start date becomes a version of the contractor's terms from that date, unless terms starting that day are already on file; one without dates is recorded as signed with the dates not stated and never becomes terms, so it can't replace dated ones; an unsigned, draft or stale row is kept as an unsigned draft. The signed copy is kept: a file attached to a Notion row is brought in with the Notion file import and filed as the agreement's signed document (linked to its terms), and a copy that's only a link is recorded as signed elsewhere. NDAs are kept apart from the contractor agreement. The role is linked to a job role by title and, for a title with several levels, the level whose pay band holds the rate; otherwise jobRoleSuggestion says what to do, and a contractor with no job role gets the one their current agreement matches. A table without start dates only brings in its NDAs. Every row is kept by the row it came from, so importing again fills in what's missing (a signed copy, a job role) instead of adding it twice. Rows for someone not in Oatmilk are left out with the reason; a rate of $1 or less is read as none. Nothing is sent to sign and nobody is emailed. A column whose name says it holds payment or tax details is never read, and problems says so when mapping names one. With preview true nothing is saved, and each row says whether it's already on file (onFile) and where the table disagrees with the terms on file (conflict). - POST
contractorOps.import.contractorsBring contractors in from a table: source notion with databaseId (the database's link or ID), or source csv with rows. mapping names the column for any fields whose column names don't match (null for none); the others are matched by name, and a column that isn't in the table is reported in problems. A required field no column matches is refused with the table's columns listed. Fields: name (required), legalName, workEmail, personalEmail, wiseEmail, wiseLink, phone, roles, jobRole, department, jurisdiction, address (split into street, city, province or state, postal code and country), city, postalCode, chargesSalesTax, salesTaxRate, discord, github, portfolio, startDate, notes, currency, and the bank columns accountHolderName, bankName, bankAddress, accountNumber, institutionNumber, transitNumber, routingNumber, iban and swiftBic. People already in Oatmilk are matched by email and then by name, and only have blanks filled in; new people are added without an invitation, and no one is emailed. jobRole is matched to a job role by title and level; a title with several levels takes the level whose pay band holds the person's current rate, and otherwise jobRoleLevels lists the levels to choose from. The address used to reach each person is work email, then personal, then Wise, unless emails (row reference to address) chooses another; a row with no email is left out until emails gives it one. Payment details are saved only when an administrator imports and only for someone with none yet, in the currency column's currency, else the bank's country's, else the person's country's, else CAD. They are validated, sealed and audited, and saving them doesn't email the contractor; a row whose details don't validate is still imported and listed in paymentProblems. Payment details are never returned, in a preview, a result or a message: each row's plan has paymentDetails with present, method (bank_transfer or wise_email), masked (at most the last four characters of the account number or IBAN, or the Wise email masked) and fields (the names of the details the row has), a Wise email is left out of emails and masked when it's the address used, and a column whose name says it holds payment or tax details is only read for the bank and Wise fields. With preview true nothing is saved and the plan for every row comes back. - POST
contractorOps.import.hoursBring past timesheets in as history from a table (source notion with databaseId, the database's link or ID, or csv with rows; mapping names the column for any fields whose column names don't match, and the others are matched by name). Fields: contractor (or firstName and lastName, or email), period (a date or a range, with optional periodEnd), hours, and optional rate, amount, tax, currency, paid, notes and submitted. Rows are matched to contractors by email, then name. Imported hours appear in each contractor's history, spending and on-time insights, but never become pay periods or payouts. A row that is a timesheet already here (the same Notion page or CSV row, or the same contractor's for the same hours starting or ending the same day) updates it instead of adding a duplicate: edited dates, hours, rate, amount, GST/HST and notes are taken, a blank cell erases nothing, and Paid marks an unpaid timesheet paid but an unpaid row never marks a paid one unpaid. The result lists what was added, corrected and marked paid in saved (counts and rows) and in summaryText, and afterwards new unpaid timesheets are settled against bank payments already linked to the contractor (payments). A column whose name says it holds payment or tax details is never read, and problems says so when mapping names one. With preview true nothing is saved and changes says what would happen. - POST
contractorOps.import.hoursSyncSync past timesheets from the Notion Contractors Hours database in one step (databaseId once, its link or ID; later syncs leave it out and use the remembered one): checks the database, then adds new timesheets, corrects edited ones (dates, hours, rate, amount, GST/HST, notes) and marks unpaid ones paid when Notion says Paid (never the reverse), without duplicating any, then settles new unpaid timesheets against bank payments already linked to the contractor. Returns counts, the change for each row, the payments that settled timesheets or now need a person's OK, and a plain-words summary. Bank and tax columns are never read. Send a new idempotencyKey for each sync. - GET
contractorOps.import.hoursSyncStatusWhether Notion is connected, the remembered Contractors Hours database (id and title), whether it is synced automatically about every 6 hours, and how the last sync went.
contractorOps.insights
contractorOps.payments
- POST
contractorOps.payments.revealShow a contractor's full payment details to an administrator in the dashboard with an audited reason. Not available to API keys or MCP clients. - POST
contractorOps.payments.updateAdministrators set a contractor's payment method (Wise email, bank transfer, Interac, or other), currency, and payment details. Omitted detail fields keep their saved values; null clears them. Responses are masked, the audit log records only which fields changed, the saved Wise recipient is reset, and approved payouts must be approved again. When the saved details can't be read (the profile's paymentDetailsState isn't readable), nothing changes unless the details sent are a complete set for the method; with an encryption key that isn't set any more, that also needs replaceUnreadable: true. - POST
contractorOps.payments.verifyRecord that an administrator confirmed a contractor's current payment details out of band, using the payment profile revision they reviewed and a note.
contractorOps.payouts
- POST
contractorOps.payouts.approveApprove a prepared payout with expectedRevision and idempotencyKey. Approval records the contractor's current payment details revision; if those details change later, the payout must be approved again before sending. - POST
contractorOps.payouts.cancelCancel a payout that has not been funded, releasing its time entries for a future payout. An unfunded Wise transfer is cancelled first. Requires a reason, expectedRevision, and idempotencyKey. - GET
contractorOps.payouts.getRead one contractor payout with its time entries, calculation, approval, Wise quote, recipient and transfer identifiers, and the related activity run. - GET
contractorOps.payouts.listList contractor payouts with amounts, tax, status, approvals, Wise transfer progress, and errors. Filter by contractor, period, or status. - POST
contractorOps.payouts.markPaidRecord a payout as paid outside Wise with a payment reference, the payment date, expectedRevision, and idempotencyKey. - POST
contractorOps.payouts.preparePrepare a payout from approved hours for a pay period (periodId), or a milestone payout for a contractor (contractorId with hoursIds or amountMinor). Amounts use exact minor units: approved minutes × hourly rate, days × day rate, or fixed and monthly rules, plus GST/HST only at a rate finance confirmed (salesTaxStatus shows when the contractor's declared tax isn't confirmed yet). Hours can never be included in two active payouts. - POST
contractorOps.payouts.sendSend an approved payout through Wise (quote, recipient, transfer, fund from balance). Requires an administrator in the dashboard, the confirmed total and currency, expectedRevision, and idempotencyKey. Refused while an hour in the payout has a correction waiting, and while an hour is still inside the wait after approval unless overrideReason says why (audited). Retries never create a second transfer. Not available to API keys or MCP clients.
contractorOps.periods
- GET
contractorOps.periods.listList contractor pay periods with period dates, hours due date, pay date, status (open, submitted, approved, paid, skipped), and submitted and approved minutes. - POST
contractorOps.periods.updateMark a pay period as skipped (no hours expected) or reopen it, with expectedRevision, an optional note, and idempotencyKey. Skipped periods receive no reminders and don't accept hours. A period with submitted or approved hours or a payout can't be skipped.
contractorOps.profiles
- GET
contractorOps.profiles.getRead one contractor's operations profile: roles, department, dates, contact details, address, rate, pay cadence, sales tax and corporation details, hours form, completeness, and a masked payment profile. Also returns their recent pay periods and payouts, and whether Oatmilk prepares their payouts automatically, so a caller can say when they are paid next. Full bank numbers are never returned. - POST
contractorOps.profiles.updateCreate or update a contractor's operations profile with contractorId, profile fields, expectedRevision (omit only when no profile exists yet), and idempotencyKey. Changing the pay cadence regenerates future empty pay periods. Changing the declared GST/HST answers stops finance's confirmation from applying and removes the GST/HST it added from payouts that haven't been paid and have no Wise transfer (approved ones go back for approval).
contractorOps.reminders
contractorOps.roles
- POST
contractorOps.roles.archiveArchive a job role so it isn't offered for new contractors, or restore it (archived false), with expectedRevision and idempotencyKey. Contractors keep a role that was archived. - POST
contractorOps.roles.archiveJobArchive, or restore (archived false), several levels of a job at once: levels with each id and expectedRevision, archived and idempotencyKey. Contractors keep a role that was archived. - POST
contractorOps.roles.assignGive a contractor a job role, or remove it with roleId null. - POST
contractorOps.roles.duplicateJobCopy a job under a new title: levels lists the ids to copy, title is the new job's name, with idempotencyKey. Each copy keeps the level, department, type, description, pay band and ladder order, and has nobody in it. - GET
contractorOps.roles.getRead one job role with its full description, pay band and current version. - POST
contractorOps.roles.importCreate or update job roles from a table: source notion with databaseId (the database's link or ID), or source csv with rows (column name to value). mapping names the column for any fields whose column names don't match (null for none); the others are matched by name, and a column that isn't in the table is reported in problems. A required field no column matches is refused with the table's columns listed. Fields: title (required), level, department, employmentType, summary, responsibilities, requirements, and either payBand text ("CAD 60–80/hour", "$90k–110k per year") or payMin, payMax, payCurrency, payUnit. Roles are matched by title and level, so importing again updates instead of duplicating, and a blank cell never erases what's written. A column whose name says it holds payment or tax details is never read, and problems says so when mapping names one. With preview true nothing is saved and the parsed roles and problems come back. - GET
contractorOps.roles.listList the company's job roles (job descriptions for contractors and employees): title, level, department, type, summary, responsibilities, requirements, pay band (currency, lowest and highest amount in minor units, paid per hour, day, week, month or year), and the active contractors who have each role. A job is every role with the same title, and its levels form a ladder in levelOrder (1 is the first rung; null until someone reorders them). Archived roles are left out unless includeArchived is true. - POST
contractorOps.roles.reorderPut a job's levels in order, first rung first: levels lists each level's id and expectedRevision in the order wanted, with idempotencyKey. - POST
contractorOps.roles.saveCreate a job role, or update one with id and expectedRevision: title, level, department, employmentType (contractor, employee or either), summary, responsibilities, requirements, an optional pay band (payCurrency, payMinMinor, payMaxMinor, payUnit) and an optional levelOrder (its place on the job's ladder). Title and level together are unique. - POST
contractorOps.roles.updateJobRename a job, and optionally set its department and type, on every level at once. levels lists each level's id and expectedRevision (include archived levels so the whole job changes together), with title and idempotencyKey. It changes nothing if any level changed since it was read, or if the new title and a level's name are already a role. A level held through an agreement can't be renamed here. - GET
contractorOps.roles.versionsList a job role's versions, newest first, with roleId: what the role said and paid at each version (title, level, department, type, summary, responsibilities, requirements and pay band), which parts changed from the version before (title, level, department, type, description or pay), who changed it and when, and how many sent agreements were made on that version. Every change to those parts is a new version, and an agreement keeps the version and pay it was made with.
contractorOps.salesTax
contractorOps.settings
- GET
contractorOps.settings.getRead contractor operations preferences: reminder timing and limits, approvers, automatic payout preparation, default cadence, time zone, and Wise source currency. - POST
contractorOps.settings.updateUpdate contractor operations preferences with idempotencyKey and optional expectedRevision. Only supplied settings change.
contractorOps.taxForms
- POST
contractorOps.taxForms.exportDownload the T4A and T4A-NR worksheet for a calendarYear with full tax numbers, to type into the CRA's web forms, with a reason. Only an administrator in the dashboard can; the download is audited with the year and the number of slips, never the numbers. Not available to API keys or MCP clients. - GET
contractorOps.taxForms.getWhat to issue or review for each contractor paid in a calendar year (calendarYear), based on the organization's formation country. For a Canadian organization: CRA administrative policy calls for a T4A when annual service fees exceed $500 before sales tax or any tax was deducted; non-residents who worked in Canada generally get a T4A-NR at any amount with 15% Regulation 105 withholding unless waived. For a U.S. organization: shows a review checklist and never labels payments as Canadian T4A slips; the accountant confirms W-9/W-8, payer and payee status, service source, payment type and channel, and applicable reporting. Certain 1099-NEC and 1099-MISC payments after 2025 use a $2,000 threshold. Foreign formations require local accountant review. Numbers are masked; addresses are not shown to accountants; no U.S. forms are filed.
contractorOps.taxInfo
- GET
contractorOps.taxInfo.getRead one contractor's tax info for slips (contractorId): masked tax numbers, the structured mailing address and its check, both legal names, identity confirmation, the last request, and the revisions an edit needs. - GET
contractorOps.taxInfo.listList every contractor with what their CRA slip still needs: the slip their residency calls for (T4A, T4A-NR, none, or unknown), tax numbers on file masked like •••-•••-286 (never in full), the mailing address check (missing or invalid parts by name), a legal name that differs between the contractor record and their profile, identity confirmation, and when their tax details were last requested. Filter with missingOnly or query. Fix a legal name with contractors.update (the record) or contractorOps.profiles.update (the profile). - POST
contractorOps.taxInfo.requestEmail a contractor who uses the portal to add what their slip still needs (their tax number and mailing address), naming the fields and asking them to use the portal, never email. Nothing is sent unless this is called. The request is recorded with its date, the record of a reasonable effort to get the SIN. - POST
contractorOps.taxInfo.updateAdd or replace a contractor's tax number (taxNumber: kind sin, bn, itn or foreign, the value, and country for a foreign number) or remove one (removeTaxNumber: the kind), with expectedRevision once that kind was saved before; and/or set the mailing address their slips go to (mailingAddress: line1 street, line2 unit, city, region province or state, postalCode, country) with expectedProfileRevision. A SIN must pass the check-digit test, a business number has 9 digits and an optional program account like RT0001, and a Canadian postal code looks like K1A 0B6. Numbers are stored encrypted and only ever returned masked; the audit log records that one changed, never the value. Finance can change a contractor's tax numbers at most 5 times an hour, and 50 times an hour across the organization (429 with Retry-After); a contractor's own portal saves have a separate budget, so neither can use up the other's. Nothing is returned about other contractors' numbers, except that an administrator in the dashboard saving a SIN or ITN is told who else has the same one (sameNumberAs).
contractorOps.terms
- POST
contractorOps.terms.cancelWithdraw an update that is waiting for signature or confirmation, with id, expectedRevision, a reason and idempotencyKey. A document already sent is voided so it can't be signed. - POST
contractorOps.terms.confirmConfirm a waiting version, such as terms Oatmilk read from an uploaded signed agreement, optionally correcting its terms or effective date and selecting a matching jobRoleId first, with id, expectedRevision and idempotencyKey. - POST
contractorOps.terms.endDecisionDecide about an agreement at or past its end date, with the active terms version id, its expectedRevision, decision, and idempotencyKey. 'continuing' keeps them working and paid without renewing and stops the end-date reminders; 'ending' lets it end, so no new pay periods start after the end date (it does not stop the contractor logging hours or their portal access); null takes the decision back. An end date alone never stops work. To extend an agreement on paper instead, send new terms with a later end date using contractorOps.terms.propose. - GET
contractorOps.terms.getRead a contractor's agreement terms: the version in effect today (role, rate, pay schedule, hour limits per day, week, month or in total, dates, notice, where the work is done, notable clauses), versions starting later, an update waiting for signature or confirmation, the full history with what each version changed, how many of today's, this week's, month's and the agreement's hours are used, the agreement that ended with no decision yet and what happened since (ended), what happened to each filed agreement's terms (reads: reading, read, queued behind an update out for signature, kept as history, or failed, with the reason), and the renewal to suggest (renewal: the same terms for another stretch, from the day after the end, or from the day after it ended when they kept working, with effectiveFrom, the new dates in terms, its length, endsIn days, and due when it ends within 45 days or already has; null with no end date, when later terms are set, or while an update is waiting). Send it with contractorOps.terms.propose, changing anything first. - POST
contractorOps.terms.linkRoleLink the current agreement's existing title to a matching active contractor job role, without changing the title or agreement terms. Requires terms id, roleId, expectedRevision and idempotencyKey. Only available when the current agreement has no role link; the change is audited. - POST
contractorOps.terms.proposeSend new agreement terms for signature, with contractorId, terms, effectiveFrom, optional jobRoleId, send, an optional company signer (an administrator or finance member who signs in Oatmilk) and idempotencyKey. A titled first agreement must link to a matching active job role; a unique match is resolved when jobRoleId is omitted. With no agreement in effect it sends the full contractor agreement; otherwise a one-page amendment listing changes to pay, hours or other terms. Use contractorOps.titleChange.propose to change a current title. The terms take effect once everyone has signed. Only one update can wait at a time. A blank rate keeps the rate already agreed, and basisTermsId (the version in effect when you read it, or null for none) refuses the change when someone else changed the agreement since. - POST
contractorOps.terms.readHave the agreement reader agent read a signed agreement already on file for a contractor (envelopeId) and propose its terms as a draft to confirm: role, rate, pay schedule, hour limits, dates, notice, where the work is done and notable clauses, each with the words it came from. Nothing takes effect until a person confirms it. - POST
contractorOps.terms.setSet a contractor's agreement terms without a signature (for example a daily or weekly hour limit, or terms of an agreement signed elsewhere), with contractorId, terms, effectiveFrom, optional jobRoleId and idempotencyKey. A titled agreement must link to an active matching job role; a unique match is resolved when jobRoleId is omitted. They take effect from effectiveFrom: hour limits apply to every timesheet entry from then on, and role, rate, pay schedule and dates update the profile. A blank rate keeps the rate already agreed (the previous version's, or the profile's). Pass basisTermsId (the version in effect when you read it, or null for none) to be refused when someone else changed the agreement since. A first version can't start after hours that are waiting to be paid.
contractorOps.timesheets
- GET
contractorOps.timesheets.listList timesheets awaiting approval, approved, or open by pay period. Hours submitted for dates no pay period covers (a manual cadence, or before the first period) are listed per contractor under unscheduled. With periodId, returns every time entry with its custom form values, missing required fields, and an estimated payout; with contractorId and unscheduled=true, returns that contractor's hours outside a pay period to review and the approved ones ready to pay (prepare them with contractorOps.payouts.prepare and hoursIds). Each listed period carries its live payout, if any, and pipeline says whether payouts are prepared automatically and whether contractors get reminder emails. - POST
contractorOps.timesheets.reviewApprove or return submitted time entries in a single atomic review, for one pay period (periodId) or for one contractor's hours outside a pay period (contractorId). Supply decisions with hoursId, expectedRevision and decision, a review reason, and idempotencyKey. A decision on a mistake the contractor reported in approved hours also carries the correctionId and decides approved or declined; approving it applies the corrected values, and declining leaves the entry as approved. Approval never sends money.
contractorOps.titleChange
- POST
contractorOps.titleChange.overrideAdministrator override of a contractor title change with a reason, job role, date and idempotency key. A pending title amendment is voided before the override is recorded; its audit and document history remain. - GET
contractorOps.titleChange.planPlan a contractor title change from the current agreement. Returns the current title, organization-local today, pending or later agreement, and every signer carried forward with whether an inactive member needs replacement; no document is sent or saved. - POST
contractorOps.titleChange.proposeCreate a title-change amendment tied to an active organization job role with a different title, copying every other current agreement term and all required signer roles. The contractor uses their current email; company signers must be active finance or admin members. Optional companySigner and signerReplacements select current recipients before send. The title stays pending until every signer signs and its effective date arrives. Requires contractorId, roleId, effectiveFrom, send and idempotencyKey.
contractorOps.wise
contractors.access
contractors.agreements
- POST
contractors.agreements.createCreate a draft agreement document for a contractor from a confirmed file (fileId), with a title and the contractor's expectedContractorRevision. Nothing is sent until contractors.agreements.send. - GET
contractors.agreements.downloadGet a one-minute download link for an agreement document, optionally for one version. - GET
contractors.agreements.getRead one portal agreement document with its status, version and the contractor's signature events. - GET
contractors.agreements.listList agreement documents sent through the contractor portal for signature (draft, requested, signed, declined or void), optionally for one contractor. For the agreement on file, signed copies, NDAs and terms, use contractorOps.agreements.list and contractorOps.terms.get. - POST
contractors.agreements.remindEmail a contractor a reminder to sign the agreement document waiting for them, at most once a day, with expectedRevision. - POST
contractors.agreements.reviseReplace an unsigned agreement document with a new confirmed file (fileId) and a reason, with expectedRevision. It goes back to draft as a new version; a signed one can't be revised (send an amendment with contractorOps.terms.propose). - POST
contractors.agreements.sendAsk a contractor who has joined the portal to sign a draft agreement document, with expectedRevision. Only they can sign it, in person, in their portal. - POST
contractors.agreements.voidVoid an unsigned agreement document with a reason and expectedRevision, so it can't be signed. The record is kept; a signed one can't be voided.
contractors
- POST
contractors.createAdd a contractor with displayName, legalName, email (unique in the organization), an optional two-letter country, an optional jobRoleId (an active role from contractorOps.roles.list; add one with contractorOps.roles.save) and idempotencyKey. No invitation is sent: invite them with contractors.invite, or share the link from contractors.invitations.link. - GET
contractors.getRead one contractor by id: display and legal name, email, country, whether portal access is on (active), job role (job_role_id), reviewed tax profile (residency, services in Canada, identity confirmed) and revision. - POST
contractors.inviteEmail a contractor an invitation to the contractor portal, with id and expectedRevision. It replaces any open invitation and lasts 7 days, at most 3 a day. The portal only shows them their own hours, agreements and payments. Refused once they've joined. - GET
contractors.listList contractors, newest first: display and legal name, email, country, whether portal access is on (active), their job role (job_role_id) and revision. Filter by query (display or legal name); page with limit and offset. For roles, rates, pay cadence, agreement on file and pending hours use contractorOps.directory.list. - GET
contractors.taxReportReport a calendar year's contractor payments for T4A and T4A-NR review (calendarYear): reviewed payments for services and returns in CAD per contractor, payouts paid outside Wise, bank debits still to attribute, and open items. It holds no tax numbers. - POST
contractors.updateChange a contractor's displayName, legalName or country with id and expectedRevision. Only an administrator can turn portal access on or off (active). Change their email with contractors.email.update and their job role with contractorOps.roles.assign.
contractors.email
contractors.files
- POST
contractors.files.confirmConfirm an uploaded contractor document (fileId) once its bytes are uploaded. Its size, SHA-256 and file type are checked against what was prepared. - POST
contractors.files.preparePrepare a private upload for a contractor document (PDF, Word, Markdown or text, up to 20 MB) with filename, mimeType, sizeBytes and sha256. PUT the unchanged bytes to uploadUrl, then call contractors.files.confirm; alreadyUploaded means the same file is already stored and only needs confirming.
contractors.hours
- GET
contractors.hours.listList time entries logged by contractors (date, minutes, description, status draft, submitted, approved or rejected, review reason), filtered by contractorId, status and from and to dates. For timesheets by pay period use contractorOps.timesheets.list. - POST
contractors.hours.reviewApprove or return one submitted time entry with id, expectedRevision, decision (approved or rejected) and a reason. Approving never sends money. To review a whole pay period at once use contractorOps.timesheets.review.
contractors.invitations
- POST
contractors.invitations.fixEmailFix the email a contractor was invited at and send a new invitation in one step, until they join the portal, with id, expectedRevision and email. Open invitations to the old address stop working and their link says it was replaced, and a new 7-day invitation is queued to the corrected address. The email must be unique in the organization and different from the current one; at most 6 invitations a day. Refused once they've joined. - POST
contractors.invitations.linkGet a portal invitation link to send a contractor yourself: an open invitation is reused, or a 7-day one is created, and nothing is emailed. The same link keeps working until it expires or is revoked. - GET
contractors.invitations.listList a contractor's portal invitations (contractorId): the email each was sent to, when it expires, and whether it was accepted or revoked. - POST
contractors.invitations.revokeRevoke an open portal invitation with id and expectedRevision, so its link stops working.
contractors.notifications
contractors.pastPayments
- POST
contractors.pastPayments.linkConfirm a Wise recipient (profileId, recipientId) is this contractor and link every past outgoing transfer to it, as services by default (treatment), with expectedContractorRevision. Transfers in a closed period are skipped and counted. Never sends money. - GET
contractors.pastPayments.suggestSuggest the Wise recipients whose past transfers were likely to this contractor (the family name and a given name match), with each one's number of transfers, total and dates, to link with contractors.pastPayments.link.
contractors.payments
- POST
contractors.payments.bindLink one bank payment (transactionId with expectedTransactionRevision) to a contractor through a confirmed Wise recipient (recipientBindingId), as services, reimbursement or unknown, with a reason. - GET
contractors.payments.getRead one outgoing bank transaction's contractor link or open identification question, with contractor choices. No payment credentials are returned. - POST
contractors.payments.identifyPreview or apply contractor identification for one outgoing bank transaction, or queue a bounded backfill for all eligible transactions. Applying requires an idempotency key and never sends money. - GET
contractors.payments.jobsList recent contractor payment identification backfill jobs with progress counts and status, without bank or recipient details. - POST
contractors.payments.linkLink an outgoing bank transaction to a contractor, with its expected revision, treatment, reason and idempotency key. Remembering its payee can identify later payments; no payout is sent. - GET
contractors.payments.listList all payments to a contractor from linked bank transfers, Oatmilk payouts and imported history, with timesheet coverage and calendar-year totals for review. - POST
contractors.payments.unbindUnlink a bank payment from a contractor with id, expectedRevision and a reason. The history is kept. - POST
contractors.payments.unlinkRemove a contractor link from a bank transaction, or mark it as unrelated to contractors, with its expected revision, reason and idempotency key. The audit history remains.
contractors.recipients
- POST
contractors.recipients.bindConfirm that a Wise recipient (profileId, recipientId) is this contractor, with a reason and the contractor's expectedContractorRevision, so transfers to it are attributed to them. A recipient can belong to one contractor. - GET
contractors.recipients.listList the Wise recipients confirmed as a contractor's (contractorId), with who confirmed each and why.
contractors.taxProfile
contractors.templates
- POST
contractors.templates.createAdd a contractor agreement template from an uploaded original (fileId from contractors.files.confirm) with a title. - GET
contractors.templates.downloadGet a template's Markdown, or a one-minute download link for its uploaded original. - GET
contractors.templates.listList contractor agreement templates: drafts written in Markdown and uploaded originals (PDF, Word or text). - GET
contractors.templates.previewRead a template to review it: Markdown, text or Word as text (up to 500,000 characters), or a one-minute link to a PDF. - POST
contractors.templates.updateReplace a Markdown template's title and content (contentMarkdown) with id and expectedRevision.
recruiting.applications
- POST
recruiting.applications.assignSet who interviews a candidate (interviewerUserIds, active members only) with applicationId, expectedRevision and idempotencyKey. Interviewers can then see the application and submit a scorecard. - POST
recruiting.applications.convertAdd a hired candidate as a contractor with applicationId and idempotencyKey: the contractor is created from their name and email (or an existing contractor with that email is linked) and given the posting's job role. No invitation is sent; invite them from Contractors when ready. - POST
recruiting.applications.createAdd a candidate to a posting yourself (a referral or someone you sourced): postingId, candidate name, email, optional phone, location and links, source (referral, sourced, other), an optional note, and idempotencyKey. A person already in recruiting with that email is reused. Only administrators and the posting's hiring manager can do this. - GET
recruiting.applications.getRead one application: the candidate, answers, cover letter, resume details, interviews, scorecards, timeline with notes, the candidate's other applications and possible duplicates (same name, different email). Interviewers see other people's scorecards only after submitting their own. Candidate details are visible to administrators and the posting's hiring manager; interviewers see only the applications they're assigned to, without email or phone. - GET
recruiting.applications.listList applications for one posting (postingId) or every posting you can see, filtered by status (active, rejected, hired) or a name search, with stage, source, resume presence, interviewers and a count of yes and no scorecards. Candidate details are visible to administrators and the posting's hiring manager; interviewers see only the applications they're assigned to, without email or phone. - POST
recruiting.applications.moveMove an application to another stage of its posting with applicationId, stage, expectedRevision and idempotencyKey. Moving to Hired marks the candidate hired. Every move is recorded on the timeline. - POST
recruiting.applications.rejectReject an application with applicationId, reason (not_qualified, experience, not_a_fit, compensation, location, withdrew, no_response, position_filled, duplicate, spam, other), an optional note, expectedRevision and idempotencyKey. Nothing is sent to the candidate. - POST
recruiting.applications.restoreReturn a rejected application to its stage with applicationId, expectedRevision and idempotencyKey. - GET
recruiting.applications.resumeGet a link, valid for one minute, to download an application's resume. Available to administrators, the hiring manager and assigned interviewers. - POST
recruiting.applications.summarizeHave AI summarize an application against the posting (a short summary, strengths, gaps and questions to ask), when the posting has aiAssist on, with applicationId and idempotencyKey. It never scores, ranks or decides; people make every decision. Administrators and the hiring manager only.
recruiting.board
- GET
recruiting.board.getRead the organization's public job board: its address, the name shown and the introduction. - POST
recruiting.board.saveSet up or change the public job board: slug (its address, 3 to 60 lowercase letters, digits and dashes), displayName, intro, and expectedRevision after the first save.
recruiting.interviews
- GET
recruiting.interviews.mineThe candidates you're assigned to interview: their posting and stage, your upcoming interviews, and whether you've submitted a scorecard. - POST
recruiting.interviews.saveAdd an interview to an application, or update one with id and expectedRevision: title, scheduledAt, durationMinutes, location (a room or a video link), interviewerUserIds, notes, and status (scheduled, completed, cancelled). No calendar invitation is sent. Interviewers on the panel are added to the application.
recruiting.notes
recruiting.postings
- GET
recruiting.postings.getRead one job posting with its description, pay, application questions, stages, hiring manager, the public job board address, and for private postings the private link to share with chosen candidates. - GET
recruiting.postings.listList job postings with status (draft, open, closed), visibility (public on the job board, or private by link), hiring manager, stages, and how many applications are in each stage. Administrators and finance see every posting; others see postings whose hiring team they're on. canViewCandidates says whether candidate details can be opened. - POST
recruiting.postings.rotateLinkReplace a posting's private link so the old link stops working, with id, expectedRevision and idempotencyKey. Returns the new link. - POST
recruiting.postings.saveCreate a job posting (often from a job role with jobRoleId), or update one with id and expectedRevision: title, department, location, workplace (remote, hybrid, onsite), employmentType, description, payText, vacancyExists, aiAssist (disclosed on the posting), visibility (public or private), questions (key, label, type text/textarea/yes_no/select/url, required, options) and stages (Applied first, Hired last). The creator becomes hiring manager unless an administrator chooses someone; only an administrator or the current hiring manager can change it. New postings start as drafts. - POST
recruiting.postings.statusOpen a posting to take applications, close it, or return it to draft, with id, status, expectedRevision and idempotencyKey. Administrators, finance and the hiring manager can do this. Public postings appear on the job board only while open.