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.
| Field | Type | Description |
|---|---|---|
401 | Unauthorized | Missing or invalid API key. |
400 | Bad Request | Invalid or missing required fields. |
404 | Not Found | Resource not found or belongs to a different team. |
429 | Too Many Requests | Read 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). |
500 | Server Error | Something went wrong on our end. |
{ "error": "Not found" }Projects
Local projects — roadmap items created directly in Fishladder (not synced from Linear).
/api/v1/projectsList all projects for the team.
/api/v1/projectsCreate a project.
/api/v1/projects/{id}Update a project.
/api/v1/projects/{id}Delete a project.
Fields
| Field | Type | Description |
|---|---|---|
title* | string | Project title (max 500 chars). |
description | string | Project description. |
status | string | planned · in_progress · completed · cancelled · paused. Defaults to planned. |
initiativeId | string | ID of a parent initiative. |
kanbanColumn | string | Quarter column, e.g. q2-2026. |
kanbanRowId | string | Theme row ID. |
startDate | string | ISO date string, e.g. 2026-04-01. |
targetDate | string | ISO date string. |
trackerProjectId | string | ID 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. |
issueStatuses | array | Read-only. Task counts per status for the linked tracker project: [{ name, color, type, count }]. Empty when unlinked. |
customFields | object | Key-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.
/api/v1/tasksList tasks. Optional: ?projectId=...
/api/v1/tasksCreate a task.
/api/v1/tasks/{id}Get a single task (same shape as list items, plus customFields).
/api/v1/tasks/{id}Partially update a task. Only fields you send are changed.
/api/v1/tasks/{id}Delete a task. Returns { ok: true }.
/api/v1/tasks/{id}/drive-linksList the Google Drive files linked to a task (its Google Drive section).
/api/v1/tasks/{id}/drive-linksAdd a Drive / Docs / Sheets / Slides link. Body: { url }. Adding the same file again returns the existing link.
/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
| Field | Type | Description |
|---|---|---|
title* | string | Task title (max 500 chars). Required on POST. |
projectId* | string | ID of the tracker project. Required on POST. |
description | string | Task description. |
assigneeId | string | User ID of the assignee. |
priority | string | none / low / medium / high / urgent. Defaults to none. |
dueDate | string | Due date (ISO date string). |
sectionId | string | Section (board column) ID — discover valid IDs via the Sections & Statuses endpoints below. |
statusId | string | Status ID — discover valid IDs (with isDefault/isClosed) via the Sections & Statuses endpoints below. |
sortOrder | number | Display order. |
completedAt | string | null | PATCH only. ISO timestamp to mark complete, or null to reopen. |
status | string | PATCH only. Pass 'done' as a convenience to set completedAt to now. |
customFields | object | Key-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.
/api/v1/tracker/projectsList all tracker projects for the team.
Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Project ID — use this as projectId when creating tasks. |
name | string | Project name. |
groupId | string | null | ID of the project group this project belongs to. |
groupName | string | null | Display name of the project group. |
color | string | null | Project colour (hex). |
icon | string | null | Project icon. |
archived | boolean | Whether the project is archived. |
sortOrder | number | Display 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.
/api/v1/tracker/projects/{id}/sectionsList a project's board sections in display order.
/api/v1/tracker/projects/{id}/sectionsCreate a section. Body: name (required), sortOrder.
/api/v1/tracker/projects/{id}/statusesList 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.
/api/v1/calendarList items due in a date window, sorted by date.
Query Parameters
| Field | Type | Description |
|---|---|---|
days | integer | Days from today to cover, including today (default 14). Negative looks back: -7 is the past week. |
start | yyyy-MM-dd | Start date. Defaults to today. Combine with end or days. |
end | yyyy-MM-dd | Inclusive end date. Give end or days, not both. Max range 400 days. |
types | string | Comma-separated: task, roadmap, crm_close, cpq_expiry, interview, ts_due, milestone, event. Default: all. |
projects | string | Comma-separated tracker board or roadmap project names or IDs (partial names match). Scopes results to those projects' tasks and target dates. |
includeCompleted | boolean | Include finished tasks and completed roadmap projects. Default false. |
timezone | string | IANA zone deciding what "today" is, e.g. America/New_York. Default UTC. |
limit | integer | Maximum 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
| Field | Type | Description |
|---|---|---|
range | object | The resolved { start, end } window. |
projects | object | null | When projects was sent: matched (ref, kind board|roadmap, id, name) and unmatched names. If nothing matched, items is empty. |
count | number | Total items matched, before limit. |
counts | object | Matched items per type. |
items[].type | string | Source type (see types). typeLabel is its display name. |
items[].date / time | string | Due date (yyyy-MM-dd) and, for timed items, HH:mm. |
items[].status | string | null | Task status, roadmap status, quote status, etc. |
items[].project | object | null | { id, name } of the tracker board, for tasks and linked roadmap projects. |
items[].company | string | null | Company for deals, quotes and questionnaires. |
items[].url | string | null | Link 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.
/api/v1/initiativesList all initiatives for the team.
/api/v1/initiativesCreate an initiative.
/api/v1/initiatives/{id}Update an initiative.
/api/v1/initiatives/{id}Delete an initiative.
Fields
| Field | Type | Description |
|---|---|---|
title* | string | Initiative title (max 500 chars). |
description | string | Initiative description. |
status | string | planned · in_progress · completed · cancelled · paused. Defaults to planned. |
color | string | Hex color, e.g. #6366f1. |
kanbanRowId | string | Theme 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.
/api/v1/feedbackList all feedback entries, ordered by priority.
/api/v1/feedbackCreate a feedback entry.
/api/v1/feedback/{id}Update a feedback entry.
/api/v1/feedback/{id}Delete a feedback entry.
Fields
| Field | Type | Description |
|---|---|---|
title* | string | Short description of the feedback (max 500 chars). |
details | string | Full details (max 10,000 chars). |
problemSolved | string | What problem this solves (max 10,000 chars). |
names | string[] | Customer names associated with this feedback. Each name is truncated to 20 chars. |
company | string | Company name (max 200 chars). |
date | string | Date the feedback was received. |
status | string | open · in_progress · done. Defaults to open. |
impact | string | low · medium · high. Defaults to medium. |
groupId | string | ID of a feedback group. |
archived | boolean | Whether the entry is archived (PUT only). |
source | string | Provenance 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.
/api/v1/goalsList all goals and goal–item links.
/api/v1/goalsCreate a goal.
/api/v1/goals/{id}Update a goal.
/api/v1/goals/{id}Delete a goal and all its item links.
Fields
| Field | Type | Description |
|---|---|---|
title* | string | Goal title (max 500 chars). |
description | string | Goal description (max 5,000 chars). |
color | string | Hex color. Defaults to #6B7280. |
status | string | active · completed · cancelled. Defaults to active. |
categoryIds | string[] | IDs of goal categories. |
sortOrder | number | Display 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.
/api/v1/rowsList all theme rows, ordered by sort order.
/api/v1/rowsCreate a theme row.
/api/v1/rows/{id}Update a theme row.
/api/v1/rows/{id}Delete a theme row.
Fields
| Field | Type | Description |
|---|---|---|
name* | string | Row name, e.g. 'Growth' (max 200 chars). |
goal | string | Optional 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.
/api/v1/meThe key's owner, its team, and the modules it can reach.
/api/v1/me/teamsEvery team the key's owner belongs to, with role and modules per team.
Response Fields — /api/v1/me
| Field | Type | Description |
|---|---|---|
id | string | User ID of the key's owner. |
name | string | Display name. |
email | string | Email address. |
role | string | Role on this key's team — one of "admin", "member", or "viewer". |
team | object | The team this key acts on: { id, name }. |
apps | string[] | Modules the owner can reach in that team. |
Response Fields — /api/v1/me/teams
| Field | Type | Description |
|---|---|---|
teams | array | One entry per team, each with id, name, role, apps, and current. |
current | string | ID 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.
/api/v1/membersList active members of the team. ?includeInactive=true adds deactivated people, marked active: false.
Response Fields
| Field | Type | Description |
|---|---|---|
id | string | User ID. |
name | string | Display name. |
email | string | Email address. |
avatarUrl | string | null | Profile image URL. |
role | string | One of "admin", "member", or "viewer". |
jobTitle | string | null | Job title set by an admin. |
department | string | null | Department set by an admin. |
active | boolean | false 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.
/api/admin/member-profilesUpdate a member's job title and/or department.
Request Body
| Field | Type | Description |
|---|---|---|
userId* | string | The ID of the team member to update. |
jobTitle | string | Job title to set. Omit to leave unchanged. |
department | string | Department 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.
/api/v1/jobsList all jobs for the team.
/api/v1/jobs/{id}Get a single job.
/api/v1/jobsCreate a job with default pipeline stages.
/api/v1/jobs/{id}Update a job.
/api/v1/jobs/{id}Delete a job.
Fields
| Field | Type | Description |
|---|---|---|
title* | string | Job title (max 500 chars). |
department | string | Department name. |
location | string | Office location. |
type | string | full-time · part-time · contract · internship. |
remotePolicy | string | onsite · hybrid · remote. |
salaryMin | number | Minimum salary (PUT only). |
salaryMax | number | Maximum salary (PUT only). |
salaryCurrency | string | Currency code, e.g. USD (PUT only). |
description | string | Job description (HTML or plain text). |
requirements | string | Job requirements. |
status | string | draft · 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.
/api/v1/candidatesList all candidates for the team.
/api/v1/candidates/{id}Get a single candidate.
/api/v1/candidatesCreate a candidate.
/api/v1/candidates/{id}Update a candidate.
/api/v1/candidates/{id}Delete a candidate.
Fields
| Field | Type | Description |
|---|---|---|
firstName* | string | First name (max 200 chars). |
lastName* | string | Last name (max 200 chars). |
email* | string | Email address (max 500 chars). |
phone | string | Phone number. |
linkedinUrl | string | LinkedIn profile URL. |
portfolioUrl | string | Portfolio or personal site URL. |
location | string | Candidate location. |
source | string | How the candidate was sourced (e.g. referral, linkedin, website). |
sourceDetail | string | Additional source details. |
tags | string[] | Tags for categorization (PUT only). |
customFields | object | Key-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.
/api/v1/applicationsList applications. Optional: ?jobId=...&candidateId=...
/api/v1/applications/{id}Get a single application.
/api/v1/applicationsCreate an application (apply a candidate to a job).
/api/v1/applications/{id}Update stage, status, rating, or rejection reason.
/api/v1/applications/{id}Delete an application.
Fields
| Field | Type | Description |
|---|---|---|
jobId* | string | ID of the job. |
candidateId* | string | ID of the candidate. |
stageId | string | Pipeline stage ID. Update this to move candidates through stages. |
status | string | active · hired · rejected. Defaults to active. |
rejectionReason | string | Reason for rejection (when status is rejected). |
rating | number | Rating 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.
/api/v1/custom-fieldsList custom field definitions. Optional: ?entityType=task|candidate|project
Field definition shape
| Field | Type | Description |
|---|---|---|
id | string | Unique field ID. |
entityType | string | task, candidate, or project. |
name | string | Display name (used as key in customFields objects). |
description | string | Help text for the field. |
fieldType | string | text, number, select, multi_select, date, checkbox, people, or formula. |
options | object[] | For select/multi_select: array of {'{value, label, color}'}. |
config | object | Type-specific config (number format, formula expression, etc.). |
required | boolean | Whether 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.
/api/v1/crm/contactsList all contacts for the team.
/api/v1/crm/contactsCreate a contact.
/api/v1/crm/contacts/{id}Get a single contact.
/api/v1/crm/contacts/{id}Update a contact.
/api/v1/crm/contacts/{id}Delete a contact.
Fields
| Field | Type | Description |
|---|---|---|
firstName* | string | First name. |
lastName* | string | Last name. |
email | string | Email address. |
phone | string | Phone number. |
title | string | Job title. |
companyId | string | ID of the associated company. |
source | string | website, referral, linkedin, cold_outreach, event, inbound, or other. |
lifecycleStage | string | prospecting, qualification, meeting, proposal, negotiation, opportunity_won, opportunity_lost, or customer. |
tags | string[] | Tags for categorization. |
ownerId | string | ID of the team member who owns this contact. |
linkedinUrl | string | LinkedIn profile URL. |
Response Fields (read-only)
| Field | Type | Description |
|---|---|---|
companyName | string | null | Name 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.
/api/v1/crm/companiesList all companies for the team.
/api/v1/crm/companiesCreate a company.
/api/v1/crm/companies/{id}Get a single company.
/api/v1/crm/companies/{id}Update a company.
/api/v1/crm/companies/{id}Delete a company.
Fields
| Field | Type | Description |
|---|---|---|
name* | string | Company name. |
domain | string | Company domain (e.g. example.com). |
industry | string | Industry vertical. |
size | string | 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, or 10001+. |
website | string | Company website URL. |
phone | string | Company phone number. |
addressLine1 | string | Street address line 1. |
addressLine2 | string | Street address line 2. |
city | string | City. |
state | string | State or region. |
country | string | Country. |
postalCode | string | Postal or ZIP code. |
lifecycleStage | string | prospecting, qualification, meeting, proposal, negotiation, opportunity_won, opportunity_lost, or customer. |
tags | string[] | Tags for categorization. |
ownerId | string | ID of the team member who owns this company. |
logoUrl | string | URL 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.
/api/v1/crm/dealsList all deals for the team.
/api/v1/crm/dealsCreate a deal.
/api/v1/crm/deals/{id}Get a single deal.
/api/v1/crm/deals/{id}Update a deal.
/api/v1/crm/deals/{id}Delete a deal.
Fields
| Field | Type | Description |
|---|---|---|
title* | string | Deal title. |
value | integer | Deal value in cents (e.g. 50000 = $500.00). |
currency | string | ISO 4217 currency code. Defaults to USD. |
pipelineId* | string | ID of the pipeline this deal belongs to. |
stageId | string | ID of the pipeline stage. Defaults to first stage. |
companyId | string | ID of the associated company. |
contactId | string | ID of the primary contact. |
ownerId | string | ID of the team member who owns this deal. |
probability | number | Win probability, 0–100. |
expectedCloseDate | string | Expected close date (ISO 8601). |
status | string | open, won, or lost. Defaults to open. |
lostReason | string | Reason for losing the deal (when status is lost). |
tags | string[] | 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.
/api/v1/crm/pipelinesList all pipelines for the team.
/api/v1/crm/pipelinesCreate a pipeline.
/api/v1/crm/pipelines/{id}Get a single pipeline with stages.
/api/v1/crm/pipelines/{id}Update a pipeline.
/api/v1/crm/pipelines/{id}Delete a pipeline.
Fields
| Field | Type | Description |
|---|---|---|
name* | string | Pipeline name. |
isDefault | boolean | Whether 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.
/api/v1/crm/activitiesList activities. Optional: ?contactId=...&companyId=...&dealId=...
/api/v1/crm/activitiesCreate an activity.
/api/v1/crm/activities/{id}Get a single activity.
/api/v1/crm/activities/{id}Update an activity.
/api/v1/crm/activities/{id}Delete an activity.
Fields
| Field | Type | Description |
|---|---|---|
type* | string | call, email, meeting, note, or task. |
subject | string | Activity subject or title. |
body | string | Activity body or notes. |
durationMins | integer | Duration in minutes. |
scheduledAt | string | Scheduled date/time (ISO 8601). |
completedAt | string | Completion date/time (ISO 8601). |
contactId | string | ID of the associated contact. |
companyId | string | ID of the associated company. |
dealId | string | ID of the associated deal. |
isPrivate | boolean | Whether 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"
}HubSpot links & search
With HubSpot connected, companies and contacts are linked to tasks, boards, roadmap projects and River pages. Links are references: nothing is written to HubSpot. Each endpoint checks the module that owns the item (Projects for tasks and boards, Roadmap for projects, River for pages) and, for tasks and boards, the board-group rule — an item the key's owner can't open answers 404. Without a HubSpot connection, search and linking answer 409.
A company linked to a board is the client of every task on it; Slack channel alerts use this. While HubSpot is connected the built-in CRM is switched off, so the CRM endpoints answer403.
/api/v1/hubspot/search?type=company&q=acmeSearch the connected portal (type: company · contact; limit up to 50). Needs member access to Projects, Roadmap or River.
/api/v1/tasks/{id}/hubspot-linksRecords linked to a task. Also /tracker/projects/{id} (boards), /projects/{id} (roadmap) and /river/pages/{id}.
/api/v1/tasks/{id}/hubspot-linksLink a record. 201 when new; 200 with the existing link when it was already linked.
/api/v1/tasks/{id}/hubspot-links?linkId=…Unlink. The one v1 DELETE open to members: it removes a reference, not content.
Fields (POST)
| Field | Type | Description |
|---|---|---|
objectType | string | company (default) · contact |
objectId* | string | The HubSpot record ID (from search). Checked in HubSpot, so a wrong ID is refused. |
Link
| Field | Type | Description |
|---|---|---|
id | string | Link ID, for unlinking. |
objectType · objectId | string | The HubSpot record. |
name · detail | string | Company name and domain, or contact name and email, as of linking. |
url | string | Opens the record in HubSpot. |
CPQ Products
Manage your product catalog. Products represent items and services available for quoting.
/api/v1/cpq/productsList all active products.
/api/v1/cpq/productsCreate a product (admin only).
/api/v1/cpq/products/{id}Update a product.
/api/v1/cpq/products/{id}Archive a product (sets isActive to false).
Fields
| Field | Type | Description |
|---|---|---|
name* | string | Product name (max 500 chars). |
description | string | Product description. |
sku | string | Stock keeping unit / product code. |
productType | string | one_time, recurring, or usage_based. Defaults to one_time. |
unitPriceCents* | integer | Unit price in cents (e.g. 9900 = $99.00). |
costPriceCents | integer | Cost price in cents for margin tracking. |
currency | string | ISO currency code. Defaults to USD. |
billingInterval | string | monthly, quarterly, or annual (for recurring products). |
taxRate | integer | Tax rate in basis points (e.g. 1000 = 10%). |
category | string | Product category. |
subcategory | string | Product subcategory. |
isActive | boolean | Whether the product is active. Defaults to true. |
metadata | object | Arbitrary 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.
/api/v1/cpq/quotesList quotes. Filter with ?status=draft|sent|accepted|etc.
/api/v1/cpq/quotesCreate a draft quote (admin only).
/api/v1/cpq/quotes/{id}Get quote with line items and sections.
/api/v1/cpq/quotes/{id}Update a quote.
/api/v1/cpq/quotes/{id}Delete a draft quote.
/api/v1/cpq/quotes/{id}/sendSend 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
| Field | Type | Description |
|---|---|---|
title* | string | Quote title (max 500 chars). |
recipientName | string | Recipient's name. |
recipientEmail | string | Recipient's email address. |
recipientCompany | string | Recipient's company name. |
currency | string | ISO currency code. Defaults to USD. |
validUntil | string | Expiration date (ISO 8601). |
paymentTerms | string | Payment terms text. |
notes | string | Notes visible to the recipient. |
internalNotes | string | Internal notes (not visible to recipient). |
templateId | string | Quote template ID for branding. |
dealId | string | CRM deal ID to link this quote to. |
contactId | string | CRM contact ID. |
companyId | string | CRM 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.
/api/v1/cpq/quotes/{quoteId}/line-itemsList line items for a quote.
/api/v1/cpq/quotes/{quoteId}/line-itemsAdd a line item.
/api/v1/cpq/quotes/{quoteId}/line-items/{id}Update a line item.
/api/v1/cpq/quotes/{quoteId}/line-items/{id}Remove a line item.
Fields
| Field | Type | Description |
|---|---|---|
name* | string | Line item name (max 500 chars). |
description | string | Line item description. |
productId | string | Link to a catalog product (optional for custom items). |
quantity* | integer | Quantity. Defaults to 1. |
unitPriceCents* | integer | Unit price in cents. |
discountType | string | percentage or fixed. |
discountValue | integer | Discount amount (percent or cents). |
taxRate | integer | Tax rate in basis points. |
billingInterval | string | monthly, quarterly, or annual. |
sectionId | string | Quote section ID for grouping. |
sortOrder | integer | Display 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.
/api/v1/labeler/projectsActive labeler projects with their labelers and managers. ?includeArchived=true adds archived ones.
/api/v1/labeler/labelersLabeler-role members and the projects they are assigned to.
/api/v1/labeler/tasksList tasks. Filters: projectId, userId, email, status, source, externalId, from, to, limit (≤500).
/api/v1/labeler/tasksCreate 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.
/api/v1/labeler/tasks/{id}Fetch one task.
/api/v1/labeler/tasks/{id}Update any task field; naming another labeler reassigns it.
/api/v1/labeler/timeLog a block of time on behalf of a labeler (source api).
/api/v1/labeler/unitsLog completed units on behalf of a labeler.
Task fields
| Field | Type | Description |
|---|---|---|
projectId* | string | Labeler project. Required on create; cannot change on an existing task. |
labelerEmail / labelerUserId* | string | Who the work belongs to. Required on create; on update, naming someone else reassigns the task. |
externalId | string | Your id for the task (max 200 chars). Unique per source; drives idempotent updates. |
source | string | api · manual · ui. Defaults to api. |
title* | string | Required on create (max 500 chars). |
description | string | Max 5000 chars. |
status | string | assigned · in_progress · done · rejected. Defaults to assigned. Entering done stamps completedAt unless you send one. |
quantity | integer | Units of work in the project's unit label. Defaults to 1. |
durationSeconds | integer | Time spent, if the platform tracks it (≤ 30 days). null clears. |
startedAt / completedAt | string | ISO timestamps. null clears. |
metadata | object | Free-form platform data, ≤ 16 KB serialised. null clears. |
Time and unit fields
| Field | Type | Description |
|---|---|---|
projectId* | string | Labeler project. |
labelerEmail / labelerUserId* | string | Who the work belongs to. |
durationSeconds* | integer | Time only. 1 to 86400. startedAt (ISO) is optional; the block ends now by default. |
quantity* | integer | Units only. Positive integer. occurredAt (ISO) is optional. |
description / note | string | Max 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.
/api/v1/river/spacesList all wiki spaces. parentSpaceId is set when a space sits inside another space (null at the top level).
/api/v1/river/spacesCreate a wiki space.
Fields
| Field | Type | Description |
|---|---|---|
name* | string | Space name (max 200 chars). |
slug | string | URL slug. Derived from the name when omitted; suffixed if taken. |
description | string | Space description. |
icon | string | Icon 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.
/api/v1/river/pagesList pages (structure only, no content). Filters: ?spaceId=, ?includeArchived=true. Max 1000 rows.
/api/v1/river/pagesCreate a page.
/api/v1/river/pages/{id}Get a page including contentMarkdown.
/api/v1/river/pages/{id}Update a page. Content replacement snapshots the previous version.
Fields
| Field | Type | Description |
|---|---|---|
title* | string | Page title (max 500 chars). Required on POST. |
spaceId* | string | Space the page belongs to. Required on POST. |
parentId | string | Parent page ID for tree placement. |
contentMarkdown | string | Page content as markdown. Supports headings, lists, task lists, blockquotes, code blocks, links, and inline marks. |
versionSummary | string | PUT only — label for the version snapshot taken before content is replaced. |
archived | boolean | PUT only — archive (true) or restore (false) the page. |
icon | string | Icon identifier. |
position | number | Sort 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.
| Field | Type | Description |
|---|---|---|
location.space | object | null | { id, name } of the page's space. |
location.spaceParents | array | Spaces that space sits inside, outermost first: [{ id, name }]. Empty for a top-level space. |
location.parents | array | Ancestor pages, outermost first: [{ id, title }]. Empty for a top-level page. |
location.breadcrumb | string | "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.
/api/v1/river/pages/{id}/referencesList entity links on a page.
/api/v1/river/pages/{id}/referencesLink a page to an entity.
Fields
| Field | Type | Description |
|---|---|---|
entityType* | string | roadmap_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* | string | ID of the linked entity. |
linkType | string | attached · embed · inbound. Defaults to attached. |
pageAnchor | string | Heading anchor on the page for deep links. |
entityTitleSnapshot | string | Ignored 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.
/api/v1/canvasList canvases (metadata and byte size only, without the documents).
/api/v1/canvasPublish a canvas. Returns its id and URL.
/api/v1/canvas/{id}Get a canvas including its full HTML.
/api/v1/canvas/{id}Update. Only the fields you send change — a rename leaves the document untouched.
/api/v1/canvas/{id}Delete a canvas. Admin only.
/api/v1/canvas/{id}/versionsPast documents, newest first. The 20 most recent are kept.
/api/v1/canvas/{id}/versionsRestore 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
| Field | Type | Description |
|---|---|---|
title* | string | Short name for the diagram (max 500 chars). |
html* | string | The complete document. Max 2MB, measured in UTF-8 bytes. |
description | string | One-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.
/api/v1/presentationsList decks with build status.
/api/v1/presentationsStart a build. Returns 202 — the deck is not ready. Admin only.
/api/v1/presentations/{id}One deck with its slides and live build status. Poll this.
/api/v1/presentations/{id}/slidesAdd a slide. Appended unless sortOrder is given. Admin only.
/api/v1/presentations/{id}/slidesEdit a slide. Body: { slideId, title?, content?, sortOrder? }. Admin only.
Fields
| Field | Type | Description |
|---|---|---|
title* | string | Deck title (max 200 chars). |
audienceType | string | executive · engineering · investor · client · team. Defaults to executive. |
theme | string | default · dark · minimal · bold, or brand:{id} for a saved brand. Defaults to default. |
config | object | Which data to include: includeInitiatives, includeProjects, includeGoals, includeFeedback (booleans), and additionalNotes (string, capped at 2000 chars). |
Slide fields
| Field | Type | Description |
|---|---|---|
title* | string | Slide title. |
slideType | string | title · section_divider · narrative_text · key_metrics · custom. Defaults to custom. |
content | object | narrative_text takes { body }; title and section_divider need none; everything else takes { bullets: [...] }. |
sortOrder | number | 0-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.
/api/v1/ai/chatConversational AI assistant (SSE streaming).
/api/v1/ai/generate-tasksExtract tasks from a transcript.
/api/v1/ai/ingestIngest text for AI feedback analysis.
/api/v1/ai/suggest-prioritySuggest priority for a task.
/api/v1/ai/suggest-assignmentSuggest an assignee for a task.
/api/v1/ai/scoreScore a candidate for a job.
/api/v1/ai/detect-blockedFind blocked/stalled tasks in a project.
/api/v1/ai/auto-groupAuto-group ungrouped feedback entries.
/api/v1/ai/suggest-roadmapSuggest projects from feedback.
/api/v1/ai/connect-goalsAuto-link projects to goals.
/api/v1/ai/pipeline-bottlenecksDetect hiring pipeline bottlenecks.
/api/v1/ai/signalsList active AI signals. ?module=...
/api/v1/ai/actionsList AI actions. ?module=...&status=...&cursor=...
/api/v1/ai/actions/{'{id}'}Manage action: approve, reject, or undo.
/api/v1/ai/settingsGet AI settings for the team.
CRM AI
AI-powered CRM features for deal scoring, action suggestions, pipeline analysis, and data enrichment.
/api/v1/ai/crm/score-dealScore a deal's likelihood of closing.
/api/v1/ai/crm/suggest-actionsGet next action suggestions for a deal or contact.
/api/v1/ai/crm/analyze-pipelineAnalyze pipeline health and forecast.
/api/v1/ai/crm/draft-emailDraft an email to a contact.
/api/v1/ai/crm/summarize-activitiesSummarize activity timeline.
/api/v1/ai/crm/enrich-contactEnrich a contact with external data.
/api/v1/ai/crm/enrich-companyEnrich a company with external data.
/api/v1/ai/crm/search-contactsSearch 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.
/api/ai/settingsGet AI settings for the current team.
/api/ai/settingsUpdate AI settings (admin only).
/api/ai/actionsList AI actions with cursor pagination. ?module=...&status=...
/api/ai/actions/{'{id}'}/approveApprove a pending AI action (admin only).
/api/ai/actions/{'{id}'}/rejectReject a pending AI action (admin only).
/api/ai/actions/{'{id}'}/undoUndo an applied AI action with optional feedback (admin only).
/api/ai/signalsList active AI signals. ?module=...
/api/ai/chatSend a message to the AI assistant (streaming SSE response).
/api/ai/feedback/ingestIngest text for AI feedback analysis.
/api/ai/tracker/generate-tasksGenerate tasks from a transcript.
/api/ai/tracker/detect-blockedDetect blocked/stalled tasks in a project.
/api/ai/roadmap/suggestSuggest roadmap projects from feedback.
/api/ai/ats/scoreScore a candidate for a job.
/api/ai/ats/pipeline-bottlenecksDetect pipeline bottlenecks for a job.
/api/ai/feedback/auto-groupAuto-group ungrouped feedback entries.
/api/ai/tracker/suggest-assignmentSuggest an assignee for a task.
/api/ai/tracker/suggest-prioritySuggest priority for a task.
/api/ai/roadmap/connect-goalsSuggest project-to-goal connections.
/api/ai/cross/route-feedbackRoute feedback to appropriate modules.
/api/ai/cross/analyze-signalsAnalyze accumulated signal patterns.
/api/ai/cross/check-completionCheck follow-through on completed items.
Settings fields
| Field | Type | Description |
|---|---|---|
defaultPermissionMode | string | act_freely, ask_before_acting, or human_only. |
moduleOverrides | object | Per-module permission overrides, e.g. { "feedback": "act_freely" }. |
enabledModules | string[] | 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.
/api/v1/webhooksList active webhook subscriptions.
/api/v1/webhooksSubscribe to events.
/api/v1/webhooks/{id}Update a subscription.
/api/v1/webhooks/{id}Unsubscribe.
Subscribe fields
| Field | Type | Description |
|---|---|---|
targetUrl* | string | URL 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.
/api/v1/poll/tasksRecent tasks, newest first.
/api/v1/poll/candidatesRecent candidates, newest first.
/api/v1/poll/applicationsRecent 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.
id, company (required), title (required), status, themesGET /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.
sourceId (required), company (required), titlePOST /api/trust-safety/questionnaires/from-template
Create a questionnaire from a template (e.g. SIG Lite) with auto-fill from Drata and previous answers.
templateId (required), company (required), titleTrust & 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.
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.
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.