Manifst API Documentation

Welcome to the Manifst REST API v1 documentation. API access is included starting from the Portfolio plan, and is billed based on the plan of the account owning the key. Accounts subscribed prior to the current pricing model retain their access.

Postman Collection
36 ready-to-use requests. Fill in baseUrl and apiKey in the collection variables.
Download

Introduction

Base URL:

https://manifst.net/api/v1/

All responses are returned in JSON format with the following structure:

// Success
{
  "data": { ... },        // object or array
  "meta": { "total": 42 } // present on list responses
}

// Error
{
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "message": "This API key does not have the required scope: 'projects:write'."
  }
}

Authentication

All requests must include your API key in the Authorization HTTP header:

Authorization: Bearer mfst_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Never expose your API key on the client side (frontend JS, mobile apps). Use it strictly server-side.

Generate a key

Navigate to Settings → API Keys in your Manifst dashboard. API keys are shown only once upon creation.

Request example

curl https://manifst.net/api/v1/projects \
  -H "Authorization: Bearer mfst_live_xxxxx"

Rate limiting

WindowLimit
Per hour1,000 requests
Per month10,000 requests

Every response includes these HTTP headers:

X-RateLimit-Limit-Hour: 1000
X-RateLimit-Remaining-Hour: 847
X-RateLimit-Limit-Month: 10000
X-RateLimit-Remaining-Month: 9231
X-RateLimit-Reset-Hour: 1711234567

When quota is exceeded, the API returns HTTP 429 with a Retry-After header (seconds until reset).

Pagination & incremental refresh

Large collection endpoints : /tickets and /time-entries : support optional pagination and incremental filtering. Without these parameters, responses remain identical to legacy versions (backward compatible).

ParameterDescription
pagePage number (default 1). Enables pagination.
per_pagePage size (default 100, max 500). Enables pagination.
updated_sinceReturns only items updated since this date (YYYY-MM-DD or ISO datetime). Ideal for Power BI incremental refresh.

The meta object always returns the true total count before pagination:

GET /api/v1/tickets?project=abc-123&page=2&per_page=50&updated_since=2026-07-01

{
  "data": [ ... ],
  "meta": { "total": 320, "count": 50, "page": 2, "per_page": 50 }
}

Error codes

CodeHTTPDescription
MISSING_API_KEY401Missing Authorization header
INVALID_API_KEY401Key not found
REVOKED_API_KEY401Key revoked
EXPIRED_API_KEY401Key expired
PLAN_REQUIRED403Portfolio plan required
ACCOUNT_INACTIVE403Account inactive
IP_NOT_ALLOWED403IP address not whitelisted
INSUFFICIENT_SCOPE403Missing scope on the API key
RATE_LIMIT_EXCEEDED429Quota limit reached
NOT_FOUND404Resource not found
EXTERNAL_ACCESS_DENIED403A project you neither own nor administer, whose owner has not opened AI assistant or API access to your role (use_ai_api permission, Settings → Access → AI assistants & API; no role by default). Such projects are also left out of GET /projects, the portfolio, metrics and notifications.
FORBIDDEN403Forbidden: project you are not a member of, or action your role in the project does not allow (a key has its owner’s rights, no more)
VALIDATION_ERROR422Missing or invalid parameter
METHOD_NOT_ALLOWED405HTTP method not supported

Scopes

Each API key is assigned a set of scopes defining its permissions. Available scopes:

ScopeDescription
projects:readRead projects and project members
projects:writeCreate, edit, and delete projects
super-epics:readRead super-epics
super-epics:writeCreate, edit, and delete super-epics
epics:readRead epics
epics:writeCreate, edit, and delete epics
sprints:readRead sprints
sprints:writeCreate and edit sprints
tickets:readRead tickets
tickets:writeCreate, edit, and delete tickets
comments:readRead ticket comments
comments:writeCreate, edit, and delete comments
team:readRead project team members
time:readRead time entries
time:writeLog time entries
statuses:readRead project statuses
finance:readRead finance / EVM indicators (project & portfolio)
roadmap:readRead roadmap dependencies and milestones
roadmap:writeCreate, update, delete roadmap milestones and dependencies
governance:readRead the risk register and the RACI matrix
governance:writeCreate, update, delete risks and the RACI matrix
notifications:readRead the key holder's notifications
notifications:writeMark as read, delete the key holder's notifications
docs:readRead MyDoc spaces, pages and files
docs:writeCreate, update, delete MyDoc pages

Project reference data follows the existing scopes: tags use tickets:read / tickets:write, ticket types use projects:read / projects:write, transition rules use projects:read, and a ticket's history uses tickets:read. Search requires projects:read and only searches the types whose read scope the key holds.

Projects

GET /api/v1/projects List projects

Required scope: projects:read

curl /api/v1/projects -H "Authorization: Bearer mfst_live_xxx"

// Response 200
{
  "data": [
    {
      "uuid": "abc-123",
      "name": "My Project",
      "description": "Description",
      "color": "#6366f1",
      "default_dod": ["Code review done", "Tests passing"],
      "contingency_pct": 10.0,
      "budget": 350000.00,
      "user_role": "owner",
      "member_count": 5,
      "created_at": "2026-01-15 10:00:00",
      "updated_at": "2026-03-01 14:22:00"
    }
  ],
  "meta": { "total": 1 }
}
GET /api/v1/projects/{uuid} Project details

Required scope: projects:read

curl /api/v1/projects/abc-123 -H "Authorization: Bearer mfst_live_xxx"
GET /api/v1/projects/{uuid}/members Project members

Required scope: team:read

POST /api/v1/projects Create project

Required scope: projects:write

Project creation automatically initializes the 6 default statuses (Backlog, To Do, In Progress, In Review, Done, Blocked) and 2 default ticket types (Story, Bug).

Body

{
  "name": "My Project",                    // required, 1 - 80 chars
  "description": "...",                    // optional
  "color": "#6366f1",                      // optional, hex #rrggbb
  "default_dod": ["Tests OK", "Code review"], // optional : Definition of Done (array)
  "contingency_pct": 10.00,                // optional : financial reserve, 0 - 100% (default: 10)
  "budget": 350000.00                      // optional : BAC / budget at completion (≥ 0, null to clear)
}

Response 201

{
  "data": {
    "uuid": "...",
    "name": "My Project",
    "description": null,
    "color": "#6366f1",
    "default_dod": null,
    "contingency_pct": 10.0,
    "budget": 350000.00,
    "user_role": "owner",
    "created_at": "2026-03-24 20:00:00",
    "updated_at": "2026-03-24 20:00:00"
  }
}
PUT /api/v1/projects/{uuid} Update project

Required scope: projects:write : owner only

Body

{
  "name": "New name",                    // optional
  "description": "...",                  // optional
  "color": "#f59e0b",                    // optional
  "default_dod": ["item 1", "item 2"],    // optional : null or [] to clear
  "contingency_pct": 15.00,               // optional : 0 - 100%
  "budget": 420000.00                     // optional : BAC (≥ 0, null to clear)
}

Response 200

