Fishladder API

REST API Reference · v1

Authentication

All API requests require a Bearer token. Generate one at Settings → API Keys. Each key is scoped to a single team — to access multiple teams, generate a key for each one.

curl https://app.ladder.fish/api/v1/projects \
  -H "Authorization: Bearer fl_your_key_here"

Rate Limits

Per API key, per hour: 1,000 reads (GET) and 200 writes (POST/PUT/PATCH/DELETE). The two budgets are separate, so exhausting reads does not block writes, and each key has its own — one busy integration cannot starve another. Exceeded requests return 429 Too Many Requests with a Retry-After header and a scope field naming which budget ran out.

Errors

All errors return JSON with an error field.

FieldTypeDescription
401UnauthorizedMissing or invalid API key.
400Bad RequestInvalid or missing required fields.
404Not FoundResource not found or belongs to a different team.
429Too Many RequestsRead or write budget exhausted for this key. Sleep for Retry-After seconds; the scope field says which budget ran out (v1-reads or v1-writes).
500Server ErrorSomething went wrong on our end.
{ "error": "Not found" }

Projects

Local projects — roadmap items created directly in Fishladder (not synced from Linear).

GET
/api/v1/projects

List all projects for the team.

POST
/api/v1/projects

Create a project.

PUT
/api/v1/projects/{id}

Update a project.

DELETE
/api/v1/projects/{id}

Delete a project.

Fields

FieldTypeDescription
title*stringProject title (max 500 chars).
descriptionstringProject description.
statusstringplanned · in_progress · completed · cancelled · paused. Defaults to planned.
initiativeIdstringID of a parent initiative.
kanbanColumnstringQuarter column, e.g. q2-2026.
kanbanRowIdstringTheme row ID.
startDatestringISO date string, e.g. 2026-04-01.
targetDatestringISO date string.
trackerProjectIdstringID of a linked tracker project (Projects app). Set to link, null to unlink. Must belong to your team. Linked projects report task status counts via issueStatuses.
issueStatusesarrayRead-only. Task counts per status for the linked tracker project: [{ name, color, type, count }]. Empty when unlinked.
customFieldsobjectKey-value map of custom field names to values. Returned on GET, accepted on POST/PUT.

Example

# Create a project
curl -X POST https://app.ladder.fish/api/v1/projects \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Rebuild onboarding flow",
    "status": "planned",
    "kanbanColumn": "q2-2026",
    "targetDate": "2026-06-30"
  }'

# Response (201)
{
  "id": "local-550e8400-...",
  "title": "Rebuild onboarding flow",
  "description": "",
  "status": "planned",
  "initiativeId": null,
  "kanbanColumn": "q2-2026",
  "kanbanRowId": null,
  "startDate": null,
  "targetDate": "2026-06-30",
  "trackerProjectId": null,
  "issueStatuses": [],
  "createdAt": "2026-03-04T10:00:00.000Z"
}

# Link a tracker project (task statuses then appear on the roadmap card)
curl -X PUT https://app.ladder.fish/api/v1/projects/local-550e8400-... \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "trackerProjectId": "tracker-91a34cd0-..." }'

Tasks

Tracker tasks — work items within tracker projects.

GET
/api/v1/tasks

List tasks. Optional: ?projectId=...

POST
/api/v1/tasks

Create a task.

GET
/api/v1/tasks/{id}

Get a single task (same shape as list items, plus customFields).

PATCH
/api/v1/tasks/{id}

Partially update a task. Only fields you send are changed.

DELETE
/api/v1/tasks/{id}

Delete a task. Returns { ok: true }.

GET
/api/v1/tasks/{id}/drive-links

List the Google Drive files linked to a task (its Google Drive section).

POST
/api/v1/tasks/{id}/drive-links

Add a Drive / Docs / Sheets / Slides link. Body: { url }. Adding the same file again returns the existing link.

DELETE
/api/v1/tasks/{id}/drive-links/{linkId}

Remove a Drive link (admin key, like every v1 DELETE).

Boards in a project group are only reachable by keys whose owner can open that group: other boards' tasks answer 404, as if they didn't exist. An assigneeId must be an active member who can open the board, otherwise the request is refused with 400.

Fields

FieldTypeDescription
title*stringTask title (max 500 chars). Required on POST.
projectId*stringID of the tracker project. Required on POST.
descriptionstringTask description.
assigneeIdstringUser ID of the assignee.
prioritystringnone / low / medium / high / urgent. Defaults to none.
dueDatestringDue date (ISO date string).
sectionIdstringSection (board column) ID — discover valid IDs via the Sections & Statuses endpoints below.
statusIdstringStatus ID — discover valid IDs (with isDefault/isClosed) via the Sections & Statuses endpoints below.
sortOrdernumberDisplay order.
completedAtstring | nullPATCH only. ISO timestamp to mark complete, or null to reopen.
statusstringPATCH only. Pass 'done' as a convenience to set completedAt to now.
customFieldsobjectKey-value map of custom field names to values. Returned on GET, accepted on POST and PATCH.

Example

# Create a task
curl -X POST https://app.ladder.fish/api/v1/tasks \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Review Q2 metrics",
    "projectId": "proj-abc123",
    "priority": "high",
    "dueDate": "2026-04-15"
  }'

# Partially update a task
curl -X PATCH https://app.ladder.fish/api/v1/tasks/task-uuid \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "priority": "urgent", "dueDate": "2026-04-10" }'

# Mark complete
curl -X PATCH https://app.ladder.fish/api/v1/tasks/task-uuid \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "status": "done" }'

# Delete
curl -X DELETE https://app.ladder.fish/api/v1/tasks/task-uuid \
  -H "Authorization: Bearer fl_your_key_here"
# Response: { "ok": true }

Tracker Projects

Projects within the Tracker app. Use these IDs when creating or listing tasks.

GET
/api/v1/tracker/projects

List all tracker projects for the team.

Response Fields

FieldTypeDescription
idstringProject ID — use this as projectId when creating tasks.
namestringProject name.
groupIdstring | nullID of the project group this project belongs to.
groupNamestring | nullDisplay name of the project group.
colorstring | nullProject colour (hex).
iconstring | nullProject icon.
archivedbooleanWhether the project is archived.
sortOrdernumberDisplay order within its group.

Example

curl https://app.ladder.fish/api/v1/tracker/projects \
  -H "Authorization: Bearer fl_your_key_here"

# Response
[
  {
    "id": "proj-abc123",
    "name": "Acme Corp",
    "groupId": "grp-xyz",
    "groupName": "CSM",
    "color": "#6366f1",
    "icon": null,
    "archived": false,
    "sortOrder": 0
  }
]

Sections & Statuses

Board columns (sections) and workflow statuses for a tracker project. Use these to discover valid sectionId/statusId values for task create/update — API-created tasks land in the first section (Backlog) unless placed explicitly.

GET
/api/v1/tracker/projects/{id}/sections

List a project's board sections in display order.

POST
/api/v1/tracker/projects/{id}/sections

Create a section. Body: name (required), sortOrder.

GET
/api/v1/tracker/projects/{id}/statuses

List a project's statuses, including isDefault and isClosed flags.

Example

curl https://app.ladder.fish/api/v1/tracker/projects/proj-abc123/statuses \
  -H "Authorization: Bearer fl_your_key_here"

