# contractorOps.import.contractors

Bring 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 /api/v1/accounting/contractorOps.import.contractors`

Permissions: `accounting:read`, `accounting:write` · Roles: admin, finance · Idempotency key required

MCP tool: `accounting_contractor_ops_import_contractors`

## Fields

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `source` | enum | Yes | notion reads a Notion database on the server; csv takes the rows of a parsed CSV file. One of: `csv`, `notion`. |
| `rows` | array of maps |  | For csv: each row as column name to value, at most 500. 1–500 items. |
| `databaseId` | string |  | For notion: the database's link or ID. The database must be shared with the Oatmilk connection. 1–2000 characters. |
| `mapping` | object |  | The column for any fields whose column names don't match (null for none). Fields left out, or every field when it's omitted, are matched by column name; the result returns the mapping it used, so a preview shows the suggested one. |
| `mapping.name` | string or null |  | The column holding Name (required), or null when the table has none. |
| `mapping.legalName` | string or null |  | The column holding Legal name, or null when the table has none. |
| `mapping.workEmail` | string or null |  | The column holding Work email, or null when the table has none. |
| `mapping.personalEmail` | string or null |  | The column holding Personal email, or null when the table has none. |
| `mapping.wiseEmail` | string or null |  | The column holding Wise email, or null when the table has none. |
| `mapping.wiseLink` | string or null |  | The column holding Wise link, or null when the table has none. |
| `mapping.phone` | string or null |  | The column holding Phone, or null when the table has none. |
| `mapping.roles` | string or null |  | The column holding Roles, or null when the table has none. |
| `mapping.jobRole` | string or null |  | The column holding Job role, or null when the table has none. |
| `mapping.department` | string or null |  | The column holding Department, or null when the table has none. |
| `mapping.jurisdiction` | string or null |  | The column holding Province or country, or null when the table has none. |
| `mapping.address` | string or null |  | The column holding Address, or null when the table has none. |
| `mapping.city` | string or null |  | The column holding City, or null when the table has none. |
| `mapping.postalCode` | string or null |  | The column holding Postal code, or null when the table has none. |
| `mapping.chargesSalesTax` | string or null |  | The column holding Charges GST/HST, or null when the table has none. |
| `mapping.salesTaxRate` | string or null |  | The column holding GST/HST rate, or null when the table has none. |
| `mapping.discord` | string or null |  | The column holding Discord, or null when the table has none. |
| `mapping.github` | string or null |  | The column holding GitHub, or null when the table has none. |
| `mapping.linkedin` | string or null |  | The column holding LinkedIn, or null when the table has none. |
| `mapping.portfolio` | string or null |  | The column holding Portfolio, or null when the table has none. |
| `mapping.startDate` | string or null |  | The column holding Start date, or null when the table has none. |
| `mapping.notes` | string or null |  | The column holding Notes, or null when the table has none. |
| `mapping.currency` | string or null |  | The column holding Payment currency, or null when the table has none. |
| `mapping.accountHolderName` | string or null |  | The column holding Account holder, or null when the table has none. |
| `mapping.bankName` | string or null |  | The column holding Bank name, or null when the table has none. |
| `mapping.bankAddress` | string or null |  | The column holding Bank address, or null when the table has none. |
| `mapping.accountNumber` | string or null |  | The column holding Account number, or null when the table has none. |
| `mapping.institutionNumber` | string or null |  | The column holding Institution number, or null when the table has none. |
| `mapping.transitNumber` | string or null |  | The column holding Transit number, or null when the table has none. |
| `mapping.routingNumber` | string or null |  | The column holding Routing number, or null when the table has none. |
| `mapping.iban` | string or null |  | The column holding IBAN, or null when the table has none. |
| `mapping.swiftBic` | string or null |  | The column holding SWIFT/BIC, or null when the table has none. |
| `preview` | boolean |  | true checks every row and saves nothing. |
| `idempotencyKey` | string | Yes | Any unique text you generate once per intended change, so a retried request only happens once. Send it as the Idempotency-Key header instead if you prefer; if you send both they must match. 8–200 characters. |
| `emails` | map |  | The address to use for a row, by the row's reference (its Notion page link, or Row 2, Row 3 for a CSV file), instead of work, personal, then Wise email. |

## Example request

```bash
curl https://app.getoatmilk.com/api/v1/accounting/contractorOps.import.contractors \
  -H "Authorization: Bearer $OATMILK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "source": "csv",
  "rows": [
    {}
  ]
}'
```

Reference page: https://app.getoatmilk.com/docs/api/contractorOps.import.contractors