{
  "data": {
    "uuid": "...",
    "name": "New name",
    "description": null,
    "color": "#f59e0b",
    "default_dod": ["item 1", "item 2"],
    "contingency_pct": 15.0,
    "budget": 420000.00,
    "user_role": "owner",
    "created_at": "2026-03-24 20:00:00",
    "updated_at": "2026-03-24 20:00:00"
  }
}
DELETE /api/v1/projects/{uuid} Delete project

Required scope: projects:write : owner only

// Response 200
{ "data": { "deleted": true } }

Super-epics

GET /api/v1/super-epics?project={uuid} List super-epics

Required scope: super-epics:read

GET /api/v1/super-epics/{uuid} Details

Required scope: super-epics:read

POST /api/v1/super-epics Create

Required scope: super-epics:write

Body

{
  "project_uuid": "abc-123",
  "title": "Title",
  "description": "...",  // optional
  "status": "backlog",   // optional : backlog | in_progress | done (default: backlog)
  "tags": ["tag-uuid-1"] // optional : array of tag UUIDs
}

Response 201

{
  "data": {
    "uuid": "...",
    "project_id": 1,
    "title": "Title",
    "description": null,
    "status": "backlog",
    "created_at": "2026-03-24 20:00:00",
    "updated_at": "2026-03-24 20:00:00",
    "tags": [{ "uuid": "tag-uuid", "name": "bug" }]
  }
}
PUT /api/v1/super-epics/{uuid} Update

Required scope: super-epics:write

Body

{
  "title": "New title",      // optional
  "description": "...",      // optional
  "status": "in_progress",   // optional : backlog | in_progress | done
  "tags": ["tag-uuid-1"]     // optional : replaces all tags
}

Response 200

{
  "data": {
    "uuid": "...",
    "project_id": 1,
    "title": "New title",
    "description": null,
    "status": "in_progress",
    "created_at": "2026-03-24 20:00:00",
    "updated_at": "2026-03-24 20:00:00",
    "tags": [{ "uuid": "tag-uuid", "name": "bug" }]
  }
}
DELETE /api/v1/super-epics/{uuid} Delete

Required scope: super-epics:write : linked epics are unlinked, not deleted.

Epics

GET /api/v1/epics?project={uuid} List epics

Required scope: epics:read

GET /api/v1/epics/{uuid} Details

Required scope: epics:read

POST /api/v1/epics Create

Required scope: epics:write

Body

{
  "project_uuid": "abc-123",
  "title": "Title",
  "description": "...",            // optional
  "super_epic_uuid": "se-uuid",    // optional
  "status": "backlog",             // optional : backlog | todo | in_progress | in_review | done | blocked (default: backlog)
  "tags": ["tag-uuid-1"]           // optional : array of tag UUIDs
}

Response 201

{
  "data": {
    "uuid": "...",
    "project_id": 1,
    "super_epic_id": null,
    "super_epic_uuid": null,
    "title": "Title",
    "description": null,
    "status": "backlog",
    "created_at": "2026-03-24 20:00:00",
    "updated_at": "2026-03-24 20:00:00",
    "tags": [{ "uuid": "tag-uuid", "name": "bug" }]
  }
}
PUT /api/v1/epics/{uuid} Update

Required scope: epics:write

Body

{
  "title": "...",           // optional
  "description": "...",     // optional
  "super_epic_uuid": null,  // optional : uuid or null to unlink
  "status": "done",         // optional : backlog | todo | in_progress | in_review | done | blocked
  "tags": ["tag-uuid-1"]    // optional : replaces all tags
}

Response 200

{
  "data": {
    "uuid": "...",
    "project_id": 1,
    "super_epic_id": null,
    "super_epic_uuid": null,
    "title": "...",
    "description": null,
    "status": "done",
    "created_at": "2026-03-24 20:00:00",
    "updated_at": "2026-03-24 20:00:00",
    "tags": [{ "uuid": "tag-uuid", "name": "bug" }]
  }
}
DELETE /api/v1/epics/{uuid} Delete

Required scope: epics:write : linked tickets are unlinked.

Sprints

GET /api/v1/sprints?project={uuid} List sprints

Required scope: sprints:read

GET /api/v1/sprints/{uuid} Details

Required scope: sprints:read

POST /api/v1/sprints Create

Required scope: sprints:write

Body

{
  "project_uuid": "abc-123",
  "title": "Sprint 1",
  "goal": "...",               // optional
  "status": "planning",        // optional : planning | active | completed (default: planning)
  "start_date": "2026-04-01",  // optional
  "end_date": "2026-04-14",    // optional
  "dod": ["item 1", "item 2"]   // optional : Sprint Definition of Done (array)
}

Response 201

{
  "data": {
    "uuid": "...",
    "project_id": 1,
    "title": "Sprint 1",
    "goal": null,
    "status": "planning",
    "start_date": "2026-04-01",
    "end_date": "2026-04-14",
    "dod": null,
    "created_at": "2026-03-24 20:00:00"
  }
}
PUT /api/v1/sprints/{uuid} Update

Required scope: sprints:write

Body

{
  "title": "Sprint 1",         // optional
  "status": "active",          // optional : planning | active | completed
  "goal": "...",               // optional
  "start_date": "2026-04-01",  // optional
  "end_date": "2026-04-14",    // optional
  "dod": ["item 1"]            // optional : null or [] to clear
}

Response 200

{
  "data": {
    "uuid": "...",
    "project_id": 1,
    "title": "Sprint 1",
    "goal": null,
    "status": "active",
    "start_date": "2026-04-01",
    "end_date": "2026-04-14",
    "dod": ["item 1"],
    "created_at": "2026-03-24 20:00:00"
  }
}

Tickets

GET /api/v1/tickets?project={uuid} List tickets

Required scope: tickets:read

Optional query filters: ?sprint={uuid} and ?epic={uuid}

GET /api/v1/tickets/{uuid} Details (with AC, dependencies, and subtasks)

Required scope: tickets:read

{
  "data": {
    "uuid": "...", "title": "...", "description": "...",
    "priority": "haute", "status": "todo", "story_points": 5,
    "epic_id": 1, "sprint_id": 2, "assignee": "Alice Martin",
    "project_uuid": "abc-123",    // project UUID : join key
    "epic_uuid": "epic-uuid",     // null when the story has no epic
    "sprint_uuid": "sprint-uuid", // null when the story is in no sprint
    "acceptance_criteria": ["Criteria 1", "Criteria 2"],
    "dependencies": [{ "uuid": "...", "type": "story", "title": "Other story" }], // tickets to finish BEFORE this one (blockers)
    "blocked_by": [{ "uuid": "...", "number": 12, "key": "US-12", "title": "Other story", "status": "todo" }],
    "blocks": [], "duplicates": [], "duplicated_by": [], "relates": [], // see Ticket links
    "tags": [{ "uuid": "tag-uuid", "name": "bug" }],
    "subtasks": [
      { "uuid": "...", "title": "Technical task", "status": "todo",
        "sort_order": 1, "assignee_id": 3, "assignee_name": "Alice Martin",
        "created_at": "2026-03-25 10:00:00", "updated_at": "2026-03-25 10:00:00" }
    ],
    "subtasks_total": 1,
    "subtasks_done": 0
  }
}
POST /api/v1/tickets Create