# Response
[
  { "id": "st-1", "projectId": "proj-abc123", "name": "Todo", "color": "#6B7280", "sortOrder": 0, "isDefault": true,  "isClosed": false },
  { "id": "st-2", "projectId": "proj-abc123", "name": "In Progress", "color": "#3B82F6", "sortOrder": 1, "isDefault": false, "isClosed": false },
  { "id": "st-3", "projectId": "proj-abc123", "name": "Done", "color": "#22C55E", "sortOrder": 2, "isDefault": false, "isClosed": true }
]

Calendar — What's Due

Everything due or scheduled in a time frame, across modules: task due dates, roadmap project target dates, deal expected-close dates, quote expirations, interviews, questionnaire due dates, audit milestones and calendar events. Requires the Calendar module. Finished work is left out unless you ask for it.

GET
/api/v1/calendar

List items due in a date window, sorted by date.

Query Parameters

FieldTypeDescription
daysintegerDays from today to cover, including today (default 14). Negative looks back: -7 is the past week.
startyyyy-MM-ddStart date. Defaults to today. Combine with end or days.
endyyyy-MM-ddInclusive end date. Give end or days, not both. Max range 400 days.
typesstringComma-separated: task, roadmap, crm_close, cpq_expiry, interview, ts_due, milestone, event. Default: all.
projectsstringComma-separated tracker board or roadmap project names or IDs (partial names match). Scopes results to those projects' tasks and target dates.
includeCompletedbooleanInclude finished tasks and completed roadmap projects. Default false.
timezonestringIANA zone deciding what "today" is, e.g. America/New_York. Default UTC.
limitintegerMaximum items returned (default 200, max 500). truncated is true when more matched.

Returns 400 for a malformed or out-of-range date (days beyond ±400, years outside 1900–2200), end together with days, an unknown type, more than 50 projects, or projects combined with only non-project types (projects carry tasks and target dates only). Reads count against the normal v1 read limit.

Response Fields

FieldTypeDescription
rangeobjectThe resolved { start, end } window.
projectsobject | nullWhen projects was sent: matched (ref, kind board|roadmap, id, name) and unmatched names. If nothing matched, items is empty.
countnumberTotal items matched, before limit.
countsobjectMatched items per type.
items[].typestringSource type (see types). typeLabel is its display name.
items[].date / timestringDue date (yyyy-MM-dd) and, for timed items, HH:mm.
items[].statusstring | nullTask status, roadmap status, quote status, etc.
items[].projectobject | null{ id, name } of the tracker board, for tasks and linked roadmap projects.
items[].companystring | nullCompany for deals, quotes and questionnaires.
items[].urlstring | nullLink to the item in Fishladder.

Example

# Everything due for the Acme project in the next 30 days
curl "https://app.ladder.fish/api/v1/calendar?days=30&projects=Acme&timezone=America/New_York" \
  -H "Authorization: Bearer fl_your_key_here"

# Response
{
  "range": { "start": "2026-09-27", "end": "2026-10-26" },
  "types": ["task", "roadmap"],
  "projects": {
    "matched": [{ "ref": "Acme", "kind": "board", "id": "proj-abc123", "name": "Acme Corp" }],
    "unmatched": []
  },
  "includeCompleted": false,
  "count": 2,
  "truncated": false,
  "counts": { "task": 1, "roadmap": 1 },
  "items": [
    {
      "id": "task-1", "type": "task", "typeLabel": "Tasks",
      "title": "Kickoff deck", "date": "2026-10-02", "time": null,
      "status": "In Progress", "completed": false,
      "project": { "id": "proj-abc123", "name": "Acme Corp" }, "company": null,
      "url": "https://app.ladder.fish/projects/proj-abc123/board?task=task-1"
    },
    {
      "id": "rm-9", "type": "roadmap", "typeLabel": "Project target dates",
      "title": "Target: Acme go-live", "date": "2026-10-20", "time": null,
      "status": "in_progress", "completed": false,
      "project": { "id": "proj-abc123", "name": "Acme Corp" }, "company": null,
      "url": "https://app.ladder.fish/kanban"
    }
  ]
}

# Deals closing and quotes expiring this month
curl "https://app.ladder.fish/api/v1/calendar?start=2026-10-01&end=2026-10-31&types=crm_close,cpq_expiry" \
  -H "Authorization: Bearer fl_your_key_here"

Initiatives

Local initiatives — group multiple projects under a strategic theme.

GET
/api/v1/initiatives

List all initiatives for the team.

POST
/api/v1/initiatives

Create an initiative.

PUT
/api/v1/initiatives/{id}

Update an initiative.

DELETE
/api/v1/initiatives/{id}

Delete an initiative.

Fields

FieldTypeDescription
title*stringInitiative title (max 500 chars).
descriptionstringInitiative description.
statusstringplanned · in_progress · completed · cancelled · paused. Defaults to planned.
colorstringHex color, e.g. #6366f1.
kanbanRowIdstringTheme row ID.

Example

curl -X POST https://app.ladder.fish/api/v1/initiatives \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "title": "Platform reliability", "color": "#6366f1" }'

Feedback

Customer feedback entries. New entries are automatically appended at the lowest priority.

GET
/api/v1/feedback

List all feedback entries, ordered by priority.

POST
/api/v1/feedback

Create a feedback entry.

PUT
/api/v1/feedback/{id}

Update a feedback entry.

DELETE
/api/v1/feedback/{id}

Delete a feedback entry.

Fields

FieldTypeDescription
title*stringShort description of the feedback (max 500 chars).
detailsstringFull details (max 10,000 chars).
problemSolvedstringWhat problem this solves (max 10,000 chars).
namesstring[]Customer names associated with this feedback. Each name is truncated to 20 chars.
companystringCompany name (max 200 chars).
datestringDate the feedback was received.
statusstringopen · in_progress · done. Defaults to open.
impactstringlow · medium · high. Defaults to medium.
groupIdstringID of a feedback group.
archivedbooleanWhether the entry is archived (PUT only).
sourcestringProvenance slug for integrations, e.g. watchrec. Free string, max 20 chars; stored trimmed and lowercased.

Example

curl -X POST https://app.ladder.fish/api/v1/feedback \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Export to CSV",
    "details": "Several enterprise customers have asked for CSV export.",
    "names": ["Acme Corp", "Globex"],
    "impact": "high",
    "status": "open"
  }'

Goals

Strategic goals. The GET endpoint returns both goals and their item links in one response.

GET
/api/v1/goals

List all goals and goal–item links.

POST
/api/v1/goals

Create a goal.

PUT
/api/v1/goals/{id}

Update a goal.

DELETE
/api/v1/goals/{id}

Delete a goal and all its item links.

Fields

FieldTypeDescription
title*stringGoal title (max 500 chars).
descriptionstringGoal description (max 5,000 chars).
colorstringHex color. Defaults to #6B7280.
statusstringactive · completed · cancelled. Defaults to active.
categoryIdsstring[]IDs of goal categories.
sortOrdernumberDisplay order.

Example

# GET response shape
{
  "goals": [
    {
      "id": "abc123",
      "title": "Improve retention by 20%",
      "description": "",
      "color": "#6366f1",
      "status": "active",
      "categoryIds": [],
      "sortOrder": 0,
      "createdAt": "2026-03-04T10:00:00.000Z"
    }
  ],
  "links": [
    { "goalId": "abc123", "itemId": "local-xyz" }
  ]
}

Theme Rows

Theme rows appear as horizontal swimlanes in the Kanban view. Projects and initiatives are assigned to a row via kanbanRowId.

GET
/api/v1/rows

