View as Markdown

Endpoints

This page lists all available endpoints on the Forkline public API, grouped by resource. Every mutating endpoint includes a request body example and response example.

User

Method Path Scope Description
GET /v1/me user:read Get your profile information
GET /v1/me/settings user:read Get your user settings
PUT /v1/me/settings user:write Update your user settings

GET /v1/me

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/me

Response (200 OK):

{
  "id": "u1234567-...",
  "email": "octocat@example.com",
  "name": "Octocat"
}

GET /v1/me/settings

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/me/settings

Response (200 OK):

{
  "settings": {}
}

PUT /v1/me/settings

Request body (UpdateAllSettingsRequest) — all fields are optional. Only provided fields are updated.

Field Type Description
default_runner_size string basic, advanced, or pro
default_model string or null Model ID or null to clear
auto_fix_ci boolean Enable/disable CI auto-fix
auto_fix_max_attempts integer 1–10
auto_fix_merge_conflicts boolean Enable/disable merge conflict auto-fix
auto_fix_merge_conflicts_max_attempts integer 1–10
max_active_runners integer or null Minimum 1, or null for no limit
auto_pr_review boolean Enable/disable auto PR review
auto_pr_review_scope string PR review scope
auto_pr_review_action string PR review action
auto_pr_review_prompt string or null Max 10,000 characters
curl -X PUT \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"auto_fix_ci": true, "auto_fix_max_attempts": 5}' \
  https://api.forkline.dev/v1/me/settings

Response (200 OK): Returns the updated CurrentUserResponse object.


Billing

Method Path Scope Description
GET /v1/billing/credits billing:read Get your credit balance

GET /v1/billing/credits

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/billing/credits

Response (200 OK):

{
  "balance": 580,
  "subscription": {
    "plan_name": "basic",
    "status": "active",
    "period_credits_remaining": 480,
    "period_ends_at": "2026-10-01T00:00:00Z",
    "daily_credits": 480,
    "billing_period_end": "2026-10-01T00:00:00Z",
    "cancel_at": null
  },
  "purchased_credits": 100
}

subscription is null if the user has no active subscription.


Repositories

Method Path Scope Description
GET /v1/repositories repositories:read List your repositories
POST /v1/repositories repositories:write Create a new repository
GET /v1/repositories/{id} repositories:read Get repository details
PUT /v1/repositories/{id} repositories:write Update repository settings
DELETE /v1/repositories/{id} repositories:write Delete a repository (two-stage: soft-delete, then hard-delete)
POST /v1/repositories/{id}/restore repositories:write Restore a soft-deleted repository
GET /v1/repositories/{id}/opencode-config repositories:read Get repository OpenCode configuration
PUT /v1/repositories/{id}/opencode-config repositories:write Update repository OpenCode configuration
DELETE /v1/repositories/{id}/opencode-config repositories:write Clear repository OpenCode configuration

Query Parameters: GET /v1/repositories

Parameter Type Default Description
offset integer 0 Number of records to skip
limit integer 50 Maximum records to return (max 100)
q string — Search by repository name
status string all Filter: all, active, or disabled
sort string updated Sort by: updated, name, or connector
include_deleted boolean false Include soft-deleted repositories
active_only boolean — Only show active repositories
connector_id UUID — Filter by connector

GET /v1/repositories

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  "https://api.forkline.dev/v1/repositories?limit=10&status=active&sort=name"

Response (200 OK):

{
  "repositories": [
    {
      "id": "a1b2c3d4-...",
      "user_id": "u1234567-...",
      "connector_id": "f0e1d2c3-...",
      "owner": "octocat",
      "name": "my-project",
      "repository_url": "https://github.com/octocat/my-project",
      "default_runner_size": "basic",
      "default_model": "anthropic/claude-sonnet-4-5",
      "trigger_policy": "collaborators",
      "allowed_users": [],
      "auto_fix_ci": true,
      "auto_fix_max_attempts": 3,
      "max_active_runners": 5,
      "created_at": "2026-01-15T10:30:00Z",
      "updated_at": "2026-09-01T08:00:00Z"
    }
  ],
  "total": 1,
  "offset": 0,
  "limit": 10,
  "has_more": false
}

POST /v1/repositories

Request body (CreateRepositoryRequest):

Field Type Required Description
connector_id UUID yes The connector this repository belongs to
owner string yes Repository owner (e.g. GitHub org or user)
name string yes Repository name
repository_url string yes Full URL to the repository
default_runner_size string no Default runner size: basic, advanced, or pro
default_model string no Default AI model (e.g. anthropic/claude-sonnet-4-5)
trigger_policy string no Trigger policy: owner_only, collaborators (default), allowlist or anyone
allowed_users string[] no Required when trigger_policy is allowlist
curl -X POST \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "connector_id": "f0e1d2c3-...",
    "owner": "octocat",
    "name": "my-project",
    "repository_url": "https://github.com/octocat/my-project",
    "default_runner_size": "basic",
    "trigger_policy": "collaborators"
  }' \
  https://api.forkline.dev/v1/repositories

Response (201 Created): Returns the full Repository object (see GET response above).

PUT /v1/repositories/{id}

Request body (UpdateRepositoryRequest) — all fields are optional. Only provided fields are updated. To clear a nullable field, set it to null.

Field Type Description
default_runner_size string or null basic, advanced, pro, or null to clear
default_model string or null Model ID or null to clear
trigger_policy string owner_only, collaborators, allowlist or anyone
allowed_users string[] Required when switching to allowlist
auto_fix_ci boolean or null Enable/disable CI auto-fix
auto_fix_max_attempts integer or null 1–10
max_active_runners integer or null Minimum 1
auto_fix_merge_conflicts boolean or null Enable/disable merge conflict auto-fix
auto_fix_merge_conflicts_max_attempts integer or null 1–10
auto_pr_review boolean or null Enable/disable auto PR review
auto_pr_review_scope string or null PR review scope
auto_pr_review_action string or null PR review action
auto_pr_review_prompt string or null Max 10,000 characters
opencode_config object or null OpenCode configuration object
curl -X PUT \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"auto_fix_ci": true, "auto_fix_max_attempts": 5}' \
  https://api.forkline.dev/v1/repositories/<repository_id>

