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

# Cogram API

Integrate Cogram with your own systems: automate project management, sync user data, and build custom integrations against a stable REST API.

## Overview

The REST API lives on its own subdomain (`api.cogram.com`), authenticates with API keys, and exposes stable, versioned endpoints built for third-party integrations.

**Key features:**

* **Stable versioned endpoints** - The API is versioned (`/v1/`) to ensure backwards compatibility
* **Interactive documentation** - Explore and test endpoints directly at [api.cogram.com/v1/docs](https://api.cogram.com/v1/docs)
* **External ID mapping** - Link Cogram resources to your own system's identifiers

## Authentication

The Cogram API uses API key authentication. API keys are organization-scoped and can be created by organization administrators.

### Creating an API Key

1. Go to [Organization Settings → Integrations → API Keys](https://app.cogram.com/dashboard/settings/admin/integrations) in the Cogram app.
2. Click **Create API Key**.
3. Give your key a descriptive name and optionally set an expiration date.
4. Copy the key immediately. It will only be shown once.

### Using Your API Key

Include the API key in the `Authorization` header as a Bearer token:

```bash
curl -X GET "https://api.cogram.com/v1/projects" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Optional Headers

> **X-Forwarded-User**: Identifier of the user in your system (e.g., user ID or email) who triggered this action. When provided, Cogram's audit logs will attribute the action to this user rather than just the API key. Omit for automated system tasks with no associated user.

## Base URL

All API requests should be made to:

```
https://api.cogram.com
```

## Available Endpoints

### Projects

Manage projects within your organization.

| Method   | Endpoint            | Description            |
| -------- | ------------------- | ---------------------- |
| `GET`    | `/v1/projects`      | List all projects      |
| `POST`   | `/v1/projects`      | Create a new project   |
| `GET`    | `/v1/projects/{id}` | Get a specific project |
| `PATCH`  | `/v1/projects/{id}` | Update a project       |
| `DELETE` | `/v1/projects/{id}` | Archive a project      |

Project create and update requests, and all project responses, include an **`is_public`** field. When `true`, the project is discoverable: it appears in the organization-wide project directory and non-members can request access. When `false`, the project is not listed in the directory. They also include **`meeting_filing_enabled`**, which controls whether Cogram suggests the project for meetings and, where your organization allows it, files meetings into it. It defaults to `true` and only takes effect while the organization has meeting filing turned on (see [Automatic meeting filing](https://docs.cogram.com/guides/projects#automatic-meeting-filing)). See the [interactive API docs](https://api.cogram.com/v1/docs) for full request/response schemas.

To set a project up from a [project template](#project-templates), pass its id as **`template_id`** on `POST /v1/projects`. The new project gets the template's project type, and each of the template's groups is added to the project with the template's role, so the members of those groups get access right away. The template's description is not copied into the project. If you leave out `email_filing_enabled` or `is_public`, the project takes the template's default for that setting. A value you send always wins. If you send no `is_public` and the template has no `is_public_default`, the project takes your organization's discoverability default, as it does in the app.

When you send `template_id`, the template is resolved as the user who created your API key. If the template cannot be applied, no project is created:

* `403`: the key's creator cannot create projects, or has been deactivated or removed from the organization.
* `404`: the template does not exist or belongs to another organization.
* `422`: the template's project type has since been deactivated, or the template binds a group as `LEAD` and the Lead role is not enabled for your organization.

#### Record counts per project

Add **`?include=counts`** to either `GET /v1/projects` or `GET /v1/projects/{id}` to populate a `related_counts` object on each project: how many emails, documents, drawings, drawing sets, submittals, RFIs, meetings, reports, observations, and transmittals are filed under it. Every field is present and defaults to `0`, so a project with nothing filed returns zeros rather than omitting keys. To request more than one section at once, see [Members per project](#members-per-project) below.

```bash
curl "https://api.cogram.com/v1/projects?include=counts&page_size=100" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Without the parameter, `related_counts` is `null` and no counting work is done; existing integrations are unaffected. The counts for a whole page cost one additional query regardless of `page_size`, so requesting them on a 100-project page is no more expensive than on one project.

Counts are commonly used to notice that a project has changed since a previous poll: fetch a page, compare against the values you stored last time, and act on the projects that moved. Three kinds of change are invisible to them, because the number of records does not change:

* An edit that leaves the record in place: a renamed document, a corrected email subject.
* A new revision of an existing drawing, or action items generated for an existing meeting. Only the top-level record is counted, so `drawings` and `meetings` stay flat.
* An addition and a deletion inside the same polling interval, which cancel out.

Soft-deleted meetings are excluded, matching the rest of the API. Cogram's built-in example records are counted; they are real records in the project.

#### Members per project

Add **`?include=members`** to either `GET /v1/projects` or `GET /v1/projects/{id}` to populate a `members` array on each project. Every entry carries the member's `user_id`, `email`, and project `role` (`OWNER`, `LEAD`, `MEMBER`, or `VIEWER`), ordered by email so two polls can be compared without sorting first. `LEAD` is a management role (like `OWNER` but without the ability to add or remove members or delete the project) and appears only for organizations that have it enabled.

Match on `user_id`, not `email`: two accounts can share an email address, and `user_id` is what the membership endpoints below are keyed by. Display names are not included; fetch `GET /v1/projects/{id}/members` when you need them.

```bash
curl "https://api.cogram.com/v1/projects?include=members" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Without the parameter, `members` is `null` and no membership work is done; existing integrations are unaffected. A project that genuinely has no members returns `[]`, which is how you tell "nobody is on it" apart from "I didn't ask".

`include` takes more than one section in a single request, spelled either way: repeat the parameter (`?include=members&include=counts`) or comma-separate the values (`?include=members,counts`). A name that matches no section is still rejected with a `422`, so a typo does not fail silently.

Two things the array does not cover:

* **Group-granted access is not listed.** Only direct per-user roles appear. Someone who can reach the project because their group was added to it is not a member here.
* **Users banned from your organization are omitted**, matching `GET /v1/projects/{id}/members`.

The roster for a whole page costs one additional query regardless of `page_size`, but unlike counts the response itself grows with every member it carries; a 100-project page returns the roster of all 100. If you only need one project's members, ask on `GET /v1/projects/{id}`.

#### Polling for changed projects

Add **`?updated_since=<timestamp>`** to `GET /v1/projects` to get only the projects that changed, instead of paging your whole organization to find out. Use it to keep an external system (a file server, a document store, a data warehouse) in step with Cogram.

```bash
curl "https://api.cogram.com/v1/projects?include_archived=true&updated_since=2026-08-17T18%3A03%3A11Z" \
  -H "Authorization: Bearer $COGRAM_API_KEY"
```

The timestamp must carry a UTC offset (`2026-08-17T18:03:11Z` or `2026-08-17T20:03:11+02:00`); one without an offset is rejected with `422`. Remember to URL-encode it: a raw `+` in a query string means a space.

It answers **what changed, not what changed to what**. Each project comes back in its current state, so hold the values you care about and compare. That is what tells you a project was archived, renamed, or had a Unanet field rewritten.

Four properties make it safe to build a loop on:

* **It reports creations too.** A project created inside the window has no `updated_at` yet, so it is matched on `created_at` instead. One cursor covers both, and you do not need a second poll for new projects.
* **Results are ordered oldest change first**, so a project modified while you are paging is appended rather than shifted into a page you already read.
* **The bound is inclusive.** Store the newest `updated_at` (or `created_at`, for a project that has never been updated) you were served and pass it back next time. You may be served that one project again; you will not miss another that shares its timestamp across a page boundary.
* **Archiving is a change, not a disappearance**, but only if you ask for it. Pass `include_archived=true`, or an archived project drops out of the response and reads as if it were deleted.

Two limits to design around:

* **Deletions are not reported.** A deleted project is absent from later responses. If that matters, reconcile against a full listing on a slower schedule.
* **`updated_at` tracks the project record only**: its name, client, archive state, and Unanet fields. Filing an email or a drawing under a project does not change it. For that, use `?include=counts` above.

### Project Members

Manage project membership and roles.

| Method   | Endpoint                              | Description            |
| -------- | ------------------------------------- | ---------------------- |
| `GET`    | `/v1/projects/{id}/members`           | List project members   |
| `PUT`    | `/v1/projects/{id}/members/{user_id}` | Add or update a member |
| `DELETE` | `/v1/projects/{id}/members/{user_id}` | Remove a member        |

Assigning the `LEAD` role (on create-with-members or `PUT .../members/{user_id}`) requires the role to be enabled for your organization; otherwise the request returns `422`. `OWNER`, `MEMBER`, and `VIEWER` are always assignable.

### Project Types

Manage the organization's project-type taxonomy (e.g. "Design-Build", "CM at Risk"). Project types are configured in the app at [Organization Settings → Projects → Project Types](https://app.cogram.com/dashboard/settings/admin/projects/project-types); the API exposes the same CRUD.

| Method   | Endpoint                 | Description                 |
| -------- | ------------------------ | --------------------------- |
| `GET`    | `/v1/project-types`      | List all project types      |
| `POST`   | `/v1/project-types`      | Create a new project type   |
| `GET`    | `/v1/project-types/{id}` | Get a specific project type |
| `PATCH`  | `/v1/project-types/{id}` | Update a project type       |
| `DELETE` | `/v1/project-types/{id}` | Delete a project type       |

Deleting a type clears the type on any project that was using it (the project is not deleted). See the [interactive API docs](https://api.cogram.com/v1/docs) for full request/response schemas.

### Project Templates

Manage the organization's project templates. A template holds a project type and a list of groups, each with a project role (`MEMBER`, `VIEWER`, or `LEAD`). Apply one when you create a project by passing `template_id` (see [Projects](#projects)).

| Method | Endpoint                     | Description                            |
| ------ | ---------------------------- | -------------------------------------- |
| `GET`  | `/v1/project-templates`      | List project templates (paginated)     |
| `POST` | `/v1/project-templates`      | Create a project template              |
| `GET`  | `/v1/project-templates/{id}` | Get a template with its groups         |
| `PUT`  | `/v1/project-templates/{id}` | Replace a template's fields and groups |

These endpoints act as the user who created your API key. Reading templates needs the same access as creating a project. Creating or changing a template needs an organization admin or owner; a key created by anyone else gets `403` with the error code `forbidden`. If the key's creator later loses the admin role, is deactivated, or is removed from the organization, the key loses that access too.

The list is ordered by name and uses the standard [pagination](#pagination) parameters (`page`, default `1`; `page_size`, default `50`, max `100`).

`POST` and `PUT` take the same body:

| Field                          | Type            | Description                                                                               |
| ------------------------------ | --------------- | ----------------------------------------------------------------------------------------- |
| `name`                         | string          | Required. 1–100 characters, unique in the organization (case-insensitive).                |
| `description`                  | string \| null  | Optional. Up to 2000 characters.                                                          |
| `groups`                       | array           | Up to 500 `{ "group_id", "role" }` pairs, each a group in your organization, listed once. |
| `project_type_id`              | string \| null  | Optional. An active project type in your organization.                                    |
| `email_filing_enabled_default` | boolean         | Email filing for projects created from the template. Defaults to `false`.                 |
| `is_public_default`            | boolean \| null | Discoverability for new projects. `null` uses the organization's discoverability default. |

`POST` returns `201` with the new template. `PUT` returns `200` and replaces the whole template. A field you leave out goes back to its default: an omitted `groups` removes every group, and an omitted `project_type_id` clears the type. Projects already created from the template do not change. Templates cannot be deleted through the API.

A missing template id returns `404`, a duplicate name returns `409`, and an invalid role, a group outside your organization, a duplicate group, an inactive project type, or `LEAD` while the Lead role is not enabled returns `422`. See the [interactive API docs](https://api.cogram.com/v1/docs) for full request/response schemas.

### Users

Manage organization members.

| Method  | Endpoint              | Description               |
| ------- | --------------------- | ------------------------- |
| `GET`   | `/v1/users`           | List organization members |
| `PATCH` | `/v1/users/{user_id}` | Update a user's role      |

### Data Exports

Request and download a multi-part zip export of your organization's data: projects, meetings, emails, documents, drawings, reports, observations, transmittals, and Procore-synced submittals and RFIs. Builds run asynchronously; once complete, each part is delivered via short-lived signed URLs (1-hour validity, regenerated on every status read).

For an end-to-end walkthrough of what's inside each zip, see [Data export package layout](/organization-administration/data-export-package-layout.md). For the UI equivalent of these endpoints, see [Data exports](/organization-administration/data-exports.md).

Data Exports are admin-equivalent: only admins or owners can issue API keys, and the endpoints can only be invoked by keys issued for the same organization. Manage keys at [Organization Settings → Integrations → API Keys](https://app.cogram.com/dashboard/settings/admin/integrations).

| Method | Endpoint                       | Description                                        |
| ------ | ------------------------------ | -------------------------------------------------- |
| `POST` | `/v1/data-exports`             | Create a new data export (returns immediately)     |
| `GET`  | `/v1/data-exports`             | List data exports for the organization (paginated) |
| `GET`  | `/v1/data-exports/{id}`        | Get one data export's status and download URLs     |
| `POST` | `/v1/data-exports/{id}/cancel` | Cancel a `pending` or `running` data export        |

`GET /v1/data-exports` returns the standard paginated envelope `{ "data": [...], "total": N, "page": N, "page_size": N }`. Use `?page=N&page_size=N` (defaults: `page=1`, `page_size=50`, max `page_size=100`) to walk through the audit trail.

Meeting **audio recordings are never exported**. Each meeting in the zip ships as `meeting.json`, `meeting.md`, and `transcript.json` (when the meeting was transcribed). A rendered `meeting.docx` is added whenever your org has a default template (see [Data Exports → templates](/organization-administration/data-exports.md#choosing-a-meeting-template-before-exporting)). Photos and uploaded meeting attachments are included; audio is intentionally kept inside Cogram.

#### Request body: `POST /v1/data-exports`

| Field              | Type                   | Description                                                                                                                                                                                                                                                                       |
| ------------------ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `project_ids`      | array\[string] \| null | Optional. Scope the export to these projects; list candidate ids with `GET /v1/projects` (add `?include_archived=true` to reach archived projects, which are still exportable). Org-wide exports (when omitted) must specify a time range under 6 calendar months.                |
| `time_filter`      | object                 | Discriminated by `mode`. `{"mode": "range", "start": <iso>, "end": <iso>}` or `{"mode": "all_time"}`. `all_time` is project-only.                                                                                                                                                 |
| `document_formats` | array\[string]         | Optional, defaults to `[]`. Pass `["docx"]` to render a `.docx` per field report alongside the JSON, plus one per observation its report does not already show. Does not affect `meeting.docx`, which every export includes. Rendering makes the build slower and the zip larger. |

#### Response fields

| Field                      | Type             | Description                                                                                                                     |
| -------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `id`                       | string           | Unique identifier (`dex_...`)                                                                                                   |
| `organization_id`          | string           | Org the export belongs to                                                                                                       |
| `source`                   | string           | `ui` or `api`: how the export was created                                                                                       |
| `requester_user_id`        | UUID \| null     | User who triggered it (UI source only)                                                                                          |
| `requester_api_key_id`     | string \| null   | API key that triggered it (API source only)                                                                                     |
| `requester_forwarded_user` | string \| null   | Value of the `X-Forwarded-User` header at create time (audit trail)                                                             |
| `requester_display`        | string           | Human-readable attribution: user's name, `API key '<name>'`, or `<forwarded_user> (via API key '<name>')` when both are present |
| `project_ids`              | array\[string]   | Scope as requested; empty for org-wide exports                                                                                  |
| `projects`                 | array            | Scope projects that still exist, with `id`, `name`, and `customer_project_id`                                                   |
| `time_filter`              | object           | Echo of the requested filter                                                                                                    |
| `document_formats`         | array\[string]   | Echo of the requested rendered document formats                                                                                 |
| `status`                   | string           | `pending`, `running`, `cancelling`, `cancelled`, `completed`, `failed`, or `expired`                                            |
| `error`                    | string \| null   | Failure reason when `status == "failed"`                                                                                        |
| `requested_at`             | datetime         | When the export was created                                                                                                     |
| `started_at`               | datetime \| null | When the build worker began                                                                                                     |
| `completed_at`             | datetime \| null | When the build finished (success, failure, or cancellation)                                                                     |
| `expires_at`               | datetime \| null | When the parts will be deleted from storage (7 days after `completed_at` for `completed`)                                       |
| `parts`                    | array            | Empty until `status == "completed"`; one entry per zip part with a freshly-signed URL                                           |

Each entry in `parts` includes:

| Field        | Type    | Description                                              |
| ------------ | ------- | -------------------------------------------------------- |
| `id`         | string  | Unique part identifier (`dxp_...`)                       |
| `part_index` | integer | Zero-indexed position within the multi-part set          |
| `size_bytes` | integer | Compressed part size                                     |
| `signed_url` | string  | One-hour pre-signed download URL; re-fetch on every read |

#### Polling pattern

```bash
# 1) Create: returns 201 with status=pending
curl -X POST "https://api.cogram.com/v1/data-exports" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"time_filter": {"mode": "range", "start": "2026-01-01T00:00:00Z", "end": "2026-04-01T00:00:00Z"}}'

# 1b) Or scope it to specific projects, where any time range is allowed
curl -X POST "https://api.cogram.com/v1/data-exports" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"project_ids": ["prj_...", "prj_..."], "time_filter": {"mode": "all_time"}}'

