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. |