Response (200 OK): Returns the updated Repository object.

DELETE /v1/repositories/{id}

Two-stage deletion:

  • First call: soft-deletes the repository (stops all runners, cancels tasks). The repository remains recoverable.
  • Second call (on an already soft-deleted repository): hard-deletes permanently.

Response: 204 No Content

POST /v1/repositories/{id}/restore

Restores a soft-deleted repository. No request body.

Response (200 OK): Returns the restored Repository object.

OpenCode Configuration

The OpenCode config endpoints manage per-repository OpenCode settings. The config is validated against a JSON schema. Certain keys are blocked and cannot be set by users: permission, autoupdate, model, provider. Maximum config size: 64 KB.

GET /v1/repositories/{id}/opencode-config

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/repositories/<repository_id>/opencode-config

Response (200 OK):

{
  "config": {
    "logLevel": "INFO",
    "instructions": ["Follow the project coding standards"],
    "mcp": {}
  }
}

config is null if no repository-level config has been set.

PUT /v1/repositories/{id}/opencode-config

Request body (UpdateOpencodeConfigRequest):

Field Type Required Description
config object yes OpenCode configuration object (max 64 KB)
curl -X PUT \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"config": {"logLevel": "DEBUG", "instructions": ["Be concise"]}}' \
  https://api.forkline.dev/v1/repositories/<repository_id>/opencode-config

Response (200 OK):

{
  "config": {
    "logLevel": "DEBUG",
    "instructions": ["Be concise"]
  }
}

DELETE /v1/repositories/{id}/opencode-config

Clears the repository-level OpenCode configuration. No request body.

Response: 204 No Content


Runners

Method Path Scope Description
GET /v1/runner-sizes runners:read List available runner sizes
GET /v1/runners runners:read List all runners
GET /v1/runners/ids runners:read List all runner IDs
GET /v1/runners/{runner_id} runners:read Get runner details
DELETE /v1/runners/{runner_id} runners:write Stop and delete a runner
GET /v1/runners/{runner_id}/events runners:read Get runner event history
POST /v1/runners/bulk-action runners:write Perform bulk actions on runners
GET /v1/repositories/{id}/runners runners:read List runners for a repository
POST /v1/repositories/{id}/runners runners:write Start a new runner
GET /v1/runners/{runner_id}/settings runners:read Get runner settings
PUT /v1/runners/{runner_id}/settings runners:write Update runner settings
POST /v1/runners/{runner_id}/restart runners:write Restart an active runner
POST /v1/runners/{runner_ref}/attach-grant runners:attach Request a short-lived grant for attaching a local OpenCode session to this runner

Query Parameters: GET /v1/runners

Parameter Type Default Description
page integer 1 Page number
per_page integer 25 Items per page (max 100)
repository_id UUID — Filter by repository
status string — Filter by runner status
connector_id UUID — Filter by connector
source string — Filter by source: manual or autonomous
sort_by string — Sort field
sort_order string — Sort direction

Query Parameters: GET /v1/repositories/{id}/runners

Parameter Type Default Description
page integer 1 Page number
per_page integer 25 Items per page (max 100)
sort_by string — Sort field
sort_order string — Sort direction

GET /v1/repositories/{id}/runners

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  "https://api.forkline.dev/v1/repositories/<repository_id>/runners?page=1&per_page=25"

Response (200 OK): the same { "runners": [...], "total": <int> } envelope as GET /v1/runners, scoped to the repository in the path.

GET /v1/runners/ids

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/runners/ids

Response (200 OK):

{
  "runner_ids": ["r4n5n6r7-...", "r8s9t0u1-..."]
}

Accepts the same query parameters as GET /v1/runners for filtering.

GET /v1/runner-sizes

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/runner-sizes

Response (200 OK):

{
  "sizes": [
    {"id": "basic", "name": "Basic", "cpu": 2, "ram": "4GB", "multiplier": 1},
    {"id": "advanced", "name": "Advanced", "cpu": 4, "ram": "8GB", "multiplier": 2},
    {"id": "pro", "name": "Pro", "cpu": 8, "ram": "16GB", "multiplier": 4}
  ]
}

GET /v1/runners

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  "https://api.forkline.dev/v1/runners?page=1&per_page=10"

Response (200 OK):

{
  "runners": [
    {
      "id": "r4n5n6r7-...",
      "short": "r4n5",
      "repository_id": "a1b2c3d4-...",
      "name": "api-test-runner",
      "status": "running",
      "source": "manual",
      "size": "basic",
      "model": "anthropic/claude-sonnet-4-5",
      "created_at": "2026-09-18T14:20:00Z",
      "started_at": "2026-09-18T14:20:30Z",
      "stopped_at": null
    }
  ],
  "total": 1
}

POST /v1/repositories/{id}/runners

Request body (StartRunnerRequest):

Field Type Required Description
name string yes Runner name
size string no Runner size: basic, advanced, or pro. Falls back to repository default, then user default
model string no AI model (e.g. anthropic/claude-sonnet-4-5). Falls back to repository default
initial_prompt string no If provided, starts an autonomous runner that executes this prompt
curl -X POST \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-runner", "size": "advanced", "model": "anthropic/claude-sonnet-4-5"}' \
  https://api.forkline.dev/v1/repositories/<repository_id>/runners

Response (201 Created): Returns the Runner object.

Note: Starting a runner requires sufficient credits. If credits are insufficient, the API returns 402 INSUFFICIENT_CREDITS.

GET /v1/runners/{runner_id}/events

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/runners/<runner_id>/events

Response (200 OK):

{
  "events": [
    {
      "id": "evt-...",
      "runner_id": "r4n5n6r7-...",
      "event_type": "started",
      "previous_status": "starting",
      "new_status": "running",
      "details": {},
      "created_at": "2026-09-18T14:20:30Z"
    }
  ]
}

DELETE /v1/runners/{runner_id}

Stops an active runner or permanently deletes a terminal runner. No request body.

Response: 204 No Content

POST /v1/runners/bulk-action

Request body (BulkRunnerActionRequest):

Field Type Required Description
action string yes stop or delete
runner_ids UUID[] yes Runner IDs to act on (max 100)
curl -X POST \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "stop", "runner_ids": ["r4n5n6r7-...", "r8s9t0u1-..."]}' \
  https://api.forkline.dev/v1/runners/bulk-action