List all theme rows, ordered by sort order.

POST
/api/v1/rows

Create a theme row.

PUT
/api/v1/rows/{id}

Update a theme row.

DELETE
/api/v1/rows/{id}

Delete a theme row.

Fields

FieldTypeDescription
name*stringRow name, e.g. 'Growth' (max 200 chars).
goalstringOptional goal description for this row.

Example

curl -X POST https://app.ladder.fish/api/v1/rows \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Platform", "goal": "Improve infrastructure reliability" }'

# Response (201)
{ "id": "550e8400-...", "name": "Platform", "goal": "Improve infrastructure reliability", "sortOrder": 2 }

Your key

Who a key belongs to, which team it acts on, and which teams its owner can reach. A key is bound to exactly one team, fixed when it was created — these endpoints are how you tell which.

GET
/api/v1/me

The key's owner, its team, and the modules it can reach.

GET
/api/v1/me/teams

Every team the key's owner belongs to, with role and modules per team.

Response Fields — /api/v1/me

FieldTypeDescription
idstringUser ID of the key's owner.
namestringDisplay name.
emailstringEmail address.
rolestringRole on this key's team — one of "admin", "member", or "viewer".
teamobjectThe team this key acts on: { id, name }.
appsstring[]Modules the owner can reach in that team.

Response Fields — /api/v1/me/teams

FieldTypeDescription
teamsarrayOne entry per team, each with id, name, role, apps, and current.
currentstringID of the team this key acts on.

Example

curl https://app.ladder.fish/api/v1/me/teams \
  -H "Authorization: Bearer fl_your_key_here"

# Response
{
  "teams": [
    { "id": "org_abc", "name": "Acme", "role": "admin", "apps": ["roadmap", "tracker"], "current": true },
    { "id": "org_xyz", "name": "Northwind", "role": "member", "apps": ["tracker"], "current": false }
  ],
  "current": "org_abc"
}

A key resolves its creator's access on every request, so demoting them — or revoking a module — narrows the key immediately. Role and modules differ per team, which is why each entry carries its own.

Members

Read-only list of team members.

GET
/api/v1/members

List active members of the team. ?includeInactive=true adds deactivated people, marked active: false.

Response Fields

FieldTypeDescription
idstringUser ID.
namestringDisplay name.
emailstringEmail address.
avatarUrlstring | nullProfile image URL.
rolestringOne of "admin", "member", or "viewer".
jobTitlestring | nullJob title set by an admin.
departmentstring | nullDepartment set by an admin.
activebooleanfalse only for deactivated people, who appear only with ?includeInactive=true and can't be assigned work.

Example

curl https://app.ladder.fish/api/v1/members \
  -H "Authorization: Bearer fl_your_key_here"

# Response
[
  {
    "id": "user_abc",
    "name": "Jane Smith",
    "email": "[email protected]",
    "avatarUrl": "https://...",
    "role": "admin",
    "jobTitle": "Engineering Manager",
    "department": "Engineering"
  }
]

Member Profiles

Update job title and department for team members. Admin-only.

PUT
/api/admin/member-profiles

Update a member's job title and/or department.

Request Body

FieldTypeDescription
userId*stringThe ID of the team member to update.
jobTitlestringJob title to set. Omit to leave unchanged.
departmentstringDepartment to set. Omit to leave unchanged.

Example

curl -X PUT https://app.ladder.fish/api/admin/member-profiles \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user_abc",
    "jobTitle": "Senior Engineer",
    "department": "Engineering"
  }'

# Response
{ "ok": true }

Jobs

Hiring jobs (positions). Creating a job automatically generates default pipeline stages.

GET
/api/v1/jobs

List all jobs for the team.

GET
/api/v1/jobs/{id}

Get a single job.

POST
/api/v1/jobs

Create a job with default pipeline stages.

PUT
/api/v1/jobs/{id}

Update a job.

DELETE
/api/v1/jobs/{id}

Delete a job.

Fields

FieldTypeDescription
title*stringJob title (max 500 chars).
departmentstringDepartment name.
locationstringOffice location.
typestringfull-time · part-time · contract · internship.
remotePolicystringonsite · hybrid · remote.
salaryMinnumberMinimum salary (PUT only).
salaryMaxnumberMaximum salary (PUT only).
salaryCurrencystringCurrency code, e.g. USD (PUT only).
descriptionstringJob description (HTML or plain text).
requirementsstringJob requirements.
statusstringdraft · open · closed · archived. Defaults to draft.

Example

curl -X POST https://app.ladder.fish/api/v1/jobs \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Senior Frontend Engineer",
    "department": "Engineering",
    "location": "San Francisco",
    "type": "full-time",
    "remotePolicy": "hybrid",
    "status": "open"
  }'

# Response (201)
{
  "id": "abc123",
  "title": "Senior Frontend Engineer",
  "department": "Engineering",
  "location": "San Francisco",
  "type": "full-time",
  "remotePolicy": "hybrid",
  "status": "open",
  "salaryMin": null,
  "salaryMax": null,
  "salaryCurrency": "USD",
  "description": null,
  "requirements": null,
  "publishedAt": null,
  "closedAt": null,
  "sortOrder": 0,
  "createdAt": "2026-03-06T10:00:00.000Z",
  "updatedAt": "2026-03-06T10:00:00.000Z"
}

Candidates

People in your hiring pipeline. Candidates can be applied to one or more jobs via applications.

GET
/api/v1/candidates

List all candidates for the team.

GET
/api/v1/candidates/{id}

Get a single candidate.

POST
/api/v1/candidates

Create a candidate.

PUT
/api/v1/candidates/{id}

Update a candidate.

DELETE
/api/v1/candidates/{id}

Delete a candidate.

Fields

FieldTypeDescription
firstName*stringFirst name (max 200 chars).
lastName*stringLast name (max 200 chars).
email*stringEmail address (max 500 chars).
phonestringPhone number.
linkedinUrlstringLinkedIn profile URL.
portfolioUrlstringPortfolio or personal site URL.
locationstringCandidate location.
sourcestringHow the candidate was sourced (e.g. referral, linkedin, website).
sourceDetailstringAdditional source details.
tagsstring[]Tags for categorization (PUT only).
customFieldsobjectKey-value map of custom field names to values. Returned on GET, accepted on POST/PUT.

Example

curl -X POST https://app.ladder.fish/api/v1/candidates \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Jane",
    "lastName": "Smith",
    "email": "[email protected]",
    "linkedinUrl": "https://linkedin.com/in/janesmith",
    "source": "referral"
  }'

# Response (201)
{
  "id": "cand-abc123",
  "firstName": "Jane",
  "lastName": "Smith",
  "email": "[email protected]",
  "phone": null,
  "linkedinUrl": "https://linkedin.com/in/janesmith",
  "portfolioUrl": null,
  "location": null,
  "source": "referral",
  "sourceDetail": null,
  "resumeUrl": null,
  "avatarUrl": null,
  "tags": [],
  "createdAt": "2026-03-06T10:00:00.000Z",
  "updatedAt": "2026-03-06T10:00:00.000Z"
}

Applications

An application connects a candidate to a job. Move candidates through pipeline stages by updating the stageId.

GET
/api/v1/applications

List applications. Optional: ?jobId=...&candidateId=...

GET
/api/v1/applications/{id}

Get a single application.

POST
/api/v1/applications

Create an application (apply a candidate to a job).

PUT
/api/v1/applications/{id}