Required scope: tickets:write

Body

{
  "project_uuid": "abc-123",
  "title": "As a user...",
  "description": "...",
  "priority": "haute",         // critique | haute | normale | faible
  "status": "backlog",         // backlog | todo | in_progress | in_review | done | blocked
  "story_points": 5,           // optional
  "epic_uuid": "epic-uuid",    // optional
  "sprint_uuid": "sprint-uuid",// optional
  "assignee_id": 3,            // optional
  "acceptance_criteria": ["Criteria 1"], // optional
  "dependencies": ["us-uuid-1"],        // optional
  "tags": ["tag-uuid-1"]                // optional : array of tag UUIDs
}

Response 201

{
  "data": {
    "uuid": "...",
    "project_id": 1,
    "epic_id": null,
    "sprint_id": null,
    "assignee_id": null,
    "assignee": null,
    "title": "As a user...",
    "description": null,
    "priority": "haute",
    "status": "backlog",
    "story_points": 5,
    "created_at": "2026-03-24 20:00:00",
    "updated_at": "2026-03-24 20:00:00",
    "acceptance_criteria": ["Criteria 1"],
    "dependencies": [],
    "tags": [{ "uuid": "tag-uuid", "name": "bug" }]
  }
}
PUT /api/v1/tickets/{uuid} Update

Required scope: tickets:write

Body

{
  "title": "...",               // optional
  "description": "...",         // optional
  "priority": "haute",          // optional : critique | haute | normale | faible
  "status": "in_progress",      // optional : backlog | todo | in_progress | in_review | done | blocked
  "story_points": 8,            // optional
  "epic_uuid": "epic-uuid",     // optional : uuid or null to unlink
  "sprint_uuid": "sprint-uuid", // optional : uuid or null to unlink
  "assignee_id": 3,             // optional : id or null
  "acceptance_criteria": ["Criteria 1"], // optional : replaces existing criteria
  "dependencies": ["us-uuid-1"],        // optional : replaces the tickets this one depends on. Loops refused: 409
  "tags": ["tag-uuid-1"]                // optional : replaces all tags
}

Response 200

{
  "data": {
    "uuid": "...",
    "project_id": 1,
    "epic_id": null,
    "sprint_id": null,
    "assignee_id": null,
    "assignee": null,
    "title": "...",
    "description": null,
    "priority": "haute",
    "status": "in_progress",
    "story_points": 8,
    "created_at": "2026-03-24 20:00:00",
    "updated_at": "2026-03-24 20:00:00",
    "acceptance_criteria": ["Criteria 1"],
    "dependencies": [],
    "tags": [{ "uuid": "tag-uuid", "name": "bug" }]
  }
}
DELETE /api/v1/tickets/{uuid} Delete

Required scope: tickets:write

Five relations, read from the ticket in the URL: blocked_by (the other ticket must be finished first), blocks (the other ticket waits for this one), duplicates (this one duplicates the other, which is the original), duplicated_by and relates (no direction). A link is saved immediately and shows on both tickets.

Refused: a link that would create a blocking loop (409 CONFLICT), two tickets duplicating each other (409), a ticket linked to itself or an unknown kind (422), a ticket from another project (404).

GET /api/v1/tickets/{uuid}/links Links of a ticket

Required scope: tickets:read

{
  "data": {
    "blocked_by": [{ "uuid": "...", "type": "story", "number": 12, "key": "US-12", "title": "Prerequisite", "status": "todo" }],
    "blocks": [],
    "duplicates": [],
    "duplicated_by": [],
    "relates": []
  }
}
POST /api/v1/tickets/{uuid}/links Link two tickets

Required scope: tickets:write · edit_us permission in the project

Body

{
  "other_uuid": "us-uuid",   // required: ticket of the same project
  "kind": "duplicates"       // required: blocked_by | blocks | duplicates | duplicated_by | relates
}

Response 201: the five lists, up to date, as with GET. Repeating an existing link does nothing.

DELETE /api/v1/tickets/{uuid}/links Remove a link

Required scope: tickets:write · edit_us permission in the project

Same body as POST (or ?other_uuid=…&kind=… in the query). Response 200: the five lists, up to date.

Ticket history

GET /api/v1/tickets/{uuid}/history Who changed what, and from where

Required scope: tickets:read

Changes from newest to oldest. A change groups the fields modified by a single action. source is app, api, mcp, import, or the tracker / Git provider that pushed the change. Optional pagination (?page, ?per_page, 200 max).

{
  "data": [
    {
      "change_id": "c0a8…",
      "created_at": "2026-10-27 14:02:11",
      "source": "api",
      "actor": { "user_id": 42, "uuid": "user-uuid", "name": "Camille Martin" },
      "items": [
        { "field": "status", "old_value": "todo", "new_value": "in_progress",
          "old_label": "To do", "new_label": "In progress" }
      ]
    }
  ],
  "meta": { "total": 1, "count": 1 }
}

Subtasks

Subtasks have fixed statuses (todo, in_progress, done), independent of project custom statuses. They serve as a technical checklist linked to a ticket.

GET /api/v1/subtasks?ticket_uuid={uuid} List subtasks for a ticket

Required scope: tickets:read

{
  "data": [
    {
      "uuid": "...",
      "title": "Technical task",
      "status": "todo",           // todo | in_progress | done
      "sort_order": 1,
      "assignee_id": 3,
      "assignee_name": "Alice Martin",
      "created_at": "2026-03-25 10:00:00",
      "updated_at": "2026-03-25 10:00:00"
    }
  ],
  "meta": { "total": 1 }
}
POST /api/v1/subtasks Create subtask

Required scope: tickets:write

Body

{
  "ticket_uuid": "us-uuid",        // required
  "title": "Technical task",   // required
  "status": "todo",            // optional : todo | in_progress | done (default: todo)
  "assignee_id": 3             // optional
}

Response 201

{
  "data": {
    "uuid": "...", "title": "Technical task", "status": "todo",
    "sort_order": 1, "assignee_id": null, "assignee_name": null,
    "created_at": "...", "updated_at": "..."
  }
}
PUT /api/v1/subtasks/{uuid} Update

Required scope: tickets:write

Body

{
  "title": "...",          // optional
  "status": "in_progress", // optional : todo | in_progress | done
  "assignee_id": 3,        // optional : id or null to unassign
  "sort_order": 2          // optional
}

Response 200

{
  "data": {
    "uuid": "...", "title": "...", "status": "in_progress",
    "sort_order": 2, "assignee_id": 3, "assignee_name": "Alice Martin",
    "created_at": "...", "updated_at": "..."
  }
}
DELETE /api/v1/subtasks/{uuid} Delete

Required scope: tickets:write

Comments

Comments are attached to a ticket. Only the author (or an administrator) can edit or delete their own comments. Content is sanitized HTML (scripts and JS event handlers removed).