Response (200 OK) (BulkRunnerActionResponse):

{
  "succeeded": 2,
  "failed": 0,
  "errors": []
}
  • stop: Stops all active runners. Non-active runners are reported in errors.
  • delete: Permanently deletes runners in a terminal state. Non-terminal runners are skipped and reported in errors.

GET /v1/runners/{runner_id}/settings

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/runners/<runner_id>/settings

Response (200 OK):

{
  "runner_id": "r4n5n6r7-...",
  "settings": {},
  "updated_at": "2026-09-18T14:20:00Z"
}

PUT /v1/runners/{runner_id}/settings

Request body (UpdateSettingsRequest):

Field Type Required Description
settings object yes Arbitrary JSON settings object
curl -X PUT \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"settings": {"logLevel": "debug"}}' \
  https://api.forkline.dev/v1/runners/<runner_id>/settings

Response (200 OK): Returns the updated RunnerSettingsResponse.

POST /v1/runners/{runner_id}/restart

Restarts an active runner. No request body. Returns 409 if the runner is not in an active state.

curl -X POST \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/runners/<runner_id>/restart

Response (200 OK): Returns the Runner object.


Models

Method Path Scope Description
GET /v1/models models:read List available AI models (filtered by your configured providers)
GET /v1/models/providers models:read List all model providers
GET /v1/providers/{provider_id}/icon models:read Get provider icon (returns SVG)

GET /v1/models

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/models

Response (200 OK):

{
  "providers": [
    {
      "id": "opencode",
      "name": "OpenCode Zen",
      "models": [
        {
          "id": "free-model",
          "name": "Free Model",
          "tool_call": true,
          "reasoning": false,
          "is_free": true
        }
      ]
    },
    {
      "id": "anthropic",
      "name": "Anthropic",
      "models": [
        {
          "id": "claude-sonnet-4-5",
          "name": "Claude Sonnet 4.5",
          "tool_call": true,
          "reasoning": false,
          "is_free": false
        }
      ]
    }
  ]
}

GET /v1/models/providers

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/models/providers

Response (200 OK):

{
  "providers": [
    {"id": "anthropic", "name": "Anthropic"},
    {"id": "openai", "name": "OpenAI"},
    {"id": "google", "name": "Google"}
  ]
}

GET /v1/providers/{provider_id}/icon

Returns an SVG image with Content-Type: image/svg+xml and Cache-Control: public, max-age=86400. Returns 404 if the icon is not found.


Analytics

All analytics endpoints accept optional date range query parameters.

Query Parameters (all analytics endpoints)

Parameter Type Default Description
start date (YYYY-MM-DD) 30 days ago Start of the date range
end date (YYYY-MM-DD) today End of the date range
Method Path Scope Description
GET /v1/analytics/overview analytics:read Get analytics overview
GET /v1/analytics/providers analytics:read Get analytics by provider
GET /v1/analytics/models analytics:read Get analytics by model
GET /v1/analytics/repositories analytics:read Get analytics by repository
GET /v1/analytics/history analytics:read Get historical analytics data

GET /v1/analytics/overview

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  "https://api.forkline.dev/v1/analytics/overview?start=2026-09-01&end=2026-09-18"

Response (200 OK):

{
  "input_tokens": 1250000,
  "output_tokens": 340000,
  "reasoning_tokens": 85000,
  "cache_read_tokens": 500000,
  "cache_write_tokens": 120000,
  "total_tokens": 2295000,
  "runner_hours": 48.5,
  "sessions_count": 120,
  "active_provider_count": 3,
  "active_model_count": 5,
  "active_repository_count": 2
}

GET /v1/analytics/providers

Response (200 OK): Array of UsageBreakdownResponse objects:

[
  {
    "key": "anthropic",
    "provider_id": "anthropic",
    "input_tokens": 800000,
    "output_tokens": 200000,
    "reasoning_tokens": 50000,
    "cache_read_tokens": 300000,
    "cache_write_tokens": 80000,
    "total_tokens": 1430000,
    "runner_hours": 30.0,
    "sessions_count": 80,
    "cache_ratio": 20.98,
    "share_percent": 62.31,
    "repository_count": 2,
    "model_count": 3
  }
]

GET /v1/analytics/history

Response (200 OK):

{
  "points": [
    {
      "day": "2026-09-18",
      "input_tokens": 50000,
      "output_tokens": 12000,
      "reasoning_tokens": 3000,
      "cache_read_tokens": 20000,
      "cache_write_tokens": 5000,
      "total_tokens": 90000,
      "runner_hours": 2.5,
      "sessions_count": 8
    }
  ]
}

Routines

Method Path Scope Description
GET /v1/routines routines:read List routines
POST /v1/routines routines:write Create a new routine
GET /v1/routines/{id} routines:read Get routine details
PUT /v1/routines/{id} routines:write Update a routine
DELETE /v1/routines/{id} routines:write Delete a routine
GET /v1/routines/{id}/runs routines:read List the runs of a routine with the reason for each outcome
GET /v1/routines/events routines:read List recent events from the last 7 days with the routines that reacted to each
GET /v1/routines/events/{event_id} routines:read Get one recent event with its reactions and the filters a new routine could use
POST /v1/routines/validate routines:read Check a routine spec without saving it: every problem with a JSON pointer, the repositories it selects and the defaults it gets (120 per minute)
POST /v1/routines/test routines:read Dry-run a routine spec against a recent event, a pasted webhook payload, a repository or a webhook body: every stage in order, the predicted outcome and the rendered prompt. Starts nothing (30 per minute, shared with dry runs)
POST /v1/routines/{id}/test routines:read The same dry run for the saved spec of a routine; the request has no spec (30 per minute, shared with dry runs)
POST /v1/routines/backtest routines:read Replay a routine spec over your stored events of the last 1 to 7 days: what it would have run, what each filter stopped, a reason per event and a weekly cost estimate. Starts nothing (60 per minute)
POST /v1/routines/{id}/backtest routines:read The same replay for the saved spec of a routine; the request has no spec (60 per minute)
POST /v1/routines/drafts/from-event routines:read Draft a valid routine spec from a recent event: the facts you keep become filters (each value says where it came from), a starter prompt, a unique name and up to two suggestions to widen the selection to sibling repositories with a 7-day replay (widen: true selects the event’s repository and the first pattern). Saves nothing (30 per minute)
GET /v1/routines/{id}/tasks routines:read Get routine task history
GET /v1/routines/{id}/tasks/{task_id} routines:read Get task detail
POST /v1/routines/{id}/publish routines:write Publish a draft routine
POST /v1/routines/{id}/disable routines:write Disable a published routine
POST /v1/routines/{id}/enable routines:write Enable a disabled routine
POST /v1/routines/{id}/run routines:execute Run now: start a routine yourself, or preview it with dry_run. Works for webhook, schedule and event routines (10 real runs per minute)
POST /v1/routines/{id}/webhook/rotate-secret routines:write Replace the generated secret of a custom webhook routine
PUT /v1/routines/{id}/webhook/secret routines:write Paste the vendor’s secret for a github or sentry webhook routine