Update stage, status, rating, or rejection reason.

DELETE
/api/v1/applications/{id}

Delete an application.

Fields

FieldTypeDescription
jobId*stringID of the job.
candidateId*stringID of the candidate.
stageIdstringPipeline stage ID. Update this to move candidates through stages.
statusstringactive · hired · rejected. Defaults to active.
rejectionReasonstringReason for rejection (when status is rejected).
ratingnumberRating 1–5.

Example

# Apply a candidate to a job
curl -X POST https://app.ladder.fish/api/v1/applications \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "jobId": "job-abc123",
    "candidateId": "cand-def456",
    "stageId": "job-abc123-stage-0"
  }'

# Move candidate to next stage
curl -X PUT https://app.ladder.fish/api/v1/applications/app-xyz \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "stageId": "job-abc123-stage-1" }'

# Mark as hired
curl -X PUT https://app.ladder.fish/api/v1/applications/app-xyz \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "status": "hired" }'

Custom Fields

Custom fields let you add team-specific data to tasks, projects, and candidates. Define fields in Settings → Custom Fields, then read and write values via the API.

GET
/api/v1/custom-fields

List custom field definitions. Optional: ?entityType=task|candidate|project

Field definition shape

FieldTypeDescription
idstringUnique field ID.
entityTypestringtask, candidate, or project.
namestringDisplay name (used as key in customFields objects).
descriptionstringHelp text for the field.
fieldTypestringtext, number, select, multi_select, date, checkbox, people, or formula.
optionsobject[]For select/multi_select: array of {'{value, label, color}'}.
configobjectType-specific config (number format, formula expression, etc.).
requiredbooleanWhether the field is required.

Reading custom fields

All GET responses for tasks, candidates, and projects include a customFields object. Keys are field names, values are typed (string, number, boolean, string[]).

// GET /api/v1/tasks response
{
  "id": "task-abc",
  "title": "Review Q2 metrics",
  "customFields": {
    "Priority Score": 85,
    "Region": "EMEA",
    "Approved": true
  }
}

Writing custom fields

Include a customFields object on POST or PUT requests. Use field names as keys. Unknown field names return a 400 error. Formula fields are read-only (computed automatically).

curl -X POST https://app.ladder.fish/api/v1/tasks \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Review Q2 metrics",
    "projectId": "proj-abc123",
    "customFields": {
      "Priority Score": 85,
      "Region": "EMEA",
      "Approved": true
    }
  }'

List definitions

curl "https://app.ladder.fish/api/v1/custom-fields?entityType=task" \
  -H "Authorization: Bearer fl_your_key_here"

# Response
[
  {
    "id": "cf-abc",
    "entityType": "task",
    "name": "Priority Score",
    "fieldType": "number",
    "options": null,
    "config": { "numberFormat": { "format": "plain", "precision": 0 } },
    "required": false,
    "sortOrder": 0
  }
]

CRM Contacts

People in your CRM. Contacts can be linked to companies, deals, and activities.

GET
/api/v1/crm/contacts

List all contacts for the team.

POST
/api/v1/crm/contacts

Create a contact.

GET
/api/v1/crm/contacts/{id}

Get a single contact.

PUT
/api/v1/crm/contacts/{id}

Update a contact.

DELETE
/api/v1/crm/contacts/{id}

Delete a contact.

Fields

FieldTypeDescription
firstName*stringFirst name.
lastName*stringLast name.
emailstringEmail address.
phonestringPhone number.
titlestringJob title.
companyIdstringID of the associated company.
sourcestringwebsite, referral, linkedin, cold_outreach, event, inbound, or other.
lifecycleStagestringprospecting, qualification, meeting, proposal, negotiation, opportunity_won, opportunity_lost, or customer.
tagsstring[]Tags for categorization.
ownerIdstringID of the team member who owns this contact.
linkedinUrlstringLinkedIn profile URL.

Response Fields (read-only)

FieldTypeDescription
companyNamestring | nullName of the associated company (if any). Read-only — set companyId to associate.

Example

curl -X POST https://app.ladder.fish/api/v1/crm/contacts \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Jane",
    "lastName": "Smith",
    "email": "[email protected]",
    "title": "VP of Engineering",
    "source": "linkedin",
    "lifecycleStage": "prospecting"
  }'

# Response (201)
{
  "id": "Cabc123-...",
  "firstName": "Jane",
  "lastName": "Smith",
  "email": "[email protected]",
  "phone": null,
  "title": "VP of Engineering",
  "companyId": null,
  "companyName": null,
  "source": "linkedin",
  "lifecycleStage": "prospecting",
  "tags": [],
  "ownerId": null,
  "linkedinUrl": null,
  "createdAt": "2026-03-13T10:00:00.000Z",
  "updatedAt": "2026-03-13T10:00:00.000Z"
}

CRM Companies

Organizations in your CRM. Companies can be linked to contacts and deals.

GET
/api/v1/crm/companies

List all companies for the team.

POST
/api/v1/crm/companies

Create a company.

GET
/api/v1/crm/companies/{id}

Get a single company.

PUT
/api/v1/crm/companies/{id}

Update a company.

DELETE
/api/v1/crm/companies/{id}

Delete a company.

Fields

FieldTypeDescription
name*stringCompany name.
domainstringCompany domain (e.g. example.com).
industrystringIndustry vertical.
sizestring1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, or 10001+.
websitestringCompany website URL.
phonestringCompany phone number.
addressLine1stringStreet address line 1.
addressLine2stringStreet address line 2.
citystringCity.
statestringState or region.
countrystringCountry.
postalCodestringPostal or ZIP code.
lifecycleStagestringprospecting, qualification, meeting, proposal, negotiation, opportunity_won, opportunity_lost, or customer.
tagsstring[]Tags for categorization.
ownerIdstringID of the team member who owns this company.
logoUrlstringURL to the company logo.

Example

curl -X POST https://app.ladder.fish/api/v1/crm/companies \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Corp",
    "domain": "acme.com",
    "industry": "SaaS",
    "size": "51-200",
    "website": "https://acme.com"
  }'

# Response (201)
{
  "id": "Aabc123-...",
  "name": "Acme Corp",
  "domain": "acme.com",
  "industry": "SaaS",
  "size": "51-200",
  "website": "https://acme.com",
  "phone": null,
  "addressLine1": null,
  "addressLine2": null,
  "city": null,
  "state": null,
  "country": null,
  "postalCode": null,
  "lifecycleStage": "prospecting",
  "tags": [],
  "ownerId": null,
  "logoUrl": null,
  "trackerProjectId": null,
  "createdAt": "2026-03-13T10:00:00.000Z",
  "updatedAt": "2026-03-13T10:00:00.000Z"
}

CRM Deals

Revenue opportunities in your pipeline. Deals are linked to a pipeline and can be associated with contacts and companies.

GET
/api/v1/crm/deals

List all deals for the team.

POST
/api/v1/crm/deals

Create a deal.

GET
/api/v1/crm/deals/{id}

Get a single deal.

PUT
/api/v1/crm/deals/{id}

Update a deal.

DELETE
/api/v1/crm/deals/{id}

Delete a deal.

Fields

