📚 Help REST API Reference

REST API Reference

The Isurus REST API is available at /api/v1/. All responses return JSON.

Authentication

Authenticate API requests using a Bearer token in the Authorization header:

curl -H "Authorization: Bearer YOUR_TOKEN" \
     https://your-isurus-instance/api/v1/user

Tokens can be created at three levels:

  • Personal tokensSettings > Security > API Tokens — access all your repositories
  • Org tokensOrg Settings > Integration > Tokens — access all repos in that org
  • Repo tokensRepo Settings > Integration > Tokens — access only that repo

Token Scopes

Scope Access
repo:read Clone/read repository content
repo:write Push to repositories, update settings
issues:read Read issues and comments
issues:write Create/update issues, add comments
pulls:read Read pull requests
pulls:write Create/update/merge pull requests
releases:read Read releases and assets
releases:write Create/update releases, upload assets
ci:read View pipeline status and logs
ci:write Trigger, cancel, and rerun pipelines
admin Full access — settings, webhooks, secrets, delete repos

Some endpoints are public (no token required). Private repositories require at least a :read scope.

Repo-scoped and org-scoped tokens are automatically restricted to their target — a repo token cannot access other repos, even if the user has access to them.

Response Format

Success

{
  "data": { ... }
}

List with Pagination

{
  "data": [ ... ],
  "pagination": {
    "page": 1,
    "per_page": 30,
    "total": 42
  }
}

Error

{
  "error": {
    "code": "not_found",
    "message": "Repository not found"
  }
}

Pagination

List endpoints accept page and per_page query parameters:

Parameter Default Max
page 1
per_page 30 100

User

Get Current User

GET /api/v1/user

Scope: Any valid token

Returns the authenticated user's profile.

Response:

{
  "data": {
    "id": 1,
    "username": "chris",
    "email": "chris@example.com",
    "display_name": "Chris",
    "is_admin": true,
    "avatar_url": "",
    "created_at": "2025-01-15T10:00:00Z"
  }
}

API Tokens

List Tokens

GET /api/v1/user/tokens

Scope: Any valid token

Returns all tokens for the authenticated user (token values are not included).

Create Token

POST /api/v1/user/tokens

Scope: Any valid token

Body:

{
  "name": "CI Release Token",
  "scopes": ["releases:write"],
  "expires_at": "2026-12-31T23:59:59Z"
}

The expires_at field is optional (RFC 3339 format). Omit it for a non-expiring token.

Response: Returns the token object with a token field containing the plaintext value. This is the only time the token value is returned — store it securely.

Revoke Token

DELETE /api/v1/user/tokens/:id

Scope: Any valid token

Permanently deletes the token.


Organizations

Get Organization

GET /api/v1/orgs/:org

Auth: Optional (public endpoint)

Response:

{
  "data": {
    "id": 1,
    "name": "leafscale",
    "description": "Leafscale, LLC",
    "created_at": "2025-01-15T10:00:00Z"
  }
}

List Organization Repositories

GET /api/v1/orgs/:org/repos

Auth: Optional. Without auth, only public repos are returned. With auth, private repos visible to the user are included.

List Organization Members

GET /api/v1/orgs/:org/members

Scope: Any valid token (must be an org member)

Returns members with their roles.

Response:

{
  "data": [
    {
      "user": { "id": 1, "username": "chris", ... },
      "role": "owner"
    }
  ]
}

Repositories

Get Repository

GET /api/v1/repos/:org/:repo

Auth: Optional (required for private repos)

Response:

{
  "data": {
    "id": 1,
    "name": "my-project",
    "org": "leafscale",
    "description": "A cool project",
    "is_private": false,
    "created_at": "2025-01-15T10:00:00Z",
    "updated_at": "2025-01-20T14:30:00Z"
  }
}

Create Repository

POST /api/v1/orgs/:org/repos

Scope: repo:write (must be an org member)