Routine Lifecycle

Routines follow a status lifecycle:

draft → published → disabled
         ↑              |
         └──────────────┘ (re-publish not supported; create a new routine)
  • draft: The routine is being edited. It cannot be run for real (a dry run works).
  • published: The routine is active. Event and schedule routines trigger automatically. Custom webhook routines run when another system calls their URL with the routine’s secret. Any published routine can also be started with Run now.
  • disabled: The routine is paused. It will not trigger on events. Run now still works.

Running a routine now: POST /v1/routines/{id}/run starts a published or disabled routine of any trigger. The response returns task_ids and session_urls for the created tasks.

Polling tasks: After a run, use GET /v1/routines/{id}/tasks to check task progress. Each task has a status field (pending, running, completed, failed) and an optional error_message.

GET /v1/routines

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/routines

Response (200 OK):

{
  "routines": [
    {
      "id": "rn-1234-...",
      "name": "auto-deploy",
      "description": "Deploy on webhook",
      "spec": {
        "on": {"webhook": {"auth": {"type": "hmac", "preset": "forkline"}}},
        "repositories": {"include": [{"name": "acme/api"}]},
        "agent": {"prompt": "Deploy {{ repository.fullName }}"}
      },
      "status": "published",
      "webhook_secret_configured": true,
      "webhook_secret_origin": "generated",
      "webhook_secret_updated_at": "2026-09-01T10:00:00Z",
      "created_at": "2026-09-01T10:00:00Z",
      "updated_at": "2026-09-15T12:00:00Z",
      "published_at": "2026-09-02T08:00:00Z"
    }
  ]
}

Note: The signing secret of a custom webhook routine is never part of spec. Responses show webhook_secret_configured, webhook_secret_origin (generated by Forkline, or provided by you) and webhook_secret_updated_at; a generated secret itself is returned once, by the request that creates or rotates it. A provided secret is never returned.

POST /v1/routines

Request body (CreateRoutineRequest):

Field Type Required Description
name string yes Alphanumeric, hyphens, underscores only. Max 100 chars
description string no Human-readable description
spec object yes Routine specification (see below)

The spec object is the routine definition (version 2). Unknown fields are rejected.

Field Required Description
on yes Exactly one of events (1 to 8 of pr.opened, pr.updated, pr.merged, pr.closed, issue.opened, comment.created, ci.failed, push), schedule (cron with optional timezone, or at) and webhook ({} for a bearer token, or auth {type: token, header} / {type: hmac, preset: forkline|github|sentry}, idempotencyHeader, example)
repositories.include yes "all" or 1 to 50 selectors: {"id": "<uuid>"} or {"name": "owner/glob"}
repositories.exclude no Up to 50 selectors
filters no branches, paths, labels, authors, draft, fork, bots, self, command and a typed CEL when; each filter must apply to every listed event
agent.prompt yes Prompt template, up to 32 KiB, checked against the variables of the events
agent.model, agent.runnerSize, agent.ref, agent.timeoutMinutes no Model, basic/advanced/pro, git ref template, 1 to 240 minutes (default 30)
limits.maxRunsPerPr no 1 to 50, default 3

The full field reference (template variables, when rules) belongs to the Routines guide, which is not published yet. A schedule or webhook routine must select between 1 and 200 repositories.

An invalid spec returns 422 Unprocessable Entity with one entry per problem, each with a JSON pointer:

{
  "error": "Invalid routine spec: /filters/labels/inlcude: Unknown field `inlcude`. Allowed here: `include`, `exclude`. Did you mean `include`?",
  "code": "UNPROCESSABLE_ENTITY",
  "errors": [
    {"pointer": "/filters/labels/inlcude", "message": "Unknown field `inlcude`. Allowed here: `include`, `exclude`. Did you mean `include`?"}
  ]
}

For an on.webhook routine with a generated secret (a token, or preset: forkline) the response to this request includes webhook_secret once. For a vendor preset (github, sentry) there is none: set it with PUT /v1/routines/{id}/webhook/secret before you publish. Store it: it cannot be read again.

curl -X POST \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "auto-deploy",
    "description": "Deploy on webhook",
    "spec": {
      "on": {"webhook": {}},
      "repositories": {"include": [{"name": "acme/api"}]},
      "agent": {"prompt": "Deploy {{ repository.fullName }} to staging: {{ payload.text }}"}
    }
  }' \
  https://api.forkline.dev/v1/routines

Response (200 OK): Returns the RoutineResponse object.

PUT /v1/routines/{id}

Request body (UpdateRoutineRequest) — all fields optional:

Field Type Description
name string New name
description string New description
spec object New routine spec (full replacement)
curl -X PUT \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"description": "Updated description"}' \
  https://api.forkline.dev/v1/routines/<routine_id>

Response (200 OK): Returns the updated RoutineResponse object.

POST /v1/routines/{id}/publish

Transitions a draft routine to published. No request body. The routine spec is validated again before publishing.

Response (200 OK): Returns the RoutineResponse with status: "published".

Returns 400 if the routine is not a draft, or is an on.webhook routine without a signing secret. Returns 409 when you already have the maximum number of published routines (25). Returns 422 if the spec no longer validates (for example when a repository it selects is gone), with the same errors list as on create.

POST /v1/routines/{id}/disable