FieldTypeDescription
title*stringDeal title.
valueintegerDeal value in cents (e.g. 50000 = $500.00).
currencystringISO 4217 currency code. Defaults to USD.
pipelineId*stringID of the pipeline this deal belongs to.
stageIdstringID of the pipeline stage. Defaults to first stage.
companyIdstringID of the associated company.
contactIdstringID of the primary contact.
ownerIdstringID of the team member who owns this deal.
probabilitynumberWin probability, 0–100.
expectedCloseDatestringExpected close date (ISO 8601).
statusstringopen, won, or lost. Defaults to open.
lostReasonstringReason for losing the deal (when status is lost).
tagsstring[]Tags for categorization.

Example

curl -X POST https://app.ladder.fish/api/v1/crm/deals \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Acme Corp — Enterprise Plan",
    "value": 12000000,
    "currency": "USD",
    "pipelineId": "pipe-abc123",
    "companyId": "company-abc123",
    "contactId": "contact-abc123",
    "probability": 60,
    "expectedCloseDate": "2026-06-30"
  }'

# Response (201)
{
  "id": "deal-abc123",
  "title": "Acme Corp — Enterprise Plan",
  "value": 12000000,
  "currency": "USD",
  "pipelineId": "pipe-abc123",
  "stageId": "stage-qualified",
  "companyId": "company-abc123",
  "contactId": "contact-abc123",
  "ownerId": null,
  "probability": 60,
  "expectedCloseDate": "2026-06-30",
  "status": "open",
  "lostReason": null,
  "tags": [],
  "createdAt": "2026-03-13T10:00:00.000Z",
  "updatedAt": "2026-03-13T10:00:00.000Z"
}

CRM Pipelines

Sales pipelines define the stages a deal moves through. Creating a pipeline automatically creates default stages.

GET
/api/v1/crm/pipelines

List all pipelines for the team.

POST
/api/v1/crm/pipelines

Create a pipeline.

GET
/api/v1/crm/pipelines/{id}

Get a single pipeline with stages.

PUT
/api/v1/crm/pipelines/{id}

Update a pipeline.

DELETE
/api/v1/crm/pipelines/{id}

Delete a pipeline.

Fields

FieldTypeDescription
name*stringPipeline name.
isDefaultbooleanWhether this is the default pipeline for new deals.

Creating a pipeline automatically creates default stages: Qualified, Meeting Booked, Proposal Sent, Negotiation, Closed Won, and Closed Lost.

Example

curl -X POST https://app.ladder.fish/api/v1/crm/pipelines \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Enterprise Sales",
    "isDefault": false
  }'

# Response (201)
{
  "id": "pipe-abc123",
  "name": "Enterprise Sales",
  "isDefault": false,
  "stages": [
    { "id": "stage-1", "name": "Qualified", "order": 0 },
    { "id": "stage-2", "name": "Meeting Booked", "order": 1 },
    { "id": "stage-3", "name": "Proposal Sent", "order": 2 },
    { "id": "stage-4", "name": "Negotiation", "order": 3 },
    { "id": "stage-5", "name": "Closed Won", "order": 4 },
    { "id": "stage-6", "name": "Closed Lost", "order": 5 }
  ],
  "createdAt": "2026-03-13T10:00:00.000Z",
  "updatedAt": "2026-03-13T10:00:00.000Z"
}

CRM Activities

Log calls, emails, meetings, notes, and tasks against contacts, companies, and deals.

GET
/api/v1/crm/activities

List activities. Optional: ?contactId=...&companyId=...&dealId=...

POST
/api/v1/crm/activities

Create an activity.

GET
/api/v1/crm/activities/{id}

Get a single activity.

PUT
/api/v1/crm/activities/{id}

Update an activity.

DELETE
/api/v1/crm/activities/{id}

Delete an activity.

Fields

FieldTypeDescription
type*stringcall, email, meeting, note, or task.
subjectstringActivity subject or title.
bodystringActivity body or notes.
durationMinsintegerDuration in minutes.
scheduledAtstringScheduled date/time (ISO 8601).
completedAtstringCompletion date/time (ISO 8601).
contactIdstringID of the associated contact.
companyIdstringID of the associated company.
dealIdstringID of the associated deal.
isPrivatebooleanWhether this activity is private to the owner.

Example

curl -X POST https://app.ladder.fish/api/v1/crm/activities \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "meeting",
    "subject": "Discovery call with Acme",
    "body": "Discussed enterprise plan requirements and timeline.",
    "durationMins": 45,
    "contactId": "contact-abc123",
    "companyId": "company-abc123",
    "dealId": "deal-abc123",
    "completedAt": "2026-03-13T14:00:00.000Z"
  }'

# Response (201)
{
  "id": "activity-abc123",
  "type": "meeting",
  "subject": "Discovery call with Acme",
  "body": "Discussed enterprise plan requirements and timeline.",
  "durationMins": 45,
  "scheduledAt": null,
  "completedAt": "2026-03-13T14:00:00.000Z",
  "contactId": "contact-abc123",
  "companyId": "company-abc123",
  "dealId": "deal-abc123",
  "isPrivate": false,
  "createdAt": "2026-03-13T10:00:00.000Z",
  "updatedAt": "2026-03-13T10:00:00.000Z"
}

CPQ Products

Manage your product catalog. Products represent items and services available for quoting.

GET
/api/v1/cpq/products

List all active products.

POST
/api/v1/cpq/products

Create a product (admin only).

PUT
/api/v1/cpq/products/{id}

Update a product.

DELETE
/api/v1/cpq/products/{id}

Archive a product (sets isActive to false).

Fields

FieldTypeDescription
name*stringProduct name (max 500 chars).
descriptionstringProduct description.
skustringStock keeping unit / product code.
productTypestringone_time, recurring, or usage_based. Defaults to one_time.
unitPriceCents*integerUnit price in cents (e.g. 9900 = $99.00).
costPriceCentsintegerCost price in cents for margin tracking.
currencystringISO currency code. Defaults to USD.
billingIntervalstringmonthly, quarterly, or annual (for recurring products).
taxRateintegerTax rate in basis points (e.g. 1000 = 10%).
categorystringProduct category.
subcategorystringProduct subcategory.
isActivebooleanWhether the product is active. Defaults to true.
metadataobjectArbitrary key-value metadata.

Example

curl -X POST https://app.ladder.fish/api/v1/cpq/products \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pro Plan",
    "productType": "recurring",
    "unitPriceCents": 9900,
    "billingInterval": "monthly",
    "currency": "USD"
  }'

CPQ Quotes

Create and manage quotes. Quotes follow a lifecycle: draft → sent → viewed → accepted/declined/expired.

GET
/api/v1/cpq/quotes

List quotes. Filter with ?status=draft|sent|accepted|etc.

POST
/api/v1/cpq/quotes

Create a draft quote (admin only).

GET
/api/v1/cpq/quotes/{id}

Get quote with line items and sections.

PUT
/api/v1/cpq/quotes/{id}

Update a quote.

DELETE
/api/v1/cpq/quotes/{id}

Delete a draft quote.

POST
/api/v1/cpq/quotes/{id}/send

Send the quote — emails a branded PDF with a secure magic link to the recipient. Returns a shareable link. Only works on draft/approved quotes with a recipient email.

Fields

FieldTypeDescription
title*stringQuote title (max 500 chars).
recipientNamestringRecipient's name.
recipientEmailstringRecipient's email address.
recipientCompanystringRecipient's company name.
currencystringISO currency code. Defaults to USD.
validUntilstringExpiration date (ISO 8601).
paymentTermsstringPayment terms text.
notesstringNotes visible to the recipient.
internalNotesstringInternal notes (not visible to recipient).
templateIdstringQuote template ID for branding.
dealIdstringCRM deal ID to link this quote to.
contactIdstringCRM contact ID.
companyIdstringCRM company ID.