GET /api/v1/comments?ticket_uuid={uuid} List comments for a ticket

Required scope: comments:read

ParameterTypeDescription
usstringTicket UUID : required

Response 200

{
  "data": [
    {
      "uuid": "...",
      "author_id": 3,
      "author_name": "Alice Martin",
      "author_initials": "AM",
      "content": "<p>Comment content</p>",
      "mentions": [4, 7],
      "ticket_uuid": "...",
      "created_at": "2026-04-02 10:00:00",
      "updated_at": "2026-04-02 10:00:00"
    }
  ],
  "meta": { "total": 1 }
}
GET /api/v1/comments/{uuid} Comment details

Required scope: comments:read

POST /api/v1/comments Create comment

Required scope: comments:write

Body

{
  "ticket_uuid":  "...",                     // required : Ticket UUID
  "content":  "<p>My comment</p>",       // required : Sanitized HTML
  "mentions": [4, 7]                     // optional : Mentioned user IDs
}

Response 201

{
  "data": {
    "uuid": "...",
    "author_id": 3,
    "author_name": "Alice Martin",
    "author_initials": "AM",
    "content": "<p>My comment</p>",
    "mentions": [4],
    "ticket_uuid": "...",
    "created_at": "2026-04-02 10:00:00",
    "updated_at": "2026-04-02 10:00:00"
  }
}
PUT /api/v1/comments/{uuid} Update comment

Required scope: comments:write : author or admin only.

Body

{
  "content":  "<p>Updated content</p>",  // optional
  "mentions": [4, 7]                       // optional : replaces full mention list
}
DELETE /api/v1/comments/{uuid} Delete comment

Required scope: comments:write : author or admin only.

{ "data": { "deleted": true } }

Team

GET /api/v1/team?project={uuid} Project team members

Required scope: team:read

{
  "data": [
    {
      "uuid": "...", "firstname": "Alice", "lastname": "Martin",
      "email": "alice@example.com", "scrum_role": "dev",
      "project_role": "member", "status": "active",
      "tjm": 550.00, "service": "Delivery"
    }
  ],
  "meta": { "total": 4 }
}

tjm = average daily rate (cost/day) of the member on this project, used as the basis for financial indicators; null if unspecified.

Backlog in one transaction

POST /api/v1/backlog Create super-epic, epics and tickets all-or-nothing

Required scopes: tickets:write, plus epics:write if the call creates epics and super-epics:write if it creates a super-epic. The role needs the matching creation rights in the project.

The Launchpad engine, opened to the API: everything is created in a single transaction. At the slightest problem (unknown ticket type, story points off the planning poker scale 0, 1, 2, 3, 5, 8, 13, 21, UUID not in the project, empty title…), nothing is created and the 422 response lists every problem. Tickets land in the backlog status, outside any sprint. Limits: 40 epics, 300 tickets and 30 criteria per ticket per call.

Each epic is either existing (epic_uuid: the tickets are added to it) or new (title): it is then attached to the super-epic created by the call, if any, or to the existing super-epic given in super_epic_uuid.

Idempotency: send a commit_token (UUID). Sending the same token again creates nothing twice and answers 200 with "idempotent": true. No sync to Jira, Azure Boards or Linear (as with Launchpad): tracker_not_synced flags it when the project is linked to a tracker.

Body

{
  "project_uuid": "abc-123",                 // required
  "commit_token": "3f2b…-uuid",              // optional, recommended
  "super_epic": { "title": "Customer portal", "description": "<p>…</p>" },   // optional
  "epics": [
    { "title": "Authentication",
      "tickets": [
        { "title": "Sign in by e-mail", "type": "story", "priority": "haute",
          "story_points": 3, "description": "<p>…</p>",
          "acceptance_criteria": ["Magic link sent", "Expires after 15 min"] }
      ] },
    { "epic_uuid": "existing-epic-uuid",
      "tickets": [ { "title": "SSO sign-in", "story_points": 8 } ] }
  ]
}

Response 201

{
  "data": {
    "super_epic_uuid": "…",
    "n_epics": 1,
    "n_tickets": 2,
    "n_epics_touched": 2,
    "created": {
      "super_epic": { "uuid": "…", "title": "Customer portal" },
      "epics": [
        { "uuid": "…", "title": "Authentication", "tickets": [ { "uuid": "…", "title": "Sign in by e-mail" } ] },
        { "uuid": "existing-epic-uuid", "existing": true, "tickets": [ { "uuid": "…", "title": "SSO sign-in" } ] }
      ]
    },
    "commit_token": "3f2b…-uuid"
  }
}

Response 422

{ "error": { "code": "VALIDATION_ERROR",
  "message": "2 problem(s), nothing was created: epics[0].tickets[1].type: unknown type 'task' (valid: story, bug); epics[1].epic_uuid: no epic with this UUID in the project." } }

Time entries

GET /api/v1/time-entries?project={uuid} List time entries

Required scope: time:read

Optional filters: ?story={uuid} for a specific story, ?user={user_id} for one member’s entries, ?from=YYYY-MM-DD and ?to=YYYY-MM-DD for the day of the work (inclusive).

POST /api/v1/time-entries Log time

Required scope: time:write

Body

{
  "ticket_uuid": "story-uuid",      // required
  "duration_minutes": 90,       // required
  "logged_at": "2026-03-24",    // required, format YYYY-MM-DD
  "comment": "Code review"      // optional
}

Response 201

{
  "data": {
    "uuid": "...",
    "ticket_id": 12,
    "user_id": 3,
    "user_name": "Alice Martin",
    "ticket_uuid": "...",
    "ticket_title": "As a user...",
    "duration_minutes": 90,
    "comment": "Code review",
    "logged_at": "2026-03-24 00:00:00",
    "created_at": "2026-03-24 20:00:00"
  }
}

Statuses

GET /api/v1/statuses?project={uuid} List project statuses

Required scope: statuses:read

Returns all project statuses (default and custom), sorted by sort_order.

Response 200

[
  {
    "key":        "backlog",
    "label":      "Backlog",
    "color":      "#64748b",
    "bg_color":   "#f1f5f9",
    "sort_order": 1,
    "is_done":    false,
    "is_default": true
  },
  {
    "key":        "in_validation",
    "label":      "In Validation",
    "color":      "#7c3aed",
    "bg_color":   "#f5f3ff",
    "sort_order": 7,
    "is_done":    false,
    "is_default": false
  }
]

Tags

Reusable labels of a project. To tag a ticket, an epic or a super-epic, send the list of tag UUIDs in the tags field of that resource. Writing: project owner / admin, or a role holding the manage_tags permission (every role by default).

GET /api/v1/tags?project={uuid} List tags

Required scope: tickets:read · GET /api/v1/tags/{uuid} for a single tag

{
  "data": [ { "uuid": "tag-uuid", "name": "Front-end", "slug": "front-end", "usage_count": 12 } ],
  "meta": { "total": 1 }
}
POST /api/v1/tags Create a tag

Required scope: tickets:write

{ "project_uuid": "proj-uuid", "name": "Front-end" }   // 50 characters max