Transitions a published routine to disabled. No request body.

Response (200 OK): Returns the RoutineResponse with status: "disabled".

DELETE /v1/routines/{id}

Permanently deletes a routine. No request body.

Response: 204 No Content

POST /v1/routines/{id}/run

Run now: starts a routine yourself, or previews what it would start. Requires routines:execute and, like every routine endpoint, the routine’s owner. Connect another system with the webhook URL instead; the same limits apply to both.

Request body (RunRoutineRequest), all fields optional:

Field Type Description
dry_run boolean true evaluates and renders only: nothing is written and no run starts. Works on a draft. Default false
repositories uuid[] Webhook and schedule routines: a subset of the routine’s selected repositories. Default: all of them. Not allowed for event routines
payload any JSON Webhook routines only: the body a delivery would carry, at most 5 MiB, like a webhook body. Default {}. Not allowed for other triggers
event_id uuid Event routines only, and required for them: a recent forge event to replay (the id from GET /v1/routines/events)

event_id cannot be combined with repositories or payload. An optional Idempotency-Key header makes a repeated request start nothing new.

curl -X POST \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"dry_run": true, "payload": {"level": "error", "title": "High error rate"}}' \
  https://api.forkline.dev/v1/routines/<routine_id>/run

Response (200 OK) (RunRoutineResponse) of a real run:

{
  "dry_run": false,
  "event_id": "0000000e-0000-4000-8000-000000000001",
  "targets": [
    {
      "repository": {"id": "0000000a-0000-4000-8000-000000000001", "full_name": "acme/api"},
      "outcome": "task_created",
      "task_id": "t1a2b3c4-...",
      "session_url": "/sessions/t1a2b3c4-...",
      "reason": "Started a run for `acme/api`."
    }
  ],
  "estimate": {"runs": 1, "credits_per_run_max": 30, "credits_max": 30},
  "task_ids": ["t1a2b3c4-..."],
  "session_urls": ["/sessions/t1a2b3c4-..."]
}

A dry run returns the same envelope with dry_run: true, no event_id or tasks, and per target a predicted_outcome, decisive_stage, reason, rendered_prompt, rendered_ref and the stages. not_evaluated lists the checks that only happen at run time. estimate is the number of runs that would start and the most credits they can use. A dry run of an event replay also lists not_stored on a target: the fields the prompt, agent.ref, when or a paths filter read that Forkline does not keep. When it is not empty, estimate.runs is 0 and a real run is refused with 422 event_fields_not_stored. The top-level reason and event_id are left out when there is none.

event_id is the event record of this run; read it with GET /v1/routines/events/{event_id}. Its source is manual, and the run history shows who started it. task_ids is empty when nothing started, for example when a run for the same group is active: the target’s outcome is then skipped_active. Run now always skips while a run is active, whatever the routine’s onConflict says.

Errors:

Status When
400 Bad JSON, an unknown field in the body (Invalid request body: unknown field ..., so a typo cannot start a run), a field that does not fit the routine’s trigger (payload on a schedule routine, payload or repositories on an event routine: `payload` and `repositories` are not allowed: this routine runs on forge events; send an `event_id`, event_id on a webhook or schedule routine, no event_id on an event routine), event_id with repositories or payload, an empty repositories, or an event that is not a forge event
404 The routine or the event does not exist
409 A real run of a draft routine (message starts with ROUTINE_DRAFT:)
413 The request is over 6 MiB, or payload is over 5 MiB
422 repository_not_selected, filters_not_matched (nothing starts; one issue per blocked repository), event_fields_not_stored, event_repository_unavailable, fan_out_too_large, or the usual Invalid routine spec issues when the saved spec no longer validates
429 More than 10 real runs a minute (dry runs count toward the 30 a minute shared with test), or Forkline is busy. A Retry-After header says when to retry. Nothing was started. A request refused before it is evaluated does not count toward the limits; a real run that its filters refuse does. forkline routine run exits with code 9
{
  "error": "Forkline is busy; retry shortly",
  "code": "TOO_MANY_REQUESTS",
  "retry_after_secs": 5
}

The routine’s concurrency settings apply as for any other trigger, except that Run now never replaces a running run.

POST /v1/routines/{id}/webhook/rotate-secret

Replaces the signing secret of an on.webhook routine. No request body. Requires routines:write.

curl -X POST \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/routines/<routine_id>/webhook/rotate-secret

Response (200 OK): the RoutineResponse with a new webhook_secret, shown only in this response. The previous secret stops working immediately. Returns 400 if the routine has no on.webhook trigger, or if its secret was pasted by you (github, sentry): rotate that one at the sender.

PUT /v1/routines/{id}/webhook/secret

Stores the signing secret that a vendor gave you, for an on.webhook routine with auth.preset github or sentry. Requires routines:write.

curl -X PUT \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"secret": "<the vendor secret>"}' \
  https://api.forkline.dev/v1/routines/<routine_id>/webhook/secret

The secret is 16 to 512 bytes; surrounding whitespace is trimmed. Response (200 OK): the RoutineResponse with webhook_secret_configured: true, webhook_secret_origin: "provided" and webhook_secret_updated_at. The secret is never returned. Returns 400 if the routine has no on.webhook trigger, if its secret is generated by Forkline (a token or preset: forkline; use rotate instead), or if the secret is too short or too long. Switching between a token and forkline keeps the secret. Moving to github or sentry, or between them, clears it, so paste the new one. Moving from a pasted secret to a token or forkline generates one, shown once. A published routine keeps answering 401 Missing signature until you paste it.

GET /v1/routines/{id}/tasks

Returns the 50 most recent tasks created by this routine.

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/routines/<routine_id>/tasks

Response (200 OK) (ListRoutineTasksResponse):

{
  "tasks": [
    {
      "id": "t1a2b3c4-...",
      "status": "completed",
      "task_type": "generic",
      "repository_url": "https://github.com/octocat/my-project",
      "result_type": "pull_request",
      "created_at": "2026-09-18T14:25:00Z",
      "started_at": "2026-09-18T14:26:00Z",
      "completed_at": "2026-09-18T14:35:00Z",
      "error_message": null
    }
  ]
}

Task statuses: pending, running, completed, failed. Poll this endpoint to track progress after a routine run.