Example — Create a quote

curl -X POST https://app.ladder.fish/api/v1/cpq/quotes \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Enterprise License — Q2 2026",
    "recipientName": "Jane Smith",
    "recipientEmail": "[email protected]",
    "recipientCompany": "Acme Corp",
    "validUntil": "2026-06-30",
    "paymentTerms": "Net 30"
  }'

Example — Get a quote with line items

# GET /api/v1/cpq/quotes/{id} returns quote with line items
{
  "quote": { "id": "...", "quoteNumber": 1, "title": "...", "status": "draft", ... },
  "lineItems": [...],
  "sections": [...]
}

Example — Send a quote

Sends a branded email to the recipient with a secure magic link and an attached PDF. The recipient verifies their identity via the magic link (24hr, single-use) or email OTP to view the interactive quote. Only works on draft or approved quotes that have a recipient email.

curl -X POST https://app.ladder.fish/api/v1/cpq/quotes/{id}/send \
  -H "Authorization: Bearer fl_your_key_here"

# Response:
# { "shareToken": "abc-123", "shareUrl": "https://app.ladder.fish/q/abc-123" }
#
# Side effects:
# - Quote status → "sent", sentAt timestamp recorded
# - Branded email with PDF attachment sent to recipientEmail
# - Magic link token generated (24hr expiry, single-use)

CPQ Line Items

Manage line items on a quote. Totals are auto-calculated when items are added or updated.

GET
/api/v1/cpq/quotes/{quoteId}/line-items

List line items for a quote.

POST
/api/v1/cpq/quotes/{quoteId}/line-items

Add a line item.

PUT
/api/v1/cpq/quotes/{quoteId}/line-items/{id}

Update a line item.

DELETE
/api/v1/cpq/quotes/{quoteId}/line-items/{id}

Remove a line item.

Fields

FieldTypeDescription
name*stringLine item name (max 500 chars).
descriptionstringLine item description.
productIdstringLink to a catalog product (optional for custom items).
quantity*integerQuantity. Defaults to 1.
unitPriceCents*integerUnit price in cents.
discountTypestringpercentage or fixed.
discountValueintegerDiscount amount (percent or cents).
taxRateintegerTax rate in basis points.
billingIntervalstringmonthly, quarterly, or annual.
sectionIdstringQuote section ID for grouping.
sortOrderintegerDisplay order.

Example

curl -X POST https://app.ladder.fish/api/v1/cpq/quotes/{quoteId}/line-items \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pro Plan — Annual",
    "quantity": 10,
    "unitPriceCents": 9900,
    "billingInterval": "annual",
    "taxRate": 1000
  }'

Labeler work tracking

For third-party labeling platforms. Every call runs on a team API key whose creator is an admin; the labeler is named per item by labelerEmail or labelerUserId (or a nested labeler: { email | userId }). A task that carries an externalId is unique per source (default api), so pushing the same batch again updates instead of duplicating. A labeler not yet assigned to the project is assigned by the call.

GET
/api/v1/labeler/projects

Active labeler projects with their labelers and managers. ?includeArchived=true adds archived ones.

GET
/api/v1/labeler/labelers

Labeler-role members and the projects they are assigned to.

GET
/api/v1/labeler/tasks

List tasks. Filters: projectId, userId, email, status, source, externalId, from, to, limit (≤500).

POST
/api/v1/labeler/tasks

Create or update one task or an array of up to 500. Returns the task (201 created / 200 updated), or {created, updated, failed, results} for an array.

GET
/api/v1/labeler/tasks/{id}

Fetch one task.

PATCH
/api/v1/labeler/tasks/{id}

Update any task field; naming another labeler reassigns it.

POST
/api/v1/labeler/time

Log a block of time on behalf of a labeler (source api).

POST
/api/v1/labeler/units

Log completed units on behalf of a labeler.

Task fields

FieldTypeDescription
projectId*stringLabeler project. Required on create; cannot change on an existing task.
labelerEmail / labelerUserId*stringWho the work belongs to. Required on create; on update, naming someone else reassigns the task.
externalIdstringYour id for the task (max 200 chars). Unique per source; drives idempotent updates.
sourcestringapi · manual · ui. Defaults to api.
title*stringRequired on create (max 500 chars).
descriptionstringMax 5000 chars.
statusstringassigned · in_progress · done · rejected. Defaults to assigned. Entering done stamps completedAt unless you send one.
quantityintegerUnits of work in the project's unit label. Defaults to 1.
durationSecondsintegerTime spent, if the platform tracks it (≤ 30 days). null clears.
startedAt / completedAtstringISO timestamps. null clears.
metadataobjectFree-form platform data, ≤ 16 KB serialised. null clears.

Time and unit fields

FieldTypeDescription
projectId*stringLabeler project.
labelerEmail / labelerUserId*stringWho the work belongs to.
durationSeconds*integerTime only. 1 to 86400. startedAt (ISO) is optional; the block ends now by default.
quantity*integerUnits only. Positive integer. occurredAt (ISO) is optional.
description / notestringMax 500 chars.

Example

# Push a batch (idempotent on externalId)
curl -X POST https://app.ladder.fish/api/v1/labeler/tasks \
  -H "Authorization: Bearer fl_..." \
  -H "Content-Type: application/json" \
  -d '[
    { "projectId": "proj_123", "labelerEmail": "[email protected]",
      "externalId": "batch-7/item-12", "title": "Label 40 frames", "quantity": 40, "status": "done" },
    { "projectId": "proj_123", "labelerEmail": "[email protected]",
      "externalId": "batch-7/item-13", "title": "Label 40 frames", "quantity": 40 }
  ]'

# Advance one task later
curl -X PATCH https://app.ladder.fish/api/v1/labeler/tasks/TASK_ID \
  -H "Authorization: Bearer fl_..." -H "Content-Type: application/json" \
  -d '{ "status": "done", "durationSeconds": 1800 }'

Webhook events: labeler.task.created, labeler.task.updated, labeler.task.completed (fires once per transition into done). Payload: { task, previousStatus }.

River Spaces

Top-level groupings in the River wiki.

GET
/api/v1/river/spaces

List all wiki spaces. parentSpaceId is set when a space sits inside another space (null at the top level).

POST
/api/v1/river/spaces

Create a wiki space.

Fields

FieldTypeDescription
name*stringSpace name (max 200 chars).
slugstringURL slug. Derived from the name when omitted; suffixed if taken.
descriptionstringSpace description.
iconstringIcon identifier.

River Pages

Wiki pages, organised as a tree via parentId. Content is submitted and returned as markdown — the server converts to the editor's native format. Updating content snapshots the previous version.

GET
/api/v1/river/pages

List pages (structure only, no content). Filters: ?spaceId=, ?includeArchived=true. Max 1000 rows.

POST
/api/v1/river/pages

Create a page.

GET
/api/v1/river/pages/{id}

Get a page including contentMarkdown.

PUT
/api/v1/river/pages/{id}

Update a page. Content replacement snapshots the previous version.

Fields

FieldTypeDescription
title*stringPage title (max 500 chars). Required on POST.
spaceId*stringSpace the page belongs to. Required on POST.
parentIdstringParent page ID for tree placement.
contentMarkdownstringPage content as markdown. Supports headings, lists, task lists, blockquotes, code blocks, links, and inline marks.
versionSummarystringPUT only — label for the version snapshot taken before content is replaced.
archivedbooleanPUT only — archive (true) or restore (false) the page.
iconstringIcon identifier.
positionnumberSort position among siblings.