# 2) Poll until status == "completed"
curl "https://api.cogram.com/v1/data-exports/dex_..." \
  -H "Authorization: Bearer YOUR_API_KEY"

# 3) Download each part. The signed URLs are valid for 1 hour from the GET
#    above and serve a Content-Disposition header so the file lands with
#    a friendly name like
#    Cogram_data_export_2026-04-01_12-00-00_part_000_all_projects_2026-01-01_to_2026-04-01.zip
#
#    With wget, use --content-disposition to honor the suggested filename:
wget --content-disposition '<signed_url>'
#
#    With curl, use -OJ (capital O capital J). Plain `curl -o name.zip`
#    forces a local name and bypasses the friendly filename entirely.
curl -OJ '<signed_url>'
```

The zip's internal structure, including folders, file types, and how to identify Cogram's example/demo data (the `is_dummy` field on JSON entities, and the `X-Cogram-Is-Dummy` header on `.eml` files), is documented in [Data Export Package Layout](/organization-administration/data-export-package-layout.md#identifying-example--demo-data).

> **Date ranges are inclusive of the `end` timestamp.** The backend filter is `<=` on the `end` value you supply. If you want "everything modified through May 31", send `end=2026-06-01T00:00:00Z` (the next-day UTC midnight). Sending `2026-05-31T00:00:00Z` would only capture exactly midnight on May 31, not the full day. The Cogram UI does this conversion for you when you pick a date in the date-picker; API callers must do it themselves.

> **Cap rule**: Org-wide exports (no `project_ids`) are limited to a 6-month range and reject `all_time`. Project-scoped exports have no time-range limit. A violation returns `400` with `"error": "range_too_large"` (typed `ErrorCode`).

> **Project scope**: A project id your organization doesn't own returns `404` with `"error": "not_found"`, the same response as an id that doesn't exist anywhere, so the API never reveals another organization's projects. One bad id rejects the whole request, so you never receive a zip that silently covers fewer projects than you asked for. The check runs before the export is accepted, so a rejected scope costs you nothing: no export row is created and the one-active-export slot stays free for an immediate retry.

> **Deprecated `project_id`**: the original single-project field is still accepted and normalizes to a one-element `project_ids`. Sending both fields returns `422`. New integrations should use `project_ids`.

> **Concurrency**: At most one active export (`pending`, `running`, or `cancelling`) per organization, enforced both at the application layer and by a Postgres partial unique index. A second `POST /v1/data-exports` while an active one exists returns `409` with `"error": "data_export_in_progress"` and the existing export's id woven into the message. Poll that id and re-issue once it terminates.

> **Cancellation**: `pending` exports cancel immediately. `running` exports transition to `cancelling`; the worker stops between zip parts and the next status read reflects the terminal state. Terminal statuses (`completed`, `failed`, `cancelled`, `expired`) return `409 conflict`.

> **Stuck-row recovery**: If a worker dies hard mid-build (rare), a periodic sweep transitions rows stuck in `running`/`cancelling` to `failed` so the per-org concurrency slot doesn't stay blocked. Retry by creating a new export. A row is only swept after \~6 hours with no sign of progress, so a large export that is still running is never cut short, however long it takes. Cancel it if you no longer want it.

### Backup Runs

Report each backup run performed by the LucidLink connector, so Cogram can tell you when backups stop.

Cogram cannot detect a backup that never starts: a connector that has stopped cannot report that it stopped. Instead, the LucidLink connector tells Cogram when a run finishes and how often it is scheduled to run. If a scheduled run is missed (no successful run within that interval), Cogram emails the recipients configured at [Organization Settings → Integrations → LucidLink Backups](https://app.cogram.com/dashboard/settings/admin/integrations#backups). You get one email per outage, and it resets after the next successful run.

You do not need to call this endpoint yourself if you use the LucidLink connector; it reports its own runs.

| Method | Endpoint          | Description                              |
| ------ | ----------------- | ---------------------------------------- |
| `POST` | `/v1/backup-runs` | Report that a backup started or finished |

#### Request body: `POST /v1/backup-runs`

| Field                     | Type            | Description                                                                                       |
| ------------------------- | --------------- | ------------------------------------------------------------------------------------------------- |
| `event`                   | string          | `started` when a run begins, `finished` when it ends.                                             |
| `schedule_interval_hours` | integer         | How often the backup is scheduled to run, in hours (1–8760). Cogram measures health against this. |
| `status`                  | string \| null  | `success` or `failed`. Required on `finished`, and must be omitted on `started`.                  |
| `projects_synced`         | integer \| null | Optional. Projects filed by this run. Zero is valid: a run with no changed projects is healthy.   |
| `error`                   | string \| null  | Optional failure detail, up to 2000 characters. Only meaningful when `status` is `failed`.        |

#### Example

```bash
# A run begins
curl -X POST 'https://api.cogram.com/v1/backup-runs' \
  -H 'Authorization: Bearer <api-key>' \
  -H 'Content-Type: application/json' \
  -d '{"event": "started", "schedule_interval_hours": 24}'