GET /v1/routines/{id}/tasks/{task_id}

Returns full detail for a single task, including prompt, result data, and platform result.

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/routines/<routine_id>/tasks/<task_id>

Response (200 OK) (TaskDetailResponse):

{
  "id": "t1a2b3c4-...",
  "status": "completed",
  "task_type": "code_change",
  "repository_url": "https://github.com/octocat/my-project",
  "prompt": "Fix the login bug...",
  "context": { "issue_number": 42 },
  "result_type": "pull_request",
  "result_data": { "title": "Fix login", "body": "...", "head": "fix/login", "base": "main" },
  "platform_result": { "url": "https://github.com/octocat/my-project/pull/99", "number": 99 },
  "error_message": null,
  "runner_id": "r1a2b3c4-...",
  "attempt_count": 1,
  "created_at": "2026-09-18T14:25:00Z",
  "started_at": "2026-09-18T14:26:00Z",
  "completed_at": "2026-09-18T14:35:00Z"
}

Returns 404 if the routine or task does not exist, or if the task does not belong to the routine.


Connectors

Method Path Scope Description
GET /v1/connectors connectors:read List your git connectors

GET /v1/connectors

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/connectors

Response (200 OK):

{
  "connectors": [
    {
      "id": "f0e1d2c3-...",
      "provider": "github",
      "name": "octocat",
      "created_at": "2026-01-10T08:00:00Z"
    }
  ]
}

API Tokens

Method Path Scope Description
GET /v1/me/api-tokens tokens:manage List your API tokens
POST /v1/me/api-tokens tokens:manage Create a new API token
DELETE /v1/me/api-tokens/{token_id} tokens:manage Revoke an API token

GET /v1/me/api-tokens

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/me/api-tokens

Response (200 OK):

{
  "api_tokens": [
    {
      "id": "t1a2b3c4-...",
      "name": "CI Pipeline",
      "scopes": ["runners:read", "runners:write"],
      "created_at": "2026-09-01T10:00:00Z",
      "expires_at": "2026-12-01T10:00:00Z",
      "last_used_at": "2026-09-18T14:00:00Z"
    }
  ]
}

POST /v1/me/api-tokens

Request body (CreateApiTokenRequest):

Field Type Required Description
name string yes Descriptive name for the token
scopes string[] yes Scopes to grant (see Authentication)
expires_in_days integer no Days until expiration (default 90)
never_expires boolean no If true, the token does not expire
curl -X POST \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "CI Pipeline", "scopes": ["runners:read", "runners:write"]}' \
  https://api.forkline.dev/v1/me/api-tokens

Response (201 Created):

{
  "token": "fl_pat_v1_...",
  "api_token": {
    "id": "t1a2b3c4-...",
    "name": "CI Pipeline",
    "scopes": ["runners:read", "runners:write"],
    "created_at": "2026-09-18T14:20:00Z",
    "expires_at": "2026-12-17T14:20:00Z",
    "last_used_at": null
  }
}

Warning: The token field is shown only once at creation. Store it securely.

DELETE /v1/me/api-tokens/{token_id}

Revokes an API token. No request body.

Response: 204 No Content


Repository Secrets

Method Path Scope Description
GET /v1/repositories/{id}/secrets secrets:manage List repository secrets
POST /v1/repositories/{id}/secrets secrets:manage Create a repository secret
PUT /v1/repositories/{id}/secrets/{name} secrets:manage Update a repository secret
DELETE /v1/repositories/{id}/secrets/{name} secrets:manage Delete a repository secret

GET /v1/repositories/{id}/secrets

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/repositories/<repository_id>/secrets

Response (200 OK):

{
  "secrets": [
    {
      "id": "s1a2b3c4-...",
      "repository_id": "a1b2c3d4-...",
      "name": "DEPLOY_KEY",
      "created_at": "2026-09-01T10:00:00Z",
      "updated_at": "2026-09-15T12:00:00Z"
    }
  ]
}

Note: Secret values are never returned in API responses.

POST /v1/repositories/{id}/secrets

Request body (CreateSecretRequest):

Field Type Required Description
name string yes Secret name (unique per repository)
value string yes Secret value
curl -X POST \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "DEPLOY_KEY", "value": "secret-value"}' \
  https://api.forkline.dev/v1/repositories/<repository_id>/secrets

Response (201 Created): Returns the RepositorySecret metadata (without the value).

PUT /v1/repositories/{id}/secrets/{name}

Request body (UpdateSecretRequest):

Field Type Required Description
value string yes New secret value
curl -X PUT \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"value": "new-secret-value"}' \
  https://api.forkline.dev/v1/repositories/<repository_id>/secrets/DEPLOY_KEY

Response: 200 OK

DELETE /v1/repositories/{id}/secrets/{name}

Deletes a repository secret. No request body.

Response: 204 No Content


Account Secrets

Account credentials that MCP servers can bind to. Values are write-only: they are stored in Vault and no response ever returns them.

Method Path Scope Description
GET /v1/me/secrets secrets:manage List account credentials (metadata only)
POST /v1/me/secrets secrets:manage Create a credential
PUT /v1/me/secrets/{name} secrets:manage Create or rotate a credential
PATCH /v1/me/secrets/{name} secrets:manage Update a credential’s description
DELETE /v1/me/secrets/{name} secrets:manage Delete a credential

Names must match ^[A-Z_][A-Z0-9_]*$ (maximum 255 characters) — the same grammar the CLI enforces, so a typo fails at the request instead of when a server is enabled.

GET /v1/me/secrets

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/me/secrets

Response (200 OK):

{
  "secrets": [
    {
      "id": "9f1c2a34-...",
      "user_id": "a1b2c3d4-...",
      "name": "GRAFANA_TOKEN",
      "description": "Read-only Grafana token",
      "rotated_at": "2026-10-06T08:12:00Z",
      "created_at": "2026-09-20T10:00:00Z",
      "updated_at": "2026-10-06T08:12:00Z",
      "used_by": [ { "server_id": "c3d4e5f6-...", "name": "Grafana" } ]
    }
  ]
}

POST /v1/me/secrets

Request body (CreateSecretRequest):