Moving a page: PUT with spaceId and/or parentId (null for top level). Child pages move with it; cycles and a parent in a different space are refused.

Location

GET, POST and PUT on a single page return location — where the page lives. The list endpoint adds spaceName to each row.

FieldTypeDescription
location.spaceobject | null{ id, name } of the page's space.
location.spaceParentsarraySpaces that space sits inside, outermost first: [{ id, name }]. Empty for a top-level space.
location.parentsarrayAncestor pages, outermost first: [{ id, title }]. Empty for a top-level page.
location.breadcrumbstring"Space / Parent / Page" (outer spaces first when the space is nested) — ready to show a person.

Example

# Create a page from markdown
curl -X POST https://app.ladder.fish/api/v1/river/pages \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "API Guide",
    "spaceId": "space-id-here",
    "contentMarkdown": "## Overview\n\nStart **here**."
  }'

River References

Links between wiki pages and other Fishladder entities. Linking checks that the key's owner holds the module the entity lives in (403 otherwise — e.g. Hiring for a candidate) and that it exists in the team (404 otherwise); tasks and boards also follow the board-group rule. The entity's real title is recorded from the entity itself. Keys with no recorded creator can't link.

GET
/api/v1/river/pages/{id}/references

List entity links on a page.

POST
/api/v1/river/pages/{id}/references

Link a page to an entity.

Fields

FieldTypeDescription
entityType*stringroadmap_project · roadmap_initiative · tracker_task · tracker_project · ts_questionnaire · ts_control · hiring_job · hiring_candidate · crm_company · crm_contact · crm_deal · cpq_quote · goal
entityId*stringID of the linked entity.
linkTypestringattached · embed · inbound. Defaults to attached.
pageAnchorstringHeading anchor on the page for deep links.
entityTitleSnapshotstringIgnored except for ts_control (Drata controls live outside Fishladder); every other entity's title is read from the record.

Canvases

Interactive HTML documents that Fishladder hosts and renders in a sandboxed frame — for diagrams whose interactivity carries the meaning, such as hover detail or expandable nodes. For a static flowchart, a Mermaid code block in a wiki page is lighter. Gated by the River module, so a viewer can read a canvas and a member can publish one.

Submit a complete, self-contained document. Inline all CSS and JavaScript and embed images as data: URIs: the page renders under a policy that blocks every outbound request, so external stylesheets, fonts, scripts and image URLs will not load. Scripts run normally.

GET
/api/v1/canvas

List canvases (metadata and byte size only, without the documents).

POST
/api/v1/canvas

Publish a canvas. Returns its id and URL.

GET
/api/v1/canvas/{id}

Get a canvas including its full HTML.

PUT
/api/v1/canvas/{id}

Update. Only the fields you send change — a rename leaves the document untouched.

DELETE
/api/v1/canvas/{id}

Delete a canvas. Admin only.

GET
/api/v1/canvas/{id}/versions

Past documents, newest first. The 20 most recent are kept.

POST
/api/v1/canvas/{id}/versions

Restore a past document. Body: { versionId }. The restore is itself snapshotted.

An update replaces the whole document, so the previous one is snapshotted first — a truncated write is recoverable. Renaming does not create a version.

Fields

FieldTypeDescription
title*stringShort name for the diagram (max 500 chars).
html*stringThe complete document. Max 2MB, measured in UTF-8 bytes.
descriptionstringOne-line summary of what the diagram shows.

Presentations

AI-built PowerPoint decks assembled from your roadmap, projects, goals and feedback. Reads need any valid key; every write requires an admin key.

Building is asynchronous. Creating returns 202 with a deck that does not exist yet — poll the detail endpoint until status reaches complete or failed. Limited to 10 builds an hour per team, 2 concurrent. Slides are PowerPoint and therefore static; for an interactive diagram publish a canvas and link to it.

GET
/api/v1/presentations

List decks with build status.

POST
/api/v1/presentations

Start a build. Returns 202 — the deck is not ready. Admin only.

GET
/api/v1/presentations/{id}

One deck with its slides and live build status. Poll this.

POST
/api/v1/presentations/{id}/slides

Add a slide. Appended unless sortOrder is given. Admin only.

PUT
/api/v1/presentations/{id}/slides

Edit a slide. Body: { slideId, title?, content?, sortOrder? }. Admin only.

Fields

FieldTypeDescription
title*stringDeck title (max 200 chars).
audienceTypestringexecutive · engineering · investor · client · team. Defaults to executive.
themestringdefault · dark · minimal · bold, or brand:{id} for a saved brand. Defaults to default.
configobjectWhich data to include: includeInitiatives, includeProjects, includeGoals, includeFeedback (booleans), and additionalNotes (string, capped at 2000 chars).

Slide fields

FieldTypeDescription
title*stringSlide title.
slideTypestringtitle · section_divider · narrative_text · key_metrics · custom. Defaults to custom.
contentobjectnarrative_text takes { body }; title and section_divider need none; everything else takes { bullets: [...] }.
sortOrdernumber0-based position. Appended when omitted.

AI

AI endpoints for triggering behaviors, reviewing actions, and managing settings. All v1 endpoints accept Bearer token authentication (API key), making them accessible from the CLI.

CLI-friendly endpoints (v1)

These use API key auth and are designed for CLI and programmatic use.

POST
/api/v1/ai/chat

Conversational AI assistant (SSE streaming).

POST
/api/v1/ai/generate-tasks

Extract tasks from a transcript.

POST
/api/v1/ai/ingest

Ingest text for AI feedback analysis.

POST
/api/v1/ai/suggest-priority

Suggest priority for a task.

POST
/api/v1/ai/suggest-assignment

Suggest an assignee for a task.

POST
/api/v1/ai/score

Score a candidate for a job.

POST
/api/v1/ai/detect-blocked

Find blocked/stalled tasks in a project.

POST
/api/v1/ai/auto-group

Auto-group ungrouped feedback entries.

POST
/api/v1/ai/suggest-roadmap

Suggest projects from feedback.

POST
/api/v1/ai/connect-goals

Auto-link projects to goals.

POST
/api/v1/ai/pipeline-bottlenecks

Detect hiring pipeline bottlenecks.

GET
/api/v1/ai/signals

List active AI signals. ?module=...

GET
/api/v1/ai/actions

List AI actions. ?module=...&status=...&cursor=...

POST
/api/v1/ai/actions/{'{id}'}

Manage action: approve, reject, or undo.

GET
/api/v1/ai/settings

Get AI settings for the team.

CRM AI

AI-powered CRM features for deal scoring, action suggestions, pipeline analysis, and data enrichment.

POST
/api/v1/ai/crm/score-deal

Score a deal's likelihood of closing.

POST
/api/v1/ai/crm/suggest-actions

Get next action suggestions for a deal or contact.

POST
/api/v1/ai/crm/analyze-pipeline

Analyze pipeline health and forecast.

POST
/api/v1/ai/crm/draft-email

Draft an email to a contact.

POST
/api/v1/ai/crm/summarize-activities

Summarize activity timeline.

POST
/api/v1/ai/crm/enrich-contact

Enrich a contact with external data.

POST
/api/v1/ai/crm/enrich-company

Enrich a company with external data.

POST
/api/v1/ai/crm/search-contacts

Search for contacts at a company.