Response 201: the tag. 409 CONFLICT if a tag with the same name (case and punctuation ignored) already exists.

PUT /api/v1/tags/{uuid} Rename a tag

Required scope: tickets:write · body { "name": "…" }

DELETE /api/v1/tags/{uuid} Delete a tag

Required scope: tickets:write · the tag is removed from every ticket, epic and super-epic that carried it.

Ticket types

A type's slug is the value of a ticket's type field; its abbreviation prefixes the ticket key (US-42). Writing: project owner / admin, or a role holding the manage_ticket_types permission.

GET /api/v1/ticket-types?project={uuid} List types

Required scope: projects:read

{
  "data": [
    { "uuid": "…", "name": "User Story", "slug": "story", "abbreviation": "US", "is_default": true, "sort_order": 1 }
  ],
  "meta": { "total": 1 }
}
POST /api/v1/ticket-types Create a type

Required scope: projects:write

{ "project_uuid": "proj-uuid", "name": "Spike", "abbreviation": "SPK" }

The abbreviation is upper-cased, 10 characters max, unique in the project (409); SE, EP and SP are reserved (422).

PUT /api/v1/ticket-types/{uuid} Update name or abbreviation

Required scope: projects:write · partial body { "name": "…", "abbreviation": "…" }. The slug does not change.

DELETE /api/v1/ticket-types/{uuid} Delete a type

Required scope: projects:write · tickets of this type become story. A default type cannot be deleted (403).

Transition rules

GET /api/v1/workflows?project={uuid} Project rules and the moves forbidden to you

Required scope: projects:read · &entity_type=ticket|epic|super_epic for a single entity type

forbidden_transitions lists, for the key holder, the status changes the rules refuse them: { from_status: { to_status: reason } }. A client knows in advance what it cannot do, instead of discovering it through a 403 WORKFLOW_BLOCKED. Checks that depend on a specific item (subtasks done, required fields, QA verdict) are not listed: they are only evaluated on PUT. Project owners / admins are not subject to conditions (bypasses_conditions). Rules are configured in the application.

{
  "data": {
    "rules": [
      { "uuid": "…", "entity_type": "ticket", "status_from": null, "status_to": "done",
        "rule_type": "condition", "action_type": "require_role",
        "config": { "roles": ["product_owner"] }, "sort_order": 1, "enabled": true }
    ],
    "forbidden_transitions": {
      "ticket": { "review": { "done": { "reason": "role", "roles": ["product_owner"] } } },
      "epic": {},
      "super_epic": {}
    },
    "bypasses_conditions": false
  },
  "meta": { "total": 1 }
}

Dependencies

Finish-to-start links between roadmap items (epic or super-epic). Each node is exposed by its UUID (resolvable via /epics and /super-epics) to reconstruct the graph and compute the critical path client-side.

GET /api/v1/dependencies?project={uuid} List dependencies

Required scope: roadmap:read

{
  "data": [
    {
      "uuid": "dep-uuid",
      "type": "FS",               // finish-to-start
      "pred_type": "epic",        // epic | super_epic
      "pred_uuid": "epic-uuid-1",
      "succ_type": "epic",
      "succ_uuid": "epic-uuid-2"
    }
  ],
  "meta": { "total": 1 }
}
POST /api/v1/dependencies Create a dependency

Required scope: roadmap:write · project owner / admin

{
  "project_uuid": "proj-uuid",
  "pred_type": "epic",          // epic | super_epic: must finish before…
  "pred_uuid": "epic-uuid-1",
  "succ_type": "epic",          // …this one starts
  "succ_uuid": "epic-uuid-2"
}

Response 201: the dependency. Refused: self-link (422), duplicate or cycle (409 CONFLICT), epic from another project (404). "Blocks / blocked by" links between tickets go through /tickets/{uuid}/links.

DELETE /api/v1/dependencies/{uuid} Delete a dependency

Required scope: roadmap:write · project owner / admin

Milestones

GET /api/v1/milestones?project={uuid} List dated milestones

Required scope: roadmap:read

{
  "data": [
    { "uuid": "...", "name": "Go-live", "date": "2026-09-30", "color": "#e11d48" }
  ],
  "meta": { "total": 1 }
}
POST /api/v1/milestones Create a milestone

Required scope: roadmap:write · project owner / admin

{
  "project_uuid": "proj-uuid",
  "name": "Go-live",            // required, 120 characters max
  "date": "2026-12-01",         // required, YYYY-MM-DD
  "color": "#e11d48"            // optional, #rgb or #rrggbb
}

Response 201: the milestone.

PUT /api/v1/milestones/{uuid} Update a milestone

Required scope: roadmap:write · partial body: only the fields sent (name, date, color) change.

DELETE /api/v1/milestones/{uuid} Delete a milestone

Required scope: roadmap:write · project owner / admin

Ticket attachments

The same gate as the app's upload form: 10 MB max per file, the project's storage quota, and the real file type detected from the content (never the name or the declared header). SVG, HTML, scripts and executables are refused. Writing requires the edit_us or create_us permission (owner / admin always).

GET /api/v1/tickets/{uuid}/attachments List a ticket's attachments

Required scope: tickets:read

{
  "data": [
    { "uuid": "...", "ticket_uuid": "...", "name": "export.csv", "size": 2048,
      "type": "text/csv", "downloadable": true, "created_at": "..." }
  ],
  "meta": { "total": 1 }
}

downloadable: false: the attachment stays in the connected tracker (Jira, Azure Boards) with no local copy.

POST /api/v1/tickets/{uuid}/attachments Upload a file

Required scope: tickets:write · edit_us or create_us permission

As multipart/form-data, field file:

curl -X POST https://manifst.net/api/v1/tickets/{uuid}/attachments \
  -H "Authorization: Bearer mfst_live_..." \
  -F "file=@export.csv"

Or as JSON with base64 content (a data URL is accepted):

{ "name": "capture.png", "content_base64": "iVBORw0KGgo..." }

Response 201: the attachment. 413 when the file exceeds 10 MB or the project quota is reached (STORAGE_QUOTA_REACHED).

GET /api/v1/attachments/{uuid} Attachment metadata

Required scope: tickets:read

GET /api/v1/attachments/{uuid}/download Download the file

Required scope: tickets:read · returns the file itself (Content-Disposition: attachment), not JSON.

DELETE /api/v1/attachments/{uuid} Delete an attachment

Required scope: tickets:write · the file is removed from disk along with the record.

Documentation (MyDoc)

MyDoc spaces and pages, with the app's rights: the MyDoc section must be open to the key holder's role in the project, then their permission on the space applies. The project owner is admin everywhere. An explicit permission prevails. A public space gives edit to every member; a private space gives nothing. Reading needs read, writing edit.

