> ## Documentation Index
> Fetch the complete documentation index at: https://docs.marginfront.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Matters (for law firms)

> Tie every AI dollar to the case it belongs to, then export a per-matter cost record that reconciles to the penny

# Matters

## What a matter is

A **matter** is the case or file a firm bills its work against. If your product serves law firms, matters let a firm answer one question for every dollar of AI cost: *which case does this belong to?*

Once usage is tied to matters, a firm can pull a per-matter cost record it can hand a client or an auditor. Anything no matter claims stays visible in an **Unassigned** bucket, so nothing gets lost.

You can manage matters on the Law tab in the dashboard, from your own code with the endpoints below, or in plain English through the [MCP tools](/mcp/tools). All three do the same work against the same numbers.

***

## How usage attaches to a matter

Every usage event lands on at most one matter. There are three ways it can get there, and when more than one applies, the strongest wins:

1. **Manual assignment (strongest).** Someone puts the event on a matter by hand, in the dashboard or through the assign endpoint below. A manual choice always wins, and it's never overwritten by tags or rules later.
2. **A tag on the event.** When you record a usage event, add the matter's own case number as `metadata.matterId`. If a matter with that exact number exists, the event attaches to it. The tag is matched against the matter's **number** (the `matterNumber` you set), not its UUID.
3. **A routing rule (weakest).** A rule says "events for this agent code, this customer, or this signal go to this matter." Rules fill in the events you didn't tag.

If none of the three apply, the event stays Unassigned until you assign it or a rule sweeps it up.

> **Tagging at record time is the cleanest path.** When your agent already knows the case it's working on, send `metadata.matterId` with the usage event (see [Usage Events](/api-reference/usage-events)). The event lands on the right matter the moment it's recorded, with nothing to clean up later. An unknown tag is never dropped and never creates a matter on its own. The event simply falls through to the rules, and to Unassigned if no rule matches.

***

## Actual cost, never an estimate

Every cost figure on a matter is the sum of that matter's real usage-event costs. It's never a guess.

