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 inerrors.delete: Permanently deletes runners in a terminal state. Non-terminal runners are skipped and reported inerrors.
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 showwebhook_secret_configured,webhook_secret_origin(generatedby Forkline, orprovidedby you) andwebhook_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
tokenfield 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 |