Content is sent as HTML (content, cleaned by the app's sanitizer) or Markdown (content_markdown). It is read as HTML, Markdown or both (?format=html|markdown|both).

GET /api/v1/docs/spaces?project={uuid} A project's readable spaces

Required scope: docs:read

{
  "data": [
    { "uuid": "...", "name": "Specifications", "description": "", "icon": "bi-journal-text",
      "color": "#6366f1", "is_public": true, "page_count": 12, "my_permission": "edit", "updated_at": "..." }
  ],
  "meta": { "total": 1 }
}
GET /api/v1/docs/spaces/{uuid}/pages A space's pages, without content

Required scope: docs:read · flat list: top-level pages first, then sub-pages (parent_uuid). Archived pages are excluded unless ?include_archived=1.

{ "data": [ { "uuid": "...", "parent_uuid": null, "title": "Accounting export", "emoji": null,
              "status": "published", "sort_order": 0, "word_count": 240, "rev": 7,
              "updated_at": "...", "updated_by_name": "Jane Martin" } ] }
GET /api/v1/docs/pages/{uuid}?format=markdown Read a page

Required scope: docs:read

{
  "data": {
    "uuid": "...", "space_uuid": "...", "space_name": "Specifications", "project_uuid": "...",
    "parent_uuid": null, "title": "Accounting export", "emoji": null, "status": "published",
    "rev": 7,
    "content_markdown": "### Goal\n\nProduce a monthly **CSV**…",
    "word_count": 240, "labels": ["finance"],
    "created_by": { "uuid": "...", "name": "Jane Martin" },
    "updated_by": { "uuid": "...", "name": "Bob Dev" },
    "created_at": "...", "updated_at": "...", "my_permission": "edit"
  }
}

Keep rev: it is required to rewrite the content.

POST /api/v1/docs/pages Create a page

Required scope: docs:write · edit permission on the space

{
  "space_uuid": "space-uuid",
  "parent_uuid": "page-uuid",        // optional: sub-page (two levels max)
  "title": "Accounting export",      // required, 500 characters max
  "emoji": "📄",                     // optional
  "status": "published",             // published (default), draft, archived
  "content_markdown": "## Goal\n\nProduce a monthly **CSV**."   // or "content" as HTML
}

Response 201: the page, with its rev.

PUT /api/v1/docs/pages/{uuid} Update a page

Required scope: docs:write · partial body: title, emoji, status, content or content_markdown.

{ "content_markdown": "…", "base_rev": 7 }

Rewriting the content requires base_rev, the rev of the page you read (or the If-Match: 7 header). Without it: 428. If the page changed in the meantime (a person co-editing, another agent): 409 CONFLICT and nothing is written. Read the page again, merge, and resend with the new rev. An accepted write archives the previous version in the page history, and tabs open on the page reload.

If someone is co-editing the page and their latest changes are not saved yet, the response is 409 PAGE_BEING_EDITED and nothing is written: retry a minute later. Their keystrokes are never overwritten.

Title, emoji and status alone need no base_rev.

DELETE /api/v1/docs/pages/{uuid} Delete a page

Required scope: docs:write · also deletes its sub-pages and attached files. To retire a page without losing it, prefer "status": "archived".

GET /api/v1/docs/pages/{uuid}/attachments · /docs/attachments/{uuid}/download A page's attached files

Required scope: docs:read · list (uuid, name, type, size, created_at, created_by_name), then download. Uploading files into a page remains in the app.

Testing (QA)

The ticket testing frame, with the app's rules:

Writing requires the perform_qa and edit_us permissions (owner / admin always). The verdict does not move the ticket: change its status afterwards with PUT /tickets/{uuid}, where the transition rules apply, including "accepted testing required".

GET /api/v1/tickets/{uuid}/qa A ticket's test sessions

Required scope: tickets:read

{
  "data": {
    "ticket_uuid": "...", "status": "in_review", "column_type": "review",
    "can_perform": true, "accepted": false,
    "sessions": [
      { "uuid": "...", "round": 1, "verdict": "open", "tester_name": "Jane Martin",
        "version_label": "1.4.0", "environment": "staging",
        "counts": { "pending": 1, "passed": 1, "failed": 0, "untestable": 0 },
        "checks": [
          { "uuid": "...", "source": "ac", "label": "Amounts are in euros",
            "result": "pending", "note": null, "orphan": false, "...": "..." }
        ] }
    ]
  }
}

result: pending, passed, failed, untestable. orphan: a check whose criterion has since been removed from the ticket.

POST /api/v1/tickets/{uuid}/qa Start a session

Required scope: tickets:write · optional body { "version_label": "1.4.0", "environment": "staging" }.

Idempotent: if a session is already open it is returned as is (200); otherwise a new round opens (201). Two agents starting at the same time work on the same session.

PUT /api/v1/qa/sessions/{uuid}/checks Record results in batch

Required scope: tickets:write

{
  "checks": [
    { "uuid": "check-1", "result": "passed" },
    { "uuid": "check-2", "result": "failed", "note": "Amount in dollars, line 12" }
  ]
}

At most 200 checks, all or nothing. A single check: PUT /qa/checks/{uuid} with { "result", "note" }.

POST /api/v1/qa/sessions/{uuid}/checks Add a free check

Required scope: tickets:write · { "label": "Export of more than 10,000 rows" }. Only free checks can be deleted (DELETE /qa/checks/{uuid}); mark a criterion or DoD item untestable instead.

PUT /api/v1/qa/sessions/{uuid} Version, environment, tester

Required scope: tickets:write · version_label, environment, tester_uuid (a project member).

POST /api/v1/qa/sessions/{uuid}/close Give the verdict

Required scope: tickets:write · { "verdict": "accepted" } or "rejected".

422 when the verdict rule is not met (the message gives the number of pending and failed checks); 409 when the session is already closed. A closed session cannot change: start a new round.

Sprint rituals

The same computations as the Sprint Review and Daily screens: no second calculation, the figures are the ones the team sees.

GET /api/v1/sprints/{uuid}/review Full Sprint Review

Required scope: sprints:read

Committed vs delivered, velocity of previous sprints, burndown, scope added or removed, carried-over tickets, epic progress, milestones, linked risks, EVM cost of the sprint. The finance and risks blocks are null when the key holder has no access to them (view_financial, Risks section), as on screen; access says which.

PUT /api/v1/sprints/{uuid}/review Notes and Sprint Goal verdict

Required scope: sprints:write · run_review permission (Product Owner and Scrum Master by default; can_review in the read response tells whether the key has it)

{
  "goal_met": "partial",                       // yes, partial, no, or null (not assessed)
  "goal_comment": "Export delivered without credit notes",
  "review_notes": "Demo approved by the client."
}

Partial: saving the verdict does not erase the notes.

GET /api/v1/sprints/{uuid}/standup Today's daily board

Required scope: sprints:read · Sprints section open to the role

Sprint progress, issues to address (blocked tickets, in progress for more than 3 days without movement, testing queue, concentrated workload), round table per person (what moved since the last working day, what is in progress). closed_today: the daily already closed today, or null. Closing the daily (report sent to the team) stays in the app.

GET /api/v1/sprints/{uuid}/retro Retrospective items

Required scope: sprints:read · Sprints section + edit_sprint permission, as on the retrospective screen

{
  "data": [ { "uuid": "...", "category": "action", "content": "Add credit notes", "done": false, "created_at": "..." } ],
  "meta": { "total": 1, "by_category": { "went_well": 0, "problem": 0, "action": 1 } }
}
POST /api/v1/sprints/{uuid}/retro · PUT/DELETE /retro-items/{uuid} Add, update, delete an item

Required scope: sprints:write · edit_sprint permission

// POST /sprints/{uuid}/retro
{ "category": "problem", "content": "Late testing" }        // went_well, problem, action
// PUT /retro-items/{uuid}
{ "done": true }                                              // or "content"

Flow metrics

GET /api/v1/metrics/flow?project={uuid} Flow, quality, scope

Required scope: tickets:read

The computation behind the dashboard's Flow, Quality and Scope blocks, over 90 days: weekly throughput, cycle and lead time, work in progress and its age, defect rate, testing results. available: false until ticket history exists.

ParameterDescription
cfd=1Cumulative flow, day by day
sprints=1Scope of recent sprints (added, removed, delivered)
forecast=1Completion forecast by Monte Carlo simulation (Portfolio plan)
projects=uuid1,uuid2Roll-up of several projects (Portfolio plan). Projects the key holder is not a member of are ignored. No cfd, sprints or forecast: they belong to one team.

Risks

The project's risk register. Severity is computed (probability × impact, 1 to 25): low up to 4, medium up to 9, high up to 14, critical above. Reading requires the Risks section to be open to the role (the same rule as the Sprint Review's risks block). Writing requires the Team plan or above and project owner / admin or the manage_risks permission. Out-of-range values are rejected (422), never silently clamped.

