Skip to content

API code samples

Working examples for the operations that show up in 90% of CI integrations. Each block is paired across curl, Python (requests), Go (net/http), and JavaScript (fetch) so you can pick the language your pipeline already speaks and stay there.

The full per-endpoint schema lives in the API explorer; this page is the cookbook on top.

Conventions used in every sample

bash
export ORBITAL_HOST="https://orbitalreg.example.com"
export ORBITAL_TOKEN="orbsa_…"     # service-account or PAT

The Python snippets assume requests (pip install requests); the Go snippets assume the standard library only; the JavaScript snippets target Node 18+ or any modern browser (both ship fetch natively).

All endpoints return JSON unless explicitly noted. Errors share a single envelope; the snippets below check r.ok / resp.StatusCode rather than wrapping each call in framework-specific exception machinery.

1. Read the public config

The unauthenticated probe used by the Login page and any health script that just wants to confirm the deployment is reachable.

bash
curl "$ORBITAL_HOST/api/public-config" | jq .
python
import os, requests

r = requests.get(f"{os.environ['ORBITAL_HOST']}/api/public-config",
                 timeout=10)
r.raise_for_status()
print(r.json())
go
resp, err := http.Get(host + "/api/public-config")
if err != nil { log.Fatal(err) }
defer resp.Body.Close()
var cfg map[string]any
json.NewDecoder(resp.Body).Decode(&cfg)
fmt.Println(cfg["version"], cfg["help_url"])
javascript
const r = await fetch(`${process.env.ORBITAL_HOST}/api/public-config`)
if (!r.ok) throw new Error(`HTTP ${r.status}`)
const cfg = await r.json()
console.log(cfg.version, cfg.help_url)

2. Verify the token (whoami)

Every CI script should run this first. A 200 with a non-null email means the token is alive and not yet revoked; a 401 means rotate.

bash
curl -fsS "$ORBITAL_HOST/api/auth/whoami" \
  -H "Authorization: Bearer $ORBITAL_TOKEN" | jq .