Field Type Required Description
name string yes Credential name (^[A-Z_][A-Z0-9_]*$, max 255 characters)
value string yes Secret value; written to Vault, never returned
curl -X POST \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "GRAFANA_TOKEN", "value": "glsa_xxx"}' \
  https://api.forkline.dev/v1/me/secrets

Response (201 Created): the AccountSecret metadata. Returns 409 if the name already exists. This endpoint takes no description — use PUT or PATCH for that.

PUT /v1/me/secrets/{name}

Request body (UpsertAccountSecretRequest): value (required) and description (optional). Creates the credential if it does not exist, otherwise rotates it and stamps rotated_at.

curl -X PUT \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"value": "glsa_new"}' \
  https://api.forkline.dev/v1/me/secrets/GRAFANA_TOKEN

Response (200 OK): the updated AccountSecret.

PATCH /v1/me/secrets/{name}

Request body (UpdateAccountSecretDescriptionRequest): { "description": "…" }. Only the description changes; the stored value is untouched. Send an empty string to clear it.

DELETE /v1/me/secrets/{name}

Response (204 No Content). Servers that bound the credential are not modified; they read needs_setup until it is provided again.

MCP Catalog

The catalog is the set of MCP servers Forkline knows about, each with one or more compiled revisions carrying a frozen install plan.

Method Path Scope Description
GET /v1/mcp-catalog mcp:read Browse catalog entries
GET /v1/mcp-catalog/{id} mcp:read One entry with its revisions

{id} accepts either the source-qualified external_id (mcp-registry/io.github.context7/context7) or the short slug (context7).

Query Parameters: GET /v1/mcp-catalog

Parameter Type Description
q string Substring search over title, slug, and canonical name. A search also returns servers that are not available on Forkline, with installable: false and an unavailable_reason
after string Cursor: the next_cursor of the previous page, passed back unchanged
limit integer Page size, default 50, clamped to 1..500

The list is two installable groups, Verified first and then Unverified (curated servers first, then by title). It has no filters. Each entry has a tier of verified, unverified, or unsupported; verified_versions lists the versions that are Verified, and latest_version is the newest installable version.

GET /v1/mcp-catalog

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  "https://api.forkline.dev/v1/mcp-catalog?q=grafana&limit=20"

Response (200 OK):

{
  "entries": [
    {
      "id": "4d7e1b22-...",
      "external_id": "mcp-registry/io.github.context7/context7",
      "source": "official_registry",
      "canonical_name": "io.github.context7/context7",
      "slug": "context7",
      "title": "Context7",
      "description": "Up-to-date documentation for any library",
      "category": "Developer Tools",
      "tags": ["docs", "libraries"],
      "repository_url": "https://github.com/upstash/context7",
      "website_url": "https://context7.com",
      "license": "MIT",
      "transport": "local",
      "version": "3.2.1",
      "tier": "tested",
      "warnings": [],
      "credential_requirements": [
        {
          "name": "CTX7_KEY",
          "description": "API key for higher rate limits",
          "required": false,
          "target": "environment",
          "is_secret": true,
          "format": null,
          "default_value": null
        }
      ],
      "plan_hash": "sha256:6f0a…",
      "compiler_version": "plan-compiler/1",
      "recipe_version": "1",
      "latest_version": "3.3.0",
      "status": "active",
      "curated": true,
      "hidden": false,
      "default_revision_id": "8ab4…",
      "created_at": "2026-08-01T00:00:00Z",
      "updated_at": "2026-10-05T04:20:00Z"
    }
  ],
  "next_cursor": "mcp-registry/io.github.context7/context7",
  "snapshot_stale_at": "2026-10-04T04:10:00Z"
}

next_cursor is null on the last page; pass it back as after to continue. A tier of unsupported carries a mandatory reason code in unsupported_reason (oci, cargo, nuget, mcpb, oauth-remote, egress, or no-plan) plus unsupported_detail and usually a remediation. snapshot_stale_at is set when the service is serving a last-known-good snapshot because the upstream registry could not be refreshed.

GET /v1/mcp-catalog/{id}

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/mcp-catalog/context7

Response (200 OK): { "entry": { … }, "revisions": [ … ] }, where each revision carries its own version, tier, plan, plan_hash, credential_requirements, warnings, evidence (the harness record behind a tested claim), and superseded_by / superseded_at when a newer revision replaced it.

MCP Servers

An MCP server is a user-owned tool server your runners load: installed from the catalog (pinned to one revision) or supplied as custom configuration. Every handler is scoped to the authenticated user: another user’s server is a 404, never a 200. A server is switched on and off with enabled; there is no publish step.

Method Path Scope Description
GET /v1/mcp-servers mcp:read List your servers with their status
POST /v1/mcp-servers mcp:write Create a server
POST /v1/mcp-servers/parse mcp:write Turn pasted JSON (Claude Code, Cursor, VS Code, OpenCode) into draft servers; stores nothing
GET /v1/mcp-servers/{id} mcp:read Get one server
PATCH /v1/mcp-servers/{id} mcp:write Update a server (one transaction)
DELETE /v1/mcp-servers/{id} mcp:write Delete a server
POST /v1/mcp-servers/{id}/upgrade mcp:write Move a catalog server to another version
GET /v1/mcp-servers/{id}/upgrade-preview mcp:read Current versus target version (version query parameter)
GET /v1/mcp-servers/{id}/repositories mcp:read + repositories:read The repositories the server reaches, with its state on each (paged)
GET /v1/repositories/{id}/mcp-servers mcp:read + repositories:read The servers that apply to one repository

The status tells you whether a server runs, never how it was applied. status.state is one of working, failing, needs_setup, unavailable (“Not running”: a problem on our side), not_used_yet and off.

POST /v1/mcp-servers

Request body (CreateMcpServerRequest):

Field Type Required Description
name string yes Display name
slug string no The Server ID, unique per user (derived from the name when omitted); 409 when taken
source object yes { "catalog": { "entry": "context7", "version": "1.2.0" } } (version defaults to the latest installable one) or { "custom": { "config": { ... } } }
repositories object yes { "include": "all" } or { "include": [ { "name": "acme/*" }, { "id": "<uuid>" } ], "exclude": [ { "name": "acme/legacy" } ] }
inputs object no Non-secret values for the server’s declared inputs
credential_bindings object no Credential name to account/<NAME> or repo/<NAME>
enabled boolean no Defaults to false. A server that is incomplete is saved off and reads needs_setup; enabling it with missing required fields is a 400 with an issues list of JSON pointers