GET /api/v1/risks?project={uuid} List risks, most severe first

Required scope: governance:read

ParameterDescription
statusopen, mitigating, monitored, closed, occurred (comma-separated)
categorytechnique, delai, budget, ressources, externe, securite, qualite
severitylow, medium, high, critical
entity_type + entity_uuidRisks linked to a ticket, an epic or a super_epic
updated_sinceChanged since this date (YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)
{
  "data": [
    {
      "uuid": "...", "title": "Stripe API delay", "description": "",
      "category": "externe", "probability": 4, "impact": 5,
      "severity_score": 20, "severity_level": "critical",
      "status": "open", "owner_uuid": "...", "owner_name": "Jane Martin", "owner_initials": "JM",
      "mitigation": "Plan B: stub", "contingency": "", "due_date": "2026-12-01",
      "links": [ { "entity_type": "ticket", "uuid": "...", "title": "Payment migration" } ],
      "created_at": "...", "updated_at": "..."
    }
  ],
  "meta": { "total": 1, "categories": ["technique", "delai", "..."] }
}
GET /api/v1/risks/{uuid} Get one risk

Required scope: governance:read

POST /api/v1/risks Create a risk

Required scope: governance:write · manage_risks permission

{
  "project_uuid": "proj-uuid",
  "title": "Stripe API delay",      // required, 160 characters max
  "category": "externe",            // required
  "probability": 4,                 // integer 1 to 5, default 3
  "impact": 5,                      // integer 1 to 5, default 3
  "status": "open",                 // default open
  "owner_uuid": "user-uuid",        // project member, optional
  "due_date": "2026-12-01",
  "description": "...",             // 5,000 characters max
  "mitigation": "...",              // 2,000 characters max
  "contingency": "...",             // 2,000 characters max
  "links": [ { "entity_type": "ticket", "uuid": "ticket-uuid" } ]
}

Response 201: the risk. A links target not found in the project rejects the whole call.

PUT /api/v1/risks/{uuid} Update a risk

Required scope: governance:write · partial body. links replaces the whole list ([] removes them); owner_uuid: null removes the owner.

DELETE /api/v1/risks/{uuid} Delete a risk

Required scope: governance:write · manage_risks permission

RACI matrix

Phases → tasks as rows, participants as columns (project members or external people without an account), one role per cell: R, A, C, I or C/I. Same rights as risks: the RACI section open to the role, and the manage_raci permission to write.

GET /api/v1/raci?project={uuid} Get the full matrix

Required scope: governance:read

{
  "data": {
    "participants": [
      { "participant_uuid": "p1", "user_uuid": "...", "is_external": false, "name": "Jane Martin", "initials": "JM", "...": "..." },
      { "participant_uuid": "p2", "user_uuid": null,  "is_external": true,  "name": "Client", "initials": "CL", "...": "..." }
    ],
    "phases": [
      { "uuid": "...", "name": "Scoping", "color": "#6366f1", "position": 0,
        "tasks": [ { "uuid": "t1", "title": "Approve the budget", "position": 0,
                     "cells": { "p1": "A", "p2": "C/I" } } ] }
    ],
    "can_edit": true
  }
}

cells is keyed by participant_uuid; empty cells are omitted. can_edit tells whether the key holder can write.

POST /api/v1/raci/phases · /raci/tasks · /raci/participants Add a phase, a task, a participant

Required scope: governance:write · manage_raci permission

// POST /raci/phases
{ "project_uuid": "proj-uuid", "name": "Scoping", "color": "#6366f1", "position": 0 }
// POST /raci/tasks
{ "phase_uuid": "phase-uuid", "title": "Approve the budget", "position": 0 }
// POST /raci/participants: a member…
{ "project_uuid": "proj-uuid", "user_uuid": "user-uuid" }
// …or an external person
{ "project_uuid": "proj-uuid", "name": "Jane Client" }

Response 201: the created item. position (0 = first) is optional; items are appended by default. A member who is already a participant returns 409.

PUT /api/v1/raci/phases/{uuid} · /raci/tasks/{uuid} · /raci/participants/{uuid} Rename, recolor, move

Required scope: governance:write · phase: name, color, position; task: title, position; participant: name, external people only (a member keeps their account name).

PUT /api/v1/raci/cells Fill cells in batch

Required scope: governance:write · manage_raci permission

{
  "cells": [
    { "task_uuid": "t1", "participant_uuid": "p1", "role": "A" },
    { "task_uuid": "t1", "participant_uuid": "p2", "role": "C/I" },
    { "task_uuid": "t2", "participant_uuid": "p1", "role": null }   // null or "" clears the cell
  ]
}

At most 500 cells, all from the same project. All or nothing: one invalid cell rejects the whole batch. Response: the updated matrix, with meta.updated. A single cell can also be sent flat: { "task_uuid", "participant_uuid", "role" }.

DELETE /api/v1/raci/phases/{uuid} · /raci/tasks/{uuid} · /raci/participants/{uuid} Delete

Required scope: governance:write · deleting a phase deletes its tasks and their cells; removing a participant deletes their column.

GET /api/v1/search?project={uuid}&q={term} Search a project

Required scope: projects:read, plus the read scope of each type searched.

The same engine as the app's search: titles, descriptions, keys (US-12, EP-3, SE-1, including a ticket's former global number), sprint goals, MyDoc pages (in readable spaces), member names and emails.