python
r = requests.get(
    f"{os.environ['ORBITAL_HOST']}/api/auth/whoami",
    headers={"Authorization": f"Bearer {os.environ['ORBITAL_TOKEN']}"},
    timeout=10,
)
r.raise_for_status()
print(r.json()["email"])
go
req, _ := http.NewRequest("GET", host+"/api/auth/whoami", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer resp.Body.Close()
if resp.StatusCode != 200 {
    log.Fatalf("whoami failed: %s", resp.Status)
}
javascript
const r = await fetch(`${host}/api/auth/whoami`, {
  headers: { Authorization: `Bearer ${token}` },
})
if (!r.ok) throw new Error(`whoami HTTP ${r.status}`)
const me = await r.json()
console.log(me.email)

3. List projects

Returns every project the calling principal can see. Cursor pagination is supported via ?cursor=…&limit=….

bash
curl -fsS "$ORBITAL_HOST/api/v1/projects?limit=100" \
  -H "Authorization: Bearer $ORBITAL_TOKEN" | jq '.[] | {id, slug, name}'
python
r = requests.get(
    f"{os.environ['ORBITAL_HOST']}/api/v1/projects",
    headers={"Authorization": f"Bearer {os.environ['ORBITAL_TOKEN']}"},
    params={"limit": 100},
    timeout=10,
)
r.raise_for_status()
for p in r.json():
    print(p["slug"], p["id"])
go
req, _ := http.NewRequest("GET", host+"/api/v1/projects?limit=100", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var projects []struct {
    ID, Slug, Name string
}
json.NewDecoder(resp.Body).Decode(&projects)
for _, p := range projects {
    fmt.Printf("%s\t%s\n", p.Slug, p.ID)
}
javascript
const r = await fetch(`${host}/api/v1/projects?limit=100`, {
  headers: { Authorization: `Bearer ${token}` },
})
const projects = await r.json()
for (const p of projects) console.log(p.slug, p.id)

4. Look up a project by slug

The same endpoint but with the ?slug= filter — saves a round-trip when your CI knows the slug but not the UUID.

bash
curl -fsS "$ORBITAL_HOST/api/v1/projects?slug=platform" \
  -H "Authorization: Bearer $ORBITAL_TOKEN" | jq '.[0].id'
python
r = requests.get(
    f"{os.environ['ORBITAL_HOST']}/api/v1/projects",
    headers={"Authorization": f"Bearer {os.environ['ORBITAL_TOKEN']}"},
    params={"slug": "platform"},
)
r.raise_for_status()
project_id = r.json()[0]["id"]
go
u, _ := url.Parse(host + "/api/v1/projects")
q := u.Query(); q.Set("slug", "platform"); u.RawQuery = q.Encode()
req, _ := http.NewRequest("GET", u.String(), nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var rows []struct{ ID string }
json.NewDecoder(resp.Body).Decode(&rows)
projectID := rows[0].ID
javascript
const r = await fetch(
  `${host}/api/v1/projects?slug=${encodeURIComponent('platform')}`,
  { headers: { Authorization: `Bearer ${token}` } },
)
const [project] = await r.json()
const projectId = project.id

5. List repositories in a project

bash
curl -fsS "$ORBITAL_HOST/api/v1/repos?project_id=$PROJECT_ID" \
  -H "Authorization: Bearer $ORBITAL_TOKEN" | jq '.[] | {slug, format, kind}'
python
r = requests.get(
    f"{os.environ['ORBITAL_HOST']}/api/v1/repos",
    headers={"Authorization": f"Bearer {os.environ['ORBITAL_TOKEN']}"},
    params={"project_id": project_id},
)
for repo in r.json():
    print(repo["slug"], repo["format"], repo["kind"])
go
u, _ := url.Parse(host + "/api/v1/repos")
q := u.Query(); q.Set("project_id", projectID); u.RawQuery = q.Encode()
req, _ := http.NewRequest("GET", u.String(), nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
javascript
const r = await fetch(
  `${host}/api/v1/repos?project_id=${projectId}`,
  { headers: { Authorization: `Bearer ${token}` } },
)
const repos = await r.json()

6. Issue a personal access token

Bundle-driven scope picker — pull for read-only CI, publish for upload-from-CI, admin for the rare case you need governance writes. The plaintext token is returned once; copy it on the spot.

bash
curl -fsS -X POST "$ORBITAL_HOST/api/v1/tokens" \
  -H "Authorization: Bearer $ORBITAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ci-publish-maven",
    "bundle": "publish",
    "format_filter": ["maven"],
    "expires_in_days": 90
  }' | jq .
python
payload = {
    "name": "ci-publish-maven",
    "bundle": "publish",
    "format_filter": ["maven"],
    "expires_in_days": 90,
}
r = requests.post(
    f"{os.environ['ORBITAL_HOST']}/api/v1/tokens",
    headers={"Authorization": f"Bearer {os.environ['ORBITAL_TOKEN']}"},
    json=payload,
)
r.raise_for_status()
result = r.json()
print("plaintext (save now):", result["plaintext"])
go
body, _ := json.Marshal(map[string]any{
    "name":            "ci-publish-maven",
    "bundle":          "publish",
    "format_filter":   []string{"maven"},
    "expires_in_days": 90,
})
req, _ := http.NewRequest("POST", host+"/api/v1/tokens", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var out struct {
    ID, Plaintext string
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println("plaintext (save now):", out.Plaintext)
javascript
const r = await fetch(`${host}/api/v1/tokens`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'ci-publish-maven',
    bundle: 'publish',
    format_filter: ['maven'],
    expires_in_days: 90,
  }),
})
const { id, plaintext } = await r.json()
console.log('plaintext (save now):', plaintext)

7. Push an artifact

The generic upload endpoint that backs the orbital push CLI verb. Body is the raw file bytes; path is the address inside the repo.

bash
curl -fsS -X PUT \
  "$ORBITAL_HOST/api/v1/artifacts/$REPO_ID/com/example/sample/1.0.0/sample-1.0.0.jar" \
  -H "Authorization: Bearer $ORBITAL_TOKEN" \
  -H "Content-Type: application/java-archive" \
  --data-binary @./sample-1.0.0.jar
python
with open("sample-1.0.0.jar", "rb") as fh:
    r = requests.put(
        f"{os.environ['ORBITAL_HOST']}/api/v1/artifacts/"
        f"{repo_id}/com/example/sample/1.0.0/sample-1.0.0.jar",
        headers={
            "Authorization": f"Bearer {os.environ['ORBITAL_TOKEN']}",
            "Content-Type": "application/java-archive",
        },
        data=fh,
    )
r.raise_for_status()
print("artifact id:", r.json()["id"])
go
fh, err := os.Open("sample-1.0.0.jar")
if err != nil { log.Fatal(err) }
defer fh.Close()
url := fmt.Sprintf("%s/api/v1/artifacts/%s/com/example/sample/1.0.0/sample-1.0.0.jar",
    host, repoID)
req, _ := http.NewRequest("PUT", url, fh)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/java-archive")
resp, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
    log.Fatalf("push failed: %s", resp.Status)
}
javascript
import { readFile } from 'node:fs/promises'

const body = await readFile('sample-1.0.0.jar')
const r = await fetch(
  `${host}/api/v1/artifacts/${repoId}/com/example/sample/1.0.0/sample-1.0.0.jar`,
  {
    method: 'PUT',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/java-archive',
    },
    body,
  },
)
if (!r.ok) throw new Error(`push HTTP ${r.status}`)

