# Quilia API - **OpenAPI Version:** `3.1.1` - **API Version:** `2026.4.0` The Quilia API lets you manage cases, clients, messages, and more from your own applications. ## Looking to connect an AI assistant? If you want to point Claude, Cursor, Claude Code, or another MCP-compatible AI assistant at your firm, you do not need this reference. There is a one-click sign-in flow at and a step-by-step walkthrough at **[quilia.com/support/firms/connect-ai-assistant](https://www.quilia.com/support/firms/connect-ai-assistant/)**. No API key required. This API reference is for developers building their own integration against Quilia using an Unkey-issued API key. ## REST API All endpoints below are available at `https://api.quilia.dev/v2`. **Authentication:** Include your API key in the `Authorization` header: ```curl curl -X GET https://api.quilia.dev/v2/cases \ -H "Authorization: Bearer your-api-key" ``` Need an API key? Create one at [portal.quilia.com/settings/integrations/api](https://portal.quilia.com/settings/integrations/api). ## Markdown reference for AI tools Need the full API reference as markdown for AI tools that don't support MCP? **[Download as Markdown →](/v2/llms.txt)** ## Changelog See the [API Changelog](https://github.com/quilia/portal/blob/development/apps/functions/CHANGELOG.md) for version history, breaking changes, and deprecations. ## Operations ### List appointments - **Method:** `GET` - **Path:** `/appointments` - **Tags:** appointments Retrieves a paginated list of appointments for the authenticated organization. Appointments can be filtered by user ID, case ID, or both. Returns appointment details including scheduling information, associations, notes, and status with configurable limit and offset for pagination. #### Parameters ##### `limit` - **In:** `query` The maximum number of appointments to return per page `[ "number", "null" ]`, default: `10` — The maximum number of appointments to return per page ##### `offset` - **In:** `query` The number of appointments to skip before starting to return results `[ "number", "null" ]`, default: `0` — The number of appointments to skip before starting to return results ##### `userId` - **In:** `query` Filter appointments by user ID. Returns only appointments assigned to this user. `string` — Filter appointments by user ID. Returns only appointments assigned to this user. ##### `caseId` - **In:** `query` Filter appointments by case ID. Returns only appointments associated with this case. `string` — Filter appointments by case ID. Returns only appointments associated with this case. #### Responses ##### Status: 200 List of appointments retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` — Array of appointments matching the filter criteria **Items:** - **`caseId` (required)** `string | null` — The case ID associated with the appointment - **`contactId` (required)** `string | null` — The contact/people ID associated with the appointment - **`created_at` (required)** `string | null` — ISO timestamp when the appointment was created - **`created_by` (required)** `string | null` — The user ID of who created the appointment - **`end` (required)** `string | null` — ISO timestamp for the appointment end time - **`id` (required)** `string` — The unique identifier of the appointment - **`notes` (required)** `string | null` — Additional notes or details about the appointment - **`start` (required)** `string` — ISO timestamp for the appointment start time - **`status` (required)** `string`, possible values: `"scheduled", "confirmed", "rescheduled", "cancelled", "canceled", "released"` — The current status of the appointment - **`updated_at` (required)** `string | null` — ISO timestamp when the appointment was last updated - **`userId` (required)** `string | null` — The user ID associated with the appointment - **`message` (required)** `string` — Success message confirming the list operation - **`pagination` (required)** `object` - **`has_more` (required)** `boolean` — Whether there is more data to fetch - **`limit` (required)** `number` — Maximum number of items returned - **`next` (required)** `string | null` — URL for the next page of items, if available - **`offset` (required)** `number` — Number of items skipped - **`previous` (required)** `string | null` — URL for the previous page of items, if available - **`total` (required)** `number` — Total number of items available **Example:** ```json { "message": "", "data": [ { "id": "", "caseId": null, "contactId": null, "userId": null, "start": "", "end": null, "notes": null, "status": "scheduled", "created_at": null, "updated_at": null, "created_by": null } ], "pagination": { "total": 1, "offset": 1, "limit": 1, "has_more": true, "previous": null, "next": null } } ``` ### Create a new appointment - **Method:** `POST` - **Path:** `/appointments` - **Tags:** appointments Creates an appointment on a case, or updates the one you already sent. **Send `external_id`.** It is your own id for the appointment, and it is the match key: a repeat call with the same `external_id` on the same case updates that appointment rather than creating a second copy, and the response echoes it back so you can map our id to yours. Without it every call inserts, which is how 189 duplicate appointments accumulated on one firm. The response `action` field says which happened. #### Request Body ##### Content-Type: application/json - **`caseId` (required)** `string`, format: `uuid` — The case ID to associate with the appointment. Links the appointment to a specific legal case or matter. - **`start` (required)** `string` — ISO 8601 timestamp for when the appointment starts. A date-only value (\`YYYY-MM-DD\`) is accepted and is the correct form for a medical visit where the source records only the day: the date is preserved and no time is shown to the client or used to schedule reminders. Send the date-only form rather than padding it to midnight — do not omit the field or skip the record. - **`contactId`** `string`, format: `uuid` — The contact/people ID associated with the appointment. Identifies the person involved in the appointment. Optional for legal appointments. - **`end`** `string` — ISO 8601 timestamp for when the appointment ends. Required for legal appointments. Accepts the same date-only form as \`start\`; omit it entirely for a medical visit with no recorded time. - **`external_id`** `string` — Your own id for this appointment. The match key: send it and a repeat call updates the appointment instead of creating a second one. Unique per case. Omit it and every call creates a new appointment. - **`notes`** `string` — Additional notes or details about the appointment. Can include agenda items, special instructions, or other relevant information. - **`title`** `string` — Title/name of the appointment. Recommended for legal appointments. - **`type`** `string`, possible values: `"medical", "legal"` — Type of appointment: 'medical' for medical appointments, 'legal' for legal calendar events. - **`userId`** `string`, format: `uuid` — The user ID to assign the appointment to. If provided, this user will be responsible for the appointment. **Example:** ```json { "caseId": "", "external_id": "", "contactId": "", "start": "", "userId": "", "end": "", "notes": "", "type": "medical", "title": "" } ``` #### Responses ##### Status: 200 Appointment created successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`id` (required)** `string` — The unique identifier of the appointment - **`start` (required)** `string` — ISO timestamp for the appointment start time - **`action`** `string`, possible values: `"created", "updated"` — Whether a new appointment was written or the one matching your \`external\_id\` was updated. - **`external_id`** `string | null` — Your own id for this appointment, echoed back so you can map our id to yours. - **`message` (required)** `string` — Success message **Example:** ```json { "data": { "id": "", "external_id": null, "start": "", "action": "created" }, "message": "" } ``` ### Retrieve an appointment - **Method:** `GET` - **Path:** `/appointments/{id}` - **Tags:** appointments Retrieves detailed information about a specific appointment by its ID. Returns comprehensive appointment details including case, contact, timing, notes, and status. Only appointments belonging to the authenticated organization can be retrieved. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the appointment to retrieve `string` — The unique identifier of the appointment to retrieve #### Responses ##### Status: 200 Appointment retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`caseId` (required)** `string | null` — The case ID associated with the appointment - **`contactId` (required)** `string | null` — The contact/people ID associated with the appointment - **`created_at` (required)** `string | null` — ISO timestamp when the appointment was created - **`created_by` (required)** `string | null` — The user ID of who created the appointment - **`end` (required)** `string | null` — ISO timestamp for the appointment end time - **`id` (required)** `string` — The unique identifier of the appointment - **`notes` (required)** `string | null` — Additional notes or details about the appointment - **`start` (required)** `string` — ISO timestamp for the appointment start time - **`status` (required)** `string`, possible values: `"scheduled", "confirmed", "rescheduled", "cancelled", "canceled", "released"` — The current status of the appointment - **`updated_at` (required)** `string | null` — ISO timestamp when the appointment was last updated - **`userId` (required)** `string | null` — The user ID associated with the appointment - **`message` (required)** `string` — Success message **Example:** ```json { "message": "", "data": { "id": "", "caseId": null, "contactId": null, "userId": null, "start": "", "end": null, "notes": null, "status": "scheduled", "created_at": null, "updated_at": null, "created_by": null } } ``` ### Update appointment information - **Method:** `PATCH` - **Path:** `/appointments/{id}` - **Tags:** appointments Updates specific fields of an existing appointment. Allows modification of scheduling details, associated case, contact, notes, and status while maintaining data integrity. Only appointments belonging to the authenticated organization can be updated. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the appointment to update `string` — The unique identifier of the appointment to update #### Request Body ##### Content-Type: application/json - **`caseId`** `string`, format: `uuid` — The case ID to associate with the appointment - **`contactId`** `string`, format: `uuid` — The contact/people ID to associate with the appointment - **`end`** `string` — ISO timestamp for the appointment end time. Accepts the same date-only form as \`start\`. - **`notes`** `string` — Additional notes or details about the appointment - **`start`** `string` — ISO timestamp for the appointment start time. A date-only value (\`YYYY-MM-DD\`) is accepted, same as on create, for a medical visit where the source records only the day. - **`status`** `string`, possible values: `"scheduled", "confirmed", "rescheduled", "cancelled", "canceled", "released"` — The status of the appointment - **`userId`** `string`, format: `uuid` — The user ID to assign the appointment to **Example:** ```json { "caseId": "", "contactId": "", "userId": "", "start": "", "end": "", "notes": "", "status": "scheduled" } ``` #### Responses ##### Status: 200 Appointment updated successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`id` (required)** `string` — The unique identifier of the updated appointment - **`start` (required)** `string` — The new start date of the updated appointment - **`message` (required)** `string` — Success message **Example:** ```json { "message": "", "data": { "id": "", "start": "" } } ``` ### Delete an appointment - **Method:** `DELETE` - **Path:** `/appointments/{id}` - **Tags:** appointments Permanently deletes an appointment from the organization's records. This action is irreversible and will remove all appointment data including scheduling details, notes, and associations. Only appointments belonging to the authenticated organization can be deleted. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the appointment to delete `string` — The unique identifier of the appointment to delete #### Responses ##### Status: 200 Appointment successfully deleted from the organization's records ###### Content-Type: application/json - **`message` (required)** `string` — Success message confirming the deletion **Example:** ```json { "message": "" } ``` ### List the firm's audiences - **Method:** `GET` - **Path:** `/audiences` - **Tags:** audiences Returns every audience the firm has, with the conditions that define it. An audience is a named, reusable definition of who a message goes to — membership resolves live from the conditions rather than being a stored list, so an audience picks up clients who qualify later. Use this to reuse an existing audience on an automation instead of creating a near-duplicate. #### Responses ##### Status: 200 Audiences retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` **Items:** - **`created_at` (required)** `string` - **`description` (required)** `string | null` - **`groups` (required)** `array` **Items:** - **`conditions` (required)** `array` **Items:** - **`field` (required)** `string` - **`operator` (required)** `string` - **`values` (required)** `array` **Items:** `string` - **`id` (required)** `string` - **`is_system` (required)** `boolean` — Seeded by Quilia (e.g. Active Cases) and not deletable - **`name` (required)** `string` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": [ { "id": "", "name": "", "description": null, "is_system": true, "created_at": "", "groups": [ { "conditions": [ { "field": "", "operator": "", "values": [ "" ] } ] } ] } ] } ``` ### Create an audience - **Method:** `POST` - **Path:** `/audiences` - **Tags:** audiences Creates a reusable audience: a named definition of who a message goes to, resolved live at send time rather than stored as a fixed list of people. The shape is one level deep. A person matches the audience if they match ANY group; within a group, ALL conditions must match; multiple values inside one condition are OR. Groups never nest — that is where this stops being a filter and becomes a query language. Read GET /v2/audiences/options first. Case phases and case types are per-firm strings, and a value the firm has no data for is not an error — the audience just matches nobody. Important: when building an audience that filters by `case_phase`, you should almost always include a condition for `case_status is open, invited` in the same group. Without this, the audience will match closed clients who happen to still carry that phase on their closed case. The only exception is if the phase is explicitly a closing phase (e.g. "Closed" or "Archived"). #### Request Body ##### Content-Type: application/json - **`groups` (required)** `array` — A person matches the audience if they match ANY group; within a group, ALL conditions must match. One level only — groups never nest. **Items:** - **`conditions` (required)** `array` — Conditions within one group are ANDed together. **Items:** - **`field` (required)** `string`, possible values: `"everyone", "case_status", "case_phase", "case_type"` — What to match on. "everyone" is the always-true condition — use it to say "every client, case or not" explicitly rather than by leaving the audience empty. - **`operator`** `string`, possible values: `"is", "is_not"`, default: `"is"` — "is" matches the values, "is\_not" excludes them. - **`values`** `array`, default: `[]` — The values to match. Multiple values are OR — "case\_status is open, closed" matches either. Must be empty for "everyone", and non-empty for every other field. Pull valid case\_phase / case\_type values from GET /v2/audiences/options; a value the firm has no data for matches nobody and reports no error. **Items:** `string` - **`name` (required)** `string` — What the firm calls this audience - **`description`** `string | null` — Optional note about who this is for **Example:** ```json { "name": "", "description": null, "groups": [ { "conditions": [ { "field": "everyone", "operator": "is", "values": [] } ] } ] } ``` #### Responses ##### Status: 200 Audience created successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`created_at` (required)** `string` - **`description` (required)** `string | null` - **`groups` (required)** `array` **Items:** - **`conditions` (required)** `array` **Items:** - **`field` (required)** `string` - **`operator` (required)** `string` - **`values` (required)** `array` **Items:** `string` - **`id` (required)** `string` - **`is_system` (required)** `boolean` — Seeded by Quilia (e.g. Active Cases) and not deletable - **`name` (required)** `string` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "name": "", "description": null, "is_system": true, "created_at": "", "groups": [ { "conditions": [ { "field": "", "operator": "", "values": [ "" ] } ] } ] } } ``` ### Values an audience condition can use - **Method:** `GET` - **Path:** `/audiences/options` - **Tags:** audiences The values this firm can build audience conditions on. Read this BEFORE creating an audience: case phases are per-firm strings — the firm's configured phase catalog plus any phase its cases still carry, so a phase with no cases in it today is still offered because an audience resolves when it is sent rather than when it is built. A guessed value is not rejected — it simply matches nobody, and the audience quietly resolves to zero people. Case types are the ones the firm has switched on in Settings → Case Types, which is not the same as the ones it currently has cases in; a type it has deactivated is not offered here even if old cases carry it. Case statuses are the full set rather than the ones in use, because a firm with no archived cases can still build an audience on "archived". #### Responses ##### Status: 200 Options retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_phase` (required)** `array` — Phases present in this firm's own cases **Items:** `string` - **`case_status` (required)** `array` — Every valid case status — not only the ones in use **Items:** `string` - **`case_type` (required)** `array` — Case types this firm has switched on in Settings → Case Types — not the ones it happens to have cases in **Items:** `string` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "case_status": [ "" ], "case_phase": [ "" ], "case_type": [ "" ] } } ``` ### Create a picked list - **Method:** `POST` - **Path:** `/audiences/picked-lists` - **Tags:** audiences Creates a fixed list of clients. Unlike an audience, membership does not change unless the list is edited. Use this for a one-person automation or a small hand-maintained recipient list. #### Request Body ##### Content-Type: application/json - **`name` (required)** `string` — What the firm calls this picked list - **`user_ids` (required)** `array` — Client user ids to place on the fixed list. Use GET /v2/clients to find them. **Items:** `string`, format: `uuid` **Example:** ```json { "name": "", "user_ids": [ "" ] } ``` #### Responses ##### Status: 200 Picked list created successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`created_at` (required)** `string` - **`id` (required)** `string`, format: `uuid` - **`members` (required)** `array` **Items:** - **`name` (required)** `string | null` - **`user_id` (required)** `string`, format: `uuid` - **`name` (required)** `string` - **`user_ids` (required)** `array` **Items:** `string`, format: `uuid` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "name": "", "user_ids": [ "" ], "members": [ { "user_id": "", "name": null } ], "created_at": "" } } ``` ### List picked lists - **Method:** `GET` - **Path:** `/audiences/picked-lists` - **Tags:** audiences Returns the firm's fixed recipient lists. Use an id as group\_id when creating or updating an automation. #### Responses ##### Status: 200 Picked lists retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` **Items:** - **`created_at` (required)** `string` - **`id` (required)** `string`, format: `uuid` - **`members` (required)** `array` **Items:** - **`name` (required)** `string | null` - **`user_id` (required)** `string`, format: `uuid` - **`name` (required)** `string` - **`user_ids` (required)** `array` **Items:** `string`, format: `uuid` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": [ { "id": "", "name": "", "user_ids": [ "" ], "members": [ { "user_id": "", "name": null } ], "created_at": "" } ] } ``` ### Edit an audience's name, description or conditions - **Method:** `PATCH` - **Path:** `/audiences/{id}` - **Tags:** audiences Changes what an audience is called or who it matches. Anything you leave out is left as it is. **Conditions are replaced, not merged.** Send `groups` and it becomes the whole definition — there is no way to add one condition to an existing group, because conditions have no stable id to address. Read the audience first (GET /v2/audiences), change what you need in the shape it gives you, and send the result back. **This changes who future sends reach, including for automations already running.** Membership is resolved at send time, not stored, so every automation pointing at this audience starts using the new definition on its next run. Messages already queued keep the recipients they were queued with — those were resolved when they were scheduled. Read GET /v2/audiences/options before changing conditions. Case phases are per-firm strings, and case types are the ones the firm has switched on in Settings → Case Types; a value outside those lists is not an error, the audience just stops matching anybody. #### Parameters ##### `id` required - **In:** `path` The audience id `string`, format: `uuid` — The audience id #### Request Body ##### Content-Type: application/json - **`description`** `string | null` — Optional note about who this is for; null clears it - **`groups`** `array` — Replaces the audience definition wholesale. A person matches if they match ANY group; within a group, ALL conditions must match. **Items:** - **`conditions` (required)** `array` — Conditions within one group are ANDed together. **Items:** - **`field` (required)** `string`, possible values: `"everyone", "case_status", "case_phase", "case_type"` — What to match on. "everyone" is the always-true condition — use it to say "every client, case or not" explicitly rather than by leaving the audience empty. - **`operator`** `string`, possible values: `"is", "is_not"`, default: `"is"` — "is" matches the values, "is\_not" excludes them. - **`values`** `array`, default: `[]` — The values to match. Multiple values are OR — "case\_status is open, closed" matches either. Must be empty for "everyone", and non-empty for every other field. Pull valid case\_phase / case\_type values from GET /v2/audiences/options; a value the firm has no data for matches nobody and reports no error. **Items:** `string` - **`name`** `string` — What the firm calls this audience **Example:** ```json { "name": "", "description": null, "groups": [ { "conditions": [ { "field": "everyone", "operator": "is", "values": [] } ] } ] } ``` #### Responses ##### Status: 200 Audience updated ###### Content-Type: application/json - **`data` (required)** `object` - **`created_at` (required)** `string` - **`description` (required)** `string | null` - **`groups` (required)** `array` **Items:** - **`conditions` (required)** `array` **Items:** - **`field` (required)** `string` - **`operator` (required)** `string` - **`values` (required)** `array` **Items:** `string` - **`id` (required)** `string` - **`is_system` (required)** `boolean` — Seeded by Quilia (e.g. Active Cases) and not deletable - **`name` (required)** `string` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "name": "", "description": null, "is_system": true, "created_at": "", "groups": [ { "conditions": [ { "field": "", "operator": "", "values": [ "" ] } ] } ] } } ``` ### Get current API key information - **Method:** `GET` - **Path:** `/auth/whoami` - **Tags:** auth Returns information about the currently authenticated API key. #### Responses ##### Status: 200 API key information retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`email` (required)** `string | null` — The email of the authenticated user. Populated for OAuth-issued JWTs; null for Unkey API keys (which do not carry email). - **`name` (required)** `string` — The name of the user who created this API key - **`org_id` (required)** `string`, format: `uuid` — The UUID of the organization - **`organization` (required)** `string` — The name of the organization associated with the API key - **`user_id` (required)** `string | null`, format: `uuid` — The UUID of the user who created this API key, or null for legacy keys - **`message` (required)** `string` — Success message confirming the authentication status **Example:** ```json { "message": "", "data": { "user_id": null, "name": "", "email": null, "organization": "", "org_id": "" } } ``` ### List the firm's automations - **Method:** `GET` - **Path:** `/automations` - **Tags:** automations Returns every automation the firm has, paused ones included — `active: false` is paused. Worth reading before creating one: an automation that duplicates an existing rule means clients get the same message twice. #### Responses ##### Status: 200 Automations retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` **Items:** - **`action` (required)** `string` — What this automation does: "send\_message" sends the subject and body; "ask\_feedback" asks for an NPS score and has no message copy of its own, so an empty subject and body are correct for it. - **`active` (required)** `boolean` — False means paused: it exists, and it sends nothing - **`anchor_field` (required)** `string` - **`anchor_offset` (required)** `object` - **`unit` (required)** `string` - **`value` (required)** `number` — Signed: negative is before the anchor - **`audience_id` (required)** `string | null` - **`channels` (required)** `object` — What this automation actually sends on. Push is always on and is not listed. A firm that never chose gets the default, so this is never empty. - **`email` (required)** `boolean` - **`sms` (required)** `string`, possible values: `"always", "fallback", "never"` - **`created_at` (required)** `string` - **`custom_send_time` (required)** `string | null` - **`group_id` (required)** `string | null` - **`id` (required)** `string` - **`message_body` (required)** `string | null` - **`message_body_markdown` (required)** `string` — The base copy as markdown, ready to send straight back to PATCH. Empty for an automation stored with no blocks. - **`message_subject` (required)** `string | null` - **`name` (required)** `string` - **`recurrence` (required)** `object | null` - **`frequency` (required)** `number | null` - **`frequency_type` (required)** `string | null` - **`translations` (required)** `array` — Every language this automation has copy for. Sending \`translations\` on an update REPLACES this whole set, so read it first and send it back complete — omitting a language deletes it. **Items:** - **`language` (required)** `string` - **`message_body` (required)** `string` — Markdown, ready to send straight back to PATCH - **`message_subject` (required)** `string | null` - **`stale` (required)** `boolean | null` — True once the English has been edited since this was written, so it now says something different. Null for translations written before the fingerprint existed — unknown, rather than a guess in either direction. - **`message` (required)** `string` **Example:** ```json { "message": "", "data": [ { "id": "", "name": "", "audience_id": null, "group_id": null, "action": "", "anchor_field": "", "anchor_offset": { "value": 1, "unit": "" }, "custom_send_time": null, "recurrence": { "frequency_type": null, "frequency": null }, "message_subject": null, "message_body": null, "message_body_markdown": "", "translations": [ { "language": "", "message_subject": null, "message_body": "", "stale": null } ], "channels": { "sms": "always", "email": true }, "active": true, "created_at": "" } ] } ``` ### Create an automation (paused) - **Method:** `POST` - **Path:** `/automations` - **Tags:** automations Creates an automation: a standing rule that turns a date into a message. Who (an audience), when (an anchor date plus an offset), how often, what to say, and how it reaches them (`channels`). **By default it sends on all three channels** — push, SMS and email. Push is always on and cannot be turned off; `channels` is how you narrow the other two. **By default it sends as the firm.** `send_as` sets whose name the client sees; pass a staff id to sign it as a person. This endpoint used to stamp the calling user as the sender with no way to say otherwise, so automations created over the API went out signed by whoever built them. **It is created paused.** An automation built over the API sends nothing until a human turns it on in the portal — the Automations list has the control. A wrong audience or a wrong offset is not a typo here, it is a message to every client who matches, so the last step stays with a person. Once it is switched on, the daily sweep picks it up and queues sends for everyone whose anchor date is still ahead of them. It never reaches backwards: a client whose anchor already passed is skipped, which is what stops switching on a "when the case closes" automation from messaging a firm's entire history. **Requires an organization admin.** An automation is a standing rule that messages the firm's clients, so it is gated like org settings and team management. #### Request Body ##### Content-Type: application/json - **`anchor` (required)** `string`, possible values: `"case_opened", "case_closed", "case_invited", "case_phase_changed", "client_birthday"` — The date to count from. case\_phase\_changed is per person per case, so co-clients on one case can trigger at different times. client\_birthday recurs every year; the others happen once. - **`message_body` (required)** `string` — The message, as MARKDOWN. Supported: paragraphs, \*\*bold\*\*, \*italic\*, \[links]\(https\://…) and bullet or numbered lists, which may be nested by indenting two spaces. Headings, images, tables, block quotes and raw HTML are rejected rather than passed through as literal characters. Merge variables: {{client\_first\_name}}, {{client\_last\_name}}, {{client\_full\_name}}, {{sender\_first\_name}}, {{sender\_last\_name}}, {{sender\_full\_name}}, {{firm\_name}}. They are filled per recipient at send time. Anything else in double braces is left as literal text in front of the client. To link into the Quilia app use https\://quilia.com/app/home, /app/inbox, /app/suggestions, /app/appointments (their treatment review), /app/case/check-in, /app/case/case-note, /app/case/wage-loss, /app/providers, /app/documents or /app/chat. These open the app for clients who have it and the web for everyone else. Three of them are conditional, and none of them tells you when it misses: two open the app on Home, exactly like an unrecognised path, and the third opens a screen the client has no other way to reach. /app/chat is CaseChat, the two-way conversation, and only works if the firm has case\_chat\_enabled on. Ask a Question is a DIFFERENT feature on its own toggle, ask\_question\_enabled — it sits on the firm's Organization screen and has no deep link, so name it rather than linking it. Both flags come back in client\_features from get\_firm\_settings, and a firm may have either, both or neither, so read them rather than guessing. /app/providers is the Doctor List, and works only on case types with a providers screen; case types such as family law, criminal defense and landlord tenant do not have one. /app/case/wage-loss is where a client records time off work, and it holds the employer and doctor verification forms almost nobody finds unless you send them there. It is available only on case types configured with the wage-loss feature, and it fails the OTHER way: on unsupported case types the link opens the screen anyway, empty and reachable by no other route, so check the audience case types for that feature rather than leaving it to the link. Its NAME also moves with the case type — Lost Income on most, Wages on workers compensation, Employment on social security disability, product liability and wc third party — so name the screen only when the audience is one case type, and otherwise write what it is for. /app/case/case-note is Notes, the client's own running journal of their case, and it is the one destination on every single case type, so it is always safe to link. /app/documents is Files, which holds only what the client uploaded, so link it to ask for something and never to offer a look at what the firm has. No other path works — an unrecognised one opens the app on Home with no error, so do not invent one. - **`message_subject` (required)** `string` — The subject line clients see. Merge variables: {{client\_first\_name}}, {{client\_last\_name}}, {{client\_full\_name}}, {{sender\_first\_name}}, {{sender\_last\_name}}, {{sender\_full\_name}}, {{firm\_name}}. They are filled per recipient at send time. Anything else in double braces is left as literal text in front of the client. - **`name` (required)** `string` — What the firm calls this automation in the Automations list - **`audience_id`** `string`, format: `uuid` — The audience to send to. Create one with POST /v2/audiences or reuse one from GET /v2/audiences. Exactly one of audience\_id or group\_id is required. - **`channels`** `object` — Which channels this automation sends on. Push is always on and is not configurable — the app is the only client surface, so every automation pushes. Omit this object entirely to send on all three, which is the standing default. - **`email`** `boolean` — Whether to email as well. Defaults to true. - **`sms`** `string`, possible values: `"always", "fallback", "never"` — "always" texts every client with a phone number; "fallback" texts only the ones the app cannot reach; "never" texts nobody. Defaults to "always". - **`direction`** `string`, possible values: `"before", "on", "after"`, default: `"on"` — "on" sends at the anchor itself and ignores the offset; "before" and "after" apply it. - **`group_id`** `string`, format: `uuid` — A picked list of people (the older model) instead of an audience. Prefer audience\_id — a list is a fixed set of people and never picks up clients who qualify later. - **`offset_unit`** `string`, possible values: `"days", "weeks", "months", "years"`, default: `"days"` — Offset unit - **`offset_value`** `integer`, default: `0` — How far from the anchor, as a positive number - **`repeat`** `string`, possible values: `"once", "interval", "yearly"`, default: `"once"` — "once" sends a single message per person; "interval" repeats every offset (e.g. every 30 days); "yearly" repeats annually. - **`send_as`** `string` — Who the message comes from, as the client sees it. "firm" (the default) sends as the firm itself. Otherwise the id of a member of staff at this firm — clients then see the message from that person by name. Client ids, staff at other firms, and Quilia support (wizard) accounts are rejected: a name on a client's message has to be someone who actually works at the firm. - **`send_time`** `string`, default: `"09:00"` — Time of day to send, 24-hour, in the recipient's own time zone. Sends are still held to the firm's allowed send window. - **`translations`** `array` — Per-language copy, written by the caller. Clients reading a language listed here receive that copy; everyone else receives the base body, so a language you leave out costs nobody their message. Do not include "en" — the base body is the English. **Items:** - **`language` (required)** `string`, possible values: `"es", "pt", "ht", "km", "ko", "th", "tl", "vi", "zh_CN", "zh_TW", "ru", "hy", "ar", "fa", "de", "pl", "it", "ja", "sr", "fr", "hmn", "mn", "hi", "pa"` — Language code this copy is for, e.g. "es". The full set is every language Quilia speaks; "en" is not one of them, because the base body is the English. - **`message_body` (required)** `string` — Body in this language, in the same markdown subset as the base - **`message_subject`** `string` — Subject in this language. Falls back to the base subject. **Example:** ```json { "name": "", "audience_id": "", "group_id": "", "anchor": "case_opened", "direction": "on", "offset_value": 0, "offset_unit": "days", "send_time": "09:00", "repeat": "once", "message_subject": "", "message_body": "", "channels": { "sms": "always", "email": true }, "send_as": "", "translations": [ { "language": "es", "message_subject": "", "message_body": "" } ] } ``` #### Responses ##### Status: 200 Automation created successfully, paused ###### Content-Type: application/json - **`data` (required)** `object` - **`action` (required)** `string` — What this automation does: "send\_message" sends the subject and body; "ask\_feedback" asks for an NPS score and has no message copy of its own, so an empty subject and body are correct for it. - **`active` (required)** `boolean` — False means paused: it exists, and it sends nothing - **`anchor_field` (required)** `string` - **`anchor_offset` (required)** `object` - **`unit` (required)** `string` - **`value` (required)** `number` — Signed: negative is before the anchor - **`audience_id` (required)** `string | null` - **`channels` (required)** `object` — What this automation actually sends on. Push is always on and is not listed. A firm that never chose gets the default, so this is never empty. - **`email` (required)** `boolean` - **`sms` (required)** `string`, possible values: `"always", "fallback", "never"` - **`created_at` (required)** `string` - **`custom_send_time` (required)** `string | null` - **`group_id` (required)** `string | null` - **`id` (required)** `string` - **`message_body` (required)** `string | null` - **`message_body_markdown` (required)** `string` — The base copy as markdown, ready to send straight back to PATCH. Empty for an automation stored with no blocks. - **`message_subject` (required)** `string | null` - **`name` (required)** `string` - **`recurrence` (required)** `object | null` - **`frequency` (required)** `number | null` - **`frequency_type` (required)** `string | null` - **`translations` (required)** `array` — Every language this automation has copy for. Sending \`translations\` on an update REPLACES this whole set, so read it first and send it back complete — omitting a language deletes it. **Items:** - **`language` (required)** `string` - **`message_body` (required)** `string` — Markdown, ready to send straight back to PATCH - **`message_subject` (required)** `string | null` - **`stale` (required)** `boolean | null` — True once the English has been edited since this was written, so it now says something different. Null for translations written before the fingerprint existed — unknown, rather than a guess in either direction. - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "name": "", "audience_id": null, "group_id": null, "action": "", "anchor_field": "", "anchor_offset": { "value": 1, "unit": "" }, "custom_send_time": null, "recurrence": { "frequency_type": null, "frequency": null }, "message_subject": null, "message_body": null, "message_body_markdown": "", "translations": [ { "language": "", "message_subject": null, "message_body": "", "stale": null } ], "channels": { "sms": "always", "email": true }, "active": true, "created_at": "" } } ``` ### Edit an automation's copy or channels, or pause it - **Method:** `PATCH` - **Path:** `/automations/{id}` - **Tags:** automations Changes what an automation says, which channels it sends on, or pauses it. Anything you leave out is left as it is. **A running automation's pending sends are rewritten too.** Both the copy and the channels are stamped onto each message when it is queued, so an edit that only touched the automation would leave everyone already in the queue receiving the old wording on the old channels. This updates both. **The schedule and audience can be changed, but only before anything has been queued.** `anchor`, `direction`, `offset_value`, `offset_unit`, `repeat`, `send_time`, `audience_id` and `group_id` are accepted while the automation is paused and has no scheduled sends — which is every automation this API creates, right up until a human switches it on. There is no queue yet, so there is nothing to rebuild. Once it has queued anything, schedule changes and audience swaps require rebuilding the queue, which stays in the portal. **It cannot turn an automation on.** `active` accepts only `false`. Activation queues sends to every matching client, which is the one step `POST /v2/automations` deliberately leaves to a person — this endpoint keeps it that way. Editing the base body does **not** re-translate. Existing translations are kept and become stale against the new English, which the portal flags so a human can rewrite them. Send `translations` yourself to replace them. **Requires an organization admin.** An automation is a standing rule that messages the firm's clients, so it is gated like org settings and team management. #### Parameters ##### `id` required - **In:** `path` The automation id `string`, format: `uuid` — The automation id #### Request Body ##### Content-Type: application/json - **`active`** `boolean`, possible values: `false` — Pass false to pause the automation. Turning one back ON is done in the portal: activation queues sends to every matching client, so it stays a human decision. - **`anchor`** `string`, possible values: `"case_opened", "case_closed", "case_invited", "case_phase_changed", "client_birthday"` — The date to count from. Only allowed while paused and unqueued. - **`audience_id`** `string`, format: `uuid` — Point the automation at a different audience. Only allowed while it is paused and has queued nothing — see the endpoint description. Setting this clears group\_id. - **`channels`** `object` — Which channels this automation sends on. Only the fields you send are changed — passing just \`{"sms": "never"}\` leaves email as it was. Like the copy, this is also applied to the sends already queued, so turning SMS off stops the texts that were about to go out rather than only the ones queued from now on. - **`email`** `boolean` — Whether to email as well. Defaults to true. - **`sms`** `string`, possible values: `"always", "fallback", "never"` — "always" texts every client with a phone number; "fallback" texts only the ones the app cannot reach; "never" texts nobody. Defaults to "always". - **`direction`** `string`, possible values: `"before", "on", "after"` — "on" sends at the anchor itself and ignores the offset; "before" and "after" apply it. Only allowed while paused and unqueued. - **`group_id`** `string`, format: `uuid` — Point the automation at a picked list instead. Same restriction as audience\_id, and setting this clears audience\_id. - **`message_body`** `string` — The message, as MARKDOWN, in the same subset the create endpoint accepts. Editing this does NOT re-translate: existing translations are kept and go stale, which the portal shows so a human can rewrite them. Merge variables: {{client\_first\_name}}, {{client\_last\_name}}, {{client\_full\_name}}, {{sender\_first\_name}}, {{sender\_last\_name}}, {{sender\_full\_name}}, {{firm\_name}}. They are filled per recipient at send time. Anything else in double braces is left as literal text in front of the client. To link into the Quilia app use https\://quilia.com/app/home, /app/inbox, /app/suggestions, /app/appointments (their treatment review), /app/case/check-in, /app/case/case-note, /app/case/wage-loss, /app/providers, /app/documents or /app/chat. These open the app for clients who have it and the web for everyone else. Three of them are conditional, and none of them tells you when it misses: two open the app on Home, exactly like an unrecognised path, and the third opens a screen the client has no other way to reach. /app/chat is CaseChat, the two-way conversation, and only works if the firm has case\_chat\_enabled on. Ask a Question is a DIFFERENT feature on its own toggle, ask\_question\_enabled — it sits on the firm's Organization screen and has no deep link, so name it rather than linking it. Both flags come back in client\_features from get\_firm\_settings, and a firm may have either, both or neither, so read them rather than guessing. /app/providers is the Doctor List, and works only on case types with a providers screen; case types such as family law, criminal defense and landlord tenant do not have one. /app/case/wage-loss is where a client records time off work, and it holds the employer and doctor verification forms almost nobody finds unless you send them there. It is available only on case types configured with the wage-loss feature, and it fails the OTHER way: on unsupported case types the link opens the screen anyway, empty and reachable by no other route, so check the audience case types for that feature rather than leaving it to the link. Its NAME also moves with the case type — Lost Income on most, Wages on workers compensation, Employment on social security disability, product liability and wc third party — so name the screen only when the audience is one case type, and otherwise write what it is for. /app/case/case-note is Notes, the client's own running journal of their case, and it is the one destination on every single case type, so it is always safe to link. /app/documents is Files, which holds only what the client uploaded, so link it to ask for something and never to offer a look at what the firm has. No other path works — an unrecognised one opens the app on Home with no error, so do not invent one. - **`message_subject`** `string` — The subject line clients see. Merge variables: {{client\_first\_name}}, {{client\_last\_name}}, {{client\_full\_name}}, {{sender\_first\_name}}, {{sender\_last\_name}}, {{sender\_full\_name}}, {{firm\_name}}. They are filled per recipient at send time. Anything else in double braces is left as literal text in front of the client. - **`name`** `string` — What the firm calls this automation in the Automations list - **`offset_unit`** `string`, possible values: `"days", "weeks", "months", "years"` — Offset unit. Only allowed while paused and unqueued. - **`offset_value`** `integer` — How far from the anchor, as a positive number. Only allowed while paused and unqueued. - **`repeat`** `string`, possible values: `"once", "interval", "yearly"` — "once" sends a single message per person; "interval" repeats every offset; "yearly" repeats annually. Only allowed while paused and unqueued. - **`send_as`** `string` — Change who the message comes from: "firm", or the id of a member of staff at this firm — not a client, and not a Quilia support (wizard) account. Applied to the sends already queued as well, so correcting a wrongly-signed automation also fixes the messages about to go out. - **`send_time`** `string` — Time of day to send, 24-hour. Only allowed while paused and unqueued. - **`translations`** `array` — Per-language copy. Sending this REPLACES every translation the automation has — send the full set, not a delta. Omit it to leave existing translations untouched. **Items:** - **`language` (required)** `string`, possible values: `"es", "pt", "ht", "km", "ko", "th", "tl", "vi", "zh_CN", "zh_TW", "ru", "hy", "ar", "fa", "de", "pl", "it", "ja", "sr", "fr", "hmn", "mn", "hi", "pa"` — Language code this copy is for, e.g. "es". The full set is every language Quilia speaks; "en" is not one of them, because the base body is the English. - **`message_body` (required)** `string` — Body in this language, in the same markdown subset as the base - **`message_subject`** `string` — Subject in this language. Falls back to the base subject. **Example:** ```json { "name": "", "message_subject": "", "message_body": "", "send_as": "", "channels": { "sms": "always", "email": true }, "translations": [ { "language": "es", "message_subject": "", "message_body": "" } ], "audience_id": "", "group_id": "", "active": false, "anchor": "case_opened", "direction": "before", "offset_value": 0, "offset_unit": "days", "send_time": "", "repeat": "once" } ``` #### Responses ##### Status: 200 Automation updated ###### Content-Type: application/json - **`data` (required)** `object` - **`action` (required)** `string` — What this automation does: "send\_message" sends the subject and body; "ask\_feedback" asks for an NPS score and has no message copy of its own, so an empty subject and body are correct for it. - **`active` (required)** `boolean` — False means paused: it exists, and it sends nothing - **`anchor_field` (required)** `string` - **`anchor_offset` (required)** `object` - **`unit` (required)** `string` - **`value` (required)** `number` — Signed: negative is before the anchor - **`audience_id` (required)** `string | null` - **`channels` (required)** `object` — What this automation actually sends on. Push is always on and is not listed. A firm that never chose gets the default, so this is never empty. - **`email` (required)** `boolean` - **`sms` (required)** `string`, possible values: `"always", "fallback", "never"` - **`created_at` (required)** `string` - **`custom_send_time` (required)** `string | null` - **`group_id` (required)** `string | null` - **`id` (required)** `string` - **`message_body` (required)** `string | null` - **`message_body_markdown` (required)** `string` — The base copy as markdown, ready to send straight back to PATCH. Empty for an automation stored with no blocks. - **`message_subject` (required)** `string | null` - **`name` (required)** `string` - **`recurrence` (required)** `object | null` - **`frequency` (required)** `number | null` - **`frequency_type` (required)** `string | null` - **`translations` (required)** `array` — Every language this automation has copy for. Sending \`translations\` on an update REPLACES this whole set, so read it first and send it back complete — omitting a language deletes it. **Items:** - **`language` (required)** `string` - **`message_body` (required)** `string` — Markdown, ready to send straight back to PATCH - **`message_subject` (required)** `string | null` - **`stale` (required)** `boolean | null` — True once the English has been edited since this was written, so it now says something different. Null for translations written before the fingerprint existed — unknown, rather than a guess in either direction. - **`message` (required)** `string` - **`queued_rewritten` (required)** `integer | null` — Number of already-queued sends rewritten by this edit. null when the edit could not reach the queue (rename, pause); 0 when nothing was queued. **Example:** ```json { "message": "", "data": { "id": "", "name": "", "audience_id": null, "group_id": null, "action": "", "anchor_field": "", "anchor_offset": { "value": 1, "unit": "" }, "custom_send_time": null, "recurrence": { "frequency_type": null, "frequency": null }, "message_subject": null, "message_body": null, "message_body_markdown": "", "translations": [ { "language": "", "message_subject": null, "message_body": "", "stale": null } ], "channels": { "sms": "always", "email": true }, "active": true, "created_at": "" }, "queued_rewritten": null } ``` ### Delete an automation and cancel what it has queued - **Method:** `DELETE` - **Path:** `/automations/{id}` - **Tags:** automations Deletes the automation and removes every send it still had waiting. The order matters and is not an implementation detail: a queued message carries its own copy and its own send date, so deleting the rule without clearing the queue leaves messages that still go out with nothing left to explain them. The queue is cleared first, and if that fails the automation is left in place rather than half-removed. Only messages still waiting are touched. Anything already sent stays exactly where it is — it is a record of something a client received, and deleting the rule that produced it does not unsend it. **Requires an organization admin.** An automation is a standing rule that messages the firm's clients, so it is gated like org settings and team management. #### Parameters ##### `id` required - **In:** `path` The automation id `string`, format: `uuid` — The automation id #### Responses ##### Status: 200 Automation deleted ###### Content-Type: application/json - **`data` (required)** `object` - **`cancelled_messages` (required)** `number` — Queued sends removed along with it - **`deleted` (required)** `boolean`, possible values: `true` - **`id` (required)** `string` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "deleted": true, "cancelled_messages": 1 } } ``` ### Create a new case - **Method:** `POST` - **Path:** `/cases` - **Tags:** cases Creates a case, optionally with its client and everything else you hold on it. **Send the whole case in one call.** `providers`, `insurance`, `employments`, `incident`, `damage` and `staff` are all optional and all accept the same body as their standalone endpoints. Pushing one case used to take eight requests. **One bad child never costs you the case.** Every child is validated and written on its own. The case and the good children land, and the response carries a `results` array with one entry per object you sent — `ok`, our `id`, whether it was `created` or `updated`, and on failure the reason. Match entries back by the `external_id` you sent, or by `index` for objects you sent without one. **Re-pushing is safe.** Send `external_id` on the case and on each child and a repeat call updates rather than duplicates, on this endpoint and on the standalone ones alike. Children are written on a re-push too, so a caller never has to know whether we already held the case. Staff are created if their email is new to the organization and then attached, which is the two calls `POST /v2/staff` and `POST /v2/cases/{id}/members` used to take. No invitation email is sent unless you ask for one. Injuries have no write endpoint yet and so cannot be sent here. #### Request Body ##### Content-Type: application/json - **`type` (required)** `string` — The type of the case. Can be a Quilia case type or a custom CMS case type that will be mapped via integration settings. - **`case_email`** `string`, format: `email` — The case's own inbox address in the external system, e.g. a CasePacer filing address. For a CMS with no API this is how Quilia delivers activity back into the case, so a case created without it is linked but unreachable. Must not already belong to another case. - **`client_id`** `string` — The ID of an existing client - **`cms_staff_names`** `array` — Staff names observed on the CMS case. The server resolves saved integration mappings and adds the matching Quilia staff, including staff configured for every case. This does not write assignments back to the CMS. **Items:** `string` - **`cms_type`** `string` — Look up integration by CMS type when ID is absent - **`created_at`** `string`, format: `date-time` — The date and time when the case was created - **`damage`** `object` — The vehicle damage. One per case, same merge behaviour as \`incident\`. Same body as \`PUT /v2/cases/{id}/damage\`. - **`employments`** `array` — Employment records. Same body as \`POST /v2/cases/{id}/employments\`. Send \`external\_id\` on each to make a re-push idempotent. **Items:** - **`external_id`** `string` — Your own id for this case. The match key: send it and a repeat call updates the case instead of creating a second one. Omit it and every call creates a new case. - **`incident`** `object` — The incident. One per case, so it is an object rather than a list and a re-push merges into the existing row. Same body as \`PUT /v2/cases/{id}/incident\`. - **`insurance`** `array` — Insurance policies. Same body as \`POST /v2/cases/{id}/insurance\`. Send \`external\_id\` on each to make a re-push idempotent. **Items:** - **`invite`** `boolean` — Whether to text the client their invite. Defaults to the firm's auto-invite setting on this integration, so omit it to keep that behaviour and send it to override the default either way. A client who has already activated is never texted again regardless. - **`native_id`** `string` — Synonym for \`external\_id\`, kept so existing integrations keep working. Prefer \`external\_id\` in new code; if both are sent, \`external\_id\` wins. - **`new_client`** `object` — The client to associate with the case. Accepts the same fields as POST /v2/clients — send everything you have on the first call rather than following up with PATCH /v2/clients/{id}. - **`name` (required)** `string` — The name of the client - **`phone` (required)** `string` — The phone number of the client. Normalized to E.164, so a 10-digit US number or one carrying spaces, dashes or parentheses is fine. - **`address_1`** `string` — The first line of the address - **`address_2`** `string` — The second line of the address - **`city`** `string` — The city - **`country`** `string` — The country - **`date_of_birth`** `string` — The date of birth. YYYY-MM-DD, or an ISO timestamp, which is truncated to its date. - **`email`** `string` — The email of the client. An address that does not parse is dropped and the case is still created, so a placeholder your staff typed to satisfy a required field will never cost you the record. - **`external_id`** `string` — Your own id for this person. Used to match them on a re-push, in preference to their phone number, and echoed back on every response so you can correlate. Send it and a mistyped phone can no longer read as a second claimant. - **`language_code`** `string` — The language code of the client - **`name_first`** `string` — The first name of the client - **`name_last`** `string` — The last name of the client - **`postal_code`** `string` — The postal code - **`state`** `string` — The state or province - **`opened_at`** `string | null`, format: `date-time` — The date and time when the case was opened - **`organization_integration_id`** `string` — The ID of the organization integration to use - **`phase`** `string` — The current phase of the case - **`providers`** `array` — Medical providers to record on the case. Same body as \`POST /v2/cases/{id}/providers\`. Send \`external\_id\` on each to make a re-push idempotent. **Items:** - **`staff`** `array` — Firm staff to put on the case. Creates the staff member if their email is new to the organization, then attaches them — the two calls that \`POST /v2/staff\` plus \`POST /v2/cases/{id}/members\` used to take, and without the silent skip that dropped staff who were not org members yet (QUILIA-5995). No invitation email is sent unless a member sets \`invite: "send"\`. **Items:** - **`status`** `string | null`, possible values: `"pending", "open", "closed"` — The status of the case - **`updated_at`** `string`, format: `date-time` — The date and time when the case was last updated **Example:** ```json { "new_client": { "external_id": "", "name": "", "name_first": "", "name_last": "", "phone": "", "language_code": "", "email": "", "address_1": "", "address_2": "", "city": "", "state": "", "postal_code": "", "country": "", "date_of_birth": "" }, "invite": true, "client_id": "", "organization_integration_id": "", "cms_type": "", "type": "", "external_id": "", "native_id": "", "case_email": "", "phase": "", "cms_staff_names": [ "" ], "status": "pending", "opened_at": null, "created_at": "", "updated_at": "", "providers": [], "insurance": [], "employments": [], "incident": null, "damage": null, "staff": [] } ``` #### Responses ##### Status: 200 Case created successfully or already exists ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`created_at` (required)** `string | null` - **`members` (required)** `array` **Items:** - **`status` (required)** `string` - **`user_id` (required)** `string` - **`external_id`** `string | null` — Your own id for this person, if we hold one. Send it on \`new\_client\` and it comes back here, so you can match our members against the objects you sent. - **`member_id`** `string` - **`opened_at` (required)** `string | null` - **`status` (required)** `string` - **`type` (required)** `string` - **`updated_at` (required)** `string | null` - **`phase`** `string` - **`staff`** `array` — The firm staff assigned to the case. Separate from \`members\`, which is claimants only, so iterating members to decide who to contact can never reach the firm. **Items:** - **`status` (required)** `string` - **`user_id` (required)** `string` - **`external_id`** `string | null` — Your own id for this staff member, if we hold one. - **`member_id`** `string` - **`role`** `string | null` — \`attorney\`, \`legal staff\`, \`admin\`. - **`staff_skipped`** `array` — Mapped Quilia staff IDs that were not inserted, including users already on the case and IDs that are not firm staff. **Items:** - **`reason` (required)** `string`, possible values: `"already_member", "not_org_member"` — \`already\_member\`: already on the case. \`not\_org\_member\`: the id is not a matching-role member of this organization — for staff, create them via POST /v2/staff first. - **`user_id` (required)** `string` — A requested user\_id that was not added - **`message` (required)** `string` — Success message confirming the creation operation - **`results`** `array` — One entry per child object you sent, in the order you sent it. Present only when the call carried children. A failed entry has not been written and the rest of the call still stands — the case is never lost to one bad provider. **Items:** - **`external_id` (required)** `string | null` - **`index` (required)** `number` — Position in the list you sent. The only handle on an object with no id of its own. - **`object` (required)** `string`, possible values: `"provider", "insurance", "employment", "incident", "damage", "staff"` - **`ok` (required)** `boolean` - **`action`** `string`, possible values: `"created", "updated"` — Whether the row was written or matched. Only the writers that distinguish the two report it — incident, damage and employment merge without saying which, so it is absent for those. - **`error`** `string` — Why it was not written. Present only when \`ok\` is false. - **`id`** `string` — Our id for the row, when one was written **Example:** ```json { "message": "", "data": { "case_id": "", "status": "", "opened_at": null, "created_at": null, "updated_at": null, "type": "", "phase": "", "members": [ { "user_id": "", "status": "", "member_id": "", "external_id": null } ], "staff": [ { "user_id": "", "member_id": "", "external_id": null, "status": "", "role": null } ], "staff_skipped": [ { "user_id": "", "reason": "already_member" } ] }, "results": [ { "object": "provider", "index": 1, "external_id": null, "ok": true, "id": "", "action": "created", "error": "" } ] } ``` ### List cases - **Method:** `GET` - **Path:** `/cases` - **Tags:** cases Retrieves a paginated list of cases for the authenticated organization. ## Filtering Use `filter[field]=value` query parameters: - `filter[status]` - Case status: "pending", "open", "closed", "invited", "archived" - `filter[type]` - Case type (e.g., "motor vehicle collision") - `filter[client_name]` - Client name (cases without a client are excluded when using this filter) - `filter[client_id]` - Client ID (filters by user\_id) **Partial matching**: All filters match substrings by default. Use `*` for explicit wildcard placement (e.g., `motor*` for "starts with"). **Fuzzy enum matching**: Enum fields (status, type) use fuzzy matching automatically. **Multiple values**: Comma-separated for OR matching (e.g., `filter[status]=open,closed`). Note: Combining wildcards with commas is not supported. #### Parameters ##### `limit` - **In:** `query` The maximum number of cases to return. `[ "number", "null" ]`, default: `10` — The maximum number of cases to return. ##### `offset` - **In:** `query` The number of cases to skip before starting to return results. `[ "number", "null" ]`, default: `0` — The number of cases to skip before starting to return results. ##### `filter[status]` - **In:** `query` Filter by status. See endpoint description for syntax. `string` — Filter by status. See endpoint description for syntax. ##### `filter[type]` - **In:** `query` Filter by type. See endpoint description for syntax. `string` — Filter by type. See endpoint description for syntax. ##### `filter[client_name]` - **In:** `query` Filter by client\_name. See endpoint description for syntax. `string` — Filter by client\_name. See endpoint description for syntax. ##### `filter[client_id]` - **In:** `query` Filter by client\_id. See endpoint description for syntax. `string` — Filter by client\_id. See endpoint description for syntax. #### Responses ##### Status: 200 List of cases retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` — Array of cases **Items:** - **`case_phase` (required)** `string | null` — The current phase of the case workflow - **`client_name` (required)** `string | null` — The name of the primary client associated with the case - **`created_at` (required)** `string | null` — The date and time when the case was created - **`id` (required)** `string` — The unique identifier of the case - **`opened_at` (required)** `string | null` — The date and time when the case was opened - **`status` (required)** `string` — The current status of the case (e.g., "open", "closed") - **`type` (required)** `string` — The type/category of the case (e.g., "motor vehicle collision") - **`updated_at` (required)** `string | null` — The date and time when the case was last updated - **`user_id` (required)** `string | null` — The ID of the primary client associated with the case - **`message` (required)** `string` — Success message confirming the retrieve operation - **`pagination` (required)** `object` - **`has_more` (required)** `boolean` — Whether there is more data to fetch - **`limit` (required)** `number` — Maximum number of items returned - **`next` (required)** `string | null` — URL for the next page of items, if available - **`offset` (required)** `number` — Number of items skipped - **`previous` (required)** `string | null` — URL for the previous page of items, if available - **`total` (required)** `number` — Total number of items available **Example:** ```json { "message": "", "data": [ { "id": "", "status": "", "type": "", "case_phase": null, "opened_at": null, "user_id": null, "client_name": null, "created_at": null, "updated_at": null } ], "pagination": { "total": 1, "offset": 1, "limit": 1, "has_more": true, "previous": null, "next": null } } ``` ### Delete a case - **Method:** `DELETE` - **Path:** `/cases/{id}` - **Tags:** cases Permanently deletes a case from the organization's records. This action is irreversible and will remove all case data, including associated members, documents, and chat history. Only cases belonging to the authenticated organization can be deleted. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case to delete `string` — The unique identifier of the case to delete #### Responses ##### Status: 200 Case successfully deleted from the organization's records ###### Content-Type: application/json - **`message` (required)** `string` — Success message confirming the delete operation **Example:** ```json { "message": "" } ``` ### Retrieve an individual case - **Method:** `GET` - **Path:** `/cases/{id}` - **Tags:** cases Retrieves detailed information about a specific case by its ID. Returns comprehensive case metadata including status, timestamps, type, associated client and organization information, and case members. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case to retrieve `string` — The unique identifier of the case to retrieve #### Responses ##### Status: 200 Case retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`created_at` (required)** `string | null` — The date and time when the case was created - **`id` (required)** `string` — The unique identifier of the case - **`members` (required)** `array` — List of case members **Items:** - **`allow_messages` (required)** `boolean` — Whether the member can send messages - **`case_id` (required)** `string` — The case ID this member belongs to - **`case_phase` (required)** `string | null` — The current phase of the case for this member - **`contactable` (required)** `boolean` — Whether the member can be contacted - **`created_at` (required)** `string | null` — When the member was added to the case - **`id` (required)** `string` — The unique identifier of the case member - **`invited_at` (required)** `string | null` — When the member was invited - **`phase_id` (required)** `string | null` — The phase ID for this member - **`phase_updated_at` (required)** `string | null` — When the phase was last updated for this member - **`status` (required)** `string | null` — The status of the member in the case - **`updated_at` (required)** `string | null` — When the member was last updated - **`user_id` (required)** `string | null` — The user ID of the member - **`opened_at` (required)** `string | null` — The date and time when the case was opened - **`organization_id` (required)** `string | null` — The ID of the organization that owns the case - **`status` (required)** `string` — The current status of the case - **`type` (required)** `string` — The type/category of the case - **`updated_at` (required)** `string | null` — The date and time when the case was last updated - **`user_id` (required)** `string | null` — The ID of the client associated with the case - **`message` (required)** `string` — Success message confirming the retrieve operation **Example:** ```json { "message": "", "data": { "id": "", "status": "", "opened_at": null, "created_at": null, "updated_at": null, "type": "", "user_id": null, "organization_id": null, "members": [ { "id": "", "case_id": "", "user_id": null, "status": null, "case_phase": null, "phase_id": null, "phase_updated_at": null, "allow_messages": true, "contactable": true, "invited_at": null, "created_at": null, "updated_at": null } ] } } ``` ### Update case details - **Method:** `PATCH` - **Path:** `/cases/{id}` - **Tags:** cases Updates specific fields of an existing case. Allows modification of case status, type, opening date, and custom CMS data. Only the provided fields will be updated, maintaining data integrity. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case to update `string` — The unique identifier of the case to update #### Request Body ##### Content-Type: application/json - **`cms_data`** `object` — Additional custom data for the case - **`opened_at`** `string` — The date and time when the case was opened (ISO 8601 format) - **`status`** `string`, possible values: `"open", "closed"` — The status of the case - **`type`** `string` — The type/category of the case **Example:** ```json { "status": "open", "type": "", "opened_at": "", "cms_data": null } ``` #### Responses ##### Status: 200 Case updated successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`cms_data` (required)** `object | null` - **`id` (required)** `string` - **`opened_at` (required)** `string | null` - **`status` (required)** `string` - **`type` (required)** `string` - **`updated_at` (required)** `string | null` - **`message` (required)** `string` — Success message confirming the update operation **Example:** ```json { "message": "", "data": { "id": "", "status": "", "type": "", "cms_data": {}, "opened_at": null, "updated_at": null } } ``` ### Invite users to a case - **Method:** `POST` - **Path:** `/cases/{id}/invite` - **Tags:** cases Invites one or more users to participate in a specific case with designated roles (client or staff). Manages case access permissions and user associations for collaborative case management. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case to invite users to `string`, format: `uuid` — The unique identifier of the case to invite users to #### Request Body ##### Content-Type: application/json - **`role` (required)** `string`, possible values: `"client", "staff"` — The role to assign to the invited users - **`users` (required)** `array` — An array of user IDs to invite to the case **Items:** `string`, format: `uuid` — User ID - **`contactable`** `boolean` — Whether the invited users' contact info is visible to clients in the app **Example:** ```json { "users": [ "" ], "role": "client", "contactable": true } ``` #### Responses ##### Status: 200 Users invited successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`invited_users` (required)** `array` — List of users successfully invited to the case **Items:** - **`case_id` (required)** `string` — The ID of the case the user was invited to - **`role` (required)** `string`, possible values: `"client", "staff"` — The role assigned to the user - **`status` (required)** `string | null` — The status of the invitation - **`user_id` (required)** `string | null` — The ID of the invited user - **`skipped` (required)** `array` — Requested users that were already members and not re-invited **Items:** - **`reason` (required)** `string`, possible values: `"already_member", "not_org_member"` — \`already\_member\`: already on the case. \`not\_org\_member\`: the id is not a matching-role member of this organization — for staff, create them via POST /v2/staff first. - **`user_id` (required)** `string` — A requested user\_id that was not added - **`message` (required)** `string` — Success message confirming the invite operation **Example:** ```json { "message": "", "data": { "invited_users": [ { "user_id": null, "case_id": "", "status": null, "role": "client" } ], "skipped": [ { "user_id": "", "reason": "already_member" } ] } } ``` ### List available case types - **Method:** `GET` - **Path:** `/cases/casetypes` - **Tags:** cases Retrieves a list of available case types that can be used when creating or filtering cases. #### Responses ##### Status: 200 List of available case types retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` — Array of available case types **Items:** `string` **Example:** ```json { "data": [ "" ] } ``` ### List members of a case - **Method:** `GET` - **Path:** `/cases/{id}/members` - **Tags:** cases Returns every member of a case with user details (name, phone, email, language, profile image) and their organization role. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case #### Responses ##### Status: 200 Case members retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`members` (required)** `array` **Items:** - **`allow_messages` (required)** `boolean` - **`case_phase` (required)** `string | null` - **`contactable` (required)** `boolean` - **`created_at` (required)** `string | null` - **`id` (required)** `string` - **`invited_at` (required)** `string | null` - **`phase_id` (required)** `string | null` - **`phase_updated_at` (required)** `string | null` - **`status` (required)** `string | null` - **`updated_at` (required)** `string | null` - **`user` (required)** `object` - **`email` (required)** `string | null` - **`id` (required)** `string` - **`language_code` (required)** `string | null` - **`name` (required)** `string | null` - **`phone` (required)** `string | null` - **`profile_image` (required)** `string | null` - **`role` (required)** `string | null` — The member's role in the organization - **`user_id` (required)** `string | null` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "case_id": "", "members": [ { "id": "", "user_id": null, "status": null, "case_phase": null, "phase_id": null, "phase_updated_at": null, "allow_messages": true, "contactable": true, "invited_at": null, "created_at": null, "updated_at": null, "user": { "id": "", "name": null, "phone": null, "email": null, "profile_image": null, "language_code": null, "role": null } } ] } } ``` ### Add members to a case - **Method:** `POST` - **Path:** `/cases/{id}/members` - **Tags:** cases Adds users as members of a case without sending any SMS invitations. Use POST /cases/{id}/invite if you want the SMS invite text to go out. Users already on the case, or users not in the org with a matching role, are NOT added and are returned in `data.skipped` with a reason (`already_member` / `not_org_member`) — check it to confirm staff actually landed. Clients are added with status `pending`. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case #### Request Body ##### Content-Type: application/json - **`role` (required)** `string`, possible values: `"client", "staff"` — Role of the users being added - **`users` (required)** `array` — User IDs to add as case members **Items:** `string` — User ID - **`contactable`** `boolean` — Whether the members' contact info is visible to clients (staff only; ignored for clients) **Example:** ```json { "users": [ "" ], "role": "client", "contactable": true } ``` #### Responses ##### Status: 200 Members added (or already present) on the case ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`members` (required)** `array` — Current members on the case after the add operation **Items:** - **`member_id` (required)** `string` — The case\_members row ID - **`status` (required)** `string | null` — The member status on the case - **`user_id` (required)** `string | null` — The user ID of the member - **`skipped` (required)** `array` — Requested user\_ids that were NOT added, each with a reason. A non-empty \`not\_org\_member\` here is the usual cause of "staff not appearing on the case". **Items:** - **`reason` (required)** `string`, possible values: `"already_member", "not_org_member"` — \`already\_member\`: already on the case. \`not\_org\_member\`: the id is not a matching-role member of this organization — for staff, create them via POST /v2/staff first. - **`user_id` (required)** `string` — A requested user\_id that was not added - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "case_id": "", "members": [ { "member_id": "", "user_id": null, "status": null } ], "skipped": [ { "user_id": "", "reason": "already_member" } ] } } ``` ### Update a case member - **Method:** `PATCH` - **Path:** `/cases/{id}/members` - **Tags:** cases Updates `contactable` and/or `allow_messages` on a case member. `contactable` is silently ignored for client members — only staff members can be toggled contactable. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case #### Request Body ##### Content-Type: application/json - **`user_id` (required)** `string` — The user ID of the member to update - **`allow_messages`** `boolean` — Whether the member can receive messages - **`contactable`** `boolean` — Whether the member's contact info is visible to clients **Example:** ```json { "user_id": "", "contactable": true, "allow_messages": true } ``` #### Responses ##### Status: 200 Member updated successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`member` (required)** `object` - **`allow_messages` (required)** `boolean` - **`contactable` (required)** `boolean` - **`id` (required)** `string` - **`status` (required)** `string | null` - **`updated_at` (required)** `string | null` - **`user_id` (required)** `string | null` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "case_id": "", "member": { "id": "", "user_id": null, "status": null, "contactable": true, "allow_messages": true, "updated_at": null } } } ``` ### Remove a member from a case - **Method:** `DELETE` - **Path:** `/cases/{id}/members` - **Tags:** cases Removes a user from a case. The user is still a member of the organization — only the case\_members row is deleted. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case #### Request Body ##### Content-Type: application/json - **`user_id` (required)** `string` — The user ID of the member to remove **Example:** ```json { "user_id": "" } ``` #### Responses ##### Status: 200 Member removed from the case ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`removed_member` (required)** `object` - **`id` (required)** `string` - **`user_id` (required)** `string | null` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "case_id": "", "removed_member": { "id": "", "user_id": null } } } ``` ### Update case phase - **Method:** `PATCH` - **Path:** `/cases/{id}/phase` - **Tags:** cases Updates the phase of a specific case, allowing progression through different stages of case workflow. Optionally updates the last modified timestamp and custom CMS data. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case to update `string` — The unique identifier of the case to update #### Request Body ##### Content-Type: application/json - **`phase` (required)** `string` — The new phase for the case - **`cms_data`** `object` — Any additional custom data for the case - **`updated_at`** `string` — The date and time when the case was last updated (ISO 8601 format) **Example:** ```json { "phase": "", "updated_at": "", "cms_data": null } ``` #### Responses ##### Status: 200 Case phase updated successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`cms_data` (required)** `object | null` - **`id` (required)** `string` - **`phase` (required)** `string | null` - **`updated_at` (required)** `string | null` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "phase": null, "updated_at": null, "cms_data": {} } } ``` ### Retrieve a case's incident record - **Method:** `GET` - **Path:** `/cases/{id}/incident` - **Tags:** cases Returns the incident attached to a case — the narrative (what happened before, during, and after), plus location, date, and time of injury. Returns `data: null` if no incident has been recorded. To structure raw narrative text into before/during/after, use POST /v2/incidents/structure first (separate utility endpoint). #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case #### Responses ##### Status: 200 Incident retrieved (may be null) ###### Content-Type: application/json - **`data` (required)** `object | null` - **`case_id` (required)** `string | null` - **`created_at` (required)** `string | null` - **`date_of_injury` (required)** `string | null` — YYYY-MM-DD - **`explanation_video` (required)** `array | null` - **`id` (required)** `string` - **`incident_location` (required)** `string | null` - **`sketch_image` (required)** `array | null` - **`time_of_injury` (required)** `string | null` — HH:MM:SS - **`updated_at` (required)** `string | null` - **`what_happened_after` (required)** `string | null` - **`what_happened_before` (required)** `string | null` - **`what_happened_during` (required)** `string | null` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "case_id": null, "what_happened_before": null, "what_happened_during": null, "what_happened_after": null, "incident_location": null, "date_of_injury": null, "time_of_injury": null, "sketch_image": [ "" ], "explanation_video": [ "" ], "created_at": null, "updated_at": null } } ``` ### Upsert a case's incident record - **Method:** `PUT` - **Path:** `/cases/{id}/incident` - **Tags:** cases Creates or updates the incident record for a case. Accepts any subset of writable fields; missing fields are left unchanged (partial merge). Media fields (sketch\_image, explanation\_video) are read-only here — uploads go through the storage endpoints. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case #### Request Body ##### Content-Type: application/json - **`date_of_injury`** `string` — Date of injury in YYYY-MM-DD format - **`incident_location`** `string` — Free-text location of the incident - **`time_of_injury`** `string` — Time of injury in HH:MM or HH:MM:SS format - **`what_happened_after`** `string` — What happened after the incident - **`what_happened_before`** `string` — What happened leading up to the incident - **`what_happened_during`** `string` — What happened during the incident itself **Example:** ```json { "what_happened_before": "", "what_happened_during": "", "what_happened_after": "", "incident_location": "", "date_of_injury": "", "time_of_injury": "" } ``` #### Responses ##### Status: 200 Incident upserted successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string | null` - **`created_at` (required)** `string | null` - **`date_of_injury` (required)** `string | null` — YYYY-MM-DD - **`explanation_video` (required)** `array | null` - **`id` (required)** `string` - **`incident_location` (required)** `string | null` - **`sketch_image` (required)** `array | null` - **`time_of_injury` (required)** `string | null` — HH:MM:SS - **`updated_at` (required)** `string | null` - **`what_happened_after` (required)** `string | null` - **`what_happened_before` (required)** `string | null` - **`what_happened_during` (required)** `string | null` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "case_id": null, "what_happened_before": null, "what_happened_during": null, "what_happened_after": null, "incident_location": null, "date_of_injury": null, "time_of_injury": null, "sketch_image": [ "" ], "explanation_video": [ "" ], "created_at": null, "updated_at": null } } ``` ### Retrieve a case's injuries by body part - **Method:** `GET` - **Path:** `/cases/{id}/injuries` - **Tags:** cases Returns the case's injuries organized by body part. Each entry uses a canonical Quilia body-part name (e.g. "Right Knee", laterality baked into the name) as the spine, with the AI-extracted detail, the providers the client mapped to treating that part, the pain history for that part (oldest first), and any injury photos nested under it. The body-part set is the union of every part referenced anywhere on the case, so nothing is dropped. Map the canonical names against your own predefined body parts. Returns empty arrays (not 404) when the case exists but has no injuries recorded. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case #### Responses ##### Status: 200 Injuries retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`injured_body_parts` (required)** `array` — The body parts the client reported as injured. **Items:** `string` - **`injuries` (required)** `array` — One entry per injured body part, with that part as the spine: details, treating providers, pain history, and photos nested under it. **Items:** - **`body_part` (required)** `string` — Canonical Quilia body-part name, e.g. "Right Knee" (laterality is in the name). Map these against your own predefined body parts. Canonical values: Head, Face, Brain, Neck, Chest, Abdomen, Upper Back, Middle Back, Lower Back, Gluteus Maximus, Right Groin, Left Groin, Right Shoulder, Right Arm, Right Elbow, Right Forearm, Right Wrist, Right Hand, Left Shoulder, Left Arm, Left Elbow, Left Forearm, Left Wrist, Left Hand, Right Hip, Right Thigh, Right Knee, Right Lower Leg, Right Ankle, Right Foot, Left Hip, Left Thigh, Left Knee, Left Lower Leg, Left Ankle, Left Foot, Groin, Right Leg, Left Leg. A small number of legacy records may carry retired names outside this list. - **`details` (required)** `object | null` - **`description`** `string | null` — Free-text description of the injury. - **`severity`** `string | null` — Reported severity, e.g. "mild", "moderate", "severe". - **`type`** `string | null` — Nature of the injury, e.g. "strain", "fracture". - **`pain_history` (required)** `array` — Pain scores recorded for this body part over time, oldest first. **Items:** - **`date` (required)** `string | null` — When the pain score was recorded (check-in timestamp). - **`score` (required)** `number` — Pain rating for this body part (typically 0–10). - **`photos` (required)** `array` — Stored asset references for injury photos linked to this body part. Photos attach at the injury level, so a photo may span multiple body parts of the same injury. **Items:** `string` - **`providers` (required)** `array` — Providers the client mapped to treating this body part. **Items:** - **`name` (required)** `string | null` - **`released_at` (required)** `string | null` — When the client was released from this provider, if complete. - **`treatment_status` (required)** `string | null` — "treating" (active care), "complete" (released), or null. - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "case_id": "", "injured_body_parts": [ "" ], "injuries": [ { "body_part": "", "details": { "type": null, "severity": null, "description": null }, "providers": [ { "name": null, "treatment_status": null, "released_at": null } ], "pain_history": [ { "date": null, "score": 1 } ], "photos": [ "" ] } ] } } ``` ### Retrieve a case's damage record - **Method:** `GET` - **Path:** `/cases/{id}/damage` - **Tags:** cases Returns the damage record attached to a case. Today's schema is vehicle-centric (year, make, model, VIN, color, trim, mileage, license plate, type); the endpoint is named generically to accommodate future non-vehicle damage (bikes, scooters, buses, eventually other property). Returns `data: null` if no damage has been recorded. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case #### Responses ##### Status: 200 Damage retrieved (may be null) ###### Content-Type: application/json - **`data` (required)** `object | null` - **`case_id` (required)** `string | null` - **`created_at` (required)** `string | null` - **`id` (required)** `string` - **`images` (required)** `array | null` - **`notes` (required)** `string | null` - **`updated_at` (required)** `string | null` - **`vehicle_color` (required)** `string | null` - **`vehicle_license_plate` (required)** `string | null` - **`vehicle_make` (required)** `string | null` - **`vehicle_mileage` (required)** `number | null` - **`vehicle_model` (required)** `string | null` - **`vehicle_trim` (required)** `string | null` - **`vehicle_type` (required)** `string | null` - **`vehicle_vin` (required)** `string | null` - **`vehicle_year` (required)** `number | null` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "case_id": null, "vehicle_year": null, "vehicle_make": null, "vehicle_model": null, "vehicle_trim": null, "vehicle_vin": null, "vehicle_color": null, "vehicle_mileage": null, "vehicle_license_plate": null, "vehicle_type": null, "notes": null, "images": [ "" ], "created_at": null, "updated_at": null } } ``` ### Upsert a case's damage record - **Method:** `PUT` - **Path:** `/cases/{id}/damage` - **Tags:** cases Creates or updates the damage record for a case. Accepts any subset of writable fields; missing fields are left unchanged (partial merge). The `images` field is storage-managed and excluded from this endpoint — uploads go through the storage endpoints. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case #### Request Body ##### Content-Type: application/json - **`notes`** `string` - **`vehicle_color`** `string` - **`vehicle_license_plate`** `string` - **`vehicle_make`** `string` - **`vehicle_mileage`** `integer` - **`vehicle_model`** `string` - **`vehicle_trim`** `string` - **`vehicle_type`** `string` - **`vehicle_vin`** `string` - **`vehicle_year`** `integer` **Example:** ```json { "vehicle_year": 1, "vehicle_make": "", "vehicle_model": "", "vehicle_trim": "", "vehicle_vin": "", "vehicle_color": "", "vehicle_mileage": 1, "vehicle_license_plate": "", "vehicle_type": "", "notes": "" } ``` #### Responses ##### Status: 200 Damage upserted successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string | null` - **`created_at` (required)** `string | null` - **`id` (required)** `string` - **`images` (required)** `array | null` - **`notes` (required)** `string | null` - **`updated_at` (required)** `string | null` - **`vehicle_color` (required)** `string | null` - **`vehicle_license_plate` (required)** `string | null` - **`vehicle_make` (required)** `string | null` - **`vehicle_mileage` (required)** `number | null` - **`vehicle_model` (required)** `string | null` - **`vehicle_trim` (required)** `string | null` - **`vehicle_type` (required)** `string | null` - **`vehicle_vin` (required)** `string | null` - **`vehicle_year` (required)** `number | null` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "case_id": null, "vehicle_year": null, "vehicle_make": null, "vehicle_model": null, "vehicle_trim": null, "vehicle_vin": null, "vehicle_color": null, "vehicle_mileage": null, "vehicle_license_plate": null, "vehicle_type": null, "notes": null, "images": [ "" ], "created_at": null, "updated_at": null } } ``` ### List a case's insurance policies - **Method:** `GET` - **Path:** `/cases/{id}/insurance` - **Tags:** cases Returns every insurance policy recorded on a case, newest first. A case commonly carries more than one — typically the client's own auto policy and their health coverage — so this always returns a list, never a single record. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case #### Responses ##### Status: 200 Insurance policies retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`insurance` (required)** `array` **Items:** - **`case_id` (required)** `string | null` - **`claim_number` (required)** `string | null` - **`company` (required)** `string | null` - **`copay` (required)** `string | null` - **`created_at` (required)** `string | null` - **`deductible` (required)** `string | null` - **`effective_date` (required)** `string | null` - **`expiration_date` (required)** `string | null` - **`external_id` (required)** `string | null` — Your own id for this policy, echoed back so you can map our id to yours. - **`group_number` (required)** `string | null` - **`id` (required)** `string` - **`insured_name` (required)** `string | null` - **`notes` (required)** `string | null` - **`phone` (required)** `string | null` - **`policy_number` (required)** `string | null` - **`policy_owner` (required)** `string | null` - **`type` (required)** `string | null` - **`updated_at` (required)** `string | null` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "case_id": "", "insurance": [ { "id": "", "case_id": null, "external_id": null, "type": null, "company": null, "policy_number": null, "group_number": null, "claim_number": null, "insured_name": null, "policy_owner": null, "phone": null, "effective_date": null, "expiration_date": null, "copay": null, "deductible": null, "notes": null, "created_at": null, "updated_at": null } ] } } ``` ### Add an insurance policy to a case - **Method:** `POST` - **Path:** `/cases/{id}/insurance` - **Tags:** cases Records an insurance policy on a case. Send one call per policy — a case normally has several, and the two that matter most on a personal injury case are the client's own auto policy (`type: "First Party Car"`) and their health coverage (`type: "Health"`). **Re-syncing is safe.** Send your own `external_id` on the policy and it becomes the match key: it is checked first, ahead of every rule below, and it matches on the case alone rather than the case and `type`, so correcting a policy's type still finds the row you wrote last night. Without one, a policy matching the same case, same `type`, and same `policy_number` is updated in place rather than duplicated; the response's `action` field tells you which happened. When you have no `policy_number`, the match falls back to the same case, same `type`, and same `company`, considering only policies that also have no number — so a re-sync still updates rather than duplicating, and it will never overwrite a policy whose number you have already sent. Send a `policy_number` whenever you hold one: it is the more precise key, and it is what lets a case carry two policies from the same carrier. Only `type` is required, plus at least one of `external_id`, `company`, `policy_number`, `claim_number` or `insured_name` so the policy can be told apart from the others on the case. Everything else is optional, so send whatever subset you hold — a claim number with no carrier yet is a policy worth recording. **Send `type` in your own vocabulary.** We map it rather than rejecting it: `Auto` is recorded as `First Party Car` (this endpoint records your client's own policies, so send `Other Party Car` explicitly for the adverse carrier), `WC` as `Workers Compensation`, `Home` as `Homeowner`. A label we cannot place is recorded as `Other` so the carrier, policy number and claim number still land. The `type` in the response is what was stored. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case #### Request Body ##### Content-Type: application/json - **`type` (required)** `string` — The kind of policy. Send your own label and we map it: "Auto" becomes "First Party Car", "WC" becomes "Workers Compensation", and anything we can't place is recorded as "Other" rather than failing the call. The response shows what was stored. Our vocabulary is: First Party Car, Other Party Car, Health, Company Car, Homeowner, Renter, Umbrella, Disability, Life, Medicare/Medicaid, Travel, Workers Compensation, Other. - **`claim_number`** `object` - **`company`** `string` — Carrier name. Optional: send the policy with just a claim or policy number when you do not have the carrier yet. - **`copay`** `object` - **`deductible`** `object` - **`effective_date`** `string` - **`expiration_date`** `string` - **`external_id`** `string` — Your own id for this policy. The match key: send it and a repeat call updates the policy instead of creating a second one. Unique per case, and it beats every other match rule below. - **`group_number`** `object` - **`insured_name`** `string` — Name of the insured, when it is not the client - **`notes`** `string` - **`phone`** `object` — The carrier's phone number - **`policy_number`** `object` - **`policy_owner`** `string` **Example:** ```json { "external_id": "", "type": "", "company": "", "policy_number": "", "group_number": "", "claim_number": "", "insured_name": "", "policy_owner": "", "phone": "", "effective_date": "", "expiration_date": "", "copay": "", "deductible": "", "notes": "" } ``` #### Responses ##### Status: 200 Insurance policy recorded successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`action` (required)** `string`, possible values: `"created", "updated"` — Whether a new policy was written or an existing one was matched and updated. See the endpoint description for the match rule. - **`case_id` (required)** `string | null` - **`claim_number` (required)** `string | null` - **`company` (required)** `string | null` - **`copay` (required)** `string | null` - **`created_at` (required)** `string | null` - **`deductible` (required)** `string | null` - **`effective_date` (required)** `string | null` - **`expiration_date` (required)** `string | null` - **`external_id` (required)** `string | null` — Your own id for this policy, echoed back so you can map our id to yours. - **`group_number` (required)** `string | null` - **`id` (required)** `string` - **`insured_name` (required)** `string | null` - **`notes` (required)** `string | null` - **`phone` (required)** `string | null` - **`policy_number` (required)** `string | null` - **`policy_owner` (required)** `string | null` - **`type` (required)** `string | null` - **`updated_at` (required)** `string | null` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "case_id": null, "external_id": null, "type": null, "company": null, "policy_number": null, "group_number": null, "claim_number": null, "insured_name": null, "policy_owner": null, "phone": null, "effective_date": null, "expiration_date": null, "copay": null, "deductible": null, "notes": null, "created_at": null, "updated_at": null, "action": "created" } } ``` ### Create a provider on a case - **Method:** `POST` - **Path:** `/cases/{id}/providers` - **Tags:** cases Records a medical provider (or other care entity) on a case. The endpoint attempts to auto-link the new person row to an existing `providers_list` master entry by normalized name + phone/fax match. When the match is confident it returns `matched: true` with a `provider_list_id`; otherwise `matched: false` and the row is left unlinked for a human to reconcile later. The endpoint does not create new `providers_list` rows — external input only extends the case-scoped `people` table. Supply `external_id` to make repeated calls idempotent. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case #### Request Body ##### Content-Type: application/json - **`name` (required)** `string` — Provider name (required). This is the only name field — a provider row carries one name, whether it is a person or a facility. Send the full name here; use \`other\_names\` for aliases or a DBA. - **`address_1`** `string` - **`address_2`** `string` - **`city`** `string` - **`country`** `string` - **`domain`** `string` - **`email`** `string` - **`external_id`** `string` — Identifier from the calling integration (e.g., the provider's ID in Ethos / Filevine / another CMS). Stored in metadata.external\_id. If a later call reuses the same external\_id on the same case, the existing row is updated instead of a duplicate being created. - **`fax`** `string` - **`other_names`** `array` **Items:** `string` - **`phone`** `string` - **`postal_code`** `string` - **`specialty`** `string` - **`state`** `string` - **`website`** `string` **Example:** ```json { "name": "", "other_names": [ "" ], "address_1": "", "address_2": "", "city": "", "state": "", "postal_code": "", "country": "", "phone": "", "fax": "", "email": "", "website": "", "domain": "", "specialty": "", "external_id": "" } ``` #### Responses ##### Status: 200 Provider upserted successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`action` (required)** `string`, possible values: `"created", "updated"` — Whether the people row was created or an existing one updated - **`case_id` (required)** `string` - **`matched` (required)** `boolean` — Whether a providers\_list match was found with high confidence - **`person_id` (required)** `string` — The public.people row id for this case - **`provider_list_id` (required)** `string | null` — The providers\_list row this person was matched to, or null if no confident match. Unmatched rows can be linked later by a human. - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "person_id": "", "case_id": "", "provider_list_id": null, "matched": true, "action": "created" } } ``` ### Retrieve a case activity feed - **Method:** `GET` - **Path:** `/cases/{id}/feed` - **Tags:** cases Returns the activity feed scoped to a single case — client uploads, messages, appointments, and case updates in chronological order. Same time-range params as the org feed (days / hours / start / end; default last 24h). This is the natural 'what's happening on this case?' read. #### Parameters ##### `id` required - **In:** `path` The case id `string`, format: `uuid` — The case id ##### `days` - **In:** `query` Days to include (> 0). Takes precedence over hours. `number` — Days to include (> 0). Takes precedence over hours. ##### `hours` - **In:** `query` Hours to include (> 0). Default 24 if neither is given. `number` — Hours to include (> 0). Default 24 if neither is given. ##### `start` - **In:** `query` Start date (ISO 8601) `string` — Start date (ISO 8601) ##### `end` - **In:** `query` End date (ISO 8601) `string` — End date (ISO 8601) #### Responses ##### Status: 200 Case feed retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` **Items:** - **`case_id` (required)** `string | null` - **`case_member_id` (required)** `string | null` - **`logs_data` (required)** `array` **Items:** - **`action` (required)** `string` - **`attachments` (required)** `array` **Items:** - **`filename` (required)** `string` - **`fileType` (required)** `string` - **`url` (required)** `string` - **`created_at` (required)** `string` - **`description` (required)** `string` - **`log_id` (required)** `string` - **`metadata` (required)** `object` - **`timezone` (required)** `string` - **`title` (required)** `string` - **`user_id` (required)** `string` - **`message` (required)** `string` — Success message confirming the retrieve operation **Example:** ```json { "message": "", "data": [ { "user_id": "", "case_id": null, "case_member_id": null, "logs_data": [ { "title": "", "action": "", "log_id": "", "metadata": {}, "timezone": "", "created_at": "", "attachments": [ { "url": "", "fileType": "", "filename": "" } ], "description": "" } ] } ] } ``` ### Retrieve a case summary - **Method:** `GET` - **Path:** `/cases/{id}/summary` - **Tags:** cases The case's living summary — a concise, always-current AI digest of the matter, the way a sharp case manager holds it in their head. It is regenerated from source whenever the underlying case data changes, structured around the Liability / Coverage / Damages pillars, and includes what's been done and what the case still needs. Read this for an instant, framed picture of a case before acting on it. `data` is null when the case exists but has no summary yet (a brand-new or quiet case that hasn't been summarized). #### Parameters ##### `id` required - **In:** `path` The case id `string` — The case id #### Responses ##### Status: 200 Case summary retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object | null` - **`case_id` (required)** `string` - **`model` (required)** `string | null` — The model that generated the current summary. - **`structured` (required)** `object | null` — The machine-readable digest parsed from the same generation (phase, assessment \[array of {pillar, read}], client\_actions, firm\_actions, open\_questions, risk\_flags, engagement). - **`summary` (required)** `string | null` — The current human-readable case summary (prose). - **`updated_at` (required)** `string | null` — When the summary was last rebuilt (ISO 8601). - **`user_id` (required)** `string | null` — The case member the summary is stored on (the case representative — usually the primary client). One summary per case. - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "case_id": "", "user_id": null, "summary": null, "structured": {}, "model": null, "updated_at": null } } ``` ### List a case's documents - **Method:** `GET` - **Path:** `/cases/{id}/documents` - **Tags:** cases Returns every document (asset) on a case with its per-document "case-manager memo" — a 1–2 sentence note of what matters in that document (e.g. a lab result's finding, a record's key facts) — and `text`, the text the analyzer read out of the document. This is the fast way to load granular per-document context for a case. `memo` and `text` are null for a document that hasn't been analyzed yet, and `text` is also null when the document had no text in it (a scene photo, an audio clip); `with_memo` and `with_text` count how many have each. #### Parameters ##### `id` required - **In:** `path` The case id `string` — The case id #### Responses ##### Status: 200 Case documents retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`documents` (required)** `array` **Items:** - **`analysis_status` (required)** `string | null` — AI analysis status (e.g. "completed", "pending") - **`created_at` (required)** `string | null` - **`description` (required)** `string | null` - **`filename` (required)** `string | null` - **`id` (required)** `string` - **`label` (required)** `string | null` — The document/asset label (e.g. "lab result", "medical record") - **`memo` (required)** `string | null` — The per-document case-manager memo — a 1–2 sentence note of what matters in this document. Null if not analyzed/memo'd yet. - **`text` (required)** `string | null` — The text the analyzer read out of this document. Null if the document hasn't been analyzed yet, or if there was no text in it (a scene photo, an audio clip). This is what the analyzer captured, not a guaranteed full transcription — long documents may be condensed. - **`total` (required)** `number` - **`with_memo` (required)** `number` — How many documents have a memo - **`with_text` (required)** `number` — How many documents have extracted text on them - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "case_id": "", "total": 1, "with_memo": 1, "with_text": 1, "documents": [ { "id": "", "filename": null, "label": null, "description": null, "analysis_status": null, "memo": null, "text": null, "created_at": null } ] } } ``` ### List a case's notes - **Method:** `GET` - **Path:** `/cases/{id}/notes` - **Tags:** cases Returns the notes on a case (the client-authored "attorney updates" — what the client has written in to the firm), newest first. Read-only: notes are written by clients, not the firm/API. Read these for the client's own running account of the case. #### Parameters ##### `id` required - **In:** `path` The case id `string` — The case id #### Responses ##### Status: 200 Case notes retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`notes` (required)** `array` **Items:** - **`created_at` (required)** `string | null` - **`created_by` (required)** `string | null` - **`id` (required)** `string` - **`notes` (required)** `string | null` — The note body (client-authored). - **`prompt_topic` (required)** `string | null` — The topic/prompt the note was written against, if any. - **`total` (required)** `number` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "case_id": "", "total": 1, "notes": [ { "id": "", "notes": null, "prompt_topic": null, "created_by": null, "created_at": null } ] } } ``` ### Resolve one case-memory source reference - **Method:** `GET` - **Path:** `/cases/{id}/sources/{ref}` - **Tags:** cases Returns the single source row a case-memory citation points at, in full. `ref` is the reference as it appears in the memory's structured `sources` (e.g. "hc:1a2b3c4d"): a short code for the table plus the first 8 characters of the row id. Codes are hc (health check-in), cn (case note), cc (CaseChat message) and ca (a check-in ask the client answered without entering data). The reference is resolved WITHIN the case, so an unknown or cross-case reference reads as not found rather than reaching another case's row. #### Parameters ##### `id` required - **In:** `path` The case id `string` — The case id ##### `ref` required - **In:** `path` The source reference from the memory, e.g. "hc:1a2b3c4d" `string` — The source reference from the memory, e.g. "hc:1a2b3c4d" #### Responses ##### Status: 200 Source record retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`record` (required)** `object` — The source row in full, in the shape of its own table. - **`ref` (required)** `string` — The reference as it appeared in the memory, e.g. "hc:1a2b3c4d". - **`table` (required)** `string` — The table the reference resolved to, e.g. "public.pain\_scores". - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "ref": "", "table": "", "case_id": "", "record": {} } } ``` ### List contacts attached to a case - **Method:** `GET` - **Path:** `/cases/{id}/contacts` - **Tags:** cases Returns every contact attached to a case, split into `people` and `companies` buckets. Use the optional `kind` query parameter to limit results to one bucket. Use the optional `contact_type` parameter to filter by category (e.g. `witness`, `police department`). Returns 400 if the combination is impossible — e.g. `kind=company` with `contact_type=witness`, since 'witness' is only valid for people contacts. Returns empty arrays (not 404) when the case exists but has no contacts. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case ##### `kind` - **In:** `query` Limit results to one bucket. Omit to return both. Use 'people' for personal contacts (witness, emergency contact, etc.) or 'company' for company contacts (insurance company, body shop, etc.). `string`, possible values: `"people", "company"` — Limit results to one bucket. Omit to return both. Use 'people' for personal contacts (witness, emergency contact, etc.) or 'company' for company contacts (insurance company, body shop, etc.). ##### `contact_type` - **In:** `query` Filter by contact category. Must be valid for the selected kind (or for either kind if `kind` is omitted). `string`, possible values: `"witness", "adjustor", "provider", "emergency contact", "supervisor", "ambulance", "emergency room", "other driver", "body shop", "medical provider", "rental company", "police department", "employer", "insurance company", "law firm", "towing company", "vehicle storage"` — Filter by contact category. Must be valid for the selected kind (or for either kind if \`kind\` is omitted). #### Responses ##### Status: 200 Contacts retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`companies` (required)** `array` **Items:** - **`address_1` (required)** `string | null` - **`address_2` (required)** `string | null` - **`case_id` (required)** `string | null` - **`city` (required)** `string | null` - **`country` (required)** `string | null` - **`created_at` (required)** `string | null` - **`id` (required)** `string` - **`name` (required)** `string | null` - **`phone` (required)** `string | null` - **`police_report_number` (required)** `string | null` - **`postal_code` (required)** `string | null` - **`state` (required)** `string | null` - **`type` (required)** `string | null` - **`user_id` (required)** `string | null` - **`people` (required)** `array` **Items:** - **`address_1` (required)** `string | null` - **`address_2` (required)** `string | null` - **`case_id` (required)** `string | null` - **`city` (required)** `string | null` - **`country` (required)** `string | null` - **`created_at` (required)** `string | null` - **`email` (required)** `string | null` - **`id` (required)** `string` - **`name` (required)** `string | null` - **`phone` (required)** `string | null` - **`postal_code` (required)** `string | null` - **`relationship` (required)** `string | null` - **`state` (required)** `string | null` - **`treatment_status` (required)** `string | null` — For provider contacts: "treating" (active care), "complete" (released), or null. Tracks where the client is in treatment. - **`type` (required)** `string | null` - **`user_id` (required)** `string | null` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "case_id": "", "people": [ { "id": "", "case_id": null, "user_id": null, "email": null, "phone": null, "name": null, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null, "type": null, "relationship": null, "treatment_status": null, "created_at": null } ], "companies": [ { "id": "", "case_id": null, "user_id": null, "name": null, "phone": null, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null, "type": null, "police_report_number": null, "created_at": null } ] } } ``` ### Update a contact's treatment status - **Method:** `PATCH` - **Path:** `/cases/{id}/contacts/{contact_id}` - **Tags:** cases Set a provider contact's treatment status for the case. "complete" marks the client released from that provider and stamps `released_at` to now; "treating" marks active care and clears `released_at`. Use this to track where the client is in treatment (the basis for no-next-appointment re-engagement). #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case ##### `contact_id` required - **In:** `path` The contact (people row) id to update `string` — The contact (people row) id to update #### Request Body ##### Content-Type: application/json - **`treatment_status` (required)** `string`, possible values: `"treating", "complete"` — Set "treating" for active care or "complete" to release the client (stamps released\_at to now). **Example:** ```json { "treatment_status": "treating" } ``` #### Responses ##### Status: 200 Treatment status updated ###### Content-Type: application/json - **`data` (required)** `object` - **`id` (required)** `string` - **`name` (required)** `string | null` - **`released_at` (required)** `string | null` - **`treatment_status` (required)** `string | null` - **`type` (required)** `string | null` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "name": null, "type": null, "treatment_status": null, "released_at": null } } ``` ### Create an employment record on a case - **Method:** `POST` - **Path:** `/cases/{id}/employments` - **Tags:** cases Creates an employment relationship for a case member, tying a client to an employer company and optional supervisor in the context of a specific case. **The short form is one call.** Send `company_name` and `job_title` and nothing else: the employer company is resolved on this case by name (case-insensitive) or created, and `user_id` defaults to the case's primary client. No `POST /v2/contacts` round trip to mint an id first. **Re-syncing is safe when you send `external_id`.** It is your own id for the employment and the match key: a repeat call with the same one on the same case updates that employment in place rather than creating a second, and only the fields you send are changed. Omit it and every call creates a new record. Send `company_id` instead when you already hold one — it wins over `company_name`. Send `user_id` explicitly on a case with several clients. Supervisors are still contacts: create one via `POST /v2/contacts` and pass `supervisor_id`. **Validation**: - `case_id` (from path) must belong to the authenticated organization. - `user_id`, given or defaulted, must be a member of this case (not just the organization). - `company_id`, if provided, must belong to the organization. - `supervisor_id`, if provided, must belong to the organization. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case #### Request Body ##### Content-Type: application/json - **`company_id`** `string`, format: `uuid` — The employer company (a \`companies\` row, typically with \`type: 'employer'\`). Send \`company\_name\` instead if you only have the name. - **`company_name`** `string` — The employer's name. Resolves to an existing employer company on this case (case-insensitive) or creates one, so you do not need a POST /v2/contacts call first. Ignored when \`company\_id\` is given. - **`external_id`** `string` — Your own id for this employment. The match key: send it and a repeat call updates the employment instead of creating a second one. Unique per case. - **`is_primary`** `boolean` — Whether this is the client's primary employment for the case. Defaults to false. Multiple primaries per case are allowed (not enforced here). - **`job_title`** `string` — Job title at this employer - **`supervisor_id`** `string`, format: `uuid` — Optional — the supervisor person (a \`people\` row, typically with \`type: 'supervisor'\`) - **`user_id`** `string`, format: `uuid` — The client (case member) this employment is for. Defaults to the case's primary client, so only send it for a case with several clients. **Example:** ```json { "external_id": "", "user_id": "", "company_id": "", "company_name": "", "job_title": "", "supervisor_id": "", "is_primary": true } ``` #### Responses ##### Status: 200 Employment created successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`company` (required)** `object | null` - **`address_1` (required)** `string | null` - **`address_2` (required)** `string | null` - **`city` (required)** `string | null` - **`country` (required)** `string | null` - **`id` (required)** `string` - **`name` (required)** `string | null` - **`phone` (required)** `string | null` - **`postal_code` (required)** `string | null` - **`state` (required)** `string | null` - **`type` (required)** `string | null` - **`company_id` (required)** `string | null` - **`created_at` (required)** `string | null` - **`external_id` (required)** `string | null` — Your own id for this employment, echoed back so you can map our id to yours. - **`id` (required)** `string` - **`is_primary` (required)** `boolean` - **`job_title` (required)** `string | null` - **`supervisor` (required)** `object | null` - **`email` (required)** `string | null` - **`id` (required)** `string` - **`name` (required)** `string | null` - **`phone` (required)** `string | null` - **`type` (required)** `string | null` - **`supervisor_id` (required)** `string | null` - **`updated_at` (required)** `string | null` - **`user_id` (required)** `string` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "case_id": "", "external_id": null, "user_id": "", "company_id": null, "job_title": null, "supervisor_id": null, "is_primary": true, "created_at": null, "updated_at": null, "company": { "id": "", "name": null, "type": null, "phone": null, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null }, "supervisor": { "id": "", "name": null, "type": null, "phone": null, "email": null } } } ``` ### List employments on a case - **Method:** `GET` - **Path:** `/cases/{id}/employments` - **Tags:** cases Returns every employment record on a case, sorted primary first. Each row includes the joined employer company and supervisor person where present. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case #### Responses ##### Status: 200 Employments retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`employments` (required)** `array` **Items:** - **`case_id` (required)** `string` - **`company` (required)** `object | null` - **`address_1` (required)** `string | null` - **`address_2` (required)** `string | null` - **`city` (required)** `string | null` - **`country` (required)** `string | null` - **`id` (required)** `string` - **`name` (required)** `string | null` - **`phone` (required)** `string | null` - **`postal_code` (required)** `string | null` - **`state` (required)** `string | null` - **`type` (required)** `string | null` - **`company_id` (required)** `string | null` - **`created_at` (required)** `string | null` - **`external_id` (required)** `string | null` — Your own id for this employment, echoed back so you can map our id to yours. - **`id` (required)** `string` - **`is_primary` (required)** `boolean` - **`job_title` (required)** `string | null` - **`supervisor` (required)** `object | null` - **`email` (required)** `string | null` - **`id` (required)** `string` - **`name` (required)** `string | null` - **`phone` (required)** `string | null` - **`type` (required)** `string | null` - **`supervisor_id` (required)** `string | null` - **`updated_at` (required)** `string | null` - **`user_id` (required)** `string` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "case_id": "", "employments": [ { "id": "", "case_id": "", "external_id": null, "user_id": "", "company_id": null, "job_title": null, "supervisor_id": null, "is_primary": true, "created_at": null, "updated_at": null, "company": { "id": "", "name": null, "type": null, "phone": null, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null }, "supervisor": { "id": "", "name": null, "type": null, "phone": null, "email": null } } ] } } ``` ### Retrieve a single employment record - **Method:** `GET` - **Path:** `/cases/{id}/employments/{employmentId}` - **Tags:** cases Returns a specific employment row with joined company and supervisor. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case ##### `employmentId` required - **In:** `path` The employment record ID `string` — The employment record ID #### Responses ##### Status: 200 Employment retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`company` (required)** `object | null` - **`address_1` (required)** `string | null` - **`address_2` (required)** `string | null` - **`city` (required)** `string | null` - **`country` (required)** `string | null` - **`id` (required)** `string` - **`name` (required)** `string | null` - **`phone` (required)** `string | null` - **`postal_code` (required)** `string | null` - **`state` (required)** `string | null` - **`type` (required)** `string | null` - **`company_id` (required)** `string | null` - **`created_at` (required)** `string | null` - **`external_id` (required)** `string | null` — Your own id for this employment, echoed back so you can map our id to yours. - **`id` (required)** `string` - **`is_primary` (required)** `boolean` - **`job_title` (required)** `string | null` - **`supervisor` (required)** `object | null` - **`email` (required)** `string | null` - **`id` (required)** `string` - **`name` (required)** `string | null` - **`phone` (required)** `string | null` - **`type` (required)** `string | null` - **`supervisor_id` (required)** `string | null` - **`updated_at` (required)** `string | null` - **`user_id` (required)** `string` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "case_id": "", "external_id": null, "user_id": "", "company_id": null, "job_title": null, "supervisor_id": null, "is_primary": true, "created_at": null, "updated_at": null, "company": { "id": "", "name": null, "type": null, "phone": null, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null }, "supervisor": { "id": "", "name": null, "type": null, "phone": null, "email": null } } } ``` ### Update an employment record - **Method:** `PATCH` - **Path:** `/cases/{id}/employments/{employmentId}` - **Tags:** cases Updates an employment record. Only provided fields are replaced. `user_id` is immutable — to change the client, delete this employment and create a new one. `company_id` and `supervisor_id`, if provided, are validated against the organization to prevent cross-org leaks. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case ##### `employmentId` required - **In:** `path` The employment record ID `string` — The employment record ID #### Request Body ##### Content-Type: application/json - **`company_id`** `string`, format: `uuid` - **`is_primary`** `boolean` - **`job_title`** `string` - **`supervisor_id`** `string`, format: `uuid` **Example:** ```json { "company_id": "", "job_title": "", "supervisor_id": "", "is_primary": true } ``` #### Responses ##### Status: 200 Employment updated successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` - **`company` (required)** `object | null` - **`address_1` (required)** `string | null` - **`address_2` (required)** `string | null` - **`city` (required)** `string | null` - **`country` (required)** `string | null` - **`id` (required)** `string` - **`name` (required)** `string | null` - **`phone` (required)** `string | null` - **`postal_code` (required)** `string | null` - **`state` (required)** `string | null` - **`type` (required)** `string | null` - **`company_id` (required)** `string | null` - **`created_at` (required)** `string | null` - **`external_id` (required)** `string | null` — Your own id for this employment, echoed back so you can map our id to yours. - **`id` (required)** `string` - **`is_primary` (required)** `boolean` - **`job_title` (required)** `string | null` - **`supervisor` (required)** `object | null` - **`email` (required)** `string | null` - **`id` (required)** `string` - **`name` (required)** `string | null` - **`phone` (required)** `string | null` - **`type` (required)** `string | null` - **`supervisor_id` (required)** `string | null` - **`updated_at` (required)** `string | null` - **`user_id` (required)** `string` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "case_id": "", "external_id": null, "user_id": "", "company_id": null, "job_title": null, "supervisor_id": null, "is_primary": true, "created_at": null, "updated_at": null, "company": { "id": "", "name": null, "type": null, "phone": null, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null }, "supervisor": { "id": "", "name": null, "type": null, "phone": null, "email": null } } } ``` ### Delete an employment record - **Method:** `DELETE` - **Path:** `/cases/{id}/employments/{employmentId}` - **Tags:** cases Deletes an employment record from a case. Returns 409 Conflict if the employment has `missed_times` rows attached — partners must delete those first to avoid silently losing time-loss claim data. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the case `string` — The unique identifier of the case ##### `employmentId` required - **In:** `path` The employment record ID `string` — The employment record ID #### Responses ##### Status: 200 Employment deleted successfully ###### Content-Type: application/json - **`message` (required)** `string` **Example:** ```json { "message": "" } ``` ### List the firm's case types - **Method:** `GET` - **Path:** `/case-types` - **Tags:** case-types Returns every case type in the Quilia catalog with the firm's configuration for it, grouped by Areas of Law category. A case type the firm has never configured is reported with `configured: false` and its catalog default. Existing case types default on; opt-in practice areas such as probate default off. #### Responses ##### Status: 200 Case types retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`categories` (required)** `array` **Items:** - **`case_types` (required)** `array` **Items:** - **`case_type` (required)** `string` — The case type identifier, e.g. "motor vehicle collision" - **`category` (required)** `string` — The Areas of Law category, e.g. "Personal Injury" - **`configured` (required)** `boolean` — Whether the firm has saved a configuration for this case type. When false, the values shown are the defaults that apply until it does. - **`name` (required)** `string` — The display name, e.g. "Motor Vehicle Collision" - **`settings_url` (required)** `string` — Portal link for the settings this API does not cover: per-screen feature visibility, required documents, and request templates - **`enabled`** `boolean` — Whether the firm uses this case type - **`inactive_reminders_enabled`** `boolean` — Send a reminder to clients on this case type who haven't opened the app in 7 days - **`is_default`** `boolean` — Use this case type as the fallback configuration for any case type without its own - **`journal_prompts_enabled`** `boolean` — Show clients on this case type AI follow-up prompts on the journal screen - **`ssn_enabled`** `boolean` — Show clients on this case type a Social Security Number field in their profile - **`category` (required)** `string` — The Areas of Law category name - **`enabled_count` (required)** `number` — How many case types in this category are enabled - **`total` (required)** `number` — How many case types this category holds - **`enabled_count` (required)** `number` — How many case types the firm has enabled in total - **`total` (required)** `number` — How many case types exist in the catalog - **`uncatalogued_in_use`** `array` — Case types assigned to active cases that are not in the Quilia catalog. These cannot be enabled or disabled here, only remapped in the CMS. **Items:** - **`case_count` (required)** `number` - **`case_type` (required)** `string` - **`message` (required)** `string` — Success message confirming the retrieve operation **Example:** ```json { "message": "", "data": { "enabled_count": 1, "total": 1, "categories": [ { "category": "", "enabled_count": 1, "total": 1, "case_types": [ { "enabled": true, "is_default": true, "inactive_reminders_enabled": true, "ssn_enabled": true, "journal_prompts_enabled": true, "case_type": "", "name": "", "category": "", "configured": true, "settings_url": "" } ] } ], "uncatalogued_in_use": [ { "case_type": "", "case_count": 1 } ] } } ``` ### Configure the firm's case types - **Method:** `PATCH` - **Path:** `/case-types` - **Tags:** case-types Turns case types on or off and sets their per-type options. Target one case type, several, or an entire Areas of Law category with `category` — disabling the 20 types a firm never files is one call, not twenty. Only the options you send are changed; the rest keep their current values. A case type with no saved configuration is created on first write. `is_default` may only be set when targeting exactly one case type, since a firm has one fallback. Requires an admin role. #### Request Body ##### Content-Type: application/json - **`case_types`** `array` — The case types to configure. Provide this or \`category\`, not both. **Items:** `string` - **`category`** `string` — Configure every case type in this Areas of Law category, e.g. "Immigration". Provide this or \`case\_types\`, not both. - **`enabled`** `boolean` — Whether the firm uses this case type - **`inactive_reminders_enabled`** `boolean` — Send a reminder to clients on this case type who haven't opened the app in 7 days - **`is_default`** `boolean` — Use this case type as the fallback configuration for any case type without its own - **`journal_prompts_enabled`** `boolean` — Show clients on this case type AI follow-up prompts on the journal screen - **`ssn_enabled`** `boolean` — Show clients on this case type a Social Security Number field in their profile **Example:** ```json { "enabled": true, "is_default": true, "inactive_reminders_enabled": true, "ssn_enabled": true, "journal_prompts_enabled": true, "case_types": [ "" ], "category": "" } ``` #### Responses ##### Status: 200 Case types configured successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_types` (required)** `array` **Items:** - **`case_type` (required)** `string` — The case type identifier, e.g. "motor vehicle collision" - **`category` (required)** `string` — The Areas of Law category, e.g. "Personal Injury" - **`configured` (required)** `boolean` — Whether the firm has saved a configuration for this case type. When false, the values shown are the defaults that apply until it does. - **`name` (required)** `string` — The display name, e.g. "Motor Vehicle Collision" - **`settings_url` (required)** `string` — Portal link for the settings this API does not cover: per-screen feature visibility, required documents, and request templates - **`enabled`** `boolean` — Whether the firm uses this case type - **`inactive_reminders_enabled`** `boolean` — Send a reminder to clients on this case type who haven't opened the app in 7 days - **`is_default`** `boolean` — Use this case type as the fallback configuration for any case type without its own - **`journal_prompts_enabled`** `boolean` — Show clients on this case type AI follow-up prompts on the journal screen - **`ssn_enabled`** `boolean` — Show clients on this case type a Social Security Number field in their profile - **`updated` (required)** `number` — How many case type configurations were written - **`message` (required)** `string` — Success message confirming the update operation **Example:** ```json { "message": "", "data": { "updated": 1, "case_types": [ { "enabled": true, "is_default": true, "inactive_reminders_enabled": true, "ssn_enabled": true, "journal_prompts_enabled": true, "case_type": "", "name": "", "category": "", "configured": true, "settings_url": "" } ] } } ``` ### List the firm's symptom vocabulary - **Method:** `GET` - **Path:** `/case-types/symptoms` - **Tags:** case-types Lists the symptoms available to the caller's firm: shared Quilia symptoms plus active firm-owned symptoms. When `case_type` is provided, each symptom indicates whether it is explicitly selected for that case type. Requires Custom Symptoms to be enabled and an admin role. #### Parameters ##### `case_type` - **In:** `query` Optional case type identifier. When provided, selected\_for\_case\_type is populated. `string` — Optional case type identifier. When provided, selected\_for\_case\_type is populated. ##### `include_hidden` - **In:** `query` Include symptoms hidden by this firm `string` — Include symptoms hidden by this firm #### Responses ##### Status: 200 Symptoms retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_type` (required)** `string | null` - **`custom_symptoms_enabled` (required)** `boolean` - **`selected_count` (required)** `number` - **`symptoms` (required)** `array` **Items:** - **`category` (required)** `string` — Symptom category - **`firm_case_types` (required)** `array | null` — Case types explicitly selected by this firm. Null means the firm uses the global default. - **`global_case_types` (required)** `array` — Case types this symptom applies to by Quilia default **Items:** `string` - **`hidden` (required)** `boolean` — Whether the firm has hidden this symptom from pickers - **`key` (required)** `string` — Stable symptom key used by the app - **`label` (required)** `string` — Client-facing symptom label - **`source` (required)** `string`, possible values: `"quilia", "firm"` — Whether this is a shared Quilia symptom or firm-owned - **`selected_for_case_type`** `boolean` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "custom_symptoms_enabled": true, "case_type": null, "selected_count": 1, "symptoms": [ { "key": "", "label": "", "category": "", "source": "quilia", "hidden": true, "global_case_types": [ "" ], "firm_case_types": [ "" ], "selected_for_case_type": true } ] } } ``` ### Add firm-owned symptoms - **Method:** `POST` - **Path:** `/case-types/symptoms` - **Tags:** case-types Adds symptoms owned by this firm. Shared Quilia symptoms are not edited. Re-adding a previously removed firm symptom reactivates it. Requires Custom Symptoms to be enabled and an admin role. #### Request Body ##### Content-Type: application/json - **`symptoms` (required)** `array` **Items:** - **`category` (required)** `string`, possible values: `"physical", "cognitive", "emotional", "ptsd"` — Symptom category - **`label` (required)** `string` — Client-facing symptom label **Example:** ```json { "symptoms": [ { "label": "", "category": "physical" } ] } ``` #### Responses ##### Status: 200 Firm symptoms added successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`added` (required)** `number` - **`requested` (required)** `number` - **`symptoms` (required)** `array` **Items:** - **`category` (required)** `string` — Symptom category - **`firm_case_types` (required)** `array | null` — Case types explicitly selected by this firm. Null means the firm uses the global default. - **`global_case_types` (required)** `array` — Case types this symptom applies to by Quilia default **Items:** `string` - **`hidden` (required)** `boolean` — Whether the firm has hidden this symptom from pickers - **`key` (required)** `string` — Stable symptom key used by the app - **`label` (required)** `string` — Client-facing symptom label - **`source` (required)** `string`, possible values: `"quilia", "firm"` — Whether this is a shared Quilia symptom or firm-owned - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "requested": 1, "added": 1, "symptoms": [ { "key": "", "label": "", "category": "", "source": "quilia", "hidden": true, "global_case_types": [ "" ], "firm_case_types": [ "" ] } ] } } ``` ### Configure a case type's symptom picker - **Method:** `PATCH` - **Path:** `/case-types/symptoms` - **Tags:** case-types Sets the complete symptom key list clients should see for one case type. Send an empty list to clear the custom case-type list and fall back to the firm's normal symptom list. Unknown or hidden symptom keys are ignored and reported. Requires Custom Symptoms to be enabled and an admin role. #### Request Body ##### Content-Type: application/json - **`case_type` (required)** `string` — Case type identifier, e.g. "carbon monoxide poisoning" - **`symptom_keys` (required)** `array` — The complete list of symptom keys to show for this case type. Send \[] to clear the custom case-type list and fall back to the firm symptom list. **Items:** `string` **Example:** ```json { "case_type": "", "symptom_keys": [ "" ] } ``` #### Responses ##### Status: 200 Case type symptom settings saved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_type` (required)** `string` - **`hidden_symptom_keys` (required)** `array` **Items:** `string` - **`selected_count` (required)** `number` - **`selected_symptom_keys` (required)** `array` **Items:** `string` - **`unknown_symptom_keys` (required)** `array` **Items:** `string` - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "case_type": "", "selected_count": 1, "selected_symptom_keys": [ "" ], "hidden_symptom_keys": [ "" ], "unknown_symptom_keys": [ "" ] } } ``` ### Create a new client - **Method:** `POST` - **Path:** `/clients` - **Tags:** clients Creates a new client record in the system with comprehensive contact information including name, phone, email, address, and demographic details. Phone number validation ensures proper formatting. #### Request Body ##### Content-Type: application/json - **`name` (required)** `string` — The name of the client - **`phone` (required)** `string` — The phone number of the client - **`address_1`** `string` — The first line of the address - **`address_2`** `string` — The second line of the address - **`city`** `string` — The city - **`country`** `string` — The country - **`date_of_birth`** `string` — The date of birth in YYYY-MM-DD format - **`email`** `string`, format: `email` — The email of the client - **`language_code`** `string` — The language code of the client - **`name_first`** `string` — The first name of the client - **`name_last`** `string` — The last name of the client - **`postal_code`** `string` — The postal code - **`state`** `string` — The state or province **Example:** ```json { "name": "", "name_first": "", "name_last": "", "phone": "", "language_code": "", "email": "", "address_1": "", "address_2": "", "city": "", "state": "", "postal_code": "", "country": "", "date_of_birth": "" } ``` #### Responses ##### Status: 200 Client created successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`user_id` (required)** `string | null` — The unique identifier of the created client - **`message` (required)** `string` — Success message confirming the creation operation **Example:** ```json { "message": "", "data": { "user_id": null } } ``` ### List clients - **Method:** `GET` - **Path:** `/clients` - **Tags:** clients Retrieves a paginated list of clients for the authenticated organization. ## Additional Fields Use `?fields=` to include: address\_1, address\_2, city, state, postal\_code, country, date\_of\_birth, language, timezone ## Filtering Use `filter[field]=value` query parameters: - `filter[name]` - Full name - `filter[name_first]` - First name - `filter[name_last]` - Last name - `filter[email]` - Email address - `filter[phone]` - Phone number **Partial matching**: All filters match substrings by default (e.g., `john` matches "Johnny"). Use `*` for explicit wildcard placement (e.g., `john*` for "starts with"). **Multiple values**: Comma-separated for OR matching (exact match). Note: Combining wildcards with commas is not supported. #### Parameters ##### `limit` - **In:** `query` The maximum number of clients to return. `[ "number", "null" ]`, default: `10` — The maximum number of clients to return. ##### `offset` - **In:** `query` The number of clients to skip before starting to return results. `[ "number", "null" ]`, default: `0` — The number of clients to skip before starting to return results. ##### `fields` - **In:** `query` Comma-separated list of additional fields to include. Available: address\_1, address\_2, city, state, postal\_code, country, date\_of\_birth, language, timezone `string` — Comma-separated list of additional fields to include. Available: address\_1, address\_2, city, state, postal\_code, country, date\_of\_birth, language, timezone ##### `filter[name]` - **In:** `query` Filter by name. See endpoint description for syntax. `string` — Filter by name. See endpoint description for syntax. ##### `filter[name_first]` - **In:** `query` Filter by name\_first. See endpoint description for syntax. `string` — Filter by name\_first. See endpoint description for syntax. ##### `filter[name_last]` - **In:** `query` Filter by name\_last. See endpoint description for syntax. `string` — Filter by name\_last. See endpoint description for syntax. ##### `filter[email]` - **In:** `query` Filter by email. See endpoint description for syntax. `string` — Filter by email. See endpoint description for syntax. ##### `filter[phone]` - **In:** `query` Filter by phone. See endpoint description for syntax. `string` — Filter by phone. See endpoint description for syntax. #### Responses ##### Status: 200 Client data retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` — Array of clients **Items:** - **`cases` (required)** `array` — Array of cases the client is associated with **Items:** - **`case_id` (required)** `string` — The unique identifier of the case - **`member_id` (required)** `string` — The case member ID for this client - **`status` (required)** `string | null` — The case status - **`type` (required)** `string | null` — The case type - **`email` (required)** `string | null` — The client's email address - **`last_seen_at` (required)** `string | null` — When the client was last active in the Quilia app (ISO 8601). Engagement signal for spotting disengaged clients; null if they have never been active. - **`name` (required)** `string | null` — The client's full name - **`phone` (required)** `string | null` — The client's phone number - **`user_id` (required)** `string` — The unique identifier of the client - **`address_1`** `string` — First line of the client's address (requires ?fields=address\_1) - **`address_2`** `string` — Second line of the client's address (requires ?fields=address\_2) - **`city`** `string` — The client's city (requires ?fields=city) - **`country`** `string` — The client's country (requires ?fields=country) - **`date_of_birth`** `string` — The client's date of birth (requires ?fields=date\_of\_birth) - **`language`** `string` — The client's preferred language (requires ?fields=language) - **`postal_code`** `string` — The client's postal code (requires ?fields=postal\_code) - **`state`** `string` — The client's state or province (requires ?fields=state) - **`timezone`** `string` — The client's timezone (requires ?fields=timezone) - **`message` (required)** `string` — Success message confirming the retrieve operation - **`pagination` (required)** `object` - **`has_more` (required)** `boolean` — Whether there is more data to fetch - **`limit` (required)** `number` — Maximum number of items returned - **`next` (required)** `string | null` — URL for the next page of items, if available - **`offset` (required)** `number` — Number of items skipped - **`previous` (required)** `string | null` — URL for the previous page of items, if available - **`total` (required)** `number` — Total number of items available **Example:** ```json { "message": "", "data": [ { "user_id": "", "name": null, "email": null, "phone": null, "last_seen_at": null, "cases": [ { "case_id": "", "member_id": "", "type": null, "status": null } ], "address_1": "", "address_2": "", "city": "", "state": "", "postal_code": "", "country": "", "date_of_birth": "", "language": "", "timezone": "" } ], "pagination": { "total": 1, "offset": 1, "limit": 1, "has_more": true, "previous": null, "next": null } } ``` ### Delete a client - **Method:** `DELETE` - **Path:** `/clients/{id}` - **Tags:** clients Permanently deletes a client from the organization's records. This action removes the client's user record, organization membership, and case memberships. Only clients belonging to the authenticated organization can be deleted. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the client to delete `string`, format: `uuid` — The unique identifier of the client to delete #### Responses ##### Status: 200 Client successfully deleted from the organization's records ###### Content-Type: application/json - **`message` (required)** `string` — Success message confirming the delete operation **Example:** ```json { "message": "" } ``` ### Update client information - **Method:** `PATCH` - **Path:** `/clients/{id}` - **Tags:** clients Updates specific fields of an existing client record. Allows modification of contact details, address information, and demographic data while maintaining data integrity. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the client to update `string`, format: `uuid` — The unique identifier of the client to update #### Request Body ##### Content-Type: application/json - **`address_1`** `string | null` — First line of the client's address - **`address_2`** `string | null` — Second line of the client's address - **`city`** `string | null` — The client's city - **`country`** `string | null` — The client's country - **`date_of_birth`** `string | null` — The client's date of birth - **`email`** `string | null`, format: `email` — The client's email address - **`language`** `string | null` — The client's preferred language - **`name`** `string` — The client's full name - **`phone`** `string | null` — The client's phone number - **`postal_code`** `string | null` — The client's postal code - **`state`** `string | null` — The client's state or province - **`timezone`** `string | null` — The client's timezone **Example:** ```json { "name": "", "email": null, "phone": null, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null, "date_of_birth": null, "language": null, "timezone": null } ``` #### Responses ##### Status: 200 Client updated successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`user_id` (required)** `string` — The unique identifier of the updated client - **`message` (required)** `string` — Success message confirming the update operation **Example:** ```json { "message": "", "data": { "user_id": "" } } ``` ### Retrieve a single client - **Method:** `GET` - **Path:** `/clients/{id}` - **Tags:** clients Retrieves detailed information for a specific client using either user\_id or member\_id. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the client to retrieve `string`, format: `uuid` — The unique identifier of the client to retrieve #### Responses ##### Status: 200 Client retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`cases` (required)** `array` — Array of cases the client is associated with **Items:** - **`case_id` (required)** `string` — The unique identifier of the case - **`member_id` (required)** `string` — The case member ID for this client - **`user_id` (required)** `string | null` — The unique identifier of the client - **`message` (required)** `string` — Success message confirming the retrieve operation **Example:** ```json { "message": "", "data": { "user_id": null, "cases": [ { "case_id": "", "member_id": "" } ] } } ``` ### Lookup client by email or phone - **Method:** `GET` - **Path:** `/clients/lookup` - **Tags:** clients Lookup a client by email or phone number within the authenticated organization. Returns the client record if found. #### Parameters ##### `email` - **In:** `query` Client email address `string`, format: `email` — Client email address ##### `phone` - **In:** `query` Client phone number `string` — Client phone number #### Responses ##### Status: 200 Client found successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`cases` (required)** `array` — Array of cases the client is associated with **Items:** - **`case_id` (required)** `string` — The unique identifier of the case - **`member_id` (required)** `string` — The case member ID for this client - **`message` (required)** `string` — Success message confirming the retrieve operation **Example:** ```json { "message": "", "data": { "cases": [ { "case_id": "", "member_id": "" } ] } } ``` ### Update client phase - **Method:** `PATCH` - **Path:** `/clients/{id}/phase` - **Tags:** clients Updates the phase of an individual client. This can be the user\_id or the member\_id. #### Parameters ##### `id` required - **In:** `path` The ID of the client to update. This can be the user\_id or the member\_id. `string`, format: `uuid` — The ID of the client to update. This can be the user\_id or the member\_id. #### Request Body ##### Content-Type: application/json - **`case_id` (required)** `string`, format: `uuid` — The ID of the case to update the client for - **`phase` (required)** `string` — The new phase for the client **Example:** ```json { "phase": "", "case_id": "" } ``` #### Responses ##### Status: 200 Client phase updated successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`member_id` (required)** `string` — The unique identifier of the case member - **`phase` (required)** `string | null` — The new phase of the client - **`updated_at` (required)** `string | null` — When the phase was last updated - **`message` (required)** `string` — Success message confirming the phase update operation **Example:** ```json { "message": "", "data": { "member_id": "", "phase": null, "updated_at": null } } ``` ### Create a new contact - **Method:** `POST` - **Path:** `/contacts` - **Tags:** contacts Creates a contact on a case or a client. **Send the fields flat.** `{ "type": "witness", "name": "Jane Doe", "case_id": "…" }` is the whole body. The older nested form — `{ "type": "people", "contact_data": { … } }` — keeps working forever, and when you send it the outer `type` is only a hint. **Send your own category label.** We match it case-insensitively against our vocabulary and route it to the right place: a `body shop` is stored as a company and a `witness` as a person whichever bucket you named. A label we cannot place is recorded as `other` rather than failing the call, and your original label is kept alongside it. Our vocabulary: - Companies: body shop, medical provider, rental company, police department, employer, insurance company, law firm, towing company, emergency room, ambulance, vehicle storage - People: witness, adjustor, provider, emergency contact, supervisor, ambulance, emergency room, other driver, beneficiary, trustee, executor, guardian, other, creditor, attorney, mediator, guardian ad litem, family, vocational expert, ssa representative, alj, consultative examiner, treating physician **Required attachment**: every contact needs a `case_id` or a `user_id`. A contact attached to neither belongs to no one and is rejected. `witness`, `adjustor`, `other driver`, `ambulance`, `emergency room`, `police department`, `body shop`, `towing company` and `vehicle storage` are bound to a case specifically, so they need `case_id`. A police department may carry `police_report_number`. #### Request Body ##### Content-Type: application/json - **`case_id`** `string`, format: `uuid` — The case this contact belongs to - **`contact_data`** `object` — The nested form, kept for existing integrations. Send the fields flat instead. - **`name`** `string` — The contact's name - **`type`** `string` — The contact category, e.g. \`witness\` or \`body shop\`. In the older nested form this is \`company\` or \`people\` instead and only hints where an unplaceable label should land. - **`user_id`** `string`, format: `uuid` — The client this contact belongs to **Example:** ```json { "type": "", "name": "", "case_id": "", "user_id": "", "contact_data": {} } ``` #### Responses ##### Status: 200 Contact created successfully ###### Content-Type: application/json **Any of:** - **`data` (required)** `object` - **`address_1` (required)** `string | null` — First line of the company address - **`address_2` (required)** `string | null` — Second line of the company address - **`case_id` (required)** `string | null` — The case ID this company is associated with - **`city` (required)** `string | null` — The company's city - **`country` (required)** `string | null` — The company's country - **`created_at` (required)** `string` — ISO timestamp when the company contact was created - **`id` (required)** `string` — The unique identifier of the company contact - **`name` (required)** `string | null` — The company name - **`phone` (required)** `string | null` — The company phone number - **`police_report_number` (required)** `string | null` — Police report number (only meaningful for police department contacts) - **`postal_code` (required)** `string | null` — The company's postal code - **`state` (required)** `string | null` — The company's state or province - **`type` (required)** `string | null` — The type or category of the company - **`user_id` (required)** `string | null` — The user ID this company is associated with - **`message` (required)** `string` — Success message confirming the creation operation * **`data` (required)** `object` - **`address_1` (required)** `string | null` — First line of the person's address - **`address_2` (required)** `string | null` — Second line of the person's address - **`case_id` (required)** `string | null` — The case ID this person is associated with - **`city` (required)** `string | null` — The person's city - **`country` (required)** `string | null` — The person's country - **`created_at` (required)** `string` — ISO timestamp when the person contact was created - **`email` (required)** `string | null` — The person's email address - **`id` (required)** `string` — The unique identifier of the person contact - **`name` (required)** `string | null` — The person's full name - **`phone` (required)** `string | null` — The person's phone number - **`postal_code` (required)** `string | null` — The person's postal code - **`relationship` (required)** `string | null`, possible values: `"spouse", "ex-spouse", "parent", "child", "sibling", "grandparent", "friend", "partner", "roommate", "relative", "other"` — The person's relationship to the case or client - **`state` (required)** `string | null` — The person's state or province - **`type` (required)** `string | null` — The type or category of the person contact - **`user_id` (required)** `string | null` — The user ID this person is associated with * **`message` (required)** `string` — Success message confirming the creation operation **Example:** ```json { "message": "", "data": { "id": "", "case_id": null, "user_id": null, "name": null, "phone": null, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null, "type": null, "police_report_number": null, "created_at": "" } } ``` ### Delete a contact - **Method:** `DELETE` - **Path:** `/contacts/{id}` - **Tags:** contacts Permanently deletes a contact from the organization's records. This action is irreversible and will remove all contact data. Supports both company and people contacts. Only contacts belonging to the authenticated organization can be deleted. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the contact to delete `string` — The unique identifier of the contact to delete ##### `type` required - **In:** `query` The type of contact to delete `string`, possible values: `"company", "people"` — The type of contact to delete #### Responses ##### Status: 200 Contact successfully deleted from the organization's records ###### Content-Type: application/json - **`message` (required)** `string` — Success message confirming the delete operation **Example:** ```json { "message": "" } ``` ### Retrieve a contact - **Method:** `GET` - **Path:** `/contacts/{id}` - **Tags:** contacts Retrieves detailed information for a specific contact (company or person) by ID. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the contact to retrieve `string` — The unique identifier of the contact to retrieve ##### `type` required - **In:** `query` The type of contact to retrieve `string`, possible values: `"company", "people"` — The type of contact to retrieve #### Responses ##### Status: 200 Contact retrieved successfully ###### Content-Type: application/json **Any of:** - **`data` (required)** `object` - **`address_1` (required)** `string | null` — First line of the company address - **`address_2` (required)** `string | null` — Second line of the company address - **`case_id` (required)** `string | null` — The case ID this company is associated with - **`city` (required)** `string | null` — The company's city - **`country` (required)** `string | null` — The company's country - **`created_at` (required)** `string` — ISO timestamp when the company contact was created - **`id` (required)** `string` — The unique identifier of the company contact - **`name` (required)** `string | null` — The company name - **`phone` (required)** `string | null` — The company phone number - **`police_report_number` (required)** `string | null` — Police report number (only meaningful for police department contacts) - **`postal_code` (required)** `string | null` — The company's postal code - **`state` (required)** `string | null` — The company's state or province - **`type` (required)** `string | null` — The type or category of the company - **`user_id` (required)** `string | null` — The user ID this company is associated with - **`message` (required)** `string` — Success message confirming the retrieve operation * **`data` (required)** `object` - **`address_1` (required)** `string | null` — First line of the person's address - **`address_2` (required)** `string | null` — Second line of the person's address - **`case_id` (required)** `string | null` — The case ID this person is associated with - **`city` (required)** `string | null` — The person's city - **`country` (required)** `string | null` — The person's country - **`created_at` (required)** `string` — ISO timestamp when the person contact was created - **`email` (required)** `string | null` — The person's email address - **`id` (required)** `string` — The unique identifier of the person contact - **`name` (required)** `string | null` — The person's full name - **`phone` (required)** `string | null` — The person's phone number - **`postal_code` (required)** `string | null` — The person's postal code - **`relationship` (required)** `string | null`, possible values: `"spouse", "ex-spouse", "parent", "child", "sibling", "grandparent", "friend", "partner", "roommate", "relative", "other"` — The person's relationship to the case or client - **`state` (required)** `string | null` — The person's state or province - **`type` (required)** `string | null` — The type or category of the person contact - **`user_id` (required)** `string | null` — The user ID this person is associated with * **`message` (required)** `string` — Success message confirming the retrieve operation **Example:** ```json { "message": "", "data": { "id": "", "case_id": null, "user_id": null, "name": null, "phone": null, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null, "type": null, "police_report_number": null, "created_at": "" } } ``` ### Update a contact - **Method:** `PATCH` - **Path:** `/contacts/{id}` - **Tags:** contacts Updates specific fields of an existing contact. Supports both company and people contacts with comprehensive contact information including name, phone, email, address, and relationship details. Only the provided fields will be updated, maintaining data integrity. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the contact to update `string` — The unique identifier of the contact to update ##### `type` required - **In:** `query` The type of contact to update `string`, possible values: `"company", "people"` — The type of contact to update #### Request Body ##### Content-Type: application/json **Any of:** - **`address_1`** `string` — First line of the company address - **`address_2`** `string` — Second line of the company address - **`case_id`** `string`, format: `uuid` — The case ID this company is associated with - **`city`** `string` — The company's city - **`country`** `string` — The company's country - **`name`** `string` — The company name - **`phone`** `string` — The company phone number - **`police_report_number`** `string` — Police report number (only meaningful for police department contacts) - **`postal_code`** `string` — The company's postal code - **`state`** `string` — The company's state or province - **`type`** `string`, possible values: `"body shop", "medical provider", "rental company", "police department", "employer", "insurance company", "law firm", "towing company", "emergency room", "ambulance", "vehicle storage"` — The type or category of the company - **`user_id`** `string`, format: `uuid` — The user ID this company is associated with * **`address_1`** `string` — First line of the person's address * **`address_2`** `string` — Second line of the person's address * **`case_id`** `string`, format: `uuid` — The case ID this person is associated with * **`city`** `string` — The person's city * **`country`** `string` — The person's country * **`email`** `object` — The person's email address * **`name`** `string` — The person's full name * **`phone`** `string` — The person's phone number * **`postal_code`** `string` — The person's postal code * **`relationship`** `string`, possible values: `"spouse", "ex-spouse", "parent", "child", "sibling", "grandparent", "friend", "partner", "roommate", "relative", "other"` — The person's relationship to the case or client * **`state`** `string` — The person's state or province * **`type`** `string`, possible values: `"witness", "adjustor", "provider", "emergency contact", "supervisor", "ambulance", "emergency room", "other driver", "beneficiary", "trustee", "executor", "guardian", "other", "creditor", "attorney", "mediator", "guardian ad litem", "family", "vocational expert", "ssa representative", "alj", "consultative examiner", "treating physician"` — The type or category of the person contact * **`user_id`** `string`, format: `uuid` — The user ID this person is associated with **Example:** ```json { "case_id": "", "user_id": "", "name": "", "phone": "", "address_1": "", "address_2": "", "city": "", "state": "", "postal_code": "", "country": "", "type": "body shop", "police_report_number": "" } ``` #### Responses ##### Status: 200 Contact updated successfully ###### Content-Type: application/json **Any of:** - **`data` (required)** `object` - **`address_1` (required)** `string | null` — First line of the company address - **`address_2` (required)** `string | null` — Second line of the company address - **`case_id` (required)** `string | null` — The case ID this company is associated with - **`city` (required)** `string | null` — The company's city - **`country` (required)** `string | null` — The company's country - **`created_at` (required)** `string` — ISO timestamp when the company contact was created - **`id` (required)** `string` — The unique identifier of the company contact - **`name` (required)** `string | null` — The company name - **`phone` (required)** `string | null` — The company phone number - **`police_report_number` (required)** `string | null` — Police report number (only meaningful for police department contacts) - **`postal_code` (required)** `string | null` — The company's postal code - **`state` (required)** `string | null` — The company's state or province - **`type` (required)** `string | null` — The type or category of the company - **`updated_at` (required)** `string` — ISO timestamp when the company contact was last updated - **`user_id` (required)** `string | null` — The user ID this company is associated with - **`message` (required)** `string` — Success message confirming the update operation * **`data` (required)** `object` - **`address_1` (required)** `string | null` — First line of the person's address - **`address_2` (required)** `string | null` — Second line of the person's address - **`case_id` (required)** `string | null` — The case ID this person is associated with - **`city` (required)** `string | null` — The person's city - **`country` (required)** `string | null` — The person's country - **`created_at` (required)** `string` — ISO timestamp when the person contact was created - **`email` (required)** `string | null` — The person's email address - **`id` (required)** `string` — The unique identifier of the person contact - **`name` (required)** `string | null` — The person's full name - **`phone` (required)** `string | null` — The person's phone number - **`postal_code` (required)** `string | null` — The person's postal code - **`relationship` (required)** `string | null`, possible values: `"spouse", "ex-spouse", "parent", "child", "sibling", "grandparent", "friend", "partner", "roommate", "relative", "other"` — The person's relationship to the case or client - **`state` (required)** `string | null` — The person's state or province - **`type` (required)** `string | null` — The type or category of the person contact - **`updated_at` (required)** `string` — ISO timestamp when the person contact was last updated - **`user_id` (required)** `string | null` — The user ID this person is associated with * **`message` (required)** `string` — Success message confirming the update operation **Example:** ```json { "message": "", "data": { "id": "", "case_id": null, "user_id": null, "name": null, "phone": null, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null, "type": null, "police_report_number": null, "created_at": "", "updated_at": "" } } ``` ### Delete documents - **Method:** `DELETE` - **Path:** `/documents` - **Tags:** documents Deletes multiple documents by their document IDs. Automatically handles cleanup of both database records and storage files. #### Request Body ##### Content-Type: application/json - **`documents` (required)** `array` — Array of document IDs to delete **Items:** `string` **Example:** ```json { "documents": [ "" ] } ``` #### Responses ##### Status: 200 Documents successfully deleted from storage and database ###### Content-Type: application/json - **`message` (required)** `string` — Success message confirming the delete operation **Example:** ```json { "message": "" } ``` ### Retrieve a document - **Method:** `GET` - **Path:** `/documents/{id}` - **Tags:** documents Retrieves a document from the cases bucket and generates a signed URL for secure access. The signed URL expires after 30 minutes for security purposes. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the document to retrieve `string` — The unique identifier of the document to retrieve #### Responses ##### Status: 200 Document successfully retrieved with a signed URL for access ###### Content-Type: application/json - **`message` (required)** `string` — Success message confirming the retrieve operation - **`signedUrl` (required)** `string` — Signed URL of the data. Expires in 30 minutes. - **`type` (required)** `string` — Type of the document **Example:** ```json { "message": "", "type": "", "signedUrl": "" } ``` ### Upload documents - **Method:** `POST` - **Path:** `/documents/upload` - **Tags:** documents Uploads multiple documents to the cases bucket with automatic file naming and type detection. Supports various file formats and generates unique identifiers for each uploaded document. Also creates tracking records in the assets database table. #### Request Body ##### Content-Type: multipart/form-data - **`case_id` (required)** `string` — The ID of the case these documents belong to - **`client_id` (required)** `string` — The ID of the client these documents belong to - **`description`** `string` — Optional description for the uploaded documents - **`file`** `number`, format: `binary` — Single file to upload to the specified bucket - **`files`** `array`, format: `binary` — Array of files to upload to the specified bucket **Items:** `number`, format: `binary` - **`type`** `string`, possible values: `"medical record", "bill", "receipt", "repair estimate", "police report", "damage estimate", "accident sketch", "explanation video", "damage photo", "injury photo", "mail", "insurance form", "lab result", "prescription", "doctor verification", "employer verification", "pay stub", "other"`, default: `"other"` — The type of documents being uploaded **Example:** ```json { "case_id": "", "client_id": "", "type": "other", "description": "", "file": 1, "files": [ 1 ] } ``` #### Responses ##### Status: 200 Files successfully uploaded to the bucket ###### Content-Type: application/json - **`documents` (required)** `array` — Array of uploaded document IDs **Items:** `string` - **`message` (required)** `string` — Success message confirming the upload operation **Example:** ```json { "message": "", "documents": [ "" ] } ``` ### Retrieve feed data - **Method:** `GET` - **Path:** `/feed` - **Tags:** feed Retrieves an organization's activity feed with configurable time ranges. Returns a chronological list of user actions, case updates, and system events. Supports filtering by days, hours, or custom start/end dates. Each feed entry includes metadata about the action, attachments, and relevant context. #### Parameters ##### `days` - **In:** `query` Number of days to include in the feed. Takes precedence over hours if both are provided. `[ "number", "null" ]` — Number of days to include in the feed. Takes precedence over hours if both are provided. ##### `hours` - **In:** `query` Number of hours to include in the feed. Default is 24 hours if neither days nor hours are provided. `[ "number", "null" ]` — Number of hours to include in the feed. Default is 24 hours if neither days nor hours are provided. ##### `start` - **In:** `query` The start date for the feed data in ISO 8601 format `string` — Start ISO timestamp ##### `end` - **In:** `query` The end date for the feed data in ISO 8601 format `string` — End ISO timestamp #### Responses ##### Status: 200 Feed data retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` **Items:** - **`case_id` (required)** `string | null` - **`case_member_id` (required)** `string | null` - **`logs_data` (required)** `array` **Items:** - **`action` (required)** `string` - **`attachments` (required)** `array` **Items:** - **`filename` (required)** `string` - **`fileType` (required)** `string` - **`url` (required)** `string` - **`created_at` (required)** `string` - **`description` (required)** `string` - **`log_id` (required)** `string` - **`metadata` (required)** `object` - **`timezone` (required)** `string` - **`title` (required)** `string` - **`user_id` (required)** `string` - **`message` (required)** `string` — Success message confirming the retrieve operation **Example:** ```json { "message": "", "data": [ { "user_id": "", "case_id": null, "case_member_id": null, "logs_data": [ { "title": "", "action": "", "log_id": "", "metadata": {}, "timezone": "", "created_at": "", "attachments": [ { "url": "", "fileType": "", "filename": "" } ], "description": "" } ] } ] } ``` ### Create a new message - **Method:** `POST` - **Path:** `/messages` - **Tags:** messages Creates a new message in the system. Messages are used to communicate between the organization and clients. The message will be associated with the authenticated organization and can include both a subject and HTML body content. #### Request Body ##### Content-Type: application/json - **`message_body` (required)** `string` — The body content of the message. Supports HTML formatting - **`message_subject` (required)** `string` — The subject line of the message - **`sender` (required)** `string` — The user ID of the sender. Must be a valid user within the organization. This tracks who actually sent the message. - **`caseId`** `string` — The case ID to associate the message with. This will be stored as recipient\_case in the database. Either recipient or caseId must be provided. On its own it addresses every client on the case; combined with recipient it scopes that one message to the case. - **`channels`** `object` — Optional delivery preferences. Push is always enabled. Omitted values default to SMS always and email enabled. - **`email`** `boolean` - **`sms`** `string`, possible values: `"always", "fallback", "never"` - **`recipient`** `string` — The user ID of the recipient. This will be stored as recipient\_user in the database. Either recipient or caseId must be provided. When caseId is given without a recipient, the message is delivered to every client on the case. - **`scheduled_date`** `string` — ISO 8601 timestamp for when the message should be sent. Must be a future date. If provided, the message will be scheduled; otherwise, it will be sent immediately. - **`sender_org`** `boolean` — If true, the message will be displayed as sent from the organization rather than the individual user. The sender field still tracks who actually sent it. **Example:** ```json { "sender": "", "sender_org": true, "recipient": "", "caseId": "", "message_subject": "", "message_body": "", "scheduled_date": "", "channels": { "sms": "always", "email": true } } ``` #### Responses ##### Status: 200 Message created successfully ###### Content-Type: application/json - **`message` (required)** `string` — Success message confirming the message creation - **`messageId` (required)** `string` — The unique identifier of the created message. When a caseId addresses several clients, this is the first of messageIds. - **`messageIds` (required)** `array` — Every message created by the request — one per recipient. A caseId with no explicit recipient creates one message per client on the case. **Items:** `string` **Example:** ```json { "messageId": "", "messageIds": [ "" ], "message": "" } ``` ### List messages - **Method:** `GET` - **Path:** `/messages` - **Tags:** messages Retrieves a paginated list of messages for the authenticated organization. Returns message metadata including sender, recipient, subject, body, status, and timestamps, with configurable limit and offset for pagination. #### Parameters ##### `limit` - **In:** `query` The maximum number of messages to return. `[ "number", "null" ]`, default: `10` — The maximum number of messages to return. ##### `offset` - **In:** `query` The number of messages to skip before starting to return results. `[ "number", "null" ]`, default: `0` — The number of messages to skip before starting to return results. #### Responses ##### Status: 200 List of messages retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` — Array of messages **Items:** - **`caseId` (required)** `string | null` — The case ID associated with the message - **`created_at` (required)** `string` — ISO timestamp when the message was created - **`id` (required)** `string` — The unique identifier of the message - **`is_unread` (required)** `boolean` — Whether the message has been read by the recipient - **`message_body` (required)** `string | null` — The body content of the message - **`message_subject` (required)** `string | null` — The subject line of the message - **`recipient` (required)** `string | null` — The user ID of the message recipient - **`sender` (required)** `string | null` — The user ID of the message sender - **`status` (required)** `string | null` — The current delivery/processing status of the message - **`updated_at` (required)** `string` — ISO timestamp when the message was last updated - **`message` (required)** `string` — Success message confirming the retrieve operation - **`pagination` (required)** `object` - **`has_more` (required)** `boolean` — Whether there is more data to fetch - **`limit` (required)** `number` — Maximum number of items returned - **`next` (required)** `string | null` — URL for the next page of items, if available - **`offset` (required)** `number` — Number of items skipped - **`previous` (required)** `string | null` — URL for the previous page of items, if available - **`total` (required)** `number` — Total number of items available **Example:** ```json { "message": "", "data": [ { "id": "", "sender": null, "recipient": null, "caseId": null, "message_subject": null, "message_body": null, "status": null, "is_unread": true, "created_at": "", "updated_at": "" } ], "pagination": { "total": 1, "offset": 1, "limit": 1, "has_more": true, "previous": null, "next": null } } ``` ### Retrieve a message - **Method:** `GET` - **Path:** `/messages/{id}` - **Tags:** messages Retrieves detailed information about a specific message by its ID. Returns comprehensive message metadata including sender, recipient, content, status, and timestamps. Only messages belonging to the authenticated organization can be retrieved. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the message to retrieve `string` — The unique identifier of the message to retrieve #### Responses ##### Status: 200 Message retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` — Message data with core fields and metadata - **`caseId` (required)** `string | null` — The case ID associated with the message - **`created_at` (required)** `string` — ISO timestamp when the message was created - **`id` (required)** `string` — The unique identifier of the message - **`is_unread` (required)** `boolean` — Whether the message has been read by the recipient - **`message_body` (required)** `string | null` — The body content of the message, may include HTML formatting - **`message_subject` (required)** `string | null` — The subject line of the message - **`recipient` (required)** `string | null` — The user ID of the message recipient - **`sender` (required)** `string | null` — The user ID of the message sender - **`status` (required)** `string | null` — The current delivery/processing status of the message - **`updated_at` (required)** `string` — ISO timestamp when the message was last updated - **`message` (required)** `string` — Success message confirming the retrieve operation **Example:** ```json { "message": "", "data": { "id": "", "sender": null, "recipient": null, "caseId": null, "message_subject": null, "message_body": null, "status": null, "is_unread": true, "created_at": "", "updated_at": "" } } ``` ### Update a scheduled message - **Method:** `PATCH` - **Path:** `/messages/{id}` - **Tags:** messages Corrects the subject or body of a message that has not gone out yet. The scheduled sender reads the row when it fires, so an edit made before then is what the client receives. Only messages the organization owns, and only while they are still `scheduled` or `draft` — a message that has already been sent is in the client's inbox and cannot be rewritten. Messages generated by an automation are refused: the automation regenerates them from its own template, so the edit would be silently reverted. Edit the automation instead. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the message to update `string` — The unique identifier of the message to update #### Request Body ##### Content-Type: application/json - **`message_body`** `string` — Replacement body. Supports HTML, the same as on create. This replaces the body outright rather than patching part of it. - **`message_subject`** `string` — Replacement subject line **Example:** ```json { "message_subject": "", "message_body": "" } ``` #### Responses ##### Status: 200 Message updated ###### Content-Type: application/json - **`data` (required)** `object` — The message as it stands after the update - **`id` (required)** `string` — The unique identifier of the message - **`message_body` (required)** `string | null` — The body as it now stands, may include HTML formatting - **`message_subject` (required)** `string | null` — The subject line as it now stands - **`scheduled_date` (required)** `string | null` — ISO timestamp of when the message is due to go out. The sender polls every 15 minutes, so this is the deadline an edit has to beat. - **`status` (required)** `string | null` — The message's status, unchanged by this route — an edit never sends or unsends - **`updated_at` (required)** `string` — ISO timestamp when the message was last updated - **`message` (required)** `string` — Success message confirming the update operation **Example:** ```json { "message": "", "data": { "id": "", "message_subject": null, "message_body": null, "status": null, "scheduled_date": null, "updated_at": "" } } ``` ### Retrieve the caller's organization profile - **Method:** `GET` - **Path:** `/org` - **Tags:** org Returns the authenticated caller's own firm profile and contact settings (name, address, phone, website, social links, hours, referral email). The organization is resolved from the credential, so there is no id in the path. #### Responses ##### Status: 200 Organization profile retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` — The firm's editable profile and contact settings - **`address_1` (required)** `string | null` — Street address line 1 - **`address_2` (required)** `string | null` — Street address line 2 - **`button_background_color` (required)** `string | null` — Background tile color on the firm's profile action buttons (hex) - **`button_icon_color` (required)** `string | null` — Foreground icon color on the firm's profile action buttons (hex) - **`city` (required)** `string | null` — City - **`country` (required)** `string | null` — Country - **`email` (required)** `string | null` — Primary contact email - **`facebook` (required)** `string | null` — Facebook URL or handle - **`faqs_enabled` (required)** `boolean | null` — Whether client-facing FAQs are enabled - **`icon_image` (required)** `string | null` — Hosted icon URL (read-only — set with POST /org/branding) - **`id` (required)** `string` — The organization id - **`instagram` (required)** `string | null` — Instagram URL or handle - **`invite_message_templates` (required)** `object | null` — Per-language custom client invite SMS templates, keyed by language code. Replaces the whole map when set; pass null to revert every language to the default message. - **`en`** `string` - **`es`** `string` - **`ht`** `string` - **`km`** `string` - **`ko`** `string` - **`pt`** `string` - **`th`** `string` - **`tl`** `string` - **`vi`** `string` - **`zh_cn`** `string` - **`zh_tw`** `string` - **`linkedin` (required)** `string | null` — LinkedIn URL - **`logo_image` (required)** `string | null` — Hosted logo URL (read-only — set with POST /org/branding) - **`name` (required)** `string | null` — The firm name - **`operation_hours_weekdays` (required)** `string | null` — Weekday hours, e.g. "09:00-17:00" - **`operation_hours_weekends` (required)** `string | null` — Weekend hours, e.g. "10:00-14:00" (or null if closed) - **`organization_alert` (required)** `object | null` — A banner alert shown to all of the firm's clients in the app (e.g. holiday closure). Pass null to clear the alert. - **`enabled` (required)** `boolean` — Whether the alert is shown to clients - **`message` (required)** `string` — The alert text shown in the app - **`expires_at`** `string | null`, format: `date-time` — UTC ISO timestamp when the alert stops showing; omit or null for no end date - **`starts_at`** `string | null`, format: `date-time` — UTC ISO timestamp when the alert starts showing; omit or null to show immediately - **`payments_url` (required)** `string | null` — Online payments URL - **`phone` (required)** `string | null` — Primary phone number - **`postal_code` (required)** `string | null` — Postal / ZIP code - **`primary_color` (required)** `string | null` — Deprecated: use button\_icon\_color instead. Brand "inside icon" color (hex) - **`referrals_email` (required)** `string | null` — Email address that receives referral notifications - **`review_link` (required)** `string | null` — Public review link - **`secondary_color` (required)** `string | null` — Deprecated: use button\_background\_color instead. Brand "outside icon" color (hex) - **`state` (required)** `string | null` — State or region - **`tiktok` (required)** `string | null` — TikTok URL or handle - **`timezone` (required)** `string | null` — Firm time zone (IANA name) - **`twitter` (required)** `string | null` — X / Twitter URL or handle - **`website` (required)** `string | null` — Firm website URL - **`client_features`** `object` — Client-facing feature flags (read-only) - **`client_language_distribution`** `object | null` — Client language distribution (base language code -> count). "unset" tracks clients with no language. - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "name": null, "phone": null, "email": null, "address_1": null, "address_2": null, "city": null, "state": null, "country": null, "postal_code": null, "website": null, "timezone": null, "review_link": null, "payments_url": null, "facebook": null, "instagram": null, "twitter": null, "linkedin": null, "tiktok": null, "operation_hours_weekdays": null, "operation_hours_weekends": null, "referrals_email": null, "button_icon_color": null, "button_background_color": null, "primary_color": null, "secondary_color": null, "logo_image": null, "icon_image": null, "invite_message_templates": { "en": "", "es": "", "ht": "", "km": "", "ko": "", "th": "", "tl": "", "zh_cn": "", "zh_tw": "", "vi": "", "pt": "" }, "organization_alert": { "enabled": true, "message": "", "starts_at": null, "expires_at": null }, "faqs_enabled": null, "client_features": { "additionalProperty": true }, "client_language_distribution": null } } ``` ### Update the caller's organization profile - **Method:** `PATCH` - **Path:** `/org` - **Tags:** org Updates the authenticated caller's own firm profile and contact settings. Only the fields provided are changed; omitted fields are left untouched. Requires an admin role at the firm. The organization is resolved from the credential, so there is no id in the path. #### Request Body ##### Content-Type: application/json - **`address_1`** `string | null` — Street address 1 - **`address_2`** `string | null` — Street address 2 - **`button_background_color`** `string | null` — Background tile color on the firm's profile action buttons (hex) - **`button_icon_color`** `string | null` — Foreground icon color on the firm's profile action buttons (hex) - **`city`** `string | null` — City - **`country`** `string | null` — Country - **`email`** `string | null` — Primary contact email - **`facebook`** `string | null` — Facebook URL/handle - **`faqs_enabled`** `boolean | null` — Whether client-facing FAQs are enabled - **`instagram`** `string | null` — Instagram URL/handle - **`invite_message_templates`** `object | null` — Per-language custom client invite SMS templates, keyed by language code. Replaces the whole map when set; pass null to revert every language to the default message. - **`en`** `string` - **`es`** `string` - **`ht`** `string` - **`km`** `string` - **`ko`** `string` - **`pt`** `string` - **`th`** `string` - **`tl`** `string` - **`vi`** `string` - **`zh_cn`** `string` - **`zh_tw`** `string` - **`linkedin`** `string | null` — LinkedIn URL - **`name`** `string` — The firm name - **`operation_hours_weekdays`** `string | null` — Weekday hours, e.g. "09:00-17:00" - **`operation_hours_weekends`** `string | null` — Weekend hours, e.g. "10:00-14:00" - **`organization_alert`** `object | null` — A banner alert shown to all of the firm's clients in the app (e.g. holiday closure). Pass null to clear the alert. - **`enabled` (required)** `boolean` — Whether the alert is shown to clients - **`message` (required)** `string` — The alert text shown in the app - **`expires_at`** `string | null`, format: `date-time` — UTC ISO timestamp when the alert stops showing; omit or null for no end date - **`starts_at`** `string | null`, format: `date-time` — UTC ISO timestamp when the alert starts showing; omit or null to show immediately - **`payments_url`** `string | null` — Online payments URL - **`phone`** `string | null` — Primary phone number - **`postal_code`** `string | null` — Postal / ZIP code - **`primary_color`** `string | null` — Deprecated: use button\_icon\_color instead. - **`referrals_email`** `string | null` — Email that receives referral notifications - **`review_link`** `string | null` — Public review link - **`secondary_color`** `string | null` — Deprecated: use button\_background\_color instead. - **`state`** `string | null` — State or region - **`tiktok`** `string | null` — TikTok URL/handle - **`timezone`** `string | null` — Firm time zone (IANA name, e.g. "America/Chicago") - **`twitter`** `string | null` — X/Twitter URL/handle - **`website`** `string | null` — Firm website URL **Example:** ```json { "name": "", "phone": null, "email": null, "address_1": null, "address_2": null, "city": null, "state": null, "country": null, "postal_code": null, "website": null, "timezone": null, "review_link": null, "payments_url": null, "facebook": null, "instagram": null, "twitter": null, "linkedin": null, "tiktok": null, "operation_hours_weekdays": null, "operation_hours_weekends": null, "referrals_email": null, "button_icon_color": null, "button_background_color": null, "primary_color": null, "secondary_color": null, "invite_message_templates": { "en": "", "es": "", "ht": "", "km": "", "ko": "", "th": "", "tl": "", "zh_cn": "", "zh_tw": "", "vi": "", "pt": "" }, "organization_alert": { "enabled": true, "message": "", "starts_at": null, "expires_at": null }, "faqs_enabled": null } ``` #### Responses ##### Status: 200 Organization updated successfully ###### Content-Type: application/json - **`data` (required)** `object` — The firm's editable profile and contact settings - **`address_1` (required)** `string | null` — Street address line 1 - **`address_2` (required)** `string | null` — Street address line 2 - **`button_background_color` (required)** `string | null` — Background tile color on the firm's profile action buttons (hex) - **`button_icon_color` (required)** `string | null` — Foreground icon color on the firm's profile action buttons (hex) - **`city` (required)** `string | null` — City - **`country` (required)** `string | null` — Country - **`email` (required)** `string | null` — Primary contact email - **`facebook` (required)** `string | null` — Facebook URL or handle - **`faqs_enabled` (required)** `boolean | null` — Whether client-facing FAQs are enabled - **`icon_image` (required)** `string | null` — Hosted icon URL (read-only — set with POST /org/branding) - **`id` (required)** `string` — The organization id - **`instagram` (required)** `string | null` — Instagram URL or handle - **`invite_message_templates` (required)** `object | null` — Per-language custom client invite SMS templates, keyed by language code. Replaces the whole map when set; pass null to revert every language to the default message. - **`en`** `string` - **`es`** `string` - **`ht`** `string` - **`km`** `string` - **`ko`** `string` - **`pt`** `string` - **`th`** `string` - **`tl`** `string` - **`vi`** `string` - **`zh_cn`** `string` - **`zh_tw`** `string` - **`linkedin` (required)** `string | null` — LinkedIn URL - **`logo_image` (required)** `string | null` — Hosted logo URL (read-only — set with POST /org/branding) - **`name` (required)** `string | null` — The firm name - **`operation_hours_weekdays` (required)** `string | null` — Weekday hours, e.g. "09:00-17:00" - **`operation_hours_weekends` (required)** `string | null` — Weekend hours, e.g. "10:00-14:00" (or null if closed) - **`organization_alert` (required)** `object | null` — A banner alert shown to all of the firm's clients in the app (e.g. holiday closure). Pass null to clear the alert. - **`enabled` (required)** `boolean` — Whether the alert is shown to clients - **`message` (required)** `string` — The alert text shown in the app - **`expires_at`** `string | null`, format: `date-time` — UTC ISO timestamp when the alert stops showing; omit or null for no end date - **`starts_at`** `string | null`, format: `date-time` — UTC ISO timestamp when the alert starts showing; omit or null to show immediately - **`payments_url` (required)** `string | null` — Online payments URL - **`phone` (required)** `string | null` — Primary phone number - **`postal_code` (required)** `string | null` — Postal / ZIP code - **`primary_color` (required)** `string | null` — Deprecated: use button\_icon\_color instead. Brand "inside icon" color (hex) - **`referrals_email` (required)** `string | null` — Email address that receives referral notifications - **`review_link` (required)** `string | null` — Public review link - **`secondary_color` (required)** `string | null` — Deprecated: use button\_background\_color instead. Brand "outside icon" color (hex) - **`state` (required)** `string | null` — State or region - **`tiktok` (required)** `string | null` — TikTok URL or handle - **`timezone` (required)** `string | null` — Firm time zone (IANA name) - **`twitter` (required)** `string | null` — X / Twitter URL or handle - **`website` (required)** `string | null` — Firm website URL - **`client_features`** `object` — Client-facing feature flags (read-only) - **`client_language_distribution`** `object | null` — Client language distribution (base language code -> count). "unset" tracks clients with no language. - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "name": null, "phone": null, "email": null, "address_1": null, "address_2": null, "city": null, "state": null, "country": null, "postal_code": null, "website": null, "timezone": null, "review_link": null, "payments_url": null, "facebook": null, "instagram": null, "twitter": null, "linkedin": null, "tiktok": null, "operation_hours_weekdays": null, "operation_hours_weekends": null, "referrals_email": null, "button_icon_color": null, "button_background_color": null, "primary_color": null, "secondary_color": null, "logo_image": null, "icon_image": null, "invite_message_templates": { "en": "", "es": "", "ht": "", "km": "", "ko": "", "th": "", "tl": "", "zh_cn": "", "zh_tw": "", "vi": "", "pt": "" }, "organization_alert": { "enabled": true, "message": "", "starts_at": null, "expires_at": null }, "faqs_enabled": null, "client_features": { "additionalProperty": true }, "client_language_distribution": null } } ``` ### List the firm's locations - **Method:** `GET` - **Path:** `/org/locations` - **Tags:** org Returns all of the caller's firm office locations. #### Responses ##### Status: 200 Locations retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` **Items:** - **`address_1` (required)** `string | null` — Street address line 1 - **`address_2` (required)** `string | null` — Street address line 2 - **`branding` (required)** `object` — The per-location branding configuration - **`details` (required)** `object | null` — The saved branding details, if any have been configured - **`button_background_color` (required)** `string | null` — Background tile color on the firm's profile action buttons (hex) - **`button_icon_color` (required)** `string | null` — Foreground icon color on the firm's profile action buttons (hex) - **`enabled` (required)** `boolean` — Whether this location overrides the firm-level branding - **`icon_image` (required)** `string | null` — Hosted icon URL - **`logo_image` (required)** `string | null` — Hosted logo URL - **`name` (required)** `string | null` — Client-facing name for this location - **`primary_color` (required)** `string | null` — Deprecated: use button\_icon\_color instead. - **`secondary_color` (required)** `string | null` — Deprecated: use button\_background\_color instead. - **`state` (required)** `string`, possible values: `"set", "cleared", "unconfigured"` — The configuration state of this location's branding - **`city` (required)** `string | null` — City - **`country` (required)** `string | null` — Country - **`email` (required)** `string | null` — Location email - **`facebook` (required)** `string | null` — Facebook URL/handle - **`id` (required)** `number` — The location id - **`instagram` (required)** `string | null` — Instagram URL/handle - **`is_primary` (required)** `boolean` — True if this is the primary location - **`is_virtual` (required)** `boolean` — Virtual location (no physical address) — clears address fields - **`linkedin` (required)** `string | null` — LinkedIn URL - **`name` (required)** `string | null` — Location name (e.g. "Downtown Office") - **`payments_url` (required)** `string | null` — Online payments URL - **`phone` (required)** `string | null` — Location phone number - **`postal_code` (required)** `string | null` — Postal / ZIP code - **`referrals_email` (required)** `string | null` — Referral notifications email - **`review_link` (required)** `string | null` — Public review link - **`state` (required)** `string | null` — State or region - **`tiktok` (required)** `string | null` — TikTok URL/handle - **`timezone` (required)** `string | null` — Location time zone (IANA name) - **`twitter` (required)** `string | null` — X/Twitter URL/handle - **`website` (required)** `string | null` — Location website URL - **`message` (required)** `string` **Example:** ```json { "message": "", "data": [ { "id": 1, "is_primary": true, "name": null, "is_virtual": true, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null, "phone": null, "email": null, "timezone": null, "website": null, "review_link": null, "payments_url": null, "referrals_email": null, "facebook": null, "instagram": null, "twitter": null, "linkedin": null, "tiktok": null, "branding": { "state": "set", "details": { "enabled": true, "name": null, "button_icon_color": null, "button_background_color": null, "primary_color": null, "secondary_color": null, "logo_image": null, "icon_image": null } } } ] } ``` ### Add a firm location - **Method:** `POST` - **Path:** `/org/locations` - **Tags:** org Adds a new office location to the firm. Requires an admin role. Set is\_virtual=true for a location with no physical address (address fields are cleared). #### Request Body ##### Content-Type: application/json - **`address_1`** `string | null` — Street address 1 - **`address_2`** `string | null` — Street address 2 - **`city`** `string | null` — City - **`country`** `string | null` — Country - **`email`** `string | null`, format: `email` — Location email - **`facebook`** `string | null` — Facebook URL/handle - **`instagram`** `string | null` — Instagram URL/handle - **`is_virtual`** `boolean` — Virtual location — when true the address fields are cleared - **`linkedin`** `string | null` — LinkedIn URL - **`name`** `string | null` — Location name - **`payments_url`** `string | null` — Online payments URL - **`phone`** `string | null` — Location phone - **`postal_code`** `string | null` — Postal / ZIP code - **`referrals_email`** `string | null`, format: `email` — Referral notifications email - **`review_link`** `string | null` — Public review link - **`state`** `string | null` — State or region - **`tiktok`** `string | null` — TikTok URL/handle - **`timezone`** `string | null` — Time zone (IANA name) - **`twitter`** `string | null` — X/Twitter URL/handle - **`website`** `string | null` — Location website **Example:** ```json { "name": null, "is_virtual": true, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null, "phone": null, "email": null, "timezone": null, "website": null, "review_link": null, "payments_url": null, "referrals_email": null, "facebook": null, "instagram": null, "twitter": null, "linkedin": null, "tiktok": null } ``` #### Responses ##### Status: 200 Location created successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`address_1` (required)** `string | null` — Street address line 1 - **`address_2` (required)** `string | null` — Street address line 2 - **`branding` (required)** `object` — The per-location branding configuration - **`details` (required)** `object | null` — The saved branding details, if any have been configured - **`button_background_color` (required)** `string | null` — Background tile color on the firm's profile action buttons (hex) - **`button_icon_color` (required)** `string | null` — Foreground icon color on the firm's profile action buttons (hex) - **`enabled` (required)** `boolean` — Whether this location overrides the firm-level branding - **`icon_image` (required)** `string | null` — Hosted icon URL - **`logo_image` (required)** `string | null` — Hosted logo URL - **`name` (required)** `string | null` — Client-facing name for this location - **`primary_color` (required)** `string | null` — Deprecated: use button\_icon\_color instead. - **`secondary_color` (required)** `string | null` — Deprecated: use button\_background\_color instead. - **`state` (required)** `string`, possible values: `"set", "cleared", "unconfigured"` — The configuration state of this location's branding - **`city` (required)** `string | null` — City - **`country` (required)** `string | null` — Country - **`email` (required)** `string | null` — Location email - **`facebook` (required)** `string | null` — Facebook URL/handle - **`id` (required)** `number` — The location id - **`instagram` (required)** `string | null` — Instagram URL/handle - **`is_primary` (required)** `boolean` — True if this is the primary location - **`is_virtual` (required)** `boolean` — Virtual location (no physical address) — clears address fields - **`linkedin` (required)** `string | null` — LinkedIn URL - **`name` (required)** `string | null` — Location name (e.g. "Downtown Office") - **`payments_url` (required)** `string | null` — Online payments URL - **`phone` (required)** `string | null` — Location phone number - **`postal_code` (required)** `string | null` — Postal / ZIP code - **`referrals_email` (required)** `string | null` — Referral notifications email - **`review_link` (required)** `string | null` — Public review link - **`state` (required)** `string | null` — State or region - **`tiktok` (required)** `string | null` — TikTok URL/handle - **`timezone` (required)** `string | null` — Location time zone (IANA name) - **`twitter` (required)** `string | null` — X/Twitter URL/handle - **`website` (required)** `string | null` — Location website URL - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": 1, "is_primary": true, "name": null, "is_virtual": true, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null, "phone": null, "email": null, "timezone": null, "website": null, "review_link": null, "payments_url": null, "referrals_email": null, "facebook": null, "instagram": null, "twitter": null, "linkedin": null, "tiktok": null, "branding": { "state": "set", "details": { "enabled": true, "name": null, "button_icon_color": null, "button_background_color": null, "primary_color": null, "secondary_color": null, "logo_image": null, "icon_image": null } } } } ``` ### Update a firm location - **Method:** `PATCH` - **Path:** `/org/locations/{id}` - **Tags:** org Updates a firm location. Only the fields provided are changed. Requires an admin role. #### Parameters ##### `id` - **In:** `path` The location id `[ "number", "null" ]` — The location id #### Request Body ##### Content-Type: application/json - **`address_1`** `string | null` — Street address 1 - **`address_2`** `string | null` — Street address 2 - **`city`** `string | null` — City - **`country`** `string | null` — Country - **`email`** `string | null`, format: `email` — Location email - **`facebook`** `string | null` — Facebook URL/handle - **`instagram`** `string | null` — Instagram URL/handle - **`is_virtual`** `boolean` — Virtual location — when true the address fields are cleared - **`linkedin`** `string | null` — LinkedIn URL - **`name`** `string | null` — Location name - **`payments_url`** `string | null` — Online payments URL - **`phone`** `string | null` — Location phone - **`postal_code`** `string | null` — Postal / ZIP code - **`referrals_email`** `string | null`, format: `email` — Referral notifications email - **`review_link`** `string | null` — Public review link - **`state`** `string | null` — State or region - **`tiktok`** `string | null` — TikTok URL/handle - **`timezone`** `string | null` — Time zone (IANA name) - **`twitter`** `string | null` — X/Twitter URL/handle - **`website`** `string | null` — Location website **Example:** ```json { "name": null, "is_virtual": true, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null, "phone": null, "email": null, "timezone": null, "website": null, "review_link": null, "payments_url": null, "referrals_email": null, "facebook": null, "instagram": null, "twitter": null, "linkedin": null, "tiktok": null } ``` #### Responses ##### Status: 200 Location updated successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`address_1` (required)** `string | null` — Street address line 1 - **`address_2` (required)** `string | null` — Street address line 2 - **`branding` (required)** `object` — The per-location branding configuration - **`details` (required)** `object | null` — The saved branding details, if any have been configured - **`button_background_color` (required)** `string | null` — Background tile color on the firm's profile action buttons (hex) - **`button_icon_color` (required)** `string | null` — Foreground icon color on the firm's profile action buttons (hex) - **`enabled` (required)** `boolean` — Whether this location overrides the firm-level branding - **`icon_image` (required)** `string | null` — Hosted icon URL - **`logo_image` (required)** `string | null` — Hosted logo URL - **`name` (required)** `string | null` — Client-facing name for this location - **`primary_color` (required)** `string | null` — Deprecated: use button\_icon\_color instead. - **`secondary_color` (required)** `string | null` — Deprecated: use button\_background\_color instead. - **`state` (required)** `string`, possible values: `"set", "cleared", "unconfigured"` — The configuration state of this location's branding - **`city` (required)** `string | null` — City - **`country` (required)** `string | null` — Country - **`email` (required)** `string | null` — Location email - **`facebook` (required)** `string | null` — Facebook URL/handle - **`id` (required)** `number` — The location id - **`instagram` (required)** `string | null` — Instagram URL/handle - **`is_primary` (required)** `boolean` — True if this is the primary location - **`is_virtual` (required)** `boolean` — Virtual location (no physical address) — clears address fields - **`linkedin` (required)** `string | null` — LinkedIn URL - **`name` (required)** `string | null` — Location name (e.g. "Downtown Office") - **`payments_url` (required)** `string | null` — Online payments URL - **`phone` (required)** `string | null` — Location phone number - **`postal_code` (required)** `string | null` — Postal / ZIP code - **`referrals_email` (required)** `string | null` — Referral notifications email - **`review_link` (required)** `string | null` — Public review link - **`state` (required)** `string | null` — State or region - **`tiktok` (required)** `string | null` — TikTok URL/handle - **`timezone` (required)** `string | null` — Location time zone (IANA name) - **`twitter` (required)** `string | null` — X/Twitter URL/handle - **`website` (required)** `string | null` — Location website URL - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": 1, "is_primary": true, "name": null, "is_virtual": true, "address_1": null, "address_2": null, "city": null, "state": null, "postal_code": null, "country": null, "phone": null, "email": null, "timezone": null, "website": null, "review_link": null, "payments_url": null, "referrals_email": null, "facebook": null, "instagram": null, "twitter": null, "linkedin": null, "tiktok": null, "branding": { "state": "set", "details": { "enabled": true, "name": null, "button_icon_color": null, "button_background_color": null, "primary_color": null, "secondary_color": null, "logo_image": null, "icon_image": null } } } } ``` ### Delete a firm location - **Method:** `DELETE` - **Path:** `/org/locations/{id}` - **Tags:** org Removes a firm location. Requires an admin role. #### Parameters ##### `id` - **In:** `path` The location id `[ "number", "null" ]` — The location id #### Responses ##### Status: 200 Location deleted successfully ###### Content-Type: application/json - **`message` (required)** `string` **Example:** ```json { "message": "" } ``` ### Set a location's branding - **Method:** `PUT` - **Path:** `/org/locations/{id}/branding` - **Tags:** org Sets a single location's branding override — its own name, colors, logo and icon shown to that location's clients instead of the firm-level branding. Set enabled=false to fall back to firm branding without discarding the values. Images are given as publicly reachable https URLs (10MB max) and re-hosted by Quilia, which resizes anything over 1600px. Omitted fields keep their current value; null clears one. Requires an admin role. #### Parameters ##### `id` - **In:** `path` The location id `[ "number", "null" ]` — The location id #### Request Body ##### Content-Type: application/json - **`enabled` (required)** `boolean` — Turn this location's own branding on or off. When off, clients see the firm-level branding. - **`button_background_color`** `string | null` — Background tile color on the firm's profile action buttons (hex) - **`button_icon_color`** `string | null` — Foreground icon color on the firm's profile action buttons (hex) - **`icon_url`** `string | null`, format: `uri` — The firm's icon. Square canvas (1:1), 512x512 px minimum, rendered as a circle in the app (keep mark inside inscribed circle with margin, corners are cropped). Legible at 32pt (monogram/brandmark, not a wordmark). Dark or full-color on transparent background (no reverse/knockout variants, no baked-in background). Publicly reachable https image URL (PNG, JPG, GIF or WebP, 10MB max) — we download it, resize anything over 1600px, and host our own copy. Pass null to remove the current image. - **`logo_url`** `string | null`, format: `uri` — The firm's full logo. \~2.6:1 aspect ratio, 1200x460 px, scaled to 64pt tall. Dark or full-color on transparent background (no reverse/knockout variants, no baked-in background). Acts as a fallback in circular slots when icon is missing. Publicly reachable https image URL (PNG, JPG, GIF or WebP, 10MB max) — we download it, resize anything over 1600px, and host our own copy. Pass null to remove the current image. - **`name`** `string | null` — Client-facing name for this location - **`primary_color`** `string | null` — Deprecated: use button\_icon\_color instead. - **`secondary_color`** `string | null` — Deprecated: use button\_background\_color instead. **Example:** ```json { "enabled": true, "name": null, "button_icon_color": null, "button_background_color": null, "primary_color": null, "secondary_color": null, "logo_url": null, "icon_url": null } ``` #### Responses ##### Status: 200 Location branding updated successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`button_background_color` (required)** `string | null` — Background tile color on the firm's profile action buttons (hex) - **`button_icon_color` (required)** `string | null` — Foreground icon color on the firm's profile action buttons (hex) - **`enabled` (required)** `boolean` — Whether this location overrides the firm-level branding - **`icon_image` (required)** `string | null` — Hosted icon URL - **`logo_image` (required)** `string | null` — Hosted logo URL - **`name` (required)** `string | null` — Client-facing name for this location - **`primary_color` (required)** `string | null` — Deprecated: use button\_icon\_color instead. - **`secondary_color` (required)** `string | null` — Deprecated: use button\_background\_color instead. - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "enabled": true, "name": null, "button_icon_color": null, "button_background_color": null, "primary_color": null, "secondary_color": null, "logo_image": null, "icon_image": null } } ``` ### Set the firm's logo and icon - **Method:** `POST` - **Path:** `/org/branding` - **Tags:** org Sets the firm's logo and/or icon from publicly reachable image URLs (for example the logo already published on the firm's website). The image is downloaded and re-hosted by Quilia, so the source URL does not need to stay available. PNG, JPG, GIF or WebP, 10MB max, resized to 1600px if larger; the icon is displayed as a square, so a square source works best. Pass null for a field to remove that image. Requires an admin role. #### Request Body ##### Content-Type: application/json - **`icon_url`** `string | null`, format: `uri` — The firm's icon. Square canvas (1:1), 512x512 px minimum, rendered as a circle in the app (keep mark inside inscribed circle with margin, corners are cropped). Legible at 32pt (monogram/brandmark, not a wordmark). Dark or full-color on transparent background (no reverse/knockout variants, no baked-in background). Publicly reachable https image URL (PNG, JPG, GIF or WebP, 10MB max) — we download it, resize anything over 1600px, and host our own copy. Pass null to remove the current image. - **`logo_url`** `string | null`, format: `uri` — The firm's full logo. \~2.6:1 aspect ratio, 1200x460 px, scaled to 64pt tall. Dark or full-color on transparent background (no reverse/knockout variants, no baked-in background). Acts as a fallback in circular slots when icon is missing. Publicly reachable https image URL (PNG, JPG, GIF or WebP, 10MB max) — we download it, resize anything over 1600px, and host our own copy. Pass null to remove the current image. **Example:** ```json { "logo_url": null, "icon_url": null } ``` #### Responses ##### Status: 200 Branding updated successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`icon_image` (required)** `string | null` — Hosted icon URL - **`logo_image` (required)** `string | null` — Hosted logo URL - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "logo_image": null, "icon_image": null } } ``` ### List the firm's FAQs - **Method:** `GET` - **Path:** `/org/faqs` - **Tags:** org Returns the firm's client-facing FAQs (shown in the Quilia app), in display order. #### Parameters ##### `language` - **In:** `query` Optional language code (e.g. es). If provided, returns FAQs translated to this language, falling back to English. `string` — Optional language code (e.g. es). If provided, returns FAQs translated to this language, falling back to English. #### Responses ##### Status: 200 FAQs retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` **Items:** - **`answer` (required)** `string` — The answer - **`id` (required)** `string` — Stable FAQ id (server-generated) - **`order` (required)** `number` — Display order (0-based) - **`question` (required)** `string` — The question - **`message` (required)** `string` **Example:** ```json { "message": "", "data": [ { "id": "", "order": 1, "question": "", "answer": "" } ] } ``` ### Replace the firm's FAQs - **Method:** `PUT` - **Path:** `/org/faqs` - **Tags:** org Replaces the firm's FAQ list with the provided questions/answers (in order). ids and ordering are assigned server-side, and English translations are rebuilt. Requires an admin role. #### Request Body ##### Content-Type: application/json - **`faqs` (required)** `array` — The complete FAQ list, in display order. Replaces existing. **Items:** - **`answer` (required)** `string` — The answer - **`question` (required)** `string` — The question - **`translations`** `array` — Translations for the FAQs **Items:** - **`faqs` (required)** `array` — Translated FAQs matching the order of the base faqs array **Items:** - **`answer` (required)** `string` — The translated answer - **`question` (required)** `string` — The translated question - **`language` (required)** `string`, possible values: `"es", "pt", "ht", "km", "ko", "th", "tl", "vi", "zh_CN", "zh_TW", "ru", "hy", "ar", "fa", "de", "pl", "it", "ja", "sr", "fr", "hmn", "mn", "hi", "pa"` — The language code **Example:** ```json { "faqs": [ { "question": "", "answer": "" } ], "translations": [ { "language": "es", "faqs": [ { "question": "", "answer": "" } ] } ] } ``` #### Responses ##### Status: 200 FAQs updated successfully ###### Content-Type: application/json - **`data` (required)** `array` **Items:** - **`answer` (required)** `string` — The answer - **`id` (required)** `string` — Stable FAQ id (server-generated) - **`order` (required)** `number` — Display order (0-based) - **`question` (required)** `string` — The question - **`message` (required)** `string` **Example:** ```json { "message": "", "data": [ { "id": "", "order": 1, "question": "", "answer": "" } ] } ``` ### Firm setup-status checklist - **Method:** `GET` - **Path:** `/org/setup-status` - **Tags:** org Returns a config-completeness checklist for the caller's firm (profile, team, locations, case types, client features, FAQs, intake forms, task lists, CMS connection). Use it to see what's set up and what's still needed when guiding onboarding. Case types count as unconfigured until the firm turns some off — they are all on by default, so an untouched firm is showing its clients every symptom list Quilia has. #### Responses ##### Status: 200 Setup status retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` **Items:** - **`complete` (required)** `boolean` — Whether this area is configured - **`hint` (required)** `string | null` — What to do next when incomplete (null when complete) - **`key` (required)** `string` — Stable identifier for the setup area - **`label` (required)** `string` — Human-readable area name - **`summary` (required)** `string` — One-line status of this area - **`client_features`** `object` — Structured feature map (only present on the features item) - **`message` (required)** `string` **Example:** ```json { "message": "", "data": [ { "key": "", "label": "", "complete": true, "summary": "", "hint": null, "client_features": { "additionalProperty": true } } ] } ``` ### List the firm's integration settings - **Method:** `GET` - **Path:** `/org/integrations` - **Tags:** org Returns every integration configured for the caller's firm, including its operational toggles, feed settings, service-specific config and connection health. Credentials, sessions, tokens and encrypted values are deliberately excluded. #### Parameters ##### `cms_type` - **In:** `query` Return only this integration service, e.g. filevine or clio `string` — Return only this integration service, e.g. filevine or clio #### Responses ##### Status: 200 Integration settings retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` **Items:** - **`auto_invite` (required)** `boolean | null` - **`auto_invite_on_activity` (required)** `boolean` - **`case_type_mappings` (required)** `array` **Items:** - **`id` (required)** `string` - **`import_id` (required)** `string | null` - **`is_default` (required)** `boolean` - **`quilia_case_type` (required)** `string | null` - **`suggested_case_type` (required)** `string | null` - **`suggested_confidence` (required)** `number | null` - **`type_name` (required)** `string` - **`required_assets`** `object` - **`cms_id` (required)** `string` — The integration account / tenant identifier - **`cms_type` (required)** `string` — Stable integration service identifier - **`connection_status` (required)** `string | null` - **`custom_allowed` (required)** `boolean` - **`enabled` (required)** `boolean` - **`integration_type` (required)** `string` - **`last_event_at` (required)** `string | null` - **`last_health_check_at` (required)** `string | null` - **`name` (required)** `string | null` — Human-readable integration name - **`phase_messages_enabled` (required)** `boolean` - **`phase_target` (required)** `string` - **`settings_url` (required)** `string`, format: `uri` - **`config`** `object` — Service-specific mapping, import, sync, and automation settings - **`feed`** `object` — Client-feed settings - **`staff_assignments`** `object` — Saved staff assignment config, including legacy fields - **`message` (required)** `string` **Example:** ```json { "message": "", "data": [ { "cms_id": "", "cms_type": "", "name": null, "integration_type": "", "enabled": true, "connection_status": null, "auto_invite": null, "auto_invite_on_activity": true, "phase_messages_enabled": true, "phase_target": "", "custom_allowed": true, "feed": null, "config": null, "staff_assignments": null, "case_type_mappings": [ { "id": "", "type_name": "", "import_id": null, "quilia_case_type": null, "is_default": true, "required_assets": null, "suggested_case_type": null, "suggested_confidence": null } ], "last_event_at": null, "last_health_check_at": null, "settings_url": "" } ] } ``` ### List the Quilia fields a CMS field can be mapped to - **Method:** `GET` - **Path:** `/org/integrations/field-mapping-targets` - **Tags:** org Quilia's own mapping targets: the case fields a CMS field can fill, and the record types a repeating CMS collection can fill. `accepts` is the kind of value each takes, and `vocabulary`, where present, is the only set of values that column allows — a firm's own label must be mapped onto one of those. `scope` says whether a target is set per project type or once for the whole integration. `contact_destinations` is where a case contact can go: a CMS returns every contact on a case under one endpoint and the firm's own type word is all that distinguishes a treating clinic from a court or a tow yard, so those words are mapped onto these and anything unmapped is not imported. The CMS half of a mapping varies per firm and per project type and is discovered separately. #### Responses ##### Status: 200 Mapping targets retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`contact_destinations` (required)** `array` **Items:** - **`description` (required)** `string` - **`destination_key` (required)** `string` - **`label` (required)** `string` - **`fields` (required)** `array` **Items:** - **`accepts` (required)** `array` **Items:** `string` - **`description` (required)** `string` - **`label` (required)** `string` - **`scope` (required)** `string`, possible values: `"projectType", "integration"` - **`target_key` (required)** `string` - **`vocabulary`** `array` **Items:** `string` - **`records` (required)** `array` **Items:** - **`description` (required)** `string` - **`fields` (required)** `array` **Items:** - **`accepts` (required)** `array` **Items:** `string` - **`description` (required)** `string` - **`key` (required)** `string` - **`label` (required)** `string` - **`required` (required)** `boolean` - **`vocabulary`** `array` **Items:** `string` - **`label` (required)** `string` - **`record_key` (required)** `string` **Example:** ```json { "data": { "fields": [ { "target_key": "", "label": "", "description": "", "accepts": [ "" ], "scope": "projectType", "vocabulary": [ "" ] } ], "contact_destinations": [ { "destination_key": "", "label": "", "description": "" } ], "records": [ { "record_key": "", "label": "", "description": "", "fields": [ { "key": "", "label": "", "description": "", "accepts": [ "" ], "required": true, "vocabulary": [ "" ] } ] } ] } } ``` ### List the CMS fields a firm can map, for one project type - **Method:** `GET` - **Path:** `/org/integrations/{cms_type}/{cms_id}/mappable-fields` - **Tags:** org The firm's own sections and fields, as last read from their CMS, with the Quilia value kind each field can fill and the firm's dropdown values where it has them. Layout fields are excluded: they hold no value and only make the list unreadable. For Clio, returns native matter Location and client address state fields for every practice area without a crawl; `discovered_at` is null. Other CMSes return the last crawl, with `discovered_at` indicating when. The portal refreshes it when someone opens the mapping screen or presses Refresh fields. A project type nobody has opened yet returns no sections. #### Parameters ##### `cms_type` required - **In:** `path` `string` ##### `cms_id` required - **In:** `path` `string` ##### `project_type_id` required - **In:** `query` CMS project type. Sections and fields are defined per project type. `string` — CMS project type. Sections and fields are defined per project type. #### Responses ##### Status: 200 Mappable fields retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`discovered_at` (required)** `string | null` - **`project_type_id` (required)** `string` - **`sections` (required)** `array` **Items:** - **`fields` (required)** `array` **Items:** - **`field_selector` (required)** `string` - **`kind` (required)** `string` - **`name` (required)** `string` - **`dropdown_items`** `array` **Items:** `string` - **`is_collection` (required)** `boolean` - **`name` (required)** `string` - **`section_selector` (required)** `string` **Example:** ```json { "data": { "project_type_id": "", "discovered_at": null, "sections": [ { "section_selector": "", "name": "", "is_collection": true, "fields": [ { "field_selector": "", "name": "", "kind": "", "dropdown_items": [ "" ] } ] } ] } } ``` ### Update one of the firm's integration configurations - **Method:** `PATCH` - **Path:** `/org/integrations/{cms_type}/{cms_id}` - **Tags:** org Updates operational integration settings, staff-on-case assignment rules, service-specific config, and CMS-to-Quilia case-type mappings. Requires an organization admin. Credentials, sessions, connection identity, and health fields cannot be changed here. #### Parameters ##### `cms_type` required - **In:** `path` `string` ##### `cms_id` required - **In:** `path` `string` #### Request Body ##### Content-Type: application/json - **`auto_invite`** `boolean` - **`auto_invite_on_activity`** `boolean` - **`case_type_mappings`** `array` — Map imported CMS case-type row ids to Quilia case types. Null clears a mapping. **Items:** - **`integration_case_type_id` (required)** `string` - **`quilia_case_type` (required)** `string | null` - **`config_patch`** `object` — Shallow patch for top-level config keys. A null value removes that key. - **`contact_type_mapping`** `object` — Which of the firm's own case-contact types import to which Quilia destination. Nothing imports until mapped: a type absent from every destination is skipped, which is how insurers and police avoid duplicating the rows their own endpoints already write. - **`contact_types` (required)** `array` — The firm's own CMS words for this destination. An empty array clears it. A word may only belong to one destination, so setting it here removes it from any other. **Items:** `string` - **`destination_key` (required)** `string` — Where the contacts go, e.g. provider, police-department - **`custom_allowed`** `boolean` - **`feed_enabled`** `boolean` - **`field_mapping`** `object` — Point one Quilia field at one CMS field. Merges into the saved mappings; other targets and other project types are untouched. - **`source` (required)** `object | null` — CMS field to take it from; null clears the mapping. - **`field_selector` (required)** `string` - **`section_selector` (required)** `string` - **`default_location_id`** `integer | null` — For Clio and Neos case.location only: existing Quilia location used when the source is blank or unmapped; 0 means the firm primary address. Null clears the fallback. Omit to preserve it when the source is unchanged; changing sources clears an omitted fallback. - **`value_map`** `object` — For Clio and Neos case.location only: map the source field's values to existing Quilia location ids. Clio takes matter/location text or client/address.state; Neos takes any case search column, including the client state one. State keys normalize to uppercase codes, and Neos compares on letters and digits so punctuation and casing in a firm's own office names do not matter. Omit to preserve the saved map when the source is unchanged; switching sources starts with an empty map if omitted. An empty object clears it. - **`target_key` (required)** `string` — Quilia field to fill, from list\_field\_mapping\_targets - **`project_type_id`** `string` — CMS project type the mapping belongs to. Sections and fields are defined per project type, so a mapping is only valid within one. Omit it for an integration-scoped target, which is set once and applies everywhere. - **`phase_messages_enabled`** `boolean` - **`phase_target`** `string` - **`record_mapping`** `object` — Map a repeating CMS collection to a Quilia record type, including what each of the firm's own values means in Quilia. - **`mapping` (required)** `object | null` — Null clears the record mapping. - **`fields` (required)** `object` - **`section_selector` (required)** `string` - **`value_maps`** `object` - **`project_type_id` (required)** `string` - **`record_key` (required)** `string` — Quilia record type, e.g. record.insurance - **`staff_assignments`** `object | null` — Replace staff-on-case assignment rules; null removes the saved rules. - **`alwaysIncludeUserIds`** `array` **Items:** `string` - **`alwaysIncludeVisibleToClient`** `boolean` - **`contactableFieldSelectors`** `array` **Items:** `string` - **`contactableRoles`** `array` **Items:** `string` - **`contactableUntitled`** `boolean` - **`fieldSelectors`** `array` **Items:** `string` - **`includeUntitled`** `boolean` - **`mode`** `string`, possible values: `"auto", "roles", "fields"` - **`roles`** `object` - **`contactable`** `array` **Items:** `string` - **`include`** `array` **Items:** `string` - **`sectionSelector`** `string` - **`subtype_mapping`** `object` — For one Filevine project type, choose the dropdown or multi-select whose values are imported as case subtypes. - **`project_type_id` (required)** `string` - **`source` (required)** `object | null` — Filevine dropdown or multi-select holding the case subtype; null clears the project-type override. - **`field_selector` (required)** `string` - **`section_selector` (required)** `string` **Example:** ```json { "auto_invite": true, "auto_invite_on_activity": true, "phase_messages_enabled": true, "phase_target": "", "custom_allowed": true, "feed_enabled": true, "config_patch": {}, "staff_assignments": { "mode": "auto", "contactableRoles": [ "" ], "roles": { "include": [ "" ], "contactable": [ "" ] }, "includeUntitled": true, "contactableUntitled": true, "fieldSelectors": [ "" ], "sectionSelector": "", "contactableFieldSelectors": [ "" ], "alwaysIncludeUserIds": [ "" ], "alwaysIncludeVisibleToClient": true }, "field_mapping": { "project_type_id": "", "target_key": "", "source": { "section_selector": "", "field_selector": "", "value_map": { "additionalProperty": 0 }, "default_location_id": null } }, "contact_type_mapping": { "destination_key": "", "contact_types": [ "" ] }, "record_mapping": { "project_type_id": "", "record_key": "", "mapping": { "section_selector": "", "fields": { "additionalProperty": "" }, "value_maps": { "additionalProperty": { "additionalProperty": "" } } } }, "subtype_mapping": { "project_type_id": "", "source": { "section_selector": "", "field_selector": "" } }, "case_type_mappings": [ { "integration_case_type_id": "", "quilia_case_type": null } ] } ``` #### Responses ##### Status: 200 Integration settings updated successfully ###### Content-Type: application/json - **`data` (required)** `array` **Items:** - **`auto_invite` (required)** `boolean | null` - **`auto_invite_on_activity` (required)** `boolean` - **`case_type_mappings` (required)** `array` **Items:** - **`id` (required)** `string` - **`import_id` (required)** `string | null` - **`is_default` (required)** `boolean` - **`quilia_case_type` (required)** `string | null` - **`suggested_case_type` (required)** `string | null` - **`suggested_confidence` (required)** `number | null` - **`type_name` (required)** `string` - **`required_assets`** `object` - **`cms_id` (required)** `string` — The integration account / tenant identifier - **`cms_type` (required)** `string` — Stable integration service identifier - **`connection_status` (required)** `string | null` - **`custom_allowed` (required)** `boolean` - **`enabled` (required)** `boolean` - **`integration_type` (required)** `string` - **`last_event_at` (required)** `string | null` - **`last_health_check_at` (required)** `string | null` - **`name` (required)** `string | null` — Human-readable integration name - **`phase_messages_enabled` (required)** `boolean` - **`phase_target` (required)** `string` - **`settings_url` (required)** `string`, format: `uri` - **`config`** `object` — Service-specific mapping, import, sync, and automation settings - **`feed`** `object` — Client-feed settings - **`staff_assignments`** `object` — Saved staff assignment config, including legacy fields - **`message` (required)** `string` **Example:** ```json { "message": "", "data": [ { "cms_id": "", "cms_type": "", "name": null, "integration_type": "", "enabled": true, "connection_status": null, "auto_invite": null, "auto_invite_on_activity": true, "phase_messages_enabled": true, "phase_target": "", "custom_allowed": true, "feed": null, "config": null, "staff_assignments": null, "case_type_mappings": [ { "id": "", "type_name": "", "import_id": null, "quilia_case_type": null, "is_default": true, "required_assets": null, "suggested_case_type": null, "suggested_confidence": null } ], "last_event_at": null, "last_health_check_at": null, "settings_url": "" } ] } ``` ### Switch the calling wizard to another organization - **Method:** `POST` - **Path:** `/org/switch` - **Tags:** org Moves the calling Quilia wizard's membership into another organization, so subsequent requests act in that firm's context. Wizard accounts only. A wizard can only move themselves. #### Request Body ##### Content-Type: application/json - **`organization` (required)** `string` — The organization to switch to: either its UUID or its name. A name is matched case-insensitively, exactly first and then as a partial match. **Example:** ```json { "organization": "" } ``` #### Responses ##### Status: 200 Organization switched successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`active` (required)** `boolean` — Whether the organization is active - **`id` (required)** `string` — The organization now in context - **`name` (required)** `string | null` — Its display name - **`message` (required)** `string` **Example:** ```json { "message": "", "data": { "id": "", "name": null, "active": true } } ``` ### Search the firm's provider directory - **Method:** `GET` - **Path:** `/providers` - **Tags:** providers Searches the firm's reusable provider directory (providers\_list) — medical providers and other care entities the firm works with across cases. These are firm-wide directory entries (distinct from a case-only person row); use the returned id to associate one to a case via POST /cases/{id}/providers. Filter by free-text query (matches name), specialty, and entity type. #### Parameters ##### `query` - **In:** `query` Free-text search matched against the provider name `string` — Free-text search matched against the provider name ##### `specialty` - **In:** `query` Filter by specialty (partial match) `string` — Filter by specialty (partial match) ##### `type` - **In:** `query` Filter by entity type `string`, possible values: `"business", "individual"` — Filter by entity type ##### `limit` - **In:** `query` Max results (default 50) `number` — Max results (default 50) ##### `offset` - **In:** `query` Results to skip (default 0) `[ "number", "null" ]` — Results to skip (default 0) #### Responses ##### Status: 200 Providers retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` **Items:** - **`address_1` (required)** `string | null` — Street address - **`categories` (required)** `array` — Provider categories / tags **Items:** `string` - **`city` (required)** `string | null` — City - **`company_name` (required)** `string | null` — Company / practice name - **`email` (required)** `string | null` — Email - **`fax` (required)** `string | null` — Fax number - **`first_name` (required)** `string | null` — First name (for person entities) - **`id` (required)** `string` — The provider directory id (provider\_list\_id) - **`last_name` (required)** `string | null` — Last name (for person entities) - **`name` (required)** `string` — Display name - **`phone` (required)** `string | null` — Phone number - **`specialty` (required)** `string | null` — Specialty (e.g. orthopedics) - **`state` (required)** `string | null` — State - **`type` (required)** `string` — Entity type (e.g. person, company) - **`zip` (required)** `string | null` — Postal / ZIP code - **`message` (required)** `string` - **`pagination` (required)** `object` - **`limit` (required)** `number` - **`offset` (required)** `number` - **`total` (required)** `number | null` **Example:** ```json { "message": "", "data": [ { "id": "", "name": "", "company_name": null, "first_name": null, "last_name": null, "type": "", "specialty": null, "phone": null, "fax": null, "email": null, "address_1": null, "city": null, "state": null, "zip": null, "categories": [ "" ] } ], "pagination": { "limit": 1, "offset": 1, "total": null } } ``` ### Report a misbehaving MCP tool - **Method:** `POST` - **Path:** `/reports/mcp-bug` - **Tags:** reports Files a structured bug report about a Quilia MCP tool that returned the wrong result. Intended to be called by an AI assistant on the user's behalf. Submitted text is rejected if it contains anything that identifies a person (email, phone, SSN, date of birth) — report defects generically. #### Request Body ##### Content-Type: application/json - **`actual` (required)** `string` — What it actually returned or did - **`expected` (required)** `string` — What the tool should have returned or done - **`tool_name` (required)** `string` — The Quilia MCP tool that misbehaved, e.g. "find\_staff" - **`arguments_used`** `object` — Argument names mapped to value SHAPES, never values — {"name": "string(5)"}, not {"name": "Jane Doe"} **Example:** ```json { "tool_name": "", "expected": "", "actual": "", "arguments_used": { "additionalProperty": "" } } ``` #### Responses ##### Status: 201 Report filed ###### Content-Type: application/json - **`data` (required)** `object` - **`filed` (required)** `boolean` **Example:** ```json { "data": { "filed": true } } ``` ### List request templates - **Method:** `GET` - **Path:** `/requests` - **Tags:** requests Retrieves the firm's saved request templates, newest first. Returns each template's fields so a caller can read a form back before editing it or sending it. Soft-deleted templates are excluded. #### Parameters ##### `limit` - **In:** `query` The maximum number of request templates to return. `[ "number", "null" ]`, default: `10` — The maximum number of request templates to return. ##### `offset` - **In:** `query` The number of request templates to skip before starting to return results. `[ "number", "null" ]`, default: `0` — The number of request templates to skip before starting to return results. ##### `type` - **In:** `query` Only return templates of this kind. 'form' is a built field list, 'signature' places signatures on an uploaded document, 'task' asks the client to do something. `string`, possible values: `"task", "form", "signature"` — Only return templates of this kind. 'form' is a built field list, 'signature' places signatures on an uploaded document, 'task' asks the client to do something. #### Responses ##### Status: 200 List of request templates retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` — Array of request templates **Items:** - **`builder_url` (required)** `string` — Portal link to open this template in the form builder - **`created_at` (required)** `string` — ISO timestamp when the template was created - **`created_by` (required)** `string` — The user ID of the template creator - **`description` (required)** `string` — The description of the request template - **`fields` (required)** `array` — The template fields, in display order **Items:** - **`id` (required)** `string` — The unique identifier of the field - **`label` (required)** `string` — The question, as the client reads it - **`required` (required)** `boolean` — Whether the client must answer - **`type` (required)** `string`, possible values: `"text", "textarea", "file", "select", "date", "time", "datetime", "signature", "richtext", "ssn"` — The kind of answer this field takes - **`description`** `string` — Helper text shown under the question - **`options`** `array` — The choices for a 'select' field **Items:** `string` - **`placeholder`** `string` — Placeholder text inside the input - **`generate_pdf_on_completion` (required)** `boolean` — Whether a PDF is produced when a client completes it - **`id` (required)** `string` — The unique identifier of the request template - **`title` (required)** `string` — The title of the request template - **`type` (required)** `string` — 'form' is a built field list, 'signature' places signatures on an uploaded document, 'task' asks the client to do something - **`message` (required)** `string` — Success message confirming the retrieve operation - **`pagination` (required)** `object` - **`has_more` (required)** `boolean` — Whether there is more data to fetch - **`limit` (required)** `number` — Maximum number of items returned - **`next` (required)** `string | null` — URL for the next page of items, if available - **`offset` (required)** `number` — Number of items skipped - **`previous` (required)** `string | null` — URL for the previous page of items, if available - **`total` (required)** `number` — Total number of items available **Example:** ```json { "message": "", "data": [ { "id": "", "title": "", "description": "", "type": "", "generate_pdf_on_completion": true, "fields": [ { "id": "", "type": "text", "label": "", "description": "", "placeholder": "", "required": true, "options": [ "" ] } ], "created_by": "", "created_at": "", "builder_url": "" } ], "pagination": { "total": 1, "offset": 1, "limit": 1, "has_more": true, "previous": null, "next": null } } ``` ### Create a request form template - **Method:** `POST` - **Path:** `/requests/templates` - **Tags:** requests Builds a reusable form the firm can send to clients — the same thing the portal's form builder produces. Field ids are assigned server-side; describe each field by type, label and whether it is required. The template is saved to the firm's library and can be sent with POST /requests/send. #### Request Body ##### Content-Type: application/json - **`fields` (required)** `array` — The fields, in the order the client will see them **Items:** - **`label` (required)** `string` — The question, as the client will read it - **`type` (required)** `string`, possible values: `"text", "textarea", "file", "select", "date", "time", "datetime", "signature", "richtext", "ssn"` — The kind of answer this field takes. 'select' needs \`options\`; 'file' asks the client to upload; 'signature' captures a signature; 'ssn' encrypts the answer onto the client's own record and stores nothing on the submission, so only one per form is captured and it always belongs to the client filling it. - **`description`** `string` — Helper text shown under the question - **`options`** `array` — The choices for a 'select' field **Items:** `string` - **`placeholder`** `string` — Placeholder text inside the input - **`required`** `boolean`, default: `false` — Whether the client must answer before submitting - **`title` (required)** `string` — The template title - **`description`** `string`, default: `""` — Instructions shown to the client above the fields - **`generate_pdf_on_completion`** `boolean`, default: `false` — Produce a PDF of the client's answers when they complete it - **`translations`** `array` — Translations for this form template **Items:** - **`language` (required)** `string`, possible values: `"es", "pt", "ht", "km", "ko", "th", "tl", "vi", "zh_CN", "zh_TW", "ru", "hy", "ar", "fa", "de", "pl", "it", "ja", "sr", "fr", "hmn", "mn", "hi", "pa"` — The language code - **`description`** `string` — The translated instructions - **`fields`** `array` — Translated fields, matching the order of the base fields array **Items:** - **`label` (required)** `string` — The translated question - **`description`** `string` — Translated helper text - **`options`** `array` — Translated choices for a 'select' field **Items:** `string` - **`placeholder`** `string` — Translated placeholder text - **`title`** `string` — The translated title **Example:** ```json { "title": "", "description": "", "fields": [ { "type": "text", "label": "", "description": "", "placeholder": "", "required": false, "options": [ "" ] } ], "generate_pdf_on_completion": false, "translations": [ { "language": "es", "title": "", "description": "", "fields": [ { "label": "", "description": "", "placeholder": "", "options": [ "" ] } ] } ] } ``` #### Responses ##### Status: 201 Form template created successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`builder_url` (required)** `string` — Portal link to open this template in the form builder - **`created_at` (required)** `string` — ISO timestamp when the template was created - **`created_by` (required)** `string` — The user ID of the template creator - **`description` (required)** `string` — The template description / instructions - **`fields` (required)** `array` — The fields, in display order **Items:** - **`id` (required)** `string` — The unique identifier of the field - **`label` (required)** `string` — The question, as the client reads it - **`required` (required)** `boolean` — Whether the client must answer - **`type` (required)** `string`, possible values: `"text", "textarea", "file", "select", "date", "time", "datetime", "signature", "richtext", "ssn"` — The kind of answer this field takes - **`description`** `string` — Helper text shown under the question - **`options`** `array` — The choices for a 'select' field **Items:** `string` - **`placeholder`** `string` — Placeholder text inside the input - **`generate_pdf_on_completion` (required)** `boolean` — Whether a PDF is produced when a client completes it - **`id` (required)** `string` — The unique identifier of the template - **`title` (required)** `string` — The template title - **`type` (required)** `string` — 'form' is a built field list, 'signature' places signatures on an uploaded document, 'task' asks the client to do something - **`translations`** `array` — Translations for this form template **Items:** - **`language` (required)** `string`, possible values: `"es", "pt", "ht", "km", "ko", "th", "tl", "vi", "zh_CN", "zh_TW", "ru", "hy", "ar", "fa", "de", "pl", "it", "ja", "sr", "fr", "hmn", "mn", "hi", "pa"` — The language code - **`description`** `string` — The translated instructions - **`fields`** `array` — Translated fields, matching the order of the base fields array **Items:** - **`label` (required)** `string` — The translated question - **`description`** `string` — Translated helper text - **`options`** `array` — Translated choices for a 'select' field **Items:** `string` - **`placeholder`** `string` — Translated placeholder text - **`title`** `string` — The translated title - **`message` (required)** `string` — Success message confirming the operation **Example:** ```json { "message": "", "data": { "id": "", "title": "", "description": "", "type": "", "generate_pdf_on_completion": true, "fields": [ { "id": "", "type": "text", "label": "", "description": "", "placeholder": "", "required": true, "options": [ "" ] } ], "created_by": "", "created_at": "", "builder_url": "", "translations": [ { "language": "es", "title": "", "description": "", "fields": [ { "label": "", "description": "", "placeholder": "", "options": [ "" ] } ] } ] } } ``` ### Update a request form template - **Method:** `PATCH` - **Path:** `/requests/templates/{id}` - **Tags:** requests Edits a form template in the firm's library. Use `append_fields` to add questions without disturbing the ones already there; use `fields` to replace the list outright when reordering or removing. Sending both is refused — they disagree about what the form should end up as. Templates already sent to clients keep the copy they were sent with, so editing does not rewrite a form somebody is midway through. #### Parameters ##### `id` required - **In:** `path` The template to update `string`, format: `uuid` — The template to update #### Request Body ##### Content-Type: application/json - **`append_fields`** `array` — Adds these fields to the end, leaving the existing ones untouched. Use this to add a question rather than resending the whole form. **Items:** - **`label` (required)** `string` — The question, as the client will read it - **`type` (required)** `string`, possible values: `"text", "textarea", "file", "select", "date", "time", "datetime", "signature", "richtext", "ssn"` — The kind of answer this field takes. 'select' needs \`options\`; 'file' asks the client to upload; 'signature' captures a signature; 'ssn' encrypts the answer onto the client's own record and stores nothing on the submission, so only one per form is captured and it always belongs to the client filling it. - **`description`** `string` — Helper text shown under the question - **`options`** `array` — The choices for a 'select' field **Items:** `string` - **`placeholder`** `string` — Placeholder text inside the input - **`required`** `boolean`, default: `false` — Whether the client must answer before submitting - **`description`** `string` — New instructions - **`fields`** `array` — Replaces the entire field list. Send every field you want the template to end up with, in order — anything omitted is removed. **Items:** - **`label` (required)** `string` — The question, as the client will read it - **`type` (required)** `string`, possible values: `"text", "textarea", "file", "select", "date", "time", "datetime", "signature", "richtext", "ssn"` — The kind of answer this field takes. 'select' needs \`options\`; 'file' asks the client to upload; 'signature' captures a signature; 'ssn' encrypts the answer onto the client's own record and stores nothing on the submission, so only one per form is captured and it always belongs to the client filling it. - **`description`** `string` — Helper text shown under the question - **`options`** `array` — The choices for a 'select' field **Items:** `string` - **`placeholder`** `string` — Placeholder text inside the input - **`required`** `boolean`, default: `false` — Whether the client must answer before submitting - **`generate_pdf_on_completion`** `boolean` — Whether to produce a PDF on completion - **`title`** `string` — A new title - **`translations`** `array` — Translations to replace existing translations **Items:** - **`language` (required)** `string`, possible values: `"es", "pt", "ht", "km", "ko", "th", "tl", "vi", "zh_CN", "zh_TW", "ru", "hy", "ar", "fa", "de", "pl", "it", "ja", "sr", "fr", "hmn", "mn", "hi", "pa"` — The language code - **`description`** `string` — The translated instructions - **`fields`** `array` — Translated fields, matching the order of the base fields array **Items:** - **`label` (required)** `string` — The translated question - **`description`** `string` — Translated helper text - **`options`** `array` — Translated choices for a 'select' field **Items:** `string` - **`placeholder`** `string` — Translated placeholder text - **`title`** `string` — The translated title **Example:** ```json { "title": "", "description": "", "fields": [ { "type": "text", "label": "", "description": "", "placeholder": "", "required": false, "options": [ "" ] } ], "append_fields": [ { "type": "text", "label": "", "description": "", "placeholder": "", "required": false, "options": [ "" ] } ], "generate_pdf_on_completion": true, "translations": [ { "language": "es", "title": "", "description": "", "fields": [ { "label": "", "description": "", "placeholder": "", "options": [ "" ] } ] } ] } ``` #### Responses ##### Status: 200 Form template updated successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`builder_url` (required)** `string` — Portal link to open this template in the form builder - **`created_at` (required)** `string` — ISO timestamp when the template was created - **`created_by` (required)** `string` — The user ID of the template creator - **`description` (required)** `string` — The template description / instructions - **`fields` (required)** `array` — The fields, in display order **Items:** - **`id` (required)** `string` — The unique identifier of the field - **`label` (required)** `string` — The question, as the client reads it - **`required` (required)** `boolean` — Whether the client must answer - **`type` (required)** `string`, possible values: `"text", "textarea", "file", "select", "date", "time", "datetime", "signature", "richtext", "ssn"` — The kind of answer this field takes - **`description`** `string` — Helper text shown under the question - **`options`** `array` — The choices for a 'select' field **Items:** `string` - **`placeholder`** `string` — Placeholder text inside the input - **`generate_pdf_on_completion` (required)** `boolean` — Whether a PDF is produced when a client completes it - **`id` (required)** `string` — The unique identifier of the template - **`title` (required)** `string` — The template title - **`type` (required)** `string` — 'form' is a built field list, 'signature' places signatures on an uploaded document, 'task' asks the client to do something - **`translations`** `array` — Translations for this form template **Items:** - **`language` (required)** `string`, possible values: `"es", "pt", "ht", "km", "ko", "th", "tl", "vi", "zh_CN", "zh_TW", "ru", "hy", "ar", "fa", "de", "pl", "it", "ja", "sr", "fr", "hmn", "mn", "hi", "pa"` — The language code - **`description`** `string` — The translated instructions - **`fields`** `array` — Translated fields, matching the order of the base fields array **Items:** - **`label` (required)** `string` — The translated question - **`description`** `string` — Translated helper text - **`options`** `array` — Translated choices for a 'select' field **Items:** `string` - **`placeholder`** `string` — Translated placeholder text - **`title`** `string` — The translated title - **`message` (required)** `string` — Success message confirming the operation **Example:** ```json { "message": "", "data": { "id": "", "title": "", "description": "", "type": "", "generate_pdf_on_completion": true, "fields": [ { "id": "", "type": "text", "label": "", "description": "", "placeholder": "", "required": true, "options": [ "" ] } ], "created_by": "", "created_at": "", "builder_url": "", "translations": [ { "language": "es", "title": "", "description": "", "fields": [ { "label": "", "description": "", "placeholder": "", "options": [ "" ] } ] } ] } } ``` ### Create and send a request (existing template, or a freeform task) - **Method:** `POST` - **Path:** `/requests/send` - **Tags:** requests Sends a request to a client in a case. Provide a `templateId` to send an existing form / signature / task-list template, OR `type: "task"` with a `title` (+ optional description / url) to create and send a freeform one-off action task with no template. The request is created with 'pending' status and the client is notified. All caller-supplied IDs are verified against the authenticated org. #### Request Body ##### Content-Type: application/json **Any of:** - **`caseId` (required)** `string` — The case ID to associate with this request - **`recipientId` (required)** `string` — The user ID of the client who will receive the request - **`senderId` (required)** `string` — The user ID of the staff member sending the request - **`templateId` (required)** `string` — The ID of an existing request template to send * **`caseId` (required)** `string` — The case ID to associate with this request * **`recipientId` (required)** `string` — The user ID of the client who will receive the request * **`senderId` (required)** `string` — The user ID of the staff member sending the request * **`title` (required)** `string` — The task title shown to the client * **`type` (required)** `string`, possible values: `"task"` — Create a freeform one-off action task (no template required) * **`description`** `string` — Optional instructions shown with the task * **`url`** `string`, format: `uri` — Optional http(s) link the client is sent to **Example:** ```json { "templateId": "", "recipientId": "", "senderId": "", "caseId": "" } ``` #### Responses ##### Status: 200 Request sent successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`id` (required)** `string` — The unique identifier of the sent request - **`status` (required)** `string` — The status of the request (typically 'pending') - **`message` (required)** `string` — Success message confirming the send operation **Example:** ```json { "message": "", "data": { "id": "", "status": "" } } ``` ### List a case's requests - **Method:** `GET` - **Path:** `/requests/list` - **Tags:** requests Lists the requests sent on a case — summaries only (no response payload). Filter by status and type. Fetch GET /requests/{type}/{id} for a request's full detail + response. #### Parameters ##### `caseId` required - **In:** `query` The case to list requests for `string`, format: `uuid` — The case to list requests for ##### `status` - **In:** `query` Filter by status (e.g. 'pending', 'completed') `string` — Filter by status (e.g. 'pending', 'completed') ##### `type` - **In:** `query` Filter by request type (task / form / signature) `string`, possible values: `"task", "form", "signature"` — Filter by request type (task / form / signature) ##### `limit` - **In:** `query` The maximum number of requests to return. `[ "number", "null" ]`, default: `25` — The maximum number of requests to return. ##### `offset` - **In:** `query` The number of requests to skip before returning results. `[ "number", "null" ]`, default: `0` — The number of requests to skip before returning results. #### Responses ##### Status: 200 Requests retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` — Array of request summaries **Items:** - **`case_id` (required)** `string` — The case this request belongs to - **`created_at` (required)** `string` — ISO timestamp when the request was created - **`id` (required)** `string` — The unique identifier of the request - **`recipient_id` (required)** `string | null` — The client the request was sent to - **`status` (required)** `string` — The request status (e.g. 'pending', 'completed') - **`submitted_at` (required)** `string | null` — ISO timestamp when the client completed it, if completed - **`task_list_id` (required)** `string | null` — The parent task list id, if this request is part of one - **`title` (required)** `string` — The request title - **`type` (required)** `string` — The request type (task / form / signature) - **`message` (required)** `string` — Success message confirming the retrieve - **`pagination` (required)** `object` - **`has_more` (required)** `boolean` — Whether there is more data to fetch - **`limit` (required)** `number` — Maximum number of items returned - **`next` (required)** `string | null` — URL for the next page of items, if available - **`offset` (required)** `number` — Number of items skipped - **`previous` (required)** `string | null` — URL for the previous page of items, if available - **`total` (required)** `number` — Total number of items available **Example:** ```json { "message": "", "data": [ { "id": "", "type": "", "status": "", "title": "", "case_id": "", "recipient_id": null, "task_list_id": null, "created_at": "", "submitted_at": null } ], "pagination": { "total": 1, "offset": 1, "limit": 1, "has_more": true, "previous": null, "next": null } } ``` ### Get request response by request ID - **Method:** `GET` - **Path:** `/requests/{requestId}/responses` - **Tags:** requests Retrieves the response submission for a specific request. Returns the most recent submission if multiple exist. Returns 404 if the request doesn't exist or has no submission. #### Parameters ##### `requestId` required - **In:** `path` The unique identifier of the request to retrieve responses for `string`, format: `uuid` — The unique identifier of the request to retrieve responses for #### Responses ##### Status: 200 Request response retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` — The request response data - **`duration` (required)** `number` — Time taken to complete the submission in milliseconds - **`fields` (required)** `array` — Array of field responses submitted by the client **Items:** - **`id` (required)** `string` — The unique identifier of the field - **`type` (required)** `string` — The type of the field (e.g., 'text', 'file', 'select') - **`files`** `array` — Array of uploaded files (for file type fields) **Items:** - **`name` (required)** `string` — The original filename - **`path` (required)** `string` — The storage path of the file - **`type` (required)** `string` — The MIME type of the file - **`label`** `string` — The label of the field - **`value`** `string` — The value entered by the user - **`id` (required)** `string` — The unique identifier of the submission - **`platform` (required)** `string` — Platform where the submission was made (e.g., 'web', 'mobile') - **`request` (required)** `object` - **`case_id` (required)** `string` — The ID of the case associated with this request - **`created_at` (required)** `string` — ISO timestamp when the request was created - **`id` (required)** `string` — The unique identifier of the request - **`status` (required)** `string` — The status of the request (e.g., 'pending', 'completed') - **`request_id` (required)** `string` — The ID of the request this submission belongs to - **`submitted_at` (required)** `string` — ISO timestamp when the submission was created - **`template` (required)** `object` - **`description` (required)** `string` — The description of the template - **`id` (required)** `string` — The unique identifier of the template - **`title` (required)** `string` — The title of the template - **`template_id` (required)** `string` — The ID of the template used for this request - **`message` (required)** `string` — Success message confirming the retrieve operation **Example:** ```json { "message": "", "data": { "id": "", "request_id": "", "template_id": "", "fields": [ { "id": "", "type": "", "label": "", "value": "", "files": [ { "path": "", "name": "", "type": "" } ] } ], "submitted_at": "", "duration": 1, "platform": "", "request": { "id": "", "status": "", "case_id": "", "created_at": "" }, "template": { "id": "", "title": "", "description": "" } } } ``` ### List request responses by client ID - **Method:** `GET` - **Path:** `/requests/responses/{clientId}` - **Tags:** requests Retrieves a paginated list of all request responses submitted by a specific client. Returns full response data including submission details, related request information, and template metadata. #### Parameters ##### `clientId` required - **In:** `path` The unique identifier of the client (user\_id) to retrieve responses for `string`, format: `uuid` — The unique identifier of the client (user\_id) to retrieve responses for ##### `limit` - **In:** `query` The maximum number of request responses to return. `[ "number", "null" ]`, default: `10` — The maximum number of request responses to return. ##### `offset` - **In:** `query` The number of request responses to skip before starting to return results. `[ "number", "null" ]`, default: `0` — The number of request responses to skip before starting to return results. #### Responses ##### Status: 200 List of request responses retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` — Array of request responses **Items:** - **`duration` (required)** `number` — Time taken to complete the submission in milliseconds - **`fields` (required)** `array` — Array of field responses submitted by the client **Items:** - **`id` (required)** `string` — The unique identifier of the field - **`type` (required)** `string` — The type of the field (e.g., 'text', 'file', 'select') - **`files`** `array` — Array of uploaded files (for file type fields) **Items:** - **`name` (required)** `string` — The original filename - **`path` (required)** `string` — The storage path of the file - **`type` (required)** `string` — The MIME type of the file - **`label`** `string` — The label of the field - **`value`** `string` — The value entered by the user - **`id` (required)** `string` — The unique identifier of the submission - **`platform` (required)** `string` — Platform where the submission was made (e.g., 'web', 'mobile') - **`request` (required)** `object` - **`case_id` (required)** `string` — The ID of the case associated with this request - **`created_at` (required)** `string` — ISO timestamp when the request was created - **`id` (required)** `string` — The unique identifier of the request - **`status` (required)** `string` — The status of the request (e.g., 'pending', 'completed') - **`request_id` (required)** `string` — The ID of the request this submission belongs to - **`submitted_at` (required)** `string` — ISO timestamp when the submission was created - **`template` (required)** `object` - **`description` (required)** `string` — The description of the template - **`id` (required)** `string` — The unique identifier of the template - **`title` (required)** `string` — The title of the template - **`template_id` (required)** `string` — The ID of the template used for this request - **`message` (required)** `string` — Success message confirming the retrieve operation - **`pagination` (required)** `object` - **`has_more` (required)** `boolean` — Whether there is more data to fetch - **`limit` (required)** `number` — Maximum number of items returned - **`next` (required)** `string | null` — URL for the next page of items, if available - **`offset` (required)** `number` — Number of items skipped - **`previous` (required)** `string | null` — URL for the previous page of items, if available - **`total` (required)** `number` — Total number of items available **Example:** ```json { "message": "", "data": [ { "id": "", "request_id": "", "template_id": "", "fields": [ { "id": "", "type": "", "label": "", "value": "", "files": [ { "path": "", "name": "", "type": "" } ] } ], "submitted_at": "", "duration": 1, "platform": "", "request": { "id": "", "status": "", "case_id": "", "created_at": "" }, "template": { "id": "", "title": "", "description": "" } } ], "pagination": { "total": 1, "offset": 1, "limit": 1, "has_more": true, "previous": null, "next": null } } ``` ### Resend a request notification - **Method:** `POST` - **Path:** `/requests/{id}/resend` - **Tags:** requests Re-fires the client notification for a pending request — a nudge. Only pending, standalone requests can be resent (task-list items are notified at the list level). #### Parameters ##### `id` required - **In:** `path` The request id to resend `string`, format: `uuid` — The request id to resend #### Responses ##### Status: 200 Notification resent successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`id` (required)** `string` — The unique identifier of the request - **`status` (required)** `string` — The request status - **`message` (required)** `string` — Success message confirming the resend **Example:** ```json { "message": "", "data": { "id": "", "status": "" } } ``` ### Get a request by type and id - **Method:** `GET` - **Path:** `/requests/{type}/{id}` - **Tags:** requests Retrieves a single request by id, namespaced by type. Returns the request's status and — once the client has completed it — the response payload (form fields / signed document) plus any completion note. 404 if the id doesn't exist in this org or isn't of the given type. (Task lists are a separate resource — a request is an item, not a list.) #### Parameters ##### `type` required - **In:** `path` The request type (task / form / signature) `string`, possible values: `"task", "form", "signature"` — The request type (task / form / signature) ##### `id` required - **In:** `path` The request id `string`, format: `uuid` — The request id #### Responses ##### Status: 200 Request retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`case_id` (required)** `string` — The case this request belongs to - **`completion_note` (required)** `string | null` — Optional note the client left when completing a task - **`created_at` (required)** `string` — ISO timestamp when the request was created - **`description` (required)** `string | null` — The request instructions / description - **`id` (required)** `string` — The unique identifier of the request - **`recipient_id` (required)** `string | null` — The client the request was sent to - **`response` (required)** `object | null` — The client submission (form / signature), null until completed - **`fields` (required)** `array` — The fields the client submitted **Items:** - **`id` (required)** `string` — The unique identifier of the field - **`type` (required)** `string` — The type of the field (e.g., 'text', 'file', 'select') - **`files`** `array` — Array of uploaded files (for file type fields) **Items:** - **`name` (required)** `string` — The original filename - **`path` (required)** `string` — The storage path of the file - **`type` (required)** `string` — The MIME type of the file - **`label`** `string` — The label of the field - **`value`** `string` — The value entered by the user - **`submitted_at` (required)** `string` — ISO timestamp of the submission - **`status` (required)** `string` — The request status (e.g. 'pending', 'completed') - **`submitted_at` (required)** `string | null` — ISO timestamp when the client completed it, if completed - **`title` (required)** `string` — The request title - **`type` (required)** `string` — The request type (task / form / signature) - **`message` (required)** `string` — Success message confirming the retrieve **Example:** ```json { "message": "", "data": { "id": "", "type": "", "status": "", "title": "", "description": null, "case_id": "", "recipient_id": null, "created_at": "", "submitted_at": null, "completion_note": null, "response": { "submitted_at": "", "fields": [ { "id": "", "type": "", "label": "", "value": "", "files": [ { "path": "", "name": "", "type": "" } ] } ] } } } ``` ### Search users - **Method:** `GET` - **Path:** `/search` - **Tags:** search Search for users (all roles) within the authenticated organization. This endpoint provides fuzzy search across all organization members including clients, staff, attorneys, and other roles. Results are returned in order of membership creation date (most recent first). ## Search Behavior The search query (`q` parameter) performs **case-insensitive partial matching** against multiple user fields simultaneously. If a UUID is provided, an exact match is attempted against the user ID. ## Searchable Fields | Field | Match Type | Description | | ------------- | ---------- | --------------- | | `id` | Exact | User's UUID | | `name` | Partial | Full name | | `name_first` | Partial | First name | | `name_last` | Partial | Last name | | `email` | Partial | Email address | | `phone` | Partial | Phone number | | `address_1` | Partial | Address line 1 | | `address_2` | Partial | Address line 2 | | `city` | Partial | City | | `state` | Partial | State/province | | `postal_code` | Partial | Postal/zip code | | `country` | Partial | Country | ## Filtering Use `filter[field]` query parameters to narrow results: - **`filter[role]`** - Filter by user role. Supports comma-separated values for OR matching. - **`filter[active]`** - Filter by active status (`true` or `false`). - **`filter[created_at]`** - Filter by membership creation date (supports wildcards). - **`filter[updated_at]`** - Filter by membership update date (supports wildcards). ## Examples ```ini # Search for users named "John" GET /v2/search?q=john # Search for users in Chicago GET /v2/search?q=chicago # Search for clients only GET /v2/search?q=john&filter[role]=client # Search for active attorneys or legal staff GET /v2/search?q=john&filter[role]=attorney,legal staff&filter[active]=true # Paginate results GET /v2/search?q=john&limit=20&offset=40 ``` #### Parameters ##### `q` required - **In:** `query` `string` — Search query string (minimum 2 characters). Searches across multiple user fields using case-insensitive partial matching. \*\*Searched fields:\*\* - \`id\` - Exact UUID match only - \`name\`, \`name\_first\`, \`name\_last\` - Full and partial names - \`email\` - Email address - \`phone\` - Phone number - \`address\_1\`, \`address\_2\` - Address lines - \`city\`, \`state\`, \`postal\_code\`, \`country\` - Location fields \*\*Examples:\*\* - \`q=john\` - Matches "John Doe", "johnny\@email.com", etc. - \`q=chicago\` - Matches users in Chicago - \`q=14aec6c6-9562-4990-9f66-7411187c4d27\` - Exact user ID match ##### `limit` - **In:** `query` `number`, default: `10` — Maximum number of results to return (1-50, default 10) ##### `offset` - **In:** `query` `[ "number", "null" ]`, default: `0` — Number of results to skip for pagination ##### `filter[role]` - **In:** `query` `string` — Filter by user role. Supports comma-separated values for OR matching. \*\*Valid values:\*\* wizard, admin, attorney, client, legal staff, provider, provider admin, medical staff, third party, wizard-finance \*\*Examples:\*\* - \`filter\[role]=client\` - Only clients - \`filter\[role]=client,attorney\` - Clients OR attorneys ##### `filter[active]` - **In:** `query` `string` — Filter by active status within the organization. \*\*Valid values:\*\* \`true\`, \`false\` \*\*Examples:\*\* - \`filter\[active]=true\` - Only active members - \`filter\[active]=false\` - Only inactive members ##### `filter[created_at]` - **In:** `query` `string` — Filter by membership creation date. Supports wildcards (\*) for partial matching. \*\*Examples:\*\* - \`filter\[created\_at]=2024\*\` - Created in 2024 - \`filter\[created\_at]=2024-01\*\` - Created in January 2024 ##### `filter[updated_at]` - **In:** `query` `string` — Filter by membership last updated date. Supports wildcards (\*) for partial matching. \*\*Examples:\*\* - \`filter\[updated\_at]=2024\*\` - Updated in 2024 - \`filter\[updated\_at]=2024-01\*\` - Updated in January 2024 #### Responses ##### Status: 200 Search completed successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`results` (required)** `array` — Array of users matching the search query, ordered by membership creation date (most recent first) **Items:** - **`active` (required)** `boolean` — Whether the user is currently active in the organization. Inactive users may have been deactivated but not deleted. - **`address_1` (required)** `string | null` — First line of the user's address - **`address_2` (required)** `string | null` — Second line of the user's address (apartment, suite, etc.) - **`city` (required)** `string | null` — User's city - **`country` (required)** `string | null` — User's country - **`created_at` (required)** `string | null` — ISO 8601 timestamp of when the organization membership was created - **`email` (required)** `string | null` — User's email address - **`job_title` (required)** `string | null` — User's job title (primarily for staff members) - **`matched_field` (required)** `string` — The field that matched the search query. Useful for highlighting or understanding why a result was returned. \*\*Possible values:\*\* id, name, name\_first, name\_last, email, phone, address\_1, address\_2, city, state, postal\_code, country, unknown - **`member_id` (required)** `string` — Organization membership ID (organization\_members.id). Use this to reference the user within the organization context. - **`name` (required)** `string | null` — User's full name - **`name_first` (required)** `string | null` — User's first name - **`name_last` (required)** `string | null` — User's last name - **`phone` (required)** `string | null` — User's phone number - **`postal_code` (required)** `string | null` — User's postal/zip code - **`profile_image` (required)** `string | null` — URL to the user's profile image - **`role` (required)** `string | null`, possible values: `"wizard", "admin", "attorney", "client", "legal staff", "provider", "provider admin", "medical staff", "third party", "wizard-finance"` — User's role within the organization. \*\*Possible values:\*\* wizard, admin, attorney, client, legal staff, provider, provider admin, medical staff, third party, wizard-finance - **`state` (required)** `string | null` — User's state or province - **`updated_at` (required)** `string | null` — ISO 8601 timestamp of when the organization membership was last updated - **`user_id` (required)** `string` — Unique identifier of the user (UUID) - **`message` (required)** `string` — Success message indicating the search completed - **`pagination` (required)** `object` - **`has_more` (required)** `boolean` — Whether more results are available beyond the current page. Use offset + limit to fetch the next page. - **`limit` (required)** `number` — Maximum results returned per page - **`offset` (required)** `number` — Current offset (number of results skipped) - **`total` (required)** `number` — Total number of users matching the search query - **`query` (required)** `string` — The search query that was executed (echoed back for reference) **Example:** ```json { "message": "Search completed", "data": { "results": [ { "user_id": "14aec6c6-9562-4990-9f66-7411187c4d27", "member_id": "25bfd7d7-a673-5a01-a077-8c22298d5e38", "name": "John Doe", "name_first": "John", "name_last": "Doe", "email": "john.doe@example.com", "phone": "+1234567890", "address_1": "123 Main Street", "address_2": "Suite 100", "city": "Chicago", "state": "IL", "postal_code": "60601", "country": "USA", "role": "client", "active": true, "job_title": "Paralegal", "profile_image": "https://storage.example.com/profiles/user123.jpg", "matched_field": "name", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-06-20T14:45:00Z" } ] }, "query": "john", "pagination": { "total": 25, "offset": 0, "limit": 10, "has_more": true } } ``` ### Create a new staff member - **Method:** `POST` - **Path:** `/staff` - **Tags:** staff Creates a staff member, idempotent on repeat calls for the same email. The `invite` field controls email behavior: `'send'` always sends an invitation email, `'skip'` never does, `'auto'` (default) sends only when the user is new or has never signed in — safe for nightly staff-sync integrations. #### Request Body ##### Content-Type: application/json - **`email` (required)** `string`, format: `email` — The email address of the staff member - **`name` (required)** `string` — The full name of the staff member - **`role` (required)** `string`, possible values: `"admin", "attorney", "legal staff"` — The role of the staff member (admin, attorney, or legal staff) - **`external_id`** `string` — The partner's own identifier for this staff member (used for idempotent creation across mutable fields like email) - **`invite`** `string`, possible values: `"send", "skip", "auto"`, default: `"auto"` — Controls invitation email behavior. \`'send'\` always sends an invite, even if the user already exists. \`'skip'\` never sends an invite — silent create for new users, no-op for existing. \`'auto'\` (default) sends an invite only when the user is net-new OR has never signed in, making it safe for repeated staff-sync calls without spamming active members. - **`job_title`** `string` — The job title of the staff member - **`location_id`** `number` — The ID of the office location to assign - **`name_first`** `string` — The first name of the staff member - **`name_last`** `string` — The last name of the staff member - **`permission_id`** `string`, format: `uuid` — The ID of the permission set to assign - **`phone`** `string` — The phone number of the staff member **Example:** ```json { "name": "", "name_first": "", "name_last": "", "email": "", "phone": "", "job_title": "", "role": "admin", "location_id": 1, "permission_id": "", "external_id": "", "invite": "auto" } ``` #### Responses ##### Status: 200 Staff member created successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`member_id` (required)** `string` — The organization membership ID of the staff member - **`user_id` (required)** `string` — The unique identifier of the created staff member - **`message` (required)** `string` — Success message confirming the creation operation **Example:** ```json { "message": "", "data": { "user_id": "", "member_id": "" } } ``` ### List staff members - **Method:** `GET` - **Path:** `/staff` - **Tags:** staff Retrieves a paginated list of staff members for the authenticated organization with optional field filtering. Staff members include admins, attorneys, and legal staff (not clients). #### Parameters ##### `limit` - **In:** `query` The maximum number of staff members to return. `[ "number", "null" ]`, default: `10` — The maximum number of staff members to return. ##### `offset` - **In:** `query` The number of staff members to skip before starting to return results. `[ "number", "null" ]`, default: `0` — The number of staff members to skip before starting to return results. ##### `fields` - **In:** `query` Comma-separated list of user fields to include in the response `string` — Comma-separated list of user fields to include in the response ##### `filter[name]` - **In:** `query` Filter by name. See endpoint description for syntax. `string` — Filter by name. See endpoint description for syntax. ##### `filter[email]` - **In:** `query` Filter by email. See endpoint description for syntax. `string` — Filter by email. See endpoint description for syntax. ##### `filter[role]` - **In:** `query` Filter by role. See endpoint description for syntax. `string` — Filter by role. See endpoint description for syntax. ##### `filter[active]` - **In:** `query` Filter by active. See endpoint description for syntax. `string` — Filter by active. See endpoint description for syntax. ##### `filter[location_id]` - **In:** `query` Filter by location\_id. See endpoint description for syntax. `string` — Filter by location\_id. See endpoint description for syntax. #### Responses ##### Status: 200 Staff data retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` — Array of staff members **Items:** - **`member_id` (required)** `string` — The organization membership ID of the staff member - **`user_id` (required)** `string` — The unique identifier of the staff member - **`active`** `boolean | null` — Whether the staff member is currently active - **`bio`** `string | null` — The staff member's bio — the 'About' field in the portal - **`city`** `string | null` — The staff member's city - **`country`** `string | null` — The staff member's country - **`created_at`** `string | null` — When the staff member was created - **`email`** `string | null` — The staff member's email address - **`external_id`** `string | null` — The partner's own identifier for this staff member, matching organization\_members.metadata.external\_id - **`facebook`** `string | null` — The staff member's Facebook profile - **`instagram`** `string | null` — The staff member's Instagram profile - **`job_title`** `string | null` — The staff member's job title - **`linkedin`** `string | null` — The staff member's LinkedIn profile - **`location_id`** `number | null` — The ID of the office location the staff member is assigned to - **`name`** `string | null` — The staff member's full name - **`name_first`** `string | null` — The staff member's first name - **`name_last`** `string | null` — The staff member's last name - **`permission_id`** `string | null` — The ID of the permission set assigned to the staff member - **`phone`** `string | null` — The staff member's phone number - **`profile_image`** `string | null` — URL to the staff member's profile image - **`role`** `string | null`, possible values: `"admin", "attorney", "legal staff"` — The staff member's role in the organization - **`state`** `string | null` — The staff member's state or province - **`timezone`** `string | null` — The staff member's IANA timezone - **`twitter`** `string | null` — The staff member's X/Twitter profile - **`updated_at`** `string | null` — When the staff member was last updated - **`message` (required)** `string` — Success message confirming the retrieve operation - **`pagination` (required)** `object` - **`has_more` (required)** `boolean` — Whether there is more data to fetch - **`limit` (required)** `number` — Maximum number of items returned - **`next` (required)** `string | null` — URL for the next page of items, if available - **`offset` (required)** `number` — Number of items skipped - **`previous` (required)** `string | null` — URL for the previous page of items, if available - **`total` (required)** `number` — Total number of items available **Example:** ```json { "message": "", "data": [ { "user_id": "", "member_id": "", "name": null, "name_first": null, "name_last": null, "email": null, "phone": null, "profile_image": null, "external_id": null, "role": "admin", "active": null, "job_title": null, "city": null, "state": null, "country": null, "timezone": null, "bio": null, "linkedin": null, "twitter": null, "facebook": null, "instagram": null, "location_id": null, "permission_id": null, "created_at": null, "updated_at": null } ], "pagination": { "total": 1, "offset": 1, "limit": 1, "has_more": true, "previous": null, "next": null } } ``` ### Delete a staff member - **Method:** `DELETE` - **Path:** `/staff/{id}` - **Tags:** staff Removes a staff member from the organization. By default, this performs a soft delete (sets active=false). Use soft\_delete=false query parameter to permanently remove the organization membership. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the staff member to delete `string`, format: `uuid` — The unique identifier of the staff member to delete ##### `soft_delete` - **In:** `query` When true (default), deactivates the staff member. When false, permanently removes the organization membership. `[ "boolean", "null" ]`, default: `true` — When true (default), deactivates the staff member. When false, permanently removes the organization membership. #### Responses ##### Status: 200 Staff member successfully deleted/deactivated ###### Content-Type: application/json - **`message` (required)** `string` — Success message confirming the delete operation **Example:** ```json { "message": "" } ``` ### Retrieve a single staff member - **Method:** `GET` - **Path:** `/staff/{id}` - **Tags:** staff Retrieves detailed information for a specific staff member using either user\_id or member\_id. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the staff member to retrieve `string`, format: `uuid` — The unique identifier of the staff member to retrieve #### Responses ##### Status: 200 Staff member retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`member_id` (required)** `string` — The organization membership ID of the staff member - **`user_id` (required)** `string` — The unique identifier of the staff member - **`active`** `boolean | null` — Whether the staff member is currently active - **`bio`** `string | null` — The staff member's bio — the 'About' field in the portal - **`city`** `string | null` — The staff member's city - **`country`** `string | null` — The staff member's country - **`created_at`** `string | null` — When the staff member was created - **`email`** `string | null` — The staff member's email address - **`external_id`** `string | null` — The partner's own identifier for this staff member, matching organization\_members.metadata.external\_id - **`facebook`** `string | null` — The staff member's Facebook profile - **`instagram`** `string | null` — The staff member's Instagram profile - **`job_title`** `string | null` — The staff member's job title - **`linkedin`** `string | null` — The staff member's LinkedIn profile - **`location_id`** `number | null` — The ID of the office location the staff member is assigned to - **`name`** `string | null` — The staff member's full name - **`name_first`** `string | null` — The staff member's first name - **`name_last`** `string | null` — The staff member's last name - **`permission_id`** `string | null` — The ID of the permission set assigned to the staff member - **`phone`** `string | null` — The staff member's phone number - **`profile_image`** `string | null` — URL to the staff member's profile image - **`role`** `string | null`, possible values: `"admin", "attorney", "legal staff"` — The staff member's role in the organization - **`state`** `string | null` — The staff member's state or province - **`timezone`** `string | null` — The staff member's IANA timezone - **`twitter`** `string | null` — The staff member's X/Twitter profile - **`updated_at`** `string | null` — When the staff member was last updated - **`message` (required)** `string` — Success message confirming the retrieve operation **Example:** ```json { "message": "", "data": { "user_id": "", "member_id": "", "name": null, "name_first": null, "name_last": null, "email": null, "phone": null, "profile_image": null, "external_id": null, "role": "admin", "active": null, "job_title": null, "city": null, "state": null, "country": null, "timezone": null, "bio": null, "linkedin": null, "twitter": null, "facebook": null, "instagram": null, "location_id": null, "permission_id": null, "created_at": null, "updated_at": null } } ``` ### Update staff member information - **Method:** `PATCH` - **Path:** `/staff/{id}` - **Tags:** staff Updates specific fields of an existing staff member record. Allows modification of user details (name, email, phone, job\_title) and membership details (role, active status, location, permissions). #### Parameters ##### `id` required - **In:** `path` The unique identifier of the staff member to update `string`, format: `uuid` — The unique identifier of the staff member to update #### Request Body ##### Content-Type: application/json - **`active`** `boolean` — Whether the staff member is active - **`allow_messages`** `boolean` — Whether the staff member can send messages in this case (requires case\_id) - **`avatar_url`** `string | null`, format: `uri` — The staff member's headshot, as a publicly reachable https image URL (PNG, JPG, GIF or WebP, 10MB max) — we download it, resize anything over 1600px, and host our own copy, so the source URL does not have to stay available. Square works best; it is rendered as a circle. Pass null to remove the current photo. - **`bio`** `string | null` — The staff member's bio — the 'About' field in the portal - **`case_id`** `string`, format: `uuid` — The case ID to update case-level settings for this staff member - **`city`** `string | null` — The staff member's city - **`contactable`** `boolean` — Whether the staff member's contact info is visible to clients in the app (requires case\_id) - **`country`** `string | null` — The staff member's country - **`email`** `string`, format: `email` — The staff member's email address - **`facebook`** `string | null` — The staff member's Facebook profile - **`instagram`** `string | null` — The staff member's Instagram profile - **`job_title`** `string | null` — The staff member's job title - **`linkedin`** `string | null` — The staff member's LinkedIn profile - **`location_id`** `number | null` — The ID of the office location - **`name`** `string` — The staff member's full name - **`name_first`** `string` — The staff member's first name - **`name_last`** `string` — The staff member's last name - **`permission_id`** `string | null`, format: `uuid` — The ID of the permission set - **`phone`** `string | null` — The staff member's phone number - **`role`** `string`, possible values: `"admin", "attorney", "legal staff"` — The staff member's role (must be admin, attorney, or legal staff) - **`state`** `string | null` — The staff member's state or province - **`timezone`** `string | null` — The staff member's IANA timezone, e.g. 'America/Denver' - **`twitter`** `string | null` — The staff member's X/Twitter profile **Example:** ```json { "name": "", "name_first": "", "name_last": "", "email": "", "phone": null, "job_title": null, "city": null, "state": null, "country": null, "timezone": null, "bio": null, "linkedin": null, "twitter": null, "facebook": null, "instagram": null, "avatar_url": null, "role": "admin", "active": true, "location_id": null, "permission_id": null, "case_id": "", "contactable": true, "allow_messages": true } ``` #### Responses ##### Status: 200 Staff member updated successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`member_id` (required)** `string` — The organization membership ID of the staff member - **`user_id` (required)** `string` — The unique identifier of the updated staff member - **`message` (required)** `string` — Success message confirming the update operation **Example:** ```json { "message": "", "data": { "user_id": "", "member_id": "" } } ``` ### Create a webhook subscription - **Method:** `POST` - **Path:** `/webhooks` - **Tags:** webhooks Creates a new webhook subscription for the authenticated organization. The subscription connects a webhook template to an external endpoint with custom configuration. The system will automatically generate a webhook secret for signature verification. #### Request Body ##### Content-Type: application/json - **`endpoint` (required)** `string`, format: `uri` — The HTTPS endpoint URL to receive webhook payloads - **`event_name` (required)** `string` — The name of the event to subscribe to (e.g., 'client.action.created') - **`retry_attempts`** `integer`, default: `3` — Number of retry attempts for failed webhook deliveries (0-5) **Example:** ```json { "event_name": "", "endpoint": "", "retry_attempts": 3 } ``` #### Responses ##### Status: 201 Webhook subscription created successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`created_at` (required)** `string` — When the subscription was created - **`endpoint` (required)** `string | null`, format: `uri` — Subscription endpoint - **`event_name` (required)** `string` — The name of the event subscribed to - **`id` (required)** `string` — The unique identifier of the webhook subscription - **`shared_secret` (required)** `string | null` — Shared secret to verify the webhook payload - **`status` (required)** `string | null`, possible values: `"pending", "enabled", "disabled"` — Status of the webhook subscription: pending, enabled, disabled - **`updated_at` (required)** `string | null` — When the subscription was last updated - **`retry_attempts`** `integer`, default: `3` — Number of retry attempts for failed webhook deliveries (0-5) - **`message` (required)** `string` — Success message confirming the create operation **Example:** ```json { "message": "", "data": { "id": "", "event_name": "", "endpoint": null, "status": "pending", "shared_secret": null, "retry_attempts": 3, "created_at": "", "updated_at": null } } ``` ### List webhook subscriptions - **Method:** `GET` - **Path:** `/webhooks` - **Tags:** webhooks Retrieves a list of all webhook subscriptions for the authenticated organization. Returns subscription details including webhook template information, endpoint configuration, and status. #### Responses ##### Status: 200 List of webhook subscriptions retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` — Array of webhook subscriptions **Items:** - **`created_at` (required)** `string` — When the subscription was created - **`endpoint` (required)** `string | null`, format: `uri` — Subscription endpoint - **`event_name` (required)** `string` — The name of the event subscribed to - **`id` (required)** `string` — The unique identifier of the webhook subscription - **`shared_secret` (required)** `string | null` — Shared secret to verify the webhook payload - **`status` (required)** `string | null`, possible values: `"pending", "enabled", "disabled"` — Status of the webhook subscription: pending, enabled, disabled - **`updated_at` (required)** `string | null` — When the subscription was last updated - **`webhook` (required)** `object` — The webhook template details - **`description` (required)** `string | null` — The webhook description - **`id` (required)** `string` — The webhook template ID - **`name` (required)** `string | null` — The webhook event name - **`body_schema`** `object` — JSON schema for the webhook payload - **`retry_attempts`** `integer`, default: `3` — Number of retry attempts for failed webhook deliveries (0-5) - **`message` (required)** `string` — Success message confirming the list operation **Example:** ```json { "message": "", "data": [ { "id": "", "event_name": "", "endpoint": null, "status": "pending", "shared_secret": null, "retry_attempts": 3, "created_at": "", "updated_at": null, "webhook": { "id": "", "name": null, "description": null, "body_schema": null } } ] } ``` ### Delete a webhook subscription - **Method:** `DELETE` - **Path:** `/webhooks/{id}` - **Tags:** webhooks Deletes a webhook subscription for the authenticated organization. This will stop all webhook deliveries for this subscription immediately. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the webhook subscription `string` — The unique identifier of the webhook subscription #### Responses ##### Status: 200 Webhook subscription deleted successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`endpoint` (required)** `string | null`, format: `uri` — Subscription endpoint - **`event_name` (required)** `string` — The name of the event subscribed to - **`id` (required)** `string` — The unique identifier of the webhook subscription - **`message` (required)** `string` — Success message confirming the delete operation **Example:** ```json { "message": "", "data": { "id": "", "event_name": "", "endpoint": null } } ``` ### Retrieve a webhook subscription - **Method:** `GET` - **Path:** `/webhooks/{id}` - **Tags:** webhooks Retrieves a specific webhook subscription by its ID for the authenticated organization. Returns subscription details including configuration and associated webhook template information. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the webhook subscription `string` — The unique identifier of the webhook subscription #### Responses ##### Status: 200 Webhook subscription retrieved successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`created_at` (required)** `string` — When the subscription was created - **`endpoint` (required)** `string | null`, format: `uri` — Subscription endpoint - **`event_name` (required)** `string` — The name of the event subscribed to - **`id` (required)** `string` — The unique identifier of the webhook subscription - **`shared_secret` (required)** `string | null` — Shared secret to verify the webhook payload - **`status` (required)** `string | null`, possible values: `"pending", "enabled", "disabled"` — Status of the webhook subscription: pending, enabled, disabled - **`updated_at` (required)** `string | null` — When the subscription was last updated - **`webhook` (required)** `object` — The webhook template details - **`body_schema` (required)** `object` — JSON schema for the webhook payload - **`description` (required)** `string | null` — The webhook description - **`id` (required)** `string` — The webhook template ID - **`name` (required)** `string | null` — The webhook event name - **`params`** `object` — Parameters schema for the webhook - **`retry_attempts`** `integer`, default: `3` — Number of retry attempts for failed webhook deliveries (0-5) - **`message` (required)** `string` — Success message confirming the retrieve operation **Example:** ```json { "message": "", "data": { "id": "", "event_name": "", "endpoint": null, "status": "pending", "shared_secret": null, "retry_attempts": 3, "created_at": "", "updated_at": null, "webhook": { "id": "", "name": null, "description": null, "body_schema": {}, "params": null } } } ``` ### Update a webhook subscription - **Method:** `PATCH` - **Path:** `/webhooks/{id}` - **Tags:** webhooks Updates a webhook subscription's configuration for the authenticated organization. The updated configuration must still conform to the webhook template's params schema. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the webhook subscription `string` — The unique identifier of the webhook subscription #### Request Body ##### Content-Type: application/json - **`endpoint`** `string | null`, format: `uri` — Subscription endpoint - **`retry_attempts`** `integer`, default: `3` — Number of retry attempts for failed webhook deliveries (0-5) - **`status`** `string | null`, possible values: `"pending", "enabled", "disabled"` — Status of the webhook subscription: pending, enabled, disabled **Example:** ```json { "endpoint": null, "retry_attempts": 3, "status": "pending" } ``` #### Responses ##### Status: 200 Webhook subscription updated successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`created_at` (required)** `string` — When the subscription was created - **`endpoint` (required)** `string | null`, format: `uri` — Subscription endpoint - **`event_name` (required)** `string` — The name of the event subscribed to - **`id` (required)** `string` — The unique identifier of the webhook subscription - **`shared_secret` (required)** `string | null` — Shared secret to verify the webhook payload - **`status` (required)** `string | null`, possible values: `"pending", "enabled", "disabled"` — Status of the webhook subscription: pending, enabled, disabled - **`updated_at` (required)** `string | null` — When the subscription was last updated - **`retry_attempts`** `integer`, default: `3` — Number of retry attempts for failed webhook deliveries (0-5) - **`message` (required)** `string` — Success message confirming the update operation **Example:** ```json { "message": "", "data": { "id": "", "event_name": "", "endpoint": null, "status": "pending", "shared_secret": null, "retry_attempts": 3, "created_at": "", "updated_at": null } } ``` ### List available webhook events - **Method:** `GET` - **Path:** `/webhooks/events` - **Tags:** webhooks Retrieves a list of available webhook events that organizations can subscribe to. Returns event definitions including event names, descriptions, and payload schemas for Quilia outgoing webhooks. #### Responses ##### Status: 200 List of available webhook events retrieved successfully ###### Content-Type: application/json - **`data` (required)** `array` — Array of available webhook events **Items:** - **`body_schema` (required)** `object | null` — JSON schema describing the event payload structure - **`description` (required)** `string | null` — Human-readable description of when this event is triggered - **`id` (required)** `string` — The unique identifier of the webhook event - **`name` (required)** `string` — Unique event name identifier **Example:** ```json { "data": [ { "id": "", "name": "", "description": null, "body_schema": null } ] } ``` ### Regenerate webhook secret - **Method:** `POST` - **Path:** `/webhooks/{id}/regenerate-secret` - **Tags:** webhooks Regenerates the shared secret for a webhook subscription. This will invalidate the previous secret and generate a new one for signature verification. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the webhook subscription `string` — The unique identifier of the webhook subscription #### Responses ##### Status: 200 Webhook secret regenerated successfully ###### Content-Type: application/json - **`data` (required)** `object` - **`id` (required)** `string` - **`new_secret` (required)** `string` **Example:** ```json { "data": { "id": "", "new_secret": "" } } ``` ### Simulate webhook event - **Method:** `POST` - **Path:** `/webhooks/simulate` - **Tags:** webhooks Simulates a webhook event by sending the example payload from the event's schema to a specified endpoint. Useful for testing webhook integrations during development. #### Request Body ##### Content-Type: application/json - **`endpoint` (required)** `string`, format: `uri` — The endpoint URL to send the simulated webhook to - **`event_name` (required)** `string` — The name of the webhook event to simulate (e.g., 'document.created', 'case.updated') - **`use_example`** `boolean`, default: `true` — Whether to use the example payload from the schema (default: true) **Example:** ```json { "event_name": "", "endpoint": "", "use_example": true } ``` #### Responses ##### Status: 200 Webhook simulation completed ###### Content-Type: application/json - **`endpoint` (required)** `string` — The endpoint that received the webhook - **`event_name` (required)** `string` — The event that was simulated - **`message` (required)** `string` — Human-readable message about the simulation result - **`payload` (required)** `object` — The payload that was sent - **`success` (required)** `boolean` — Whether the webhook simulation was successful - **`error`** `string` — Error message if the simulation failed - **`response`** `object` — Response details from the endpoint - **`status` (required)** `number` — HTTP status code from the endpoint - **`body`** `string` — Response body from the endpoint - **`headers`** `object` — Response headers from the endpoint **Example:** ```json { "success": true, "message": "", "event_name": "", "endpoint": "", "payload": {}, "response": { "status": 1, "body": "", "headers": { "additionalProperty": "" } }, "error": "" } ``` ### Test webhook endpoint - **Method:** `POST` - **Path:** `/webhooks/test` - **Tags:** webhooks Tests a webhook endpoint by sending a test payload. Can be used to verify endpoint connectivity before creating a subscription. #### Request Body ##### Content-Type: application/json - **`endpoint` (required)** `string`, format: `uri` — The webhook endpoint URL to test **Example:** ```json { "endpoint": "" } ``` #### Responses ##### Status: 200 Webhook test completed ###### Content-Type: application/json - **`message` (required)** `string` - **`success` (required)** `boolean` - **`details`** `string` - **`response`** `string` **Example:** ```json { "success": true, "message": "", "response": "", "details": "" } ``` ### Test webhook subscription - **Method:** `POST` - **Path:** `/webhooks/{id}/test` - **Tags:** webhooks Tests a specific webhook subscription by sending a test payload to the configured endpoint. If the endpoint returns a 2xx response, the subscription status will be updated to 'enabled'. #### Parameters ##### `id` required - **In:** `path` The unique identifier of the webhook subscription `string` — The unique identifier of the webhook subscription #### Responses ##### Status: 200 Webhook subscription test completed ###### Content-Type: application/json - **`message` (required)** `string` - **`success` (required)** `boolean` - **`activation`** `boolean` - **`data`** `object` - **`id` (required)** `string` - **`activated_at`** `string` - **`status`** `string`, possible values: `"enabled", "pending", "disabled"` - **`details`** `string` - **`response`** `string` **Example:** ```json { "success": true, "message": "", "activation": true, "response": "", "details": "", "data": { "id": "", "status": "enabled", "activated_at": "" } } ```