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 compatibilityInteractive documentation - Explore and test endpoints directly at 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
Go to Organization Settings → Integrations → API Keys in the Cogram app.
Click Create API Key.
Give your key a descriptive name and optionally set an expiration date.
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:
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:
Available Endpoints
Projects
Manage projects within your organization.
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 for full request/response schemas.
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 below.
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
drawingsandmeetingsstay 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.
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.
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_atyet, so it is matched oncreated_atinstead. 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(orcreated_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 simply absent from later responses. If that matters, reconcile against a full listing on a slower schedule.
updated_attracks 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=countsabove.
Project Members
Manage project membership and roles.
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; the API exposes the same CRUD.
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 for full request/response schemas.
Users
Manage organization members.
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. For the UI equivalent of these endpoints, see Data Exports.
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.
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), 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
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.
Response fields
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
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:
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
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.
Date ranges are inclusive of the
endtimestamp. The backend filter is<=on theendvalue you supply. If you want "everything modified through May 31", sendend=2026-06-01T00:00:00Z(the next-day UTC midnight). Sending2026-05-31T00:00:00Zwould 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 rejectall_time. Project-scoped exports have no time-range limit. A violation returns400with"error": "range_too_large"(typedErrorCode).
Project scope: A project id your organization doesn't own returns
404with"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-elementproject_ids. Sending both fields returns422. New integrations should useproject_ids.
Concurrency: At most one active export (
pending,running, orcancelling) per organization, enforced both at the application layer and by a Postgres partial unique index. A secondPOST /v1/data-exportswhile an active one exists returns409with"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:
pendingexports cancel immediately.runningexports transition tocancelling; the worker stops between zip parts and the next status read reflects the terminal state. Terminal statuses (completed,failed,cancelled,expired) return409 conflict.
Stuck-row recovery: If a worker dies hard mid-build (rare), a periodic sweep transitions rows stuck in
running/cancellingpast the build-deadline cap (~6 hours) tofailedso the per-org concurrency slot doesn't stay blocked. Retry by creating a new export.
Backup Runs
Report each backup run performed by the Cogram 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 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 → 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 Cogram connector — it reports its own runs.
POST
/v1/backup-runs
Report that a backup started or finished
Request body: POST /v1/backup-runs
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
Only successful runs count as healthy. A
failedrun 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 → Backups. An empty recipient list turns alerting off.
Action Items
Retrieve action items extracted from meetings within your organization.
GET
/v1/action-items
List action items
Filtering and search
The list endpoint supports the following query parameters:
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. Unknownproject_idormeeting_idvalues 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:
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.
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:
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_idormeeting_idvalues 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:
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.
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_statusAssignee change:
new_assignee != previous_assignee(or the*_user_idequivalents)Priority change:
new_priority != previous_priorityDue-date change:
new_due_date != previous_due_dateComment:
commentis set and there is no other deltaAutomated change:
changed_by_user_idisnullandtriggered_by_meeting_idis 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:
Common Error Codes
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.
Pagination
List endpoints support pagination using query parameters:
page
1
-
Page number (1-indexed)
page_size
50
100
Number of items per page
Example:
Paginated response format:
API Reference
For detailed endpoint documentation, request/response schemas, error codes and an interactive API explorer, visit:
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.
Last updated
Was this helpful?