8. Pull an artifact

Same shape, the GET verb. The server may answer a 302 to a presigned S3 URL when the deployment's S3 endpoint is browser-reachable; the snippets below all follow redirects by default.

A 403 here carries the why-blocked envelope — print X-Orbital-Block-Reason from the response headers when surfacing the failure to a developer.

bash
curl -fsSL -o sample-1.0.0.jar \
  "$ORBITAL_HOST/api/v1/artifacts/$REPO_ID/com/example/sample/1.0.0/sample-1.0.0.jar" \
  -H "Authorization: Bearer $ORBITAL_TOKEN"
python
url = (f"{os.environ['ORBITAL_HOST']}/api/v1/artifacts/{repo_id}/"
       "com/example/sample/1.0.0/sample-1.0.0.jar")
with requests.get(
    url,
    headers={"Authorization": f"Bearer {os.environ['ORBITAL_TOKEN']}"},
    stream=True,
) as r:
    if r.status_code == 403:
        print("blocked:", r.headers.get("X-Orbital-Block-Reason"))
        return
    r.raise_for_status()
    with open("sample-1.0.0.jar", "wb") as out:
        for chunk in r.iter_content(64 * 1024):
            out.write(chunk)
go
url := fmt.Sprintf("%s/api/v1/artifacts/%s/com/example/sample/1.0.0/sample-1.0.0.jar",
    host, repoID)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer resp.Body.Close()
if resp.StatusCode == 403 {
    log.Fatalf("blocked: %s", resp.Header.Get("X-Orbital-Block-Reason"))
}
out, _ := os.Create("sample-1.0.0.jar")
defer out.Close()
io.Copy(out, resp.Body)
javascript
import { writeFile } from 'node:fs/promises'

const r = await fetch(
  `${host}/api/v1/artifacts/${repoId}/com/example/sample/1.0.0/sample-1.0.0.jar`,
  { headers: { Authorization: `Bearer ${token}` } },
)
if (r.status === 403) {
  throw new Error(`blocked: ${r.headers.get('x-orbital-block-reason')}`)
}
if (!r.ok) throw new Error(`pull HTTP ${r.status}`)
await writeFile('sample-1.0.0.jar', Buffer.from(await r.arrayBuffer()))

9. Search for an artifact

Trigram-indexed search across artifact paths and repo names — same backend the topbar search field uses.

bash
curl -fsS "$ORBITAL_HOST/api/v1/search?q=log4j&limit=20" \
  -H "Authorization: Bearer $ORBITAL_TOKEN" | jq '.[] | {kind, path, repo}'
python
r = requests.get(
    f"{os.environ['ORBITAL_HOST']}/api/v1/search",
    headers={"Authorization": f"Bearer {os.environ['ORBITAL_TOKEN']}"},
    params={"q": "log4j", "limit": 20},
)
for hit in r.json():
    print(hit["kind"], hit.get("path") or hit.get("repo"))