Examples

# Chat with AI from CLI
curl -N -X POST https://app.ladder.fish/api/v1/ai/chat \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "message": "What tasks are blocked this week?" }'

# Generate tasks from meeting notes
curl -X POST https://app.ladder.fish/api/v1/ai/generate-tasks \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "proj-abc123",
    "transcript": "We need to fix the login bug, update the docs, and add CSV export..."
  }'

# Suggest priority for a task
curl -X POST https://app.ladder.fish/api/v1/ai/suggest-priority \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "taskId": "task-xyz" }'

# Approve a pending AI action
curl -X POST https://app.ladder.fish/api/v1/ai/actions/action-id-here \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "action": "approve" }'

Session-authenticated endpoints

These use cookie session auth and are used by the Fishladder web UI.

GET
/api/ai/settings

Get AI settings for the current team.

PUT
/api/ai/settings

Update AI settings (admin only).

GET
/api/ai/actions

List AI actions with cursor pagination. ?module=...&status=...

POST
/api/ai/actions/{'{id}'}/approve

Approve a pending AI action (admin only).

POST
/api/ai/actions/{'{id}'}/reject

Reject a pending AI action (admin only).

POST
/api/ai/actions/{'{id}'}/undo

Undo an applied AI action with optional feedback (admin only).

GET
/api/ai/signals

List active AI signals. ?module=...

POST
/api/ai/chat

Send a message to the AI assistant (streaming SSE response).

POST
/api/ai/feedback/ingest

Ingest text for AI feedback analysis.

POST
/api/ai/tracker/generate-tasks

Generate tasks from a transcript.

POST
/api/ai/tracker/detect-blocked

Detect blocked/stalled tasks in a project.

POST
/api/ai/roadmap/suggest

Suggest roadmap projects from feedback.

POST
/api/ai/ats/score

Score a candidate for a job.

POST
/api/ai/ats/pipeline-bottlenecks

Detect pipeline bottlenecks for a job.

POST
/api/ai/feedback/auto-group

Auto-group ungrouped feedback entries.

POST
/api/ai/tracker/suggest-assignment

Suggest an assignee for a task.

POST
/api/ai/tracker/suggest-priority

Suggest priority for a task.

POST
/api/ai/roadmap/connect-goals

Suggest project-to-goal connections.

POST
/api/ai/cross/route-feedback

Route feedback to appropriate modules.

POST
/api/ai/cross/analyze-signals

Analyze accumulated signal patterns.

POST
/api/ai/cross/check-completion

Check follow-through on completed items.

Settings fields

FieldTypeDescription
defaultPermissionModestringact_freely, ask_before_acting, or human_only.
moduleOverridesobjectPer-module permission overrides, e.g. { "feedback": "act_freely" }.
enabledModulesstring[]Enabled modules: feedback, tracker, roadmap, ats, cross.

Rate limits

AI endpoints have their own rate limits: Chat is 60 requests/hour, feedback ingestion is 20 requests/hour. Other AI endpoints follow standard rate limits.

Webhooks

Subscribe to real-time event notifications. See the Integrations Guide for setup details and example payloads.

GET
/api/v1/webhooks

List active webhook subscriptions.

POST
/api/v1/webhooks

Subscribe to events.

PUT
/api/v1/webhooks/{id}

Update a subscription.

DELETE
/api/v1/webhooks/{id}

Unsubscribe.

Subscribe fields

FieldTypeDescription
targetUrl*stringURL to receive webhook POST requests.
events*string[]Event types to subscribe to (e.g. task.created, task.completed, labeler.task.completed).

Example

curl -X POST https://app.ladder.fish/api/v1/webhooks \
  -H "Authorization: Bearer fl_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "targetUrl": "https://hooks.zapier.com/hooks/catch/123/abc/",
    "events": ["task.created", "task.completed"]
  }'

Polling

Polling endpoints return recent items sorted newest-first. Use the optional since query param (ISO date) to only fetch items created/updated after a given time.

GET
/api/v1/poll/tasks

Recent tasks, newest first.

GET
/api/v1/poll/candidates

Recent candidates, newest first.

GET
/api/v1/poll/applications

Recent applications, newest first.

Example

curl "https://app.ladder.fish/api/v1/poll/tasks?since=2026-03-01T00:00:00Z" \
  -H "Authorization: Bearer fl_your_key_here"

Trust & Safety — Questionnaires

GET /api/trust-safety/questionnaires

List all questionnaires for the team.

POST /api/trust-safety/questionnaires

Create a new questionnaire. Requires admin role.

Body: id, company (required), title (required), status, themes

GET /api/trust-safety/questionnaires/[id]

Get a single questionnaire by ID.

PUT /api/trust-safety/questionnaires/[id]

Update a questionnaire. Requires admin role.

DELETE /api/trust-safety/questionnaires/[id]

Delete a questionnaire and all its questions. Requires admin role.

POST /api/trust-safety/questionnaires/duplicate

Duplicate a questionnaire for a different company. Questions are copied without answers.

Body: sourceId (required), company (required), title

POST /api/trust-safety/questionnaires/from-template

Create a questionnaire from a template (e.g. SIG Lite) with auto-fill from Drata and previous answers.

Body: templateId (required), company (required), title

Trust & Safety — Questions

GET /api/trust-safety/questions?questionnaireId=...

List questions for a questionnaire.

POST /api/trust-safety/questions

Create a question. Requires admin role.

PUT /api/trust-safety/questions/[id]

Update a question (answer, status, assignment, etc). Requires admin role.

DELETE /api/trust-safety/questions/[id]

Delete a question. Requires admin role.

Trust & Safety — Files

GET /api/trust-safety/files?questionId=...

List file attachments for a question.

POST /api/trust-safety/files

Upload a file attachment to a question (multipart form data, max 10MB).

DELETE /api/trust-safety/files?fileId=...

Delete a file attachment. Requires admin role.

GET /api/trust-safety/files/download?fileId=...

Get a presigned download URL for a file.

GET /api/trust-safety/company-files?company=...

List files attached to a company (not a specific questionnaire).

POST /api/trust-safety/company-files

Upload a file to a company. Multipart form data with file and company.

Trust & Safety — AI

POST /api/ai/trust-safety/auto-complete

Run AI auto-complete on all pending questions in a questionnaire. Requires admin role. Rate limited: 20/hour.

Body: questionnaireId (required)

POST /api/ai/trust-safety/question-chat

Chat with AI about a specific question. Supports conversation history. Rate limited: 60 AI chats per hour per team, separately from the read and write budgets.

Body: questionId (required), message (required), history (optional array)

POST /api/ai/trust-safety/suggest-assignment

AI suggests a team member to assign a question to. Requires admin role.

Trust & Safety — Other

GET /api/trust-safety/hydrate

Bulk load all questionnaires and questions for the team (used by the frontend store).

GET /api/trust-safety/activity?questionnaireId=...

Get the activity log for a questionnaire (answer changes, status changes, assignments).

GET /api/trust-safety/comments?questionId=...

List comments on a question.

POST /api/trust-safety/comments

Add a comment to a question.

GET /api/trust-safety/ai-settings

Get AI settings for the team (enabled toggles, Drive folder).

PUT /api/trust-safety/ai-settings

Update AI settings. Requires admin role.

Related guides

  • CLI Tool — Getting started with the fish CLI and common workflows. Every command is listed in the CLI reference.

Fishladder API v1 · Questions? Contact your workspace admin.