# The same run finishes
curl -X POST 'https://api.cogram.com/v1/backup-runs' \
  -H 'Authorization: Bearer <api-key>' \
  -H 'Content-Type: application/json' \
  -d '{"event": "finished", "schedule_interval_hours": 24, "status": "success", "projects_synced": 12}'
```

> **Only successful runs count as healthy.** A `failed` run tells Cogram the connector is alive, but not that your data is backed up, so it does not clear a stale-backup alert.

> **Alerts are off until you add recipients.** Configure them under [Organization Settings → Integrations → LucidLink Backups](https://app.cogram.com/dashboard/settings/admin/integrations#backups). An empty recipient list turns alerting off.

### Action Items

Retrieve action items extracted from meetings within your organization.

| Method | Endpoint           | Description       |
| ------ | ------------------ | ----------------- |
| `GET`  | `/v1/action-items` | List action items |

#### Filtering and search

The list endpoint supports the following query parameters:

| Parameter          | Type    | Default      | Description                                                                  |
| ------------------ | ------- | ------------ | ---------------------------------------------------------------------------- |
| `page`             | integer | `1`          | Page number (1-indexed)                                                      |
| `page_size`        | integer | `50`         | Items per page (max 100)                                                     |
| `status`           | string  | —            | Filter by status: `SUGGESTED`, `ACCEPTED`, `REJECTED`, or `COMPLETED`        |
| `include_archived` | boolean | `false`      | Set to `true` to include archived items                                      |
| `project_id`       | string  | —            | Filter to items from meetings linked to this project                         |
| `meeting_id`       | string  | —            | Filter to items from a specific meeting                                      |
| `assignee`         | string  | —            | Case-insensitive partial match on assignee name                              |
| `q`                | string  | —            | Case-insensitive search in item description                                  |
| `sort_by`          | string  | `created_at` | Sort field: `created_at`, `due_date`, `status`, `assignee`, or `description` |
| `sort_dir`         | string  | `asc`        | Sort direction: `asc` or `desc`                                              |

> When filtering by `project_id`, action items from meetings not linked to any project are excluded. Unknown `project_id` or `meeting_id` values return an empty list, not a 404 error.

> Items with no value in nullable sort fields (`due_date`, `assignee`, `status`, `description`) are always placed at the bottom, regardless of sort direction.

#### Response fields

Each action item in the `data` array includes:

| Field          | Type             | Description                                         |
| -------------- | ---------------- | --------------------------------------------------- |
| `id`           | string           | Unique identifier                                   |
| `description`  | string \| null   | Action item text                                    |
| `assignee`     | string \| null   | Assigned person's name                              |
| `status`       | string \| null   | `SUGGESTED`, `ACCEPTED`, `REJECTED`, or `COMPLETED` |
| `origin`       | string \| null   | `AUTO` (AI-extracted) or `MANUAL` (user-created)    |
| `due_date`     | datetime \| null | Due date in ISO 8601 format                         |
| `completed_at` | datetime \| null | Timestamp when marked done                          |
| `archived`     | boolean          | Whether the item has been archived                  |
| `meeting_id`   | string           | ID of the meeting this item came from               |
| `meeting_name` | string \| null   | Name of the meeting                                 |
| `project_id`   | string \| null   | ID of the project the meeting belongs to, if any    |
| `created_at`   | datetime         | Creation timestamp in ISO 8601 format               |
| `updated_at`   | datetime \| null | Last update timestamp                               |

### Board Items

Retrieve kanban board items: action items that have been promoted to a project's board, either manually by users or automatically via meeting reconciliation. Read-only in v1.

| Method | Endpoint               | Description                    |
| ------ | ---------------------- | ------------------------------ |
| `GET`  | `/v1/board-items`      | List board items               |
| `GET`  | `/v1/board-items/{id}` | Get a single item with history |

#### Filtering and search

The list endpoint supports the following query parameters:

| Parameter            | Type    | Default      | Description                                                                                        |
| -------------------- | ------- | ------------ | -------------------------------------------------------------------------------------------------- |
| `page`               | integer | `1`          | Page number (1-indexed)                                                                            |
| `page_size`          | integer | `50`         | Items per page (max 100)                                                                           |
| `status`             | string  | —            | Filter by status: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`, or `DISMISSED`                         |
| `include_archived`   | boolean | `false`      | Set to `true` to include archived items                                                            |
| `project_id`         | string  | —            | Filter to items in this project                                                                    |
| `meeting_id`         | string  | —            | Filter to items originated from this meeting (matches `source_meeting_id`)                         |
| `meeting_series_uid` | string  | —            | Filter to items tied to a recurring meeting series                                                 |
| `assignee`           | string  | —            | Case-insensitive partial match on assignee name                                                    |
| `q`                  | string  | —            | Case-insensitive search across `title` and `body`                                                  |
| `sort_by`            | string  | `created_at` | Sort field: `created_at`, `updated_at`, `last_status_changed_at`, `status`, `title`, or `assignee` |
| `sort_dir`           | string  | `asc`        | Sort direction: `asc` or `desc`                                                                    |

