# contractorOps.roles.import

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

`POST /api/v1/accounting/contractorOps.roles.import`

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

MCP tool: `accounting_contractor_ops_roles_import`

## 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.title` | string or null |  | The column holding Title (required), or null when the table has none. |
| `mapping.level` | string or null |  | The column holding Level, or null when the table has none. |
| `mapping.department` | string or null |  | The column holding Department, or null when the table has none. |
| `mapping.employmentType` | string or null |  | The column holding Type, or null when the table has none. |
| `mapping.summary` | string or null |  | The column holding Summary, or null when the table has none. |
| `mapping.responsibilities` | string or null |  | The column holding Responsibilities, or null when the table has none. |
| `mapping.requirements` | string or null |  | The column holding Requirements, or null when the table has none. |
| `mapping.payBand` | string or null |  | The column holding Pay band, or null when the table has none. |
| `mapping.payMin` | string or null |  | The column holding Pay from, or null when the table has none. |
| `mapping.payMax` | string or null |  | The column holding Pay up to, or null when the table has none. |
| `mapping.payCurrency` | string or null |  | The column holding Currency, or null when the table has none. |
| `mapping.payUnit` | string or null |  | The column holding Paid per, 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. |

## Example request

```bash
curl https://app.getoatmilk.com/api/v1/accounting/contractorOps.roles.import \
  -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.roles.import