go
u, _ := url.Parse(host + "/api/v1/search")
q := u.Query(); q.Set("q", "log4j"); q.Set("limit", "20"); u.RawQuery = q.Encode()
req, _ := http.NewRequest("GET", u.String(), nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
javascript
const r = await fetch(
  `${host}/api/v1/search?q=${encodeURIComponent('log4j')}&limit=20`,
  { headers: { Authorization: `Bearer ${token}` } },
)
const hits = await r.json()

10. List scan findings for an artifact

Used by promotion gates and SOC dashboards. Filter by minimum severity to skip noise.

bash
curl -fsS \
  "$ORBITAL_HOST/api/v1/detection/findings?artifact_id=$ARTIFACT_ID&severity=high&status=open" \
  -H "Authorization: Bearer $ORBITAL_TOKEN" | jq '.[] | {cve, severity, scanner}'
python
r = requests.get(
    f"{os.environ['ORBITAL_HOST']}/api/v1/detection/findings",
    headers={"Authorization": f"Bearer {os.environ['ORBITAL_TOKEN']}"},
    params={"artifact_id": artifact_id,
            "severity": "high", "status": "open"},
)
findings = r.json()
if findings:
    print(f"{len(findings)} high+ findings; not promoting")
go
u, _ := url.Parse(host + "/api/v1/detection/findings")
q := u.Query()
q.Set("artifact_id", artifactID)
q.Set("severity", "high")
q.Set("status", "open")
u.RawQuery = q.Encode()
req, _ := http.NewRequest("GET", u.String(), nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
javascript
const params = new URLSearchParams({
  artifact_id: artifactId,
  severity: 'high',
  status: 'open',
})
const r = await fetch(`${host}/api/v1/detection/findings?${params}`, {
  headers: { Authorization: `Bearer ${token}` },
})
const findings = await r.json()

11. Create and execute a promotion

Two-step pattern: create the row (pending if require_approval is true), then execute it. The execute call runs the CVE / license / signing gates and returns 422 with a structured reason if any gate blocks.

bash
PROMOTION_ID=$(curl -fsS -X POST "$ORBITAL_HOST/api/v1/promotions" \
  -H "Authorization: Bearer $ORBITAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"src_repo_id\": \"$STAGING_REPO_ID\",
    \"dst_repo_id\": \"$RELEASE_REPO_ID\",
    \"artifact_id\": \"$ARTIFACT_ID\",
    \"note\": \"promoted by ci\"
  }" | jq -r .id)

curl -fsS -X POST "$ORBITAL_HOST/api/v1/promotions/$PROMOTION_ID/execute" \
  -H "Authorization: Bearer $ORBITAL_TOKEN"
python
hdr = {"Authorization": f"Bearer {os.environ['ORBITAL_TOKEN']}"}
host = os.environ["ORBITAL_HOST"]

create = requests.post(
    f"{host}/api/v1/promotions",
    headers=hdr,
    json={
        "src_repo_id": staging_repo_id,
        "dst_repo_id": release_repo_id,
        "artifact_id": artifact_id,
        "note": "promoted by ci",
    },
)
create.raise_for_status()
pid = create.json()["id"]

exec_resp = requests.post(
    f"{host}/api/v1/promotions/{pid}/execute", headers=hdr,
)
if exec_resp.status_code == 422:
    raise SystemExit(f"gate blocked: {exec_resp.json()['error']['message']}")
exec_resp.raise_for_status()
go
type promo struct {
    SrcRepoID, DstRepoID, ArtifactID, Note string
    ID                                     string `json:"id,omitempty"`
}
body, _ := json.Marshal(promo{
    SrcRepoID: stagingRepoID, DstRepoID: releaseRepoID,
    ArtifactID: artifactID, Note: "promoted by ci",
})
req, _ := http.NewRequest("POST", host+"/api/v1/promotions",
    bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var created promo
json.NewDecoder(resp.Body).Decode(&created)

req2, _ := http.NewRequest("POST",
    host+"/api/v1/promotions/"+created.ID+"/execute", nil)
req2.Header.Set("Authorization", "Bearer "+token)
exec, _ := http.DefaultClient.Do(req2)
defer exec.Body.Close()
if exec.StatusCode == 422 {
    log.Fatalf("gate blocked: %s", exec.Status)
}
javascript
const hdr = {
  Authorization: `Bearer ${token}`,
  'Content-Type': 'application/json',
}

const create = await fetch(`${host}/api/v1/promotions`, {
  method: 'POST',
  headers: hdr,
  body: JSON.stringify({
    src_repo_id: stagingRepoId,
    dst_repo_id: releaseRepoId,
    artifact_id: artifactId,
    note: 'promoted by ci',
  }),
})
const { id } = await create.json()

const exec = await fetch(`${host}/api/v1/promotions/${id}/execute`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}` },
})
if (exec.status === 422) {
  const { error } = await exec.json()
  throw new Error(`gate blocked: ${error.message}`)
}

12. Reverse-dep audit (who pulled what)

The SOC's first stop after a CVE drops: which developers / build hosts pulled this artifact in the last N days?

bash
curl -fsS \
  "$ORBITAL_HOST/api/v1/security/who-pulled?artifact_id=$ARTIFACT_ID&since_days=30" \
  -H "Authorization: Bearer $ORBITAL_TOKEN" | jq .
python
r = requests.get(
    f"{os.environ['ORBITAL_HOST']}/api/v1/security/who-pulled",
    headers={"Authorization": f"Bearer {os.environ['ORBITAL_TOKEN']}"},
    params={"artifact_id": artifact_id, "since_days": 30},
)
for row in r.json():
    print(row["pulled_at"], row["principal"], row["client_ip"])
go
u, _ := url.Parse(host + "/api/v1/security/who-pulled")
q := u.Query()
q.Set("artifact_id", artifactID)
q.Set("since_days", "30")
u.RawQuery = q.Encode()
req, _ := http.NewRequest("GET", u.String(), nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
javascript
const params = new URLSearchParams({
  artifact_id: artifactId,
  since_days: '30',
})
const r = await fetch(
  `${host}/api/v1/security/who-pulled?${params}`,
  { headers: { Authorization: `Bearer ${token}` } },
)
const audits = await r.json()

13. Submit build info

Standard build-info envelope used by orbital build-info publish and any CI lane that already emits the same shape.

bash
curl -fsS -X PUT "$ORBITAL_HOST/api/build/" \
  -H "Authorization: Bearer $ORBITAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d @./build-info.json
python
with open("build-info.json", "rb") as fh:
    r = requests.put(
        f"{os.environ['ORBITAL_HOST']}/api/build/",
        headers={
            "Authorization": f"Bearer {os.environ['ORBITAL_TOKEN']}",
            "Content-Type": "application/json",
        },
        data=fh,
    )
r.raise_for_status()
go
fh, err := os.Open("build-info.json")
if err != nil { log.Fatal(err) }
defer fh.Close()
req, _ := http.NewRequest("PUT", host+"/api/build/", fh)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer resp.Body.Close()
javascript
import { readFile } from 'node:fs/promises'

const body = await readFile('build-info.json')
const r = await fetch(`${host}/api/build/`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body,
})
if (!r.ok) throw new Error(`build-info HTTP ${r.status}`)

Recipes that bundle several calls

Promote-on-clean-scan

The full promotion gate that most release pipelines want: pull the findings, decide, then promote.

python
import os, sys, requests

host = os.environ["ORBITAL_HOST"]
hdr  = {"Authorization": f"Bearer {os.environ['ORBITAL_TOKEN']}"}

artifact = sys.argv[1]
src, dst = sys.argv[2], sys.argv[3]

bad = requests.get(
    f"{host}/api/v1/detection/findings",
    headers=hdr,
    params={"artifact_id": artifact, "severity": "high", "status": "open"},
).json()
if bad:
    sys.exit(f"refusing to promote — {len(bad)} unresolved high+ findings")

p = requests.post(
    f"{host}/api/v1/promotions", headers=hdr,
    json={"src_repo_id": src, "dst_repo_id": dst, "artifact_id": artifact},
).json()
ex = requests.post(
    f"{host}/api/v1/promotions/{p['id']}/execute", headers=hdr,
)
if ex.status_code == 422:
    sys.exit(f"gate blocked: {ex.json()['error']['message']}")
ex.raise_for_status()
print("promoted", p["id"])

Block-on-CVE notifier

Watch the findings stream during a release, abort on first critical CVE.

javascript
const host  = process.env.ORBITAL_HOST
const token = process.env.ORBITAL_TOKEN

const params = new URLSearchParams({
  severity: 'critical', status: 'open',
})
const r = await fetch(`${host}/api/v1/detection/findings?${params}`, {
  headers: { Authorization: `Bearer ${token}` },
})
const open = await r.json()
if (open.length > 0) {
  console.error(`abort release: ${open.length} critical findings open`)
  process.exit(1)
}

Where to go next

  • The Getting started with the API guide walks one full flow end-to-end (sign in, mint a token, push, pull, promote) instead of cataloguing operations.
  • The API explorer renders every operation, including the ones not covered here, with full request/response schemas.
  • For shell-friendly versions of all of the above, the orbital CLI already wraps every operation in this page.

Released under the Apache-2.0 License.