> For the complete documentation index, see [llms.txt](https://docs.joinflo.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.joinflo.com/flo-apis/matter-api/matter-csv-controller.md).

# Matter Csv Controller

## Uploads matters from the csv file

> Use POST /matters-csv/upsert instead for idempotent behavior. This endpoint silently skips rows that already exist in the review cycle. Creates new matter for each row in the csv file. Rejects all rows if any single row contain errors.

```json
{"openapi":"3.1.0","info":{"title":"Flo Web API — Matters","version":"2.0.0"},"servers":[{"url":"https://florecruit.com/api/v2","description":"Flo production API"}],"paths":{"/matters-csv/upload-csv":{"post":{"deprecated":true,"description":"Use POST /matters-csv/upsert instead for idempotent behavior. This endpoint silently skips rows that already exist in the review cycle. Creates new matter for each row in the csv file. Rejects all rows if any single row contain errors.","operationId":"uploadMattersCSV","parameters":[{"description":"The UUID of the review cycle to be associated with the uploaded matters.","in":"query","name":"reviewCycleId","required":true,"schema":{"type":"string","format":"uuid","description":"The UUID of the review cycle to be associated with the uploaded matters."}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"Comma-separated value file. Must be less than 10MB in size.\nHeader row is required, and each subsequent row corresponds to one matter entry.\nHeader row must match: \"Matter ID,Matter Name,Attorney Email,Billing Attorney Email,Start Date,End Date,Hours Billed,Client Name,Description\"\nThe columns must follow these requirements:\n- Matter ID: String, Required. Together with Client Name, identifies the Client Matter this entry rolls up to. Multiple rows may share the same (Matter ID, Client Name) - one row per attorney working the matter; when they do, all Client Matter fields (Matter Name, Description) must be identical across those rows.\n- Matter Name: String, Required. Must be <= 255 characters. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).\n- Attorney Email: String, Required. Email must match an existing Perform member's email.\n- Billing Attorney Email: String, Required. Email must match an existing Perform member's email.\n- Start Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 6/30/2025. Blank persists as null on the entry.\n- End Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 10/5/2025. When both dates are present, End Date must be on or after Start Date. Blank persists as null on the entry.\n- Hours Billed: Integer or Float, Required. Must be between 0 and 999999.99.\n- Client Name: String, Required. Must be <= 255 characters. Together with Matter ID, identifies the Client Matter this entry rolls up to.\n- Description: String, Optional. Must be <= 500 characters. This is the matter's description, not a description of the individual attorney's work on that matter. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).\n"}},"required":["file"]}}}},"responses":{"200":{"description":"Matters created successfully (no response body)"},"400":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Invalid CSV file format, or CSV file contains one or more invalid rows. Use /validate-csv for row level error details."},"401":{"description":"Unauthorized"},"403":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Forbidden"},"404":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Review Cycle not found"},"409":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Conflict"},"500":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Uploads matters from the csv file","tags":["matter-csv-controller"]}}}}
```

## Upserts matter entries from a CSV file

> Upserts a matter entry for each row in the CSV file. Uniqueness of a\
> matter entry within a review cycle is by the combined (Matter ID,\
> Client Name, Attorney Email) key.\
> \
> CSV formatting:\
> \- Field values containing a comma, double quote, or newline must be\
> &#x20; wrapped in double quotes. Embedded double quotes are escaped by\
> &#x20; doubling. Example: a Description of Smith, Jones & Associates\
> &#x20; must be written as "Smith, Jones & Associates".\
> \
> Behavior:\
> \- If a matter entry with that composite key already exists in the cycle,\
> &#x20; its non-key fields are updated in place when they differ from the row.\
> &#x20; Rows whose non-key fields already match the stored entry are no-ops\
> &#x20; and are not counted in the response's updated total. The row's id is\
> &#x20; preserved, so any existing review-cycle assignments referencing the\
> &#x20; entry survive.\
> \- If no such entry exists, a new one is created.\
> \- Multiple entries under the same Matter ID + Client Name (different\
> &#x20; Attorney Emails) are supported - they share a single Client Matter\
> &#x20; parent whose canonical Matter Name and Description also refresh on\
> &#x20; each upsert.\
> \
> Idempotency:\
> \- Uploading the same CSV twice produces the same end state - safe to retry.\
> \- Non-key fields updated: Matter Name, Billing Attorney Email, Start Date,\
> &#x20; End Date, Hours Billed, Description.\
> \- Matter Name and Description also refresh on the shared Client Matter\
> &#x20; parent; all existing entries under the same Matter ID + Client Name see\
> &#x20; the update.\
> \
> Validation:\
> \- All rows must pass validation or the entire upload is rejected (no\
> &#x20; partial commits).\
> \- Rows are rejected when:\
> &#x20;   \- Any field-level validation fails (see per-column rules below).\
> &#x20;   \- Two rows share the same upsert key (Matter ID, Client Name, Attorney\
> &#x20;     Email). Each matter entry must appear in the CSV at most once.\
> &#x20;   \- Two rows share the same Client Matter (Matter ID, Client Name) but\
> &#x20;     disagree on any Client Matter field (Matter Name or Description).\
> \- Pre-flight validation is available with POST /matters-csv/validate-upsert;\
> &#x20; it returns per-row, per-column error details without committing.\
> \
> Limits:\
> \- File size <= 10 MB.\
> \- Row count <= 10 000.<br>

```json
{"openapi":"3.1.0","info":{"title":"Flo Web API — Matters","version":"2.0.0"},"servers":[{"url":"https://florecruit.com/api/v2","description":"Flo production API"}],"paths":{"/matters-csv/upsert":{"post":{"description":"Upserts a matter entry for each row in the CSV file. Uniqueness of a\nmatter entry within a review cycle is by the combined (Matter ID,\nClient Name, Attorney Email) key.\n\nCSV formatting:\n- Field values containing a comma, double quote, or newline must be\n  wrapped in double quotes. Embedded double quotes are escaped by\n  doubling. Example: a Description of Smith, Jones & Associates\n  must be written as \"Smith, Jones & Associates\".\n\nBehavior:\n- If a matter entry with that composite key already exists in the cycle,\n  its non-key fields are updated in place when they differ from the row.\n  Rows whose non-key fields already match the stored entry are no-ops\n  and are not counted in the response's updated total. The row's id is\n  preserved, so any existing review-cycle assignments referencing the\n  entry survive.\n- If no such entry exists, a new one is created.\n- Multiple entries under the same Matter ID + Client Name (different\n  Attorney Emails) are supported - they share a single Client Matter\n  parent whose canonical Matter Name and Description also refresh on\n  each upsert.\n\nIdempotency:\n- Uploading the same CSV twice produces the same end state - safe to retry.\n- Non-key fields updated: Matter Name, Billing Attorney Email, Start Date,\n  End Date, Hours Billed, Description.\n- Matter Name and Description also refresh on the shared Client Matter\n  parent; all existing entries under the same Matter ID + Client Name see\n  the update.\n\nValidation:\n- All rows must pass validation or the entire upload is rejected (no\n  partial commits).\n- Rows are rejected when:\n    - Any field-level validation fails (see per-column rules below).\n    - Two rows share the same upsert key (Matter ID, Client Name, Attorney\n      Email). Each matter entry must appear in the CSV at most once.\n    - Two rows share the same Client Matter (Matter ID, Client Name) but\n      disagree on any Client Matter field (Matter Name or Description).\n- Pre-flight validation is available with POST /matters-csv/validate-upsert;\n  it returns per-row, per-column error details without committing.\n\nLimits:\n- File size <= 10 MB.\n- Row count <= 10 000.\n","operationId":"upsertMattersCSV","parameters":[{"description":"The UUID of the review cycle to associate with the upserted matter entries.","in":"query","name":"reviewCycleId","required":true,"schema":{"type":"string","format":"uuid","description":"The UUID of the review cycle to associate with the upserted matter entries."}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"Comma-separated value file. Must be less than 10MB in size.\nHeader row is required, and each subsequent row corresponds to one matter entry.\nHeader row must match: \"Matter ID,Matter Name,Attorney Email,Billing Attorney Email,Start Date,End Date,Hours Billed,Client Name,Description\"\nThe columns must follow these requirements:\n- Matter ID: String, Required. Together with Client Name, identifies the Client Matter this entry rolls up to. Multiple rows may share the same (Matter ID, Client Name) - one row per attorney working the matter; when they do, all Client Matter fields (Matter Name, Description) must be identical across those rows.\n- Matter Name: String, Required. Must be <= 255 characters. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).\n- Attorney Email: String, Required. Email must match an existing Perform member's email.\n- Billing Attorney Email: String, Required. Email must match an existing Perform member's email.\n- Start Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 6/30/2025. Blank persists as null on the entry.\n- End Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 10/5/2025. When both dates are present, End Date must be on or after Start Date. Blank persists as null on the entry.\n- Hours Billed: Integer or Float, Required. Must be between 0 and 999999.99.\n- Client Name: String, Required. Must be <= 255 characters. Together with Matter ID, identifies the Client Matter this entry rolls up to.\n- Description: String, Optional. Must be <= 500 characters. This is the matter's description, not a description of the individual attorney's work on that matter. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).\n"}},"required":["file"]}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MattersCSVUpsertResponse"}}},"description":"Matter entries upserted successfully. Response body reports created/updated counts."},"400":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"The CSV was rejected. Causes:\n- Unsupported file format or empty file.\n- File exceeds 10 MB or contains more than 10 000 rows.\n- One or more rows failed field-level validation.\n- Two rows share the same upsert key (Matter ID, Client Name,\n  Attorney Email).\n- Two rows share the same Client Matter (Matter ID, Client Name)\n  but disagree on any Client Matter field (Matter Name, Description).\nUse POST /matters-csv/validate-upsert to identify the specific rows\nand columns with errors."},"401":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Unauthorized"},"403":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Forbidden"},"404":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Review Cycle not found for the given reviewCycleId."},"409":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Conflict"},"500":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Upserts matter entries from a CSV file","tags":["matter-csv-controller"]}}},"components":{"schemas":{"MattersCSVUpsertResponse":{"type":"object","description":"Counts from a matter entry CSV upsert.","properties":{"created":{"type":"integer","format":"int32","description":"Number of matter entries newly created by this upsert."},"updated":{"type":"integer","format":"int32","description":"Number of existing matter entries whose stored fields were changed by this upsert. Rows whose non-key fields already matched the stored entry are not counted."}},"required":["created","updated"]}}}}
```

## Validates matters csv file

> Use POST /matters-csv/validate-upsert instead. This endpoint validates against the /upload-csv rules and does not flag duplicate upsert keys within a file. Returns row and column level errors for matters in csv file. Only performs validation and does not upload the file. Email errors occur if no members exist with the specified email.

```json
{"openapi":"3.1.0","info":{"title":"Flo Web API — Matters","version":"2.0.0"},"servers":[{"url":"https://florecruit.com/api/v2","description":"Flo production API"}],"paths":{"/matters-csv/validate-csv":{"post":{"deprecated":true,"description":"Use POST /matters-csv/validate-upsert instead. This endpoint validates against the /upload-csv rules and does not flag duplicate upsert keys within a file. Returns row and column level errors for matters in csv file. Only performs validation and does not upload the file. Email errors occur if no members exist with the specified email.","operationId":"validateMattersCSV","requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"Comma-separated value file. Must be less than 10MB in size.\nHeader row is required, and each subsequent row corresponds to one matter entry.\nHeader row must match: \"Matter ID,Matter Name,Attorney Email,Billing Attorney Email,Start Date,End Date,Hours Billed,Client Name,Description\"\nThe columns must follow these requirements:\n- Matter ID: String, Required. Together with Client Name, identifies the Client Matter this entry rolls up to. Multiple rows may share the same (Matter ID, Client Name) - one row per attorney working the matter; when they do, all Client Matter fields (Matter Name, Description) must be identical across those rows.\n- Matter Name: String, Required. Must be <= 255 characters. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).\n- Attorney Email: String, Required. Email must match an existing Perform member's email.\n- Billing Attorney Email: String, Required. Email must match an existing Perform member's email.\n- Start Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 6/30/2025. Blank persists as null on the entry.\n- End Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 10/5/2025. When both dates are present, End Date must be on or after Start Date. Blank persists as null on the entry.\n- Hours Billed: Integer or Float, Required. Must be between 0 and 999999.99.\n- Client Name: String, Required. Must be <= 255 characters. Together with Matter ID, identifies the Client Matter this entry rolls up to.\n- Description: String, Optional. Must be <= 500 characters. This is the matter's description, not a description of the individual attorney's work on that matter. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).\n"}},"required":["file"]}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MattersCSVValidationResponse"}}},"description":"Validation performed. Response includes rows numbers and column names that contain errors. Email errors occur if no members exist with the specified email."},"400":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Bad Request"},"401":{"content":{"*/*":{"schema":{"$ref":"#/components/schemas/MattersCSVValidationResponse"}}},"description":"Unauthorized"},"403":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Forbidden"},"404":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Not Found"},"409":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Conflict"},"500":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Validates matters csv file","tags":["matter-csv-controller"]}}},"components":{"schemas":{"MattersCSVValidationResponse":{"type":"object","properties":{"fileError":{"$ref":"#/components/schemas/MatterCSVFileError","description":"Present only when the whole file was rejected before per-row validation; null when the file parsed."},"numValidRows":{"type":"integer","format":"int32","description":"The number of valid rows in the CSV file."},"rowErrors":{"type":"array","description":"Details for each row containing errors. Empty if no rows have errors.","items":{"$ref":"#/components/schemas/MatterCSVRowErrors"}}}},"MatterCSVFileError":{"type":"object","properties":{"description":{"type":"string","description":"A human-readable explanation of the rejection, with enough detail to correct the file."},"rowNumber":{"type":"integer","format":"int32","description":"The file row that could not be parsed (the header is row 1). Populated only for COLUMN_COUNT_MISMATCH."},"type":{"type":"string","description":"The kind of file-level rejection.","enum":["INVALID_FILE_SIZE","INVALID_ROW_COUNT","COLUMN_COUNT_MISMATCH","UNREADABLE_FILE"]}},"required":["description","type"]},"MatterCSVRowErrors":{"type":"object","properties":{"columnsWithErrors":{"type":"array","description":"The column(s) that failed validation. Emails will fail validation if the addresses are not associated with existing perform member accounts.","items":{"type":"string"},"uniqueItems":true},"rowNumber":{"type":"integer","format":"int32","description":"The row number for the error."}}}}}}
```

## Pre-flight-validates a matter entry CSV for the upsert endpoint

> Validates the CSV against the same rules POST /matters-csv/upsert enforces.\
> Returns per-row, per-column error details. Does not persist any data.\
> Uniqueness of a matter entry within a review cycle is by the combined\
> (Matter ID, Client Name, Attorney Email) key.<br>

```json
{"openapi":"3.1.0","info":{"title":"Flo Web API — Matters","version":"2.0.0"},"servers":[{"url":"https://florecruit.com/api/v2","description":"Flo production API"}],"paths":{"/matters-csv/validate-upsert":{"post":{"description":"Validates the CSV against the same rules POST /matters-csv/upsert enforces.\nReturns per-row, per-column error details. Does not persist any data.\nUniqueness of a matter entry within a review cycle is by the combined\n(Matter ID, Client Name, Attorney Email) key.\n","operationId":"validateUpsertCSV","requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"Comma-separated value file. Must be less than 10MB in size.\nHeader row is required, and each subsequent row corresponds to one matter entry.\nHeader row must match: \"Matter ID,Matter Name,Attorney Email,Billing Attorney Email,Start Date,End Date,Hours Billed,Client Name,Description\"\nThe columns must follow these requirements:\n- Matter ID: String, Required. Together with Client Name, identifies the Client Matter this entry rolls up to. Multiple rows may share the same (Matter ID, Client Name) - one row per attorney working the matter; when they do, all Client Matter fields (Matter Name, Description) must be identical across those rows.\n- Matter Name: String, Required. Must be <= 255 characters. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).\n- Attorney Email: String, Required. Email must match an existing Perform member's email.\n- Billing Attorney Email: String, Required. Email must match an existing Perform member's email.\n- Start Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 6/30/2025. Blank persists as null on the entry.\n- End Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 10/5/2025. When both dates are present, End Date must be on or after Start Date. Blank persists as null on the entry.\n- Hours Billed: Integer or Float, Required. Must be between 0 and 999999.99.\n- Client Name: String, Required. Must be <= 255 characters. Together with Matter ID, identifies the Client Matter this entry rolls up to.\n- Description: String, Optional. Must be <= 500 characters. This is the matter's description, not a description of the individual attorney's work on that matter. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).\n"}},"required":["file"]}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MattersCSVValidationResponse"}}},"description":"Validation performed. Response includes row numbers and column names that contain errors."},"400":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Bad Request"},"401":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Unauthorized"},"403":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Forbidden"},"404":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Not Found"},"409":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Conflict"},"500":{"content":{"*/*":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Pre-flight-validates a matter entry CSV for the upsert endpoint","tags":["matter-csv-controller"]}}},"components":{"schemas":{"MattersCSVValidationResponse":{"type":"object","properties":{"fileError":{"$ref":"#/components/schemas/MatterCSVFileError","description":"Present only when the whole file was rejected before per-row validation; null when the file parsed."},"numValidRows":{"type":"integer","format":"int32","description":"The number of valid rows in the CSV file."},"rowErrors":{"type":"array","description":"Details for each row containing errors. Empty if no rows have errors.","items":{"$ref":"#/components/schemas/MatterCSVRowErrors"}}}},"MatterCSVFileError":{"type":"object","properties":{"description":{"type":"string","description":"A human-readable explanation of the rejection, with enough detail to correct the file."},"rowNumber":{"type":"integer","format":"int32","description":"The file row that could not be parsed (the header is row 1). Populated only for COLUMN_COUNT_MISMATCH."},"type":{"type":"string","description":"The kind of file-level rejection.","enum":["INVALID_FILE_SIZE","INVALID_ROW_COUNT","COLUMN_COUNT_MISMATCH","UNREADABLE_FILE"]}},"required":["description","type"]},"MatterCSVRowErrors":{"type":"object","properties":{"columnsWithErrors":{"type":"array","description":"The column(s) that failed validation. Emails will fail validation if the addresses are not associated with existing perform member accounts.","items":{"type":"string"},"uniqueItems":true},"rowNumber":{"type":"integer","format":"int32","description":"The row number for the error."}}}}}}
```