ParameterDescription
q2 to 200 characters
typessuper_epic (super-epics:read), epic (epics:read), sprint (sprints:read), ticket (tickets:read), doc (docs:read), user (team:read). Default: all.
limitResults per type, 1 to 50 (default 10)
{
  "data": [
    { "type": "ticket", "uuid": "...", "key": "US-12", "title": "Payment migration", "status": "in_progress", "matched_legacy_key": false },
    { "type": "user", "uuid": "...", "user_id": 42, "title": "Jane Martin", "email": "jane@example.com" }
  ],
  "meta": { "total": 2, "types_searched": ["super_epic", "epic", "sprint", "ticket", "user"] }
}

A type whose read scope the key lacks is not searched and is listed in meta.types_skipped. user_id is the identifier expected by mentions on comments.

Notifications

The key holder's notification bell, across all projects: mentions, assignments, owner designations, status changes… These are the app's notifications, under the same conditions (expired ones and projects left are excluded). A key never sees another account's notifications.

GET /api/v1/notifications List your notifications, newest first

Required scope: notifications:read

ParameterDescription
unread1: unread only
typeOne or more comma-separated types: mention, doc_mention, story_assigned, lot_owner_assigned, ticket_updated, poker_started…
projectA project UUID
sinceReceived since this date (server time)
langfr or en for message; defaults to the account's language
page, per_pagePagination, 50 by default, 200 max
{
  "data": [
    {
      "uuid": "...", "type": "mention",
      "message": "Bob Dev mentioned you on “Payment migration”",
      "params": { "actor_name": "Bob Dev", "ticket_title": "Payment migration" },
      "project_uuid": "...", "project_name": "Redesign",
      "subject_type": "ticket", "subject_uuid": "ticket-uuid",
      "is_read": false, "read_at": null,
      "created_at": "2026-10-27 09:12:00", "age_seconds": 340
    }
  ],
  "meta": { "total": 1, "count": 1, "unread": 3, "page": 1, "per_page": 50 }
}

For an agent watching its inbox: ?unread=1&per_page=1 returns the counter in meta.unread in a single call. Read subject_uuid with the endpoint of its subject_type (for instance GET /tickets/{uuid}).

PUT /api/v1/notifications/{uuid} Mark read or unread

Required scope: notifications:write · body { "read": true } (default) or { "read": false }.

POST /api/v1/notifications/mark-read Mark several notifications as read

Required scope: notifications:write

{ "uuids": ["notif-uuid-1", "notif-uuid-2"] }   // 500 max
{ "all": true }                                  // everything received so far

Response: { "marked": 2, "unread": 1 }. Prefer uuids: all also marks what arrived since the agent last read its inbox.

DELETE /api/v1/notifications/{uuid} Delete a notification

Required scope: notifications:write

Finance / EVM

Project Earned Value Management (EVM) performance indicators, computed server-side from story points, logged time (× TJM), and the declared BAC (projects.budget).

Indicators depending on budget (bac, pv, ev, cpi, cv, sv, eac, etc, vac, tcpi) evaluate to null as long as no BAC is declared for the project. Progress and cost metrics (ac, ac_total, no_sprint_cost, engaged_charge, spi, progress_pct, planned_pct) remain calculated.
Two actual costs, two uses. ac is the cost of the sprint scope: the only basis comparable to ev, hence the only one admissible in the cpi denominator and in cv. ac_total = ac + no_sprint_cost is total actual spend: time booked outside any sprint receives no share of the BAC (which is spread pro rata across sprint story points) so it earns no value, yet the money is gone. Forecasting (eac, tcpi) therefore uses ac_total. On a project with no out-of-sprint booking, ac_total == ac and eac collapses exactly onto BAC ÷ CPI.
GET /api/v1/finance?project={uuid} Project EVM indicators

Required scope: finance:read

The key owner must also be allowed to read the project finances (the “Finance” right of their role, or project owner/admin); otherwise 403 FORBIDDEN.

{
  "data": {
    "project_uuid": "abc-123",
    "bac": 350000.00,          // budget at completion (declared)
    "base_budget": 315000.00,  // BAC − contingency reserve
    "reserve": 35000.00,       // contingency reserve (included in BAC)
    "contingency_pct": 10.00,
    "pv": 326341.95,           // planned value = % planned SP to date × BAC
    "ev": 257455.27,           // earned value  = % completed SP × BAC
    "ac": 186573.75,           // sprint-scope cost = Σ (time ÷ 480) × daily rate in force on the work date
    "no_sprint_cost": 8250.00, // time booked on tickets with no sprint
    "ac_total": 194823.75,     // total actual spend = ac + no_sprint_cost
    "engaged_charge": 137253.75, // actual cost of unfinished epics
    "cpi": 1.38,               // EV ÷ ac         (> 1 = under budget)
    "spi": 0.79,               // EV ÷ PV         (> 1 = ahead of schedule)
    "tcpi": 0.60,              // (BAC − EV) ÷ (BAC − ac_total)
    "eac": 261889.45,          // estimate at completion = ac_total + etc
    "etc": 67065.70,           // estimate to complete = (BAC − EV) ÷ CPI
    "vac": 88110.55,           // variance at completion = BAC − EAC
    "cv": 70881.52,            // cost variance = EV − ac
    "sv": -68886.68,           // schedule variance = EV − PV
    "total_sp": 503,
    "done_sp": 374,            // story points physically completed (raw progress)
    "earned_sp": 370,          // earned SP = Σ min(done, committed) per sprint: the EV basis
    "planned_sp": 469.10,
    "undated_sp": 12,          // SP committed in sprints with no dates: they count
                               // towards EV but contribute 0 to PV — they inflate SPI
    "progress_pct": 73.56,     // earned_sp ÷ total_sp
    "planned_pct": 93.24       // % planned SP to date
  }
}

Portfolio

EVM roll-up across all projects accessible to the API key holder : a single request replacing project-by-project polling. Consolidated indexes are calculated on the sum of values (EV, PV, AC), not an average of ratios.

GET /api/v1/portfolio/finance Multi-project EVM roll-up

Required scope: finance:read

Optional query filter: ?projects=uuid1,uuid2 to target a specific team / program. Only projects whose finances you can read are rolled up; the others are counted in totals.excluded_no_financial_access.

{
  "data": {
    "totals": {
      "projects": 12, "with_bac": 4, "without_bac": 8,
      "bac": 1250000.00, "pv": 910000.00, "ev": 742000.00, "ac": 690000.00,
      "ac_total": 712000.00,
      "engaged_charge": 305000.00,
      "cpi": 1.08, "spi": 0.82,
      "etc": 472398.92, "eac": 1184398.92, "vac": 65601.08,
      "cv": 52000.00, "sv": -168000.00,
      "ac_all": 812000.00,       // total actual spend across ALL projects (BAC declared or not)
      "engaged_all": 410000.00   // committed charge across all projects
    },
    "projects": [
      { "project_uuid": "abc-123", "project_name": "CMA-CGM", "bac": 350000.00, "ev": 257455.27, "ac": 186573.75, "cpi": 1.38, "spi": 0.79, "vac": 96360.55, "engaged_charge": 137253.75, "...": "..." }
    ]
  },
  "meta": { "total": 12 }
}

Manifst API v1 Documentation · Back to site