> Unknown `project_id` or `meeting_id` values return an empty list, not a 404 error.

> Items with no value in nullable sort fields (`assignee`, `last_status_changed_at`) are always placed at the bottom, regardless of sort direction.

> The list endpoint excludes archived items by default. The get-by-id endpoint returns archived items.

#### Response fields

Each board item in the `data` array (and in the detail response) includes:

| Field                    | Type             | Description                                                          |
| ------------------------ | ---------------- | -------------------------------------------------------------------- |
| `id`                     | string           | Unique identifier (`pbi_...`)                                        |
| `project_id`             | string           | ID of the project this item belongs to                               |
| `project_name`           | string           | Name of the project, for convenience                                 |
| `title`                  | string           | Short item title                                                     |
| `body`                   | string \| null   | Optional long-form markdown description                              |
| `status`                 | string           | `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`, or `DISMISSED`             |
| `priority`               | string \| null   | `URGENT`, `HIGH`, `NORMAL`, or `LOW`; `null` if unset                |
| `due_date`               | date \| null     | Calendar due date in ISO 8601 (`YYYY-MM-DD`); `null` if unset        |
| `assignee`               | string \| null   | Free-text assignee name (may differ from linked user's display name) |
| `assignee_user_id`       | UUID \| null     | Cogram user the item is assigned to, if matched                      |
| `source_meeting_id`      | string \| null   | Meeting this item was originated from, if any                        |
| `source_meeting_name`    | string \| null   | Name of the source meeting                                           |
| `meeting_series_uid`     | string \| null   | Identifier of the recurring meeting series, if applicable            |
| `archived`               | boolean          | Whether the item has been archived                                   |
| `created_at`             | datetime         | Creation timestamp in ISO 8601 format                                |
| `updated_at`             | datetime \| null | Last update timestamp                                                |
| `last_status_changed_at` | datetime \| null | Timestamp of the most recent history entry, or `null` if none        |

#### History (detail endpoint only)

`GET /v1/board-items/{id}` additionally returns a `history` array of timeline entries for the item, ordered oldest-first. Each entry represents a `BoardItemHistoryEntry` row, which may record a status transition, an assignee change, a priority shift, a due-date move, a comment, or any combination.

| Field                       | Type           | Description                                                                                                                 |
| --------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `id`                        | string         | Unique identifier (`bsc_...`)                                                                                               |
| `changed_at`                | datetime       | When the change was applied                                                                                                 |
| `changed_by_user_id`        | UUID \| null   | Cogram user who made the change; `null` for automated (LLM) changes                                                         |
| `changed_by_user_name`      | string \| null | Display name of the user, for convenience                                                                                   |
| `triggered_by_meeting_id`   | string \| null | Meeting that triggered this change, if any                                                                                  |
| `triggered_by_meeting_name` | string \| null | Name of the triggering meeting                                                                                              |
| `previous_status`           | string \| null | Status before the change (`null` on the initial creation entry, or on a non-status row)                                     |
| `new_status`                | string \| null | Status after the change. `null` on rows that aren't status transitions (assignee, priority, due-date, or pure-comment rows) |
| `previous_assignee`         | string \| null | Free-text assignee before the change                                                                                        |
| `new_assignee`              | string \| null | Free-text assignee after the change                                                                                         |
| `previous_assignee_user_id` | UUID \| null   | Linked user before the change                                                                                               |
| `new_assignee_user_id`      | UUID \| null   | Linked user after the change                                                                                                |
| `previous_priority`         | string \| null | Priority before the change (`URGENT`, `HIGH`, `NORMAL`, `LOW`, or `null`)                                                   |
| `new_priority`              | string \| null | Priority after the change                                                                                                   |
| `previous_due_date`         | date \| null   | Due date (ISO `YYYY-MM-DD`) before the change                                                                               |
| `new_due_date`              | date \| null   | Due date after the change                                                                                                   |
| `reason`                    | string \| null | LLM-authored justification (only set on reconciliation-triggered rows)                                                      |
| `comment`                   | string \| null | User-authored comment, if any                                                                                               |

To distinguish entry types from a single row:

* **Status transition**: `previous_status != new_status`
* **Assignee change**: `new_assignee != previous_assignee` (or the `*_user_id` equivalents)
* **Priority change**: `new_priority != previous_priority`
* **Due-date change**: `new_due_date != previous_due_date`
* **Comment**: `comment` is set and there is no other delta
* **Automated change**: `changed_by_user_id` is `null` and `triggered_by_meeting_id` is set

### Directory

Cogram builds a company for a firm and a contact for each person you correspond with. A contact only gets its company automatically when a transmittal goes out. A contact added in the directory, imported from a file, or created by inbound email keeps an empty company, even when your directory already holds the firm that owns the email domain.

These two endpoints close that gap. `GET` proposes a company for each such contact, and `POST` writes the proposals you accept.

| Method | Endpoint                              | Description                             |
| ------ | ------------------------------------- | --------------------------------------- |
| `GET`  | `/v1/directory/contact-company-links` | List proposed contact-to-company links  |
| `POST` | `/v1/directory/contact-company-links` | Apply proposed contact-to-company links |

`GET` returns the standard paginated envelope and writes nothing. A contact appears only when it has no company and a company in your directory lists the domain of its primary email. Archived contacts are not listed, and an archived company proposes nothing.

#### Response fields: `GET /v1/directory/contact-company-links`

| Field           | Type           | Description                                                                                       |
| --------------- | -------------- | ------------------------------------------------------------------------------------------------- |
| `contact_id`    | string         | The contact that has no company (`ctc_...`)                                                       |
| `primary_email` | string         | The contact's primary email address                                                               |
| `first_name`    | string \| null | Given name, if the contact has one                                                                |
| `last_name`     | string \| null | Family name, if the contact has one                                                               |
| `domain`        | string         | Domain of `primary_email`, lowercased                                                             |
| `company`       | object         | The company `POST` would link: `{ "id": "cmp_...", "name": "..." }`                               |
| `also_claiming` | array          | Other companies that list `domain`. Usually empty. When it is not, two companies carry one domain |

The proposal uses the oldest company that lists the domain, which is the same company a transmittal send would pick. Only the primary email decides. An address in a contact's additional emails is often a personal mailbox, so Cogram does not read a firm from it.

#### Request body: `POST /v1/directory/contact-company-links`

| Field         | Type                   | Description                                                                                                         |
| ------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `contact_ids` | array\[string] \| null | Optional. Apply only the proposals for these contacts. Omit to apply all. Unknown or already-linked ids are ignored |

#### Response fields: `POST /v1/directory/contact-company-links`

| Field     | Type    | Description                                                                                           |
| --------- | ------- | ----------------------------------------------------------------------------------------------------- |
| `applied` | array   | Every link the call wrote, as `{ "contact_id": "ctc_...", "company_id": "cmp_..." }`                  |
| `linked`  | integer | How many contacts were given a company                                                                |
| `skipped` | integer | Proposals the call did not write, because the contact gained a company between the read and the write |

Keep the `applied` list. It names the contacts that had no company before the call, so it is what you reverse a wrong run from — nothing else records that.

Read the proposal before you apply it. If a company in your directory lists a consumer domain such as `gmail.com`, every personal address in your organization is proposed for that company, and Cogram cannot tell that apart from a real firm. A contact that already has a company is never changed, so an assignment made by a person always wins.

```bash
# 1) Read the proposals
curl "https://api.cogram.com/v1/directory/contact-company-links?page=1&page_size=100" \
  -H "Authorization: Bearer YOUR_API_KEY"

# 2) Apply all of them
curl -X POST "https://api.cogram.com/v1/directory/contact-company-links" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

# 2b) Or apply only the contacts you reviewed
curl -X POST "https://api.cogram.com/v1/directory/contact-company-links" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contact_ids": ["ctc_...", "ctc_..."]}'
```

### Audit Log

The audit log lists every email and meeting that Cogram deleted from your organization, or whose content it removed, with the reason. Use it to check that deletions happen as your retention policy says. For example, a script can run each day or week and ask: "What was removed between date A and date B?"

| Method | Endpoint        | Description                                     |
| ------ | --------------- | ----------------------------------------------- |
| `GET`  | `/v1/audit-log` | List the deletions in a time window (paginated) |

The log starts with the release that introduced it. There is no backfill: Cogram does not add entries for earlier deletions. Entries are never changed or removed.

An entry never holds content: no subject, body, sender or meeting title. An email entry holds the email's `internet_message_id` (its RFC 5322 Message-ID). This is the identifier that your mail server and your archive also keep, so you can match a deleted email to its original outside Cogram.

#### Query parameters

| Parameter       | Type     | Default | Description                                                                                              |
| --------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------- |
| `occurred_from` | datetime | —       | Required. Start of the window, included. Must include a UTC offset, e.g. `2026-10-20T00:00:00Z`          |
| `occurred_to`   | datetime | —       | Required. End of the window, excluded. Must be later than `occurred_from`, and at most 366 days after it |
| `entity_type`   | string   | —       | Only entries about `EMAIL` or `MEETING` records                                                          |
| `action`        | string   | —       | Only entries with this action: `DELETED` or `CONTENT_REDACTED`                                           |
| `reason`        | string   | —       | Only entries with this reason (see the table below)                                                      |
| `page`          | integer  | `1`     | Page number (1-indexed)                                                                                  |
| `page_size`     | integer  | `50`    | Entries per page (max 100)                                                                               |

The window includes `occurred_from` and excludes `occurred_to`. Entries come oldest first, ordered by `occurred_at` and then `id`. URL-encode the timestamps: a `+` in a UTC offset such as `+02:00` must be sent as `%2B`, or the request fails with `422`.

An entry appears when its deletion is complete, which can be some time after its `occurred_at`. Query windows that ended at least 1 hour ago. When each run then starts its window where the last run ended, no entry is read twice and none is missed.

A window can span at most 366 days. A longer window returns `400` with error code `bad_request`: query a longer range in several windows. Poll in bounded windows, for example one day at a time, and page through each window. Do not page through a long range such as a whole year: each page re-reads the entries before it, so a deep page is slow. Each page counts toward your key's rate limit (see [Rate Limiting](#rate-limiting)). When a request returns `429`, wait for the `Retry-After` seconds, then retry.

#### Response fields

| Field             | Type           | Description                                                                                                                                                                                                                                                                                                 |
| ----------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`              | integer        | Unique identifier. It grows in the order entries were written                                                                                                                                                                                                                                               |
| `organization_id` | string         | Your organization                                                                                                                                                                                                                                                                                           |
| `entity_type`     | string         | `EMAIL` or `MEETING`                                                                                                                                                                                                                                                                                        |
| `entity_id`       | string         | The record's Cogram id (`eml...` or `mtg...`)                                                                                                                                                                                                                                                               |
| `action`          | string         | What happened to the record (see below)                                                                                                                                                                                                                                                                     |
| `reason`          | string         | Why it happened (see below)                                                                                                                                                                                                                                                                                 |
| `project_id`      | string \| null | The project the record was in when it was removed                                                                                                                                                                                                                                                           |
| `actor_user_id`   | UUID \| null   | The user who caused the removal. `null` when Cogram removed it, for example at retention expiry. It can name a user outside your organization, whom your API key cannot look up: a Cogram support user, or a guest from another organization who deleted their own meeting in a project you share with them |
| `occurred_at`     | datetime       | When the removal happened                                                                                                                                                                                                                                                                                   |
| `context`         | object \| null | Extra facts, by entity type (see below)                                                                                                                                                                                                                                                                     |

**Actions:**

* `DELETED`: the record is gone. A deleted email no longer exists in Cogram. A deleted meeting is no longer visible.
* `CONTENT_REDACTED`: the record's content is gone, but the record stays visible as a shell. Data retention does this to a meeting: its transcript, notes, insights, action items, photos and attachments are deleted, and its title, time and attendees stay.

**Reasons:**

| Reason              | Entity  | When                                                                                                                                                                                                                                                                                                                                                                         |
| ------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `USER_DELETE`       | both    | A user deleted the email or the meeting, in Cogram, Gmail or Outlook                                                                                                                                                                                                                                                                                                         |
| `BULK_UNFILE`       | email   | A user unfiled the email with the Outlook bulk unfile                                                                                                                                                                                                                                                                                                                        |
| `DUPLICATE_REMOVED` | email   | A refile or a project merge dropped this copy, because the other project already held the same email. The email still exists in Cogram                                                                                                                                                                                                                                       |
| `PROJECT_DELETED`   | email   | The email's project was deleted                                                                                                                                                                                                                                                                                                                                              |
| `MANUAL_PURGE`      | email   | A user purged the email under your email retention ruleset                                                                                                                                                                                                                                                                                                                   |
| `RETENTION_EXPIRY`  | both    | The retention period ended                                                                                                                                                                                                                                                                                                                                                   |
| `USER_OFFBOARDED`   | meeting | An admin or Cogram support removed the meeting's owner from Cogram. When an admin offboards an active user, Cogram deletes the user's scheduled meetings. When an admin removes a pending user, or Cogram support removes a user with all their data, Cogram deletes all of the user's meetings. `actor_user_id` can be a Cogram support user from outside your organization |
| `OTHER`             | meeting | Cogram deleted a meeting that had content for another reason. `context` names the reason                                                                                                                                                                                                                                                                                     |

A meeting gets an entry only when it had content. For example, a calendar invite canceled before the meeting took place gets no entry.

**`context` for an email:**

| Field                 | Type           | Description                                                                            |
| --------------------- | -------------- | -------------------------------------------------------------------------------------- |
| `internet_message_id` | string         | The email's Message-ID                                                                 |
| `sent_at`             | datetime       | When the email was sent                                                                |
| `retention_class`     | string \| null | `SHORT` or `ARCHIVE`, or `null` when your organization uses no email retention ruleset |
| `ruleset`             | string \| null | The email retention ruleset your organization used when the email was removed          |

`retention_class` is the class the email held when it was removed. `ruleset` is your organization's effective ruleset at that moment.

**`context` for a meeting:** `null`, except for reason `OTHER`, where it is `{ "meeting_deletion_reason": "<reason>" }`.

```bash
# Everything removed on 20 October 2026 (UTC)
curl "https://api.cogram.com/v1/audit-log?occurred_from=2026-10-20T00:00:00Z&occurred_to=2026-10-21T00:00:00Z&page_size=100" \
  -H "Authorization: Bearer $COGRAM_API_KEY"

# Only what retention expiry removed in that window
curl "https://api.cogram.com/v1/audit-log?occurred_from=2026-10-20T00:00:00Z&occurred_to=2026-10-21T00:00:00Z&reason=RETENTION_EXPIRY" \
  -H "Authorization: Bearer $COGRAM_API_KEY"
```

## Rate Limiting

API requests are rate limited on a per-key basis. The default limit is **1000 requests per minute**.

When you exceed the rate limit, the API returns a `429 Too Many Requests` response with a `Retry-After` header indicating when you can retry.

## Error Handling

The API uses standard HTTP status codes and returns errors in a consistent JSON format:

```json
{
  "error": "error_code",
  "message": "Human-readable description"
}
```

### Common Error Codes

| HTTP Status | Error Code                | Description                                                        |
| ----------- | ------------------------- | ------------------------------------------------------------------ |
| 400         | `range_too_large`         | Org-wide Data Export exceeded the 6-month cap (or used `all_time`) |
| 401         | `api_key_missing`         | No API key provided                                                |
| 401         | `api_key_invalid`         | API key not found or incorrect                                     |
| 401         | `api_key_expired`         | API key has expired                                                |
| 403         | `forbidden`               | The API key's creator lacks permission for this action             |
| 404         | `not_found`               | Resource does not exist                                            |
| 409         | `conflict`                | Resource already exists                                            |
| 409         | `data_export_in_progress` | Org already has an active Data Export; poll its id and retry       |
| 422         | `validation_error`        | Invalid request body                                               |
| 429         | `rate_limit_exceeded`     | Too many requests                                                  |

This is not an exhaustive list. For all possible error responses per each endpoint and all error codes, see the interactive documentation at [api.cogram.com/v1/docs](https://api.cogram.com/v1/docs).

## Pagination

List endpoints support pagination using query parameters:

| Parameter   | Default | Max | Description              |
| ----------- | ------- | --- | ------------------------ |
| `page`      | 1       | -   | Page number (1-indexed)  |
| `page_size` | 50      | 100 | Number of items per page |

Example:

```bash
curl "https://api.cogram.com/v1/projects?page=2&page_size=25" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Paginated response format:

```json
{
  "data": [...],
  "total": 150,
  "page": 2,
  "page_size": 25
}
```

## API Reference

For detailed endpoint documentation, request/response schemas, error codes and an interactive API explorer, visit:

[**api.cogram.com/v1/docs**](https://api.cogram.com/v1/docs)

## Security Best Practices

* **Keep your API keys secret** - Never expose them in client-side code or public repositories
* **Use environment variables** - Store API keys in environment variables, not in code
* **Rotate keys regularly** - Create new keys and revoke old ones periodically
* **Use descriptive names** - Name your keys by their purpose to track usage
* **Revoke unused keys** - Delete keys that are no longer needed

## Support

Questions or issues with the API? Email <support@cogram.com> or use the in-app Help menu.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.cogram.com/integrations/cogram-api.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
