Skip to content

REST API

The gateway exposes three HTTP surfaces with different authentication:

Surface Paths Authentication
Web / API everything except the facade's paths below OIDC session cookie
Machine API /api/** with an Authorization: Bearer header Machine API credential
Git facade /git/**, and the revocation check at /status/v1/** Personal access token over HTTP Basic

The three do not overlap. A personal access token reaches the facade and nothing else; a machine API credential reaches the API and nothing else — it cannot clone a marketplace, including the marketplaces an empty fetch scope would otherwise grant. Which surface answers a request is decided by what it carries, not by what it asks for.

This section documents the first. For the second see Git smart-HTTP facade.

A live reference ships with the gateway

/docs renders the OpenAPI document at /v3/api-docs. Both are behind the OIDC login. These pages describe the same surface with the surrounding reasoning.

The document describes more than the paths the gateway answers on. Its top-level webhooks object describes the deliveries the gateway sends — one entry per lifecycle event, with its transport headers and body schema — so a receiver is written against the same contract, and against the same additive promise, as a client.

Conventions

Authentication. Every /api/** endpoint is reached by exactly one of two paths:

  • an authenticated OIDC session, which is what the portal uses; or
  • a machine API credential presented as Authorization: Bearer <secret>, which is what infrastructure-as-code and CI use.

A request carrying no bearer header takes the session path, exactly as it always has. A request carrying one takes the machine path and is authenticated strictly — including when skills-gateway.dev-insecure-auth=true, which opens the browser surface and never the bearer path.

A machine request must carry no cookie

The machine path refuses a request that presents both a bearer credential and a Cookie header, rather than resolving it to either. Ambiguity about which credential authorised a request is where confused-deputy defects live.

The practical consequence: a client behind a load balancer that injects its own session-affinity cookie (AWSALB, GCLB-style) is refused with a bare 401. Strip the cookie, or exclude the gateway's hostname from affinity.

What a machine credential can reach is an allowlist, described in Machine API credentials. Every act of human judgement — approving, rejecting, waiving — every operation that retracts or republishes content, every role grant and the whole of /api/v1/tokens/** is outside it, and no combination of scopes and no role reaches them.

Authorization. Every mutation and the audit surface require a role and answer 403 without one; the browsing surface stays open. There is no configuration that relaxes this. Each endpoint's page states its requirement, and Delegated administration has the matrix. Access tokens are scoped to the calling principal server-side in either mode.

Unauthenticated requests to /api/** receive a clean 401, not a 302 to the identity provider, so an expired session surfaces as an error rather than an HTML login page rendered into a fetch().

CSRF. The session path carries a token. Every response sets an XSRF-TOKEN cookie readable by script, and a state-changing request must echo it in an X-XSRF-TOKEN header or receive 403: a session cookie on its own does not authorise a mutation. The portal's fetch wrapper sends it on every call, so a portal user does nothing differently. One consequence worth knowing before you meet it: the Scalar reference at /docs sends its requests from the browser without that header, so a mutation tried from there is refused even though a read from there works. Why the session path needs a token at all is in Trust boundaries.

The machine path carries no token and needs none. It earns that the way the git facade does rather than borrowing the session path's: it is stateless, creates no session, honours no cookie and refuses a request that carries one, so every request there authenticates itself.

Driving the session path from a script

The curl examples in these pages show the path and the body, not the credential. Reads are unaffected; a mutation needs the cookie jar and the header, and that holds under skills-gateway.dev-insecure-auth=true too — the hatch opens authentication, not this.

$ curl -sS -c jar -b jar -o /dev/null localhost:8080/api/v1/marketplaces
$ curl -sS -c jar -b jar -X POST localhost:8080/api/v1/snapshots/1/approve \
    -H "X-XSRF-TOKEN: $(grep XSRF-TOKEN jar | cut -f7)"

A pipeline should use a machine API credential instead, which needs neither the jar nor the header.

Compatibility. Within a major, this surface only grows; a breaking change moves the path prefix and ships as a major. See The API contract for what counts as breaking and how it is enforced.

Errors are RFC 7807 ProblemDetail documents, served as application/problem+json, whatever inside the gateway refused the request (GW_API_0007):

{"type":"about:blank","title":"Bad Request","status":400,
 "detail":"url scheme must be one of [http, https]",
 "instance":"/api/v1/marketplaces"}

detail is the field to show a user: it is the only part that says why this request was refused. Every non-success response in this document declares this shape, so a client has one body to parse rather than one per endpoint.

Status codes used across the API:

Status Meaning
400 The request violated a trust-boundary rule — a disallowed URL scheme, or a non-default ref.
403 The session lacks the role the endpoint requires; or a machine credential reached an endpoint its scopes do not cover, or one no scope covers.
404 No such marketplace, snapshot or token.
409 A state conflict — a duplicate name, a decision on a snapshot that is already approved or rejected, or a re-vet of one that is not approved.
422 A name failed ^[a-z0-9][a-z0-9_-]*$; a marketplace name is also at most 63 characters.
502 Ingestion failed against the upstream.

Endpoint index

Area Endpoints
Marketplaces and snapshots Register, list, ingest, inspect contents, licenses, vetting, waivers, re-vet, fetchers, approve, reject, provenance
Access tokens Create, list, revoke and rotate personal access tokens; provision and administer machine API credentials
Audit Read the ledger; stream it as NDJSON; register, replay and delete export sinks
Adoption Windowed adoption report per marketplace and SHA; identities not on the served tip
Webhooks Register, list and delete subscribers; list delivery attempts
Retention Preview candidates, evaluate, compact, soft-delete and restore snapshots
Roles List, grant and revoke delegated-administration roles
Estate Read the last declarative-estate reconciliation report; trigger a reconcile
Policy Create, list, update and delete CEL deny rules; test expressions in the playground

Session

GET /api/v1/me returns the current principal, the session's effective roles with the source of each, whether the identity provider truncated the membership claim, and the version of the running gateway build. The portal uses it for the user menu, to adapt its controls to the roles, and to state the build in the sidebar footer.

version is read from the build artifact and from no configurable source, so it cannot be set to something the gateway is not. It is absent — not an empty string, and not "unknown" — when the artifact carries no build information, which is the case for a gateway run from an exploded build rather than the packaged jar.

{"username": "alice@example.com",
 "roles": [{"role": "approver", "marketplace": "acme", "source": "grant"},
           {"role": "auditor", "marketplace": null, "source": "claim"}],
 "claimsTruncated": false,
 "version": "0.3.0"}
source Where the role came from
config skills-gateway.roles.admins. No grant row; unrevocable through the API.
grant A row in the grants API.
claim An identity-provider claim value mapped by skills-gateway.roles.mappings.

The same role and marketplace is reported once, attributed to the most durable source that produced it. claimsTruncated: true means the provider dropped the membership claim rather than the session having none — the roles listed are then incomplete; see Identity providers.

Non-API endpoints

Path Notes
/actuator/health The only unauthenticated path. Use the bare path for probes.
/status/v1/snapshots The revocation check: a facade-credentialled read, not part of /api/**.
/actuator/sbom CycloneDX SBOM. Authenticated.
/v3/api-docs, /docs OpenAPI document and Scalar UI. Authenticated.
/oauth2/authorization/idp, /login/oauth2/code/idp OIDC login and callback.
/, /marketplaces, /marketplaces/{name}, /review, /audit, /vetting, /adoption, /tokens, /integrations, /integrations/webhooks, /integrations/sinks, /webhooks Forwarded to the single-page application.