When an event's cost isn't resolved yet (its model isn't in the pricing table), that event adds **0** to the total and is counted separately in an `eventsMissingCost` field. A nonzero `eventsMissingCost` means the true cost is *higher* than the number shown, and it tells you exactly how many events to fix first. See [Usage Events](/api-reference/usage-events) for how to resolve an unpriced model.

***

## Who can manage matters

Every endpoint on this page needs a **secret** key that belongs to an **owner**, **admin**, **finance**, or **legal** user. Any other key gets a `403 Forbidden` naming the role it needs. A publishable key (`mf_pk_...`) is refused.

### The legal key

A **legal key** is a secret key scoped for exactly this surface, so a firm can hand it to its own agent without handing over the books. An owner or admin mints one under **Build → API keys**.

A legal key can reach:

* Every endpoint on this page.
* The usage-recording surface: record events, `/v1/verify`, its own key info, and read-only [Spend Controls](./spend-controls).

Everything else answers `403 Forbidden`: customers, invoices, usage events, analytics, pricing, and portal sessions. That limit is always on and enforced on the server.

***

## Matters

### List matters

```
GET /v1/matters
```

Returns your matters, each with its all-time actual AI cost and event count.

| Query param | Type   | Required | Notes                                       |
| ----------- | ------ | -------- | ------------------------------------------- |
| `page`      | number | No       | Page number. Defaults to `1`.               |
| `limit`     | number | No       | Results per page (1-100). Defaults to `10`. |

**Response (`200 OK`):**

```json theme={null}
{
  "results": [
    {
      "id": "a1b2c3d4-0000-0000-0000-000000000000",
      "matterNumber": "2026-0142",
      "name": "Acme v. Widgets",
      "clientName": "Acme Corp",
      "status": "open",
      "responsibleUserId": null,
      "createdAt": "2026-09-01T00:00:00.000Z",
      "updatedAt": "2026-09-01T00:00:00.000Z",
      "actualCost": 46.0,
      "eventCount": 14,
      "eventsMissingCost": 0
    }
  ],
  "page": 1,
  "limit": 10,
  "totalPages": 1,
  "totalResults": 1
}
```

***

### Create a matter

```
POST /v1/matters
```

**Request body:**

```json theme={null}
{
  "matterNumber": "2026-0142",
  "name": "Acme v. Widgets",
  "clientName": "Acme Corp"
}
```

| Field               | Type   | Required | Notes                                                                                                           |
| ------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------- |
| `matterNumber`      | string | Yes      | The firm's own case number, unique in your organization. This is the value events tag with `metadata.matterId`. |
| `name`              | string | Yes      | The matter name, such as the case caption.                                                                      |
| `clientName`        | string | Yes      | The client this matter is billed to.                                                                            |
| `status`            | string | No       | `open` or `closed`. Defaults to `open`.                                                                         |
| `responsibleUserId` | string | No       | The responsible timekeeper, by user UUID.                                                                       |

**Response (`201 Created`):** the created matter.

```json theme={null}
{
  "id": "a1b2c3d4-0000-0000-0000-000000000000",
  "matterNumber": "2026-0142",
  "name": "Acme v. Widgets",
  "clientName": "Acme Corp",
  "status": "open",
  "responsibleUserId": null,
  "createdAt": "2026-09-01T00:00:00.000Z",
  "updatedAt": "2026-09-01T00:00:00.000Z"
}
```

**Common errors:**

* `409 Conflict`: a matter with that `matterNumber` already exists in your organization.

***

### Get one matter

```
GET /v1/matters/{id}
```

Returns the matter, its all-time cost and event count, and its 100 most recent events. The full record lives on the [audit endpoint](#the-audit-record) below; `eventsCap` tells you the cutoff.

**Response (`200 OK`):**

```json theme={null}
{
  "matter": {
    "id": "a1b2c3d4-0000-0000-0000-000000000000",
    "matterNumber": "2026-0142",
    "name": "Acme v. Widgets",
    "clientName": "Acme Corp",
    "status": "open",
    "responsibleUserId": null,
    "createdAt": "2026-09-01T00:00:00.000Z",
    "updatedAt": "2026-09-01T00:00:00.000Z"
  },
  "actualCost": 46.0,
  "eventCount": 14,
  "eventsMissingCost": 0,
  "events": [
    {
      "eventId": "e1000000-0000-0000-0000-000000000000",
      "usageDate": "2026-09-05T12:00:00.000Z",
      "agentName": "Research Bot",
      "signalName": "documents_reviewed",
      "quantity": 1,
      "usageCost": 5.0,
      "source": "tag"
    }
  ],
  "eventsCap": 100
}
```

An event's `usageCost` is `null` when its cost isn't resolved yet. `source` is `tag`, `rule`, `manual`, or `null`.

**Common errors:**

* `404 Not Found`: no matter with that ID in your organization.

***

### Update a matter

```
PATCH /v1/matters/{id}
```

Only the fields you send change. Send `responsibleUserId: null` to clear the responsible timekeeper. Closing a matter (`status: "closed"`) is a label only. It doesn't stop anything at record time.

**Response (`200 OK`):** the updated matter, same shape as create.

**Common errors:**

* `404 Not Found`: no matter with that ID in your organization.
* `409 Conflict`: the new `matterNumber` is already used by another matter.

***

### Delete a matter

```
DELETE /v1/matters/{id}
```

Deleting a matter doesn't delete its usage events. They survive and return to Unassigned, where a rule or a manual assignment can pick them up again. The matter's routing rules are deleted with it. The response counts both.

**Response (`200 OK`):**

```json theme={null}
{
  "deleted": true,
  "eventsReturnedToUnassigned": 14,
  "routingRulesDeleted": 2
}
```

**Common errors:**

* `404 Not Found`: no matter with that ID in your organization.

***

## Unassigned events

### List unassigned events

```
GET /v1/matters/unassigned
```

Returns the usage events no matter has claimed. `totalResults` is the true all-time count of unassigned events (it's never limited to the page or a date window), and `totalCost` is the cost of the whole unassigned set.

| Query param | Type   | Required | Notes                                       |
| ----------- | ------ | -------- | ------------------------------------------- |
| `page`      | number | No       | Page number. Defaults to `1`.               |
| `limit`     | number | No       | Results per page (1-100). Defaults to `10`. |

**Response (`200 OK`):**

```json theme={null}
{
  "results": [
    {
      "eventId": "e2000000-0000-0000-0000-000000000000",
      "usageDate": "2026-09-06T09:30:00.000Z",
      "agentName": "Intake Bot",
      "signalName": "messages",
      "quantity": 3,
      "usageCost": 0.75,
      "source": null
    }
  ],
  "page": 1,
  "limit": 10,
  "totalPages": 3,
  "totalResults": 25,
  "totalCost": 25.0,
  "eventsMissingCost": 0
}
```

***

### Assign an event to a matter

```
POST /v1/matters/assign
```

Puts one usage event on a matter by hand, or pulls it off. A manual assignment outranks a tag or a rule, and a manual unassignment is never swept back up by the apply-rules pass.

**Request body:**

```json theme={null}
{
  "eventId": "e2000000-0000-0000-0000-000000000000",
  "matterId": "a1b2c3d4-0000-0000-0000-000000000000"
}
```

| Field      | Type   | Required | Notes                                                      |
| ---------- | ------ | -------- | ---------------------------------------------------------- |
| `eventId`  | string | Yes      | The usage event to move, by UUID.                          |
| `matterId` | string | Yes      | The target matter's UUID, or `null` to unassign the event. |

**Response (`201 Created`):**

```json theme={null}
{
  "assigned": true,
  "eventId": "e2000000-0000-0000-0000-000000000000",
  "matterId": "a1b2c3d4-0000-0000-0000-000000000000",
  "source": "manual"
}
```

**Common errors:**

* `404 Not Found`: the event or the target matter isn't in your organization.

***

## Routing rules

A **routing rule** attaches events to a matter automatically. A rule matches an event when **every** matcher it sets matches: `agentCode` exactly, `customerExternalId` exactly, and `signalPattern` as a case-insensitive substring of the signal name. A rule needs at least one matcher.

Rules run in **evaluation order**: lowest `priority` first, and older rules before newer ones on a tie. The first rule that matches wins.

### List routing rules

```
GET /v1/matter-rules
```

Returns your rules in the exact order they're evaluated at record time and when you apply them.

| Query param | Type   | Required | Notes                                       |
| ----------- | ------ | -------- | ------------------------------------------- |
| `page`      | number | No       | Page number. Defaults to `1`.               |
| `limit`     | number | No       | Results per page (1-100). Defaults to `10`. |

**Response (`200 OK`):**

```json theme={null}
{
  "results": [
    {
      "id": "r1000000-0000-0000-0000-000000000000",
      "matterId": "a1b2c3d4-0000-0000-0000-000000000000",
      "matterNumber": "2026-0142",
      "matterName": "Acme v. Widgets",
      "priority": 100,
      "agentCode": "research-bot",
      "customerExternalId": null,
      "signalPattern": null
    }
  ],
  "page": 1,
  "limit": 10,
  "totalPages": 1,
  "totalResults": 1
}
```

***

### Create a routing rule

```
POST /v1/matter-rules
```

**Request body:**

```json theme={null}
{
  "matterId": "a1b2c3d4-0000-0000-0000-000000000000",
  "agentCode": "research-bot"
}
```

| Field                | Type   | Required | Notes                                                           |
| -------------------- | ------ | -------- | --------------------------------------------------------------- |
| `matterId`           | string | Yes      | The matter this rule assigns events to, by UUID.                |
| `priority`           | number | No       | Lower runs first. Defaults to `100`.                            |
| `agentCode`          | string | No       | Match this exact agent code.                                    |
| `customerExternalId` | string | No       | Match this exact customer external ID.                          |
| `signalPattern`      | string | No       | Match this text anywhere in the signal name (case-insensitive). |

At least one of `agentCode`, `customerExternalId`, or `signalPattern` is required.

**Response (`201 Created`):** the rule, including its matter's number and name.

**Common errors:**

* `400 Bad Request`: no matcher was provided.
* `404 Not Found`: the target matter isn't in your organization.

***

### Apply routing rules

```
POST /v1/matter-rules/apply
```

Sweeps your rules across events that are still Unassigned, in priority order. It touches **only** events with no matter and no attribution history, so it never overrides a tag, an earlier rule, or a manual choice.

**Response (`201 Created`):**

```json theme={null}
{
  "assigned": 12
}
```

If the sweep stops partway, the response says how many events were assigned before it stopped. Run it again to finish the rest.

***

### Update a routing rule

```
PATCH /v1/matter-rules/{id}
```

Only the fields you send change. Send a matcher as `null` to clear it. A rule must always keep at least one matcher.

**Response (`200 OK`):** the updated rule.

**Common errors:**

* `404 Not Found`: no rule with that ID in your organization.

***

### Delete a routing rule

```
DELETE /v1/matter-rules/{id}
```

Deleting a rule stops future matching only. Events the rule already assigned keep their matter.

**Response (`200 OK`):**

```json theme={null}
{
  "deleted": true
}
```

**Common errors:**

* `404 Not Found`: no rule with that ID in your organization.

***

## The audit record

### Export the matter audit

```
GET /v1/matters/audit
```

This is the record a firm shows a client or a carrier: the actual AI cost per matter over a window. The default `summary` view is one row per matter, plus an **Unassigned** row, plus organization totals. It reconciles exactly with the Law tab CSV export for the same window.

| Query param | Type   | Required | Notes                                                                                                 |
| ----------- | ------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `start`     | string | No       | Window start, as `YYYY-MM-DD` (start of day UTC) or a full ISO timestamp. Defaults to 30 days back.   |
| `end`       | string | No       | Window end, as `YYYY-MM-DD` (end of day UTC) or a full ISO timestamp. Capped at now. Defaults to now. |
| `format`    | string | No       | `summary` (default) or `detail`. `detail` adds one row per event.                                     |
| `matterId`  | string | No       | Slice to one matter, by UUID. A sliced view has no Unassigned row and no organization totals.         |

**Response (`200 OK`, `summary`):**

```json theme={null}
{
  "periodStart": "2026-08-11T00:00:00.000Z",
  "periodEnd": "2026-09-10T23:59:59.999Z",
  "summary": [
    {
      "matterNumber": "2026-0142",
      "matterName": "Acme v. Widgets",
      "clientName": "Acme Corp",
      "cost": 21.0,
      "eventCount": 5,
      "eventsMissingCost": 0
    },
    {
      "matterNumber": null,
      "matterName": null,
      "clientName": null,
      "cost": 25.0,
      "eventCount": 9,
      "eventsMissingCost": 0
    }
  ],
  "totalCost": 46.0,
  "totalEvents": 14,
  "eventsMissingCost": 0
}
```

The row with a `null` `matterNumber` is the Unassigned row. It's present whenever unassigned events fall in the window.

When you pass `format=detail`, the response also carries a `rows` array, one entry per event, each with its matter, date, agent, signal, the models used, cost, quantity, and attribution `source`.

Costs that aren't resolved yet count as `0` in every total and are flagged in `eventsMissingCost`. They're never invented, so the number you hand an auditor is honest.

***

## A quick pattern

A firm that wants clean records from day one usually does this:

1. Create a matter for each open case, using the firm's own case number as `matterNumber`.
2. Tag each usage event with `metadata.matterId` set to that case number, so events land on the right matter as they're recorded.
3. Add a routing rule or two for the work that's hard to tag at the source.
4. Check the Unassigned list now and then, and assign anything left over by hand.
5. Export the audit for a billing window when it's time to bill.

Steps 1 through 4 keep the record accurate. Step 5 turns it into a number a client can trust.
