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 tokens — Settings > Security > API Tokens — access all your repositories
- Org tokens — Org Settings > Integration > Tokens — access all repos in that org
- Repo tokens — Repo 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