A custom config is internally tagged on transport (local with command, args, environment, or remote_http with url, headers). {env:NAME} inside environment and headers is a reference the runner resolves, never a value.

curl -X POST \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Context7",
    "source": { "catalog": { "entry": "context7" } },
    "repositories": { "include": [ { "name": "acme/*" } ] },
    "credential_bindings": { "CTX7_KEY": "account/CTX7_KEY" },
    "enabled": true
  }' \
  https://api.forkline.dev/v1/mcp-servers

Response (201 Created): the McpServer.

GET /v1/mcp-servers

Response (200 OK): { "servers": [ ... ] }, each with id, name, slug, enabled, generation, a short source, summary, the repositories selector, repository_count, matched (the first three repository names and the count) and status.

GET /v1/mcp-servers/{id}

Response (200 OK):

{
  "id": "c3d4e5f6-...",
  "name": "Context7",
  "slug": "context7",
  "enabled": true,
  "generation": 3,
  "repositories": { "include": [ { "name": "acme/*" } ] },
  "source": {
    "kind": "catalog",
    "entry_id": "4d7e1b22-...",
    "external_id": "mcp-registry/io.github.upstash/context7",
    "title": "Context7",
    "version": "1.2.0",
    "tier": "verified",
    "latest_version": null,
    "latest_tier": null,
    "entry_available": true
  },
  "requirements": [],
  "inputs": {},
  "credential_bindings": { "CTX7_KEY": "account/CTX7_KEY" },
  "status": {
    "state": "working",
    "reason": null,
    "stage": null,
    "since": "2026-10-06T09:05:00Z",
    "repositories_total": 4,
    "repositories_ok": 4,
    "repositories_failing": 0,
    "repositories_needs_setup": 0,
    "repositories_not_running": 0,
    "repositories_not_used": 0,
    "top_problem": null,
    "last_reported_at": "2026-10-06T09:06:00Z"
  },
  "matched": { "count": 4, "names": ["acme/api", "acme/web", "acme/docs"] },
  "launch": { "transport": "local", "args": [], "env": [], "headers": [], "opencode_config": {} },
  "summary": "npx @upstash/context7-mcp@1.2.0",
  "created_at": "2026-10-06T09:00:00Z",
  "updated_at": "2026-10-06T09:05:00Z"
}

launch shows the saved server as it will run, with secrets as names, never values. source.latest_version is set when the entry has a newer installable version than the pinned one.

PATCH /v1/mcp-servers/{id}

Request body (UpdateMcpServerRequest): any of name, slug, enabled, repositories, inputs, credential_bindings, config (custom servers only; a catalog server rejects it) and generation. Absent fields keep their value; the whole request is one transaction. Send the generation you read to get optimistic concurrency: a stale value is a 409. Response (200 OK): the McpServer.

DELETE /v1/mcp-servers/{id}

Response (204 No Content). The server is deleted, not archived.

POST /v1/mcp-servers/{id}/upgrade

Request body: version (an exact version of the catalog entry), plus optional inputs and credential_bindings for anything the new version requires. The server keeps its inputs and bindings otherwise. Response (200 OK): the McpServer. Use GET /v1/mcp-servers/{id}/upgrade-preview?version= first to see what changes: the launch of both versions, whether the package or the publisher changed, and the requirements added or removed.

GET /v1/mcp-servers/{id}/repositories

Lists the repositories the server reaches, evaluated with the same targeting rules task dispatch uses, with the state on each, problems first. It names repository owners and names, so the token needs both mcp:read and repositories:read; with only one of them the request is refused with 403. Query parameters: offset (default 0) and limit (default 50, max 200). Response (200 OK): { "items": [ ... ], "total": 12, "next_offset": 50 }; next_offset is null on the last page. A row with platform: true is a problem on our side: show a neutral message and not reason.

GET /v1/repositories/{id}/mcp-servers

The effective view for one repository (mcp:read and repositories:read): servers (each with server_id, name, slug, summary and that repository’s status), read-only raw entries from your account and repository opencode_config.mcp with shadowed_by when a server overrides one, and warnings.

POST /v1/mcp-servers/parse

Request body: { "text": "<pasted JSON>" }. Accepts the mcpServers / servers shape of Claude Code, Cursor and VS Code, and OpenCode’s mcp. Response (200 OK): the detected format and the draft servers, each with key, name, slug, config, credential_bindings, secret_hints (literal secret-looking values to move into account secrets) and problems. Nothing is stored. A syntax error is a 400 with the line and column.

Custom Providers

Method Path Scope Description
GET /v1/custom-providers providers:manage List custom providers
POST /v1/custom-providers/import providers:manage Import a custom provider
DELETE /v1/custom-providers/{provider_id} providers:manage Delete a custom provider

GET /v1/custom-providers

curl -H "Authorization: Bearer $FORKLINE_TOKEN" \
  https://api.forkline.dev/v1/custom-providers

Response (200 OK):

{
  "providers": [
    {
      "id": "cp-1234-...",
      "provider_id": "my-provider",
      "name": "My Provider",
      "base_url": "https://api.my-provider.com/v1",
      "created_at": "2026-09-01T10:00:00Z"
    }
  ]
}

POST /v1/custom-providers/import

Imports a custom provider from an OpenCode provider configuration object.

Request body (ImportCustomProviderRequest):

Field Type Required Description
provider object yes Map with a single key (provider ID) and its configuration
curl -X POST \
  -H "Authorization: Bearer $FORKLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": {
      "my-provider": {
        "name": "My Provider",
        "options": { "baseURL": "https://api.my-provider.com/v1" }
      }
    }
  }' \
  https://api.forkline.dev/v1/custom-providers/import

Response (201 Created): Returns the CustomProvider object.

DELETE /v1/custom-providers/{provider_id}

Deletes a custom provider by its database ID. No request body.

Response: 204 No Content


API Documentation

Method Path Scope Description
GET /v1/docs (public) Interactive Swagger UI
GET /v1/docs/openapi.json (public) OpenAPI specification