Overview
Matter API
The Matter API ingests a CSV of matter entries into a Perform review cycle. Each row in the file represents one attorney's work on a matter for a client; entries with the same (Matter ID, Client Name) share a single Client Matter parent (its Matter Name and Description).
There are two operations — upsert and validate — each with a recommended endpoint and a deprecated legacy endpoint:
Write to a review cycle
POST /matters-csv/upsert
POST /matters-csv/upload-csv
Pre-flight validation
POST /matters-csv/validate-upsert
POST /matters-csv/validate-csv
Use /upsert and its pre-flight /validate-upsert. The legacy endpoints are retained for backwards compatibility only and behave differently in ways that make retries and re-uploads unsafe (see Choosing the right endpoint).
Pair the validation endpoint with the write endpoint it targets: /validate-upsert mirrors the rules /upsert enforces (including in-file duplicate detection), while /validate-csv mirrors the older /upload-csv rules and will not catch upsert-key duplicates within a file.
Choosing the right endpoint
POST /matters-csv/upsert (recommended)
POST /matters-csv/upload-csv (deprecated)
Idempotency
Safe to retry. Uploading the same CSV twice produces the same end state.
Not idempotent in the useful sense — rows whose Matter ID already exists on the cycle are silently skipped, so a corrected re-upload is a no-op for those rows.
Uniqueness key
(Matter ID, Client Name, Attorney Email) — one entry per attorney's work on a client matter.
(Matter ID, Matter Name, Attorney Email, Billing Attorney Email, Client Name) — the identifier the endpoint uses to decide whether a row already exists on the target review cycle.
Existing matches
Non-key fields (Matter Name, Billing Attorney Email, Start Date, End Date, Hours Billed, Description) are updated in place. The entry's id is preserved, so review-cycle assignments referencing the entry survive. Rows already matching the stored entry are no-ops and are not counted in updated.
Silently skipped. Corrections to Start Date, End Date, Hours Billed, or Description cannot be applied via re-upload — the existing entry is left untouched.
Shared Client Matter fields
Matter Name and Description refresh on the shared (Matter ID, Client Name) parent, updating every entry beneath it.
Not refreshed.
Duplicates within one file
Rejected. Two rows sharing the upsert key (Matter ID, Client Name, Attorney Email) fail validation regardless of whether other fields agree.
Exact-duplicate rows (every field identical) are silently collapsed to one entry. Rows sharing (Matter ID, Client Name, Attorney Email) but differing on non-key fields are not detected at the CSV validation layer.
Rows disagreeing on Client Matter fields
Rejected. Two rows with the same (Matter ID, Client Name) that disagree on Matter Name or Description fail validation.
Cross-row check is limited to Matter Name across rows sharing a Matter ID (regardless of Client Name) — disagreement rejects the file. Description is never compared across rows; Client Name is not compared across rows.
Response body
JSON: { created, updated }.
Empty on success.
Row-count limit
10,000 rows.
10,000 rows.
Companion pre-flight
POST /matters-csv/validate-upsert
POST /matters-csv/validate-csv
CSV file format
Applies to all four endpoints.
Encoding: UTF-8. A leading BOM is tolerated and stripped on read. Line endings may be LF or CRLF.
Size limit: file must be less than 10 MB.
Row limit: at most 10,000 data rows.
Header row: required. Column names must include all nine columns below. Matching is case-insensitive and column order does not matter — columns may appear in any order.
Escaping: field values containing a comma, double quote, or newline must be wrapped in double quotes. Embedded double quotes are escaped by doubling. Example — a
DescriptionofSmith, Jones & Associatesmust be written as"Smith, Jones & Associates".Whitespace: leading and trailing whitespace in field values is trimmed. Whitespace-only values are treated as absent — persisted as
nullfor optional columns; rejected as missing for required columns.Blank rows: fully blank rows are ignored.
Columns
Each subsequent row corresponds to one matter entry.
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.
Matter Name
String
Required
Max 255 characters. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).
Attorney Email
String
Required
Must match an existing Perform member's email.
Billing Attorney Email
String
Required
Must match an existing Perform member's email.
Start Date
Date
Optional
When present, format must be M/d/yyyy (e.g. 6/30/2025). Blank persists as null.
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.
Hours Billed
Integer or Float
Required
Between 0 and 999999.99 inclusive. Values are written in US format: . as the decimal separator, and an optional , as a thousands separator (e.g. 1,234.56). European-style formatting (1.234,56) is not accepted.
Client Name
String
Required
Max 255 characters. Together with Matter ID, identifies the Client Matter this entry rolls up to.
Description
String
Optional
Max 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).
POST /matters-csv/upsert
POST /matters-csv/upsertUpserts a matter entry for each row in the CSV. Recommended.
Uniqueness of a matter entry within a review cycle is by the combined (Matter ID, Client Name, Attorney Email) key.
Request
Content-Type:
multipart/form-dataQuery parameter:
reviewCycleId(UUID, required) — the review cycle to associate the upserted entries with.
Form parts:
file(required) — CSV file, format as above.
Behavior
If a matter entry with the composite key already exists in the cycle, its non-key fields are updated in place when they differ from the row. Rows whose non-key fields already match the stored entry are no-ops and are not counted in
updated. The entry's id is preserved, so any existing review-cycle assignments referencing the entry survive.If no such entry exists, a new one is created.
Multiple entries under the same
(Matter ID, Client Name)(differentAttorney Emails) are supported — they share a single Client Matter parent whose canonicalMatter NameandDescriptionrefresh on each upsert.
Idempotency
Uploading the same CSV twice produces the same end state — safe to retry.
Non-key fields updated on the entry:
Matter Name,Billing Attorney Email,Start Date,End Date,Hours Billed,Description.Matter NameandDescriptionalso refresh on the shared Client Matter parent; all existing entries under the same(Matter ID, Client Name)see the update.
Validation
All rows must pass validation or the entire upload is rejected — no partial commits.
Rows are rejected when:
Any field-level validation fails (see per-column rules above).
Two rows share the same upsert key
(Matter ID, Client Name, Attorney Email). Each matter entry must appear in the CSV at most once.Two rows share the same Client Matter
(Matter ID, Client Name)but disagree on any Client Matter field (Matter NameorDescription).
For pre-flight validation without committing, use POST /matters-csv/validate-upsert — it returns per-row, per-column error details.
Responses
200 OK — matter entries upserted successfully.
created
integer
Number of matter entries newly created by this upsert.
updated
integer
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.
400 Bad Request — the CSV was rejected. Causes:
Unsupported file format or empty file.
File exceeds 10 MB or contains more than 10,000 rows.
One or more rows failed field-level validation.
Two rows share the same upsert key
(Matter ID, Client Name, Attorney Email).Two rows share the same Client Matter
(Matter ID, Client Name)but disagree on any Client Matter field (Matter Name,Description).
Use POST /matters-csv/validate-upsert to identify the specific rows and columns with errors.
401 Unauthorized / 403 Forbidden — see Authentication.
404 Not Found — no review cycle exists for the given reviewCycleId.
POST /matters-csv/validate-upsert
POST /matters-csv/validate-upsertPre-flight-validates a matter entry CSV for the upsert endpoint. Does not persist any data.
Validates against the same rules /upsert enforces — including in-file uniqueness on (Matter ID, Client Name, Attorney Email) and Client Matter field agreement across rows sharing the same (Matter ID, Client Name).
Request
Content-Type:
multipart/form-dataForm parts:
file(required) — CSV file, format as above.
No reviewCycleId is required — validation is against the file's structure and rules, not against an existing cycle's contents.
Responses
200 OK — validation performed. Response includes row numbers and column names that contain errors.
numValidRows
integer
Number of rows in the CSV that pass all validation checks.
rowErrors
array
Details for each row containing errors. Empty if no rows have errors.
rowErrors[].rowNumber
integer
The row number containing the error.
rowErrors[].columnsWithErrors
array of string
Column names that failed validation. Email columns fail validation if the address is not associated with an existing Perform member account.
fileError
object, nullable
Present when a file-level constraint is violated. See File error object. null when there is no file-level error.
401 Unauthorized / 403 Forbidden — see Authentication.
File error object
When either validation endpoint (/validate-upsert or /validate-csv) reports a file-level error, the fileError field carries this shape:
type
string (enum)
One of the four error types below.
rowNumber
integer, nullable
Populated only for COLUMN_COUNT_MISMATCH — the 1-based number of the offending data row. null for the other types.
description
string
Human-readable description of the error.
Error types:
INVALID_FILE_SIZE— the file is empty, or larger than 10 MB.INVALID_ROW_COUNT— the file has zero data rows, or more than 10,000.COLUMN_COUNT_MISMATCH— a data row has a different field count than the header row (typically caused by an unescaped comma inside a field value).rowNumberidentifies the offending row.UNREADABLE_FILE— the file could not be parsed for another reason (malformed CSV, I/O read failure).
Example:
Deprecated endpoints
These endpoints are retained for backwards compatibility. Migrate callers to
/upsertand/validate-upsert.
POST /matters-csv/upload-csv (deprecated)
POST /matters-csv/upload-csv (deprecated)Use
POST /matters-csv/upsertinstead for idempotent behavior. This endpoint silently skips rows that already exist in the review cycle.
Creates a new matter entry for each row in the CSV. Rejects all rows if any single row contains errors.
Content-Type:
multipart/form-dataQuery parameter:
reviewCycleId(UUID, required) — the review cycle to associate the uploaded matters with.Form parts:
file(required) — CSV file, format as above.
Behavioral differences from /upsert:
Rows whose full identifier
(Matter ID, Matter Name, Attorney Email, Billing Attorney Email, Client Name)matches an existing matter entry on the target review cycle are silently skipped. Rows differing on any of those five fields are treated as new entries.Exact-duplicate rows within one file are silently collapsed to one entry. Rows sharing
(Matter ID, Client Name, Attorney Email)but differing on non-key fields are not detected at the CSV validation layer.Cross-row check across the file is limited to
Matter Name— rows sharing aMatter IDmust agree onMatter Nameor the file is rejected.DescriptionandClient Nameare not compared across rows.No response body on success.
Responses:
200 OK— matters created successfully (no response body).400 Bad Request— invalid CSV file format, or CSV file contains one or more invalid rows. Use/validate-csvfor row-level error details.401 Unauthorized/403 Forbidden— see Authentication.404 Not Found— review cycle not found.
POST /matters-csv/validate-csv (deprecated)
POST /matters-csv/validate-csv (deprecated)Use
POST /matters-csv/validate-upsertinstead.
Pre-flight-validates a matter entry CSV for the /upload-csv endpoint. Does not persist any data.
Validation rules differ from /validate-upsert: this endpoint validates against the /upload-csv rules only — it does not flag duplicate upsert keys within a file, and it does not check that rows sharing (Matter ID, Client Name) agree on Description. Do not use this endpoint as a pre-flight for /upsert.
Request — same shape as /validate-upsert.
Responses — same shape as /validate-upsert.
Authentication
All four endpoints require an authenticated Perform administrator. Unauthenticated requests return 401 Unauthorized; authenticated non-admin callers return 403 Forbidden.
Last updated