Body:

{
  "name": "new-repo",
  "description": "My new repository",
  "is_private": false
}

Update Repository

PATCH /api/v1/repos/:org/:repo

Scope: repo:write (must be an org owner)

Body (all fields optional):

{
  "description": "Updated description",
  "is_private": true
}

Delete Repository

DELETE /api/v1/repos/:org/:repo

Scope: admin (must be an org owner)

Warning: This permanently deletes the repository and all its data.


Repository Contents

List Directory

GET /api/v1/repos/:org/:repo/tree/*path

Auth: Optional (required for private repos)

Query parameters:

Parameter Default Description
rev tip Revision (changeset hash, tag, bookmark, or tip)

Response:

{
  "data": [
    { "name": "src", "is_dir": true, "size": 0 },
    { "name": "README.md", "is_dir": false, "size": 1234 }
  ]
}

Get File Contents

GET /api/v1/repos/:org/:repo/blob/*path

Auth: Optional (required for private repos)

Query parameters:

Parameter Default Description
rev tip Revision

Response:

{
  "data": {
    "path": "README.md",
    "content": "# My Project\n...",
    "size": 1234
  }
}

Get Commit Log

GET /api/v1/repos/:org/:repo/log

Auth: Optional (required for private repos)

Query parameters:

Parameter Default Description
rev tip Starting revision
page 1 Page number
per_page 30 Results per page

Get Changeset

GET /api/v1/repos/:org/:repo/changeset/:rev

Auth: Optional (required for private repos)

Returns changeset details including the diff.

Response:

{
  "data": {
    "id": "abc123...",
    "short_id": "abc123",
    "author": "Chris <chris@example.com>",
    "date": "2025-01-20T14:30:00Z",
    "message": "Full commit message",
    "subject": "First line of commit",
    "branch": "default",
    "tags": ["v1.0"],
    "diff": "diff -r ..."
  }
}

Issues

Issues have three states: new, open, and closed. New issues start in the new state. Triaging an issue moves it to open. Closing an issue moves it to closed. Reopening a closed issue returns it to open.

Note: Label management, issue templates, assignee management, and issue pinning are currently available through the web interface only. API endpoints for these features are planned for a future release.

List Issues

GET /api/v1/repos/:org/:repo/issues

Auth: Optional (required for private repos)

Query parameters:

Parameter Default Description
state open Filter by state: new, open, or closed
page 1 Page number
per_page 30 Results per page

Response:

{
  "data": [
    {
      "id": 1,
      "number": 1,
      "title": "Bug: something is broken",
      "body": "Steps to reproduce...",
      "state": "new",
      "author": {
        "id": 1,
        "username": "chris",
        "email": "chris@example.com",
        "display_name": "Chris",
        "is_admin": true,
        "avatar_url": "",
        "created_at": "2025-01-15T10:00:00Z"
      },
      "labels": [
        { "id": 1, "name": "bug", "color": "#DC2626" }
      ],
      "created_at": "2025-01-20T14:30:00Z",
      "updated_at": "2025-01-20T14:30:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 30,
    "total": 5
  }
}

The labels array is included when the issue has labels assigned. The author object is included when available.

Get Issue

GET /api/v1/repos/:org/:repo/issues/:number

Auth: Optional (required for private repos)

Returns the issue with all comments.

Response:

{
  "data": {
    "id": 1,
    "number": 1,
    "title": "Bug: something is broken",
    "body": "Steps to reproduce...",
    "state": "open",
    "author": {
      "id": 1,
      "username": "chris",
      "display_name": "Chris"
    },
    "labels": [
      { "id": 1, "name": "bug", "color": "#DC2626" }
    ],
    "comments": [
      {
        "id": 1,
        "body": "I can reproduce this on rev abc123.",
        "author": {
          "id": 2,
          "username": "alice",
          "display_name": "Alice"
        },
        "created_at": "2025-01-21T09:00:00Z"
      }
    ],
    "created_at": "2025-01-20T14:30:00Z",
    "updated_at": "2025-01-21T09:00:00Z"
  }
}

The comments array is only included when the issue has comments.

Create Issue

POST /api/v1/repos/:org/:repo/issues

Scope: issues:write

Body:

{
  "title": "Bug: something is broken",
  "body": "Steps to reproduce..."
}

New issues are created in the new state.

Update Issue

PATCH /api/v1/repos/:org/:repo/issues/:number

Scope: issues:write (must be author or org owner)

Body (all fields optional):

{
  "title": "Updated title",
  "body": "Updated description",
  "state": "closed"
}

Set state to "closed" to close the issue, or "open" to reopen a closed issue.

Comment on Issue

POST /api/v1/repos/:org/:repo/issues/:number/comments

Scope: issues:write

Body:

{
  "body": "Thanks for the report, this is fixed in rev abc123."
}

Pull Requests

List Pull Requests

GET /api/v1/repos/:org/:repo/pulls

Auth: Optional (required for private repos)

Query parameters:

Parameter Default Description
status open Filter: open, closed, or merged
page 1 Page number
per_page 30 Results per page

Get Pull Request

GET /api/v1/repos/:org/:repo/pulls/:number

Auth: Optional (required for private repos)

Returns the PR with all comments.

Create Pull Request

POST /api/v1/repos/:org/:repo/pulls

Scope: pulls:write (must be an org member)

Body:

{
  "title": "Add new feature",
  "body": "This PR adds...",
  "source_ref": "my-feature",
  "target_ref": "@"
}

The API accepts the VCS-neutral names source_ref/target_ref; PR responses additionally include a ref_type field (bookmark for Mercurial, branch for Git/Fossil) indicating the kind of ref. ref_type is response-only and is not accepted in requests.

If target_ref is omitted, it defaults to the mainline @ bookmark for Mercurial repos, or the repository's default branch for Git/Fossil repos.

Update Pull Request

PATCH /api/v1/repos/:org/:repo/pulls/:number

Scope: pulls:write (must be author or org owner)

Body (all fields optional):

{
  "title": "Updated title",
  "body": "Updated description"
}

Merge Pull Request

POST /api/v1/repos/:org/:repo/pulls/:number/merge

Scope: pulls:write (must be an org member)

Merges the source bookmark into mainline (the @ bookmark). Fast-forwards when mainline hasn't diverged, otherwise creates a merge commit; @ advances to the result.

Comment on Pull Request

POST /api/v1/repos/:org/:repo/pulls/:number/comments

Scope: pulls:write

Body:

{
  "body": "LGTM, ship it!"
}

CI/CD Pipelines

List Pipelines

GET /api/v1/repos/:org/:repo/pipelines

Auth: Optional (required for private repos)

Query parameters:

Parameter Default Description
status (all) Filter by status: pending, running, success, failure, cancelled
page 1 Page number
per_page 20 Results per page (max 50)

Response:

{
  "total": 42,
  "page": 1,
  "per_page": 20,
  "pipelines": [
    {
      "id": 1,
      "number": 7,
      "commit_node": "a1b2c3d4e5f6...",
      "branch": "default",
      "tag": "",
      "event": "push",
      "events": ["push"],
      "status": "success",
      "trigger_by": 1,
      "trigger_user": { "id": 1, "username": "chris" },
      "created_at": "2025-01-20T14:30:00Z",
      "started_at": "2025-01-20T14:30:05Z",
      "finished_at": "2025-01-20T14:32:10Z"
    }
  ]
}

tag is the tag name for tag-triggered pipelines (empty string otherwise). events is the full list of events that triggered the pipeline (e.g. ["push", "pull_request"] for a dual-event pipeline) — event remains the single primary event. error_message (human-readable failure reason) is included only when set, e.g. on a failure or error status pipeline:

{
  "error_message": "clone failed: repository not found"
}

Get Pipeline

GET /api/v1/repos/:org/:repo/pipelines/:number

Auth: Optional (required for private repos)

Returns the pipeline with all steps.

Response:

{
  "id": 1,
  "number": 7,
  "commit_node": "a1b2c3d4e5f6...",
  "branch": "default",
  "tag": "",
  "event": "push",
  "events": ["push"],
  "status": "success",
  "trigger_by": 1,
  "trigger_user": { "id": 1, "username": "chris" },
  "created_at": "2025-01-20T14:30:00Z",
  "started_at": "2025-01-20T14:30:05Z",
  "finished_at": "2025-01-20T14:32:10Z",
  "steps": [
    {
      "id": 1,
      "number": 1,
      "name": "test",
      "image": "golang:1.26",
      "status": "success",
      "exit_code": 0,
      "started_at": "2025-01-20T14:30:05Z",
      "finished_at": "2025-01-20T14:32:10Z"
    }
  ]
}

Cancel Pipeline

POST /api/v1/repos/:org/:repo/pipelines/:number/cancel

Scope: ci:write (must be an org member)

Cancels a running or pending pipeline and all its pending/running steps.

Re-run Pipeline

POST /api/v1/repos/:org/:repo/pipelines/:number/rerun

Scope: ci:write (must be an org member)

Creates a new pipeline from the same commit. Returns the new pipeline object.

Note: Pipeline deletion (single, bulk, and delete-all-failed) is available through the web interface only. API endpoints for pipeline deletion are planned for a future release.


CI Agent API

These endpoints are used by isurus-agent workers. They use agent token authentication (not user API tokens).

Agent Authentication

Agents authenticate with a Bearer token issued during agent registration:

curl -H "Authorization: Bearer AGENT_TOKEN" \
     -X POST https://your-isurus-instance/api/v1/ci/poll

Poll for Job

POST /api/v1/ci/poll

Claims the next pending pipeline. Returns 204 No Content if no work is available.

Response (200):

{
  "pipeline_id": 42,
  "number": 7,
  "commit_node": "a1b2c3d4e5f6...",
  "branch": "default",
  "event": "push",
  "clone_url": "https://hg.example.com/leafscale/isurus",
  "org": "leafscale",
  "repo": "isurus",
  "steps": [
    {
      "id": 1,
      "number": 1,
      "name": "test",
      "image": "golang:1.26",
      "commands": ["go test -v ./..."],
      "environment": { "CGO_ENABLED": "0" },
      "status": "pending"
    }
  ],
  "ci_env": {
    "CI": "true",
    "CI_PIPELINE_ID": "42",
    "CI_COMMIT": "a1b2c3d4e5f6...",
    "CI_BRANCH": "default",
    "CI_REPO": "leafscale/isurus"
  }
}

Heartbeat

POST /api/v1/ci/heartbeat

Updates the agent's last-seen timestamp. Called on each poll cycle.

Update Step Status

PUT /api/v1/ci/steps/:id/status

Body:

{
  "status": "running",
  "exit_code": 0,
  "started_at": "2025-01-20T14:30:05Z",
  "finished_at": "2025-01-20T14:32:10Z"
}

Status values: pending, running, success, failure, skipped, cancelled

Update Pipeline Status

PUT /api/v1/ci/pipelines/:id/status

Body:

{
  "status": "success",
  "finished_at": "2025-01-20T14:32:10Z"
}

Status values: pending, running, success, failure, cancelled, error

Append Step Log

POST /api/v1/ci/steps/:id/log

Appends raw log data to the step's log file. Body is raw text (not JSON). Maximum 1MB per request.


Webhooks

List Webhooks

GET /api/v1/repos/:org/:repo/webhooks

Scope: admin (must be an org owner)

Create Webhook

POST /api/v1/repos/:org/:repo/webhooks

Scope: admin (must be an org owner)

Body:

{
  "url": "https://example.com/webhook",
  "secret": "my-secret",
  "events": ["push", "tag"]
}

Event types: push, tag, pull_request.opened, pull_request.closed, pull_request.merged, pull_request.commented, issue.opened, issue.closed, issue.reopened, issue.commented

tag is subscribed independently of push — a repository that only wants tag notifications does not need push checked, and vice versa. See Webhook Payloads below for what each event delivers.

Update Webhook

PATCH /api/v1/repos/:org/:repo/webhooks/:webhook_id

Scope: admin (must be an org owner)

Body (all fields optional):

{
  "url": "https://example.com/new-endpoint",
  "events": ["push"],
  "is_active": false
}

Delete Webhook

DELETE /api/v1/repos/:org/:repo/webhooks/:webhook_id

Scope: admin (must be an org owner)

List Webhook Deliveries

GET /api/v1/repos/:org/:repo/webhooks/:webhook_id/deliveries

Scope: admin (must be an org owner)

Returns the 20 most recent deliveries.

Response:

{
  "data": [
    {
      "id": 1,
      "event_type": "push",
      "response_code": 200,
      "duration_ms": 150,
      "success": true,
      "delivered_at": "2025-01-20T14:30:00Z"
    }
  ]
}

Webhook Payloads

Every delivery is an HTTP POST with Content-Type: application/json and a common envelope. The event-specific data is nested under payload.

Headers:

Header Description
X-Isurus-Event The event type, e.g. push, tag, issue.opened
X-Isurus-Delivery A UUID unique to this delivery attempt (retries of the same trigger share the delivery ID)
X-Isurus-Signature-256 sha256=<hex hmac> — HMAC-SHA256 of the raw request body using the webhook's configured secret. Omitted if the webhook has no secret set.
User-Agent Isurus-Webhooks/1.0

Envelope:

{
  "event": "push",
  "delivery_id": "b3f2a8e0-1e5b-4a3a-9c2e-2b7a6f0d9c1a",
  "repository": {
    "id": 42,
    "name": "reef-lang",
    "org": "leafscale",
    "url": "https://example.com/leafscale/reef-lang"
  },
  "sender": {
    "id": 7,
    "display_name": "Chris Tusa"
  },
  "timestamp": "2026-07-07T14:03:11Z",
  "payload": { }
}

Verify the signature by computing hmac_sha256(secret, raw_request_body) and comparing (constant-time) to the hex digest after sha256=.

Push payload

Fires on push. ref_type is "branch" for Git and Fossil pushes (ref is the branch name). For Mercurial, ref_type is "bookmark" only when a bookmark actually moved (ref is the bookmark name, e.g. @); an anonymous push to the default line reports ref "default" with ref_type "branch". commits holds up to 50 entries, newest-first; if the push introduced more than 50 commits, truncated is true and only the newest 50 are included. before/after are the repository tip before and after the push — for Mercurial these are changegroup-scoped tips, not a single ref's prior/new value. deleted is reserved and always false (ref deletion isn't fired yet).

{
  "ref": "default",
  "ref_type": "branch",
  "default": true,
  "before": "a1b2c3d4e5f60000000000000000000000000000",
  "after": "f6e5d4c3b2a10000000000000000000000000000",
  "created": false,
  "deleted": false,
  "commits": [
    {
      "id": "f6e5d4c3b2a10000000000000000000000000000",
      "short_id": "f6e5d4c3b2a1",
      "message": "Fix off-by-one in tag walk\n\nAlso adds a regression test.",
      "subject": "Fix off-by-one in tag walk",
      "author": {
        "name": "Chris Tusa",
        "email": "chris.tusa@leafscale.com",
        "date": "2026-07-07T14:03:05Z"
      },
      "url": "https://example.com/leafscale/reef-lang/commit/f6e5d4c3b2a10000000000000000000000000000"
    }
  ],
  "head_commit": {
    "id": "f6e5d4c3b2a10000000000000000000000000000",
    "short_id": "f6e5d4c3b2a1",
    "message": "Fix off-by-one in tag walk\n\nAlso adds a regression test.",
    "subject": "Fix off-by-one in tag walk",
    "author": {
      "name": "Chris Tusa",
      "email": "chris.tusa@leafscale.com",
      "date": "2026-07-07T14:03:05Z"
    },
    "url": "https://example.com/leafscale/reef-lang/commit/f6e5d4c3b2a10000000000000000000000000000"
  },
  "total_commits": 1,
  "truncated": false
}
Field Type Description
ref string The branch/bookmark name that moved, or "default" for an anonymous Mercurial push
ref_type string "branch" (Git, Fossil, and anonymous Mercurial default-line pushes) or "bookmark" (a Mercurial bookmark moved)
default bool Whether ref is the repository's default/mainline line
before string Repo tip before the push (empty when the ref was newly created)
after string Repo tip after the push
created bool Whether this push created the ref
deleted bool Reserved; always false
commits array Up to 50 commits introduced by the push, newest-first
head_commit object | null The first (newest) entry of commits, or null if commits is empty
total_commits int Number of commits in the commits array (i.e. len(commits), capped at 50)
truncated bool true if the push introduced more than 50 commits

Tag payload

Fires on tag — subscribable independently of push. A tag event is raised for a Git tag push, a Fossil tag, or a Mercurial commit that adds/modifies .hgtags (in Mercurial, applying a tag is itself a commit). commit is the tagged commit (same shape as a push commit) and is omitted if it could not be resolved.

{
  "ref": "v1.4.0",
  "ref_type": "tag",
  "created": true,
  "target": "f6e5d4c3b2a10000000000000000000000000000",
  "commit": {
    "id": "f6e5d4c3b2a10000000000000000000000000000",
    "short_id": "f6e5d4c3b2a1",
    "message": "Release 1.4.0",
    "subject": "Release 1.4.0",
    "author": {
      "name": "Chris Tusa",
      "email": "chris.tusa@leafscale.com",
      "date": "2026-07-07T14:03:05Z"
    },
    "url": "https://example.com/leafscale/reef-lang/commit/f6e5d4c3b2a10000000000000000000000000000"
  }
}
Field Type Description
ref string Tag name
ref_type string Always "tag"
created bool Always true (tag deletion events are not yet fired)
target string Commit the tag points at
commit object Present when the target commit could be resolved; omitted otherwise

Pull request and issue event payloads are unchanged from their existing shapes (see the Pull Requests and Issues docs).


Releases

List Releases

GET /api/v1/repos/:org/:repo/releases

Returns paginated releases. Draft releases are only visible to org members.

Query parameters: page, per_page

Get Release

GET /api/v1/repos/:org/:repo/releases/:id

Get Release by Tag

GET /api/v1/repos/:org/:repo/releases/tags/:tag

Create Release

POST /api/v1/repos/:org/:repo/releases

Requires: releases:write scope

{
  "tag_name": "v1.0.0",
  "name": "Version 1.0.0",
  "body": "Release notes in markdown",
  "draft": false,
  "prerelease": false
}

Update Release

PATCH /api/v1/repos/:org/:repo/releases/:id

Requires: releases:write scope. All fields are optional — only provided fields are updated.

Delete Release

DELETE /api/v1/repos/:org/:repo/releases/:id

Requires: releases:write scope. Deletes the release and all its attachments.

List Release Assets

GET /api/v1/repos/:org/:repo/releases/:id/assets

Upload Release Asset

POST /api/v1/repos/:org/:repo/releases/:id/assets

Requires: releases:write scope. Send as multipart/form-data with field name attachment.

Delete Release Asset

DELETE /api/v1/repos/:org/:repo/releases/:id/assets/:asset_id

Requires: releases:write scope

×