# Cogram API

The Cogram API allows you to programmatically integrate Cogram with your own systems and workflows. Use it to automate project management, synchronize user data, and build custom integrations.

## Overview

Cogram provides a RESTful API served from a dedicated subdomain (`api.cogram.com`). It uses API key authentication and provides stable, versioned endpoints designed 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 **Settings** > **Administration** > **Integrations** > **API Keys** 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. See the [interactive API docs](https://api.cogram.com/v1/docs) for full request/response schemas.

### 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        |

### 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.

### 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](/administration/data-export-package-layout.md). For the UI equivalent of these endpoints, see [Data Exports](/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`, the rendered `meeting.docx` (when your org has a default template — see [Data Exports → templates](/administration/data-exports.md#choosing-a-meeting-template-before-exporting)), and `transcript.json` (when the meeting was transcribed). Photos and uploaded meeting attachments are included; audio is intentionally kept inside Cogram.

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

| Field         | Type           | Description                                                                                                                       |
| ------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `project_id`  | string \| null | Optional. Scope the export to a single project. Org-wide exports (when `null`) 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. |

#### 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_id`               | string \| null   | Set if the export is scoped to a single project                                                                                 |
| `time_filter`              | object           | Echo of the requested filter                                                                                                    |
| `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"}}'

# 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 — 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](/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_id`) 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`).

> **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` past the build-deadline cap (\~6 hours) to `failed` so the per-org concurrency slot doesn't stay blocked. Retry by creating a new export.

### 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

## 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                                                |
| 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

Contact support through the Cogram app if you have questions or need help with the Cogram API.


---

# 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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.cogram.com/administration/cogram-api.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
