Skip to content

Endpoints ​

Reference for the public /api/v1/* routes — grouped by area (accounts, subscription and billing, signing keys, artifacts), each with its method, path, authentication requirement, request and response shapes, and the error responses a caller has to handle.

All paths are relative to https://hub.sevorix.com. Conventions, the shape of an error body, and the meaning of each status code are on the Overview; the rules behind 401, 403, and 404 are on Authentication.

Every endpoint below can also return 429 (rate limited) and 500 (unhandled server error); those are not repeated in the per-endpoint error tables.

Accounts ​

POST /api/v1/register ​

Create an account. Unauthenticated.

Request

FieldTypeRequiredNotes
emailstringyesStored as given. Must be unique.
passwordstringyes8–1024 bytes. See password rules.
json
{ "email": "you@example.com", "password": "correct horse battery staple" }

Response — 201 Created

json
{
  "id": "6f1c0e2a-4b9d-4a1f-8f0c-2f4a9d3e7b11",
  "email": "you@example.com"
}

No token is issued. Call POST /api/v1/login next. The new account starts unapproved, which means publishing and endorsing return 403 until it is approved.

Errors

StatusCondition
400email or password empty; password shorter than 8 bytes or longer than 1024.
409"email '…' is already registered".
429Registration is limited to a 5-request burst refilling at 1/minute per IP.

POST /api/v1/login ​

Exchange credentials for a bearer token. Unauthenticated.

Request

FieldTypeRequired
emailstringyes
passwordstringyes

Response — 200 OK

json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "email": "you@example.com",
  "is_endorsed": false,
  "require_email_update": false
}

require_email_update is true for accounts whose address was backfilled by a migration; such an account should call PATCH /api/v1/me/email before doing anything else.

Errors

StatusCondition
401"invalid credentials" — returned identically for an unknown address and a wrong password, so the endpoint does not reveal which accounts exist.
42910 requests/minute per IP.

GET /api/v1/me ​

The authenticated caller's own profile. Requires a bearer token.

Response — 200 OK

json
{
  "id": "6f1c0e2a-4b9d-4a1f-8f0c-2f4a9d3e7b11",
  "email": "you@example.com",
  "is_endorsed": false,
  "is_approved": true,
  "created_at": "2026-03-14T09:21:07.481293Z"
}
FieldTypeMeaning
is_endorsedbooleanThe account is a recognised publisher; surfaced next to its artifacts as owner_is_endorsed.
is_approvedbooleanThe account may publish and endorse. false until approved.

Errors

StatusCondition
401Missing, malformed, invalid, or expired token; or a token naming an account that no longer exists.

PATCH /api/v1/me/email ​

Change the authenticated caller's email address. Requires a bearer token.

Request

FieldTypeRequiredNotes
emailstringyesTrimmed and lowercased before use. Must contain @.

Response — 200 OK

json
{
  "email": "new@example.com",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

A new token is issued and returned, because the email claim inside the old one is now stale. Replace your stored token with this one; the old token remains technically valid until it expires but carries the wrong address.

Errors

StatusCondition
400Empty address, or one with no @.
401Missing or invalid token.
409"email already in use" — another account already has that address.

PATCH /api/v1/me/password ​

Change the authenticated caller's password, proving knowledge of the current one. Requires a bearer token.

Request

FieldTypeRequiredNotes
current_passwordstringyesAt most 1024 bytes. Verified, never stored.
new_passwordstringyes8–1024 bytes; must differ from the current one.

Response — 200 OK

json
{ "status": "password updated" }

No token is issued or invalidated. Existing tokens — including any an attacker may hold — keep working until they expire. See token properties.

Errors

StatusCondition
400Either field over 1024 bytes; new_password under 8 bytes; new_password equal to current_password.
401Missing or invalid bearer token — or a correct token with the wrong current_password ("invalid credentials").
42910 requests/minute per IP.

The new_password checks run only after current_password verifies, so their messages cannot be used to probe whether a guessed current password was right.

POST /api/v1/password-reset/request ​

Start a password reset. Unauthenticated.

Request

FieldTypeRequired
emailstringyes

Response — 202 Accepted

json
{ "status": "if that address is registered, a reset link has been sent" }

Identical for a registered address, an unregistered one, and an account that has exhausted its 3-per-hour allowance.

Errors

StatusCondition
400The request body could not be deserialized.
503This instance has no email provider configured, so no link can be sent.
4295-request burst per IP, refilling slowly.

See Password reset for token lifetime and per-account limits.

POST /api/v1/password-reset/confirm ​

Redeem a reset token and set a new password. Unauthenticated.

Request

FieldTypeRequiredNotes
tokenstringyesThe value from the reset link. At most 256 bytes.
new_passwordstringyes8–1024 bytes.

Response — 200 OK

json
{ "status": "password updated" }

Errors

StatusCondition
400"invalid or expired reset token" — one message for unknown, expired, already-used, and concurrently-redeemed tokens alike, and also for a token over the length bound.
400new_password fails the length rules. Checked only once the token is known good, and before it is consumed.
42910 requests/minute per IP.

GET /api/v1/users/:user_id ​

A user's public profile. Unauthenticated.

Path parameters

NameType
user_idUUID

Response — 200 OK — the same shape as GET /api/v1/me.

Errors

StatusCondition
400user_id is not a well-formed UUID. Produced by the framework's path parsing, so the body is plain text rather than the usual {"error": …}.
404"user not found".

Subscription and billing ​

POST /api/v1/me/checkout-session ​

Create a Stripe Checkout Session for the authenticated caller and return its hosted URL to redirect a browser to. Requires a bearer token.

Takes no request body.

Response — 200 OK

json
{ "url": "https://checkout.stripe.com/c/pay/cs_live_a1..." }

The session is created with the hub's own user id as client_reference_id, so the resulting Stripe customer is linked back to this account by a value the hub controls rather than one the client supplies. There is no endpoint for self-reporting a Stripe customer id.

Errors

StatusCondition
401Missing or invalid token.
500Stripe is not configured on this instance, or the call to Stripe failed. The body is the generic "internal server error".

GET /api/v1/me/subscription ​

The authenticated caller's cached subscription status, signed. Requires a bearer token.

This reads the status the hub last recorded from Stripe; it deliberately does not call Stripe live.

Response — 200 OK

json
{
  "status": "active",
  "updated_at": "2026-03-12T17:03:55.204881Z",
  "subscription_id": "sub_1PabcDEFghIJklmn",
  "expires_at": 1773480127,
  "signature": "8Wm5b0v0Yc2u1H1oQ0V0aFq0m3n5oS0F0aQ2c..."
}
FieldTypeNotes
statusstringThe Stripe subscription status as last seen (active, past_due, canceled, …), or the account's default when it has never had one.
updated_atstring | nullWhen the status was last written. null if never.
subscription_idstring | nullStripe subscription id, if one is linked.
expires_atintegerUnix seconds. 15 minutes after the response was produced.
signaturestringBase64 Ed25519 signature over the SHA-256 of the canonical JSON encoding of {status, subscription_id, expires_at}.

signature is the point of this endpoint. The Sevorix daemon verifies it against a public key compiled into its own binary rather than trusting the response body, so pointing the daemon at a look-alike hub — via a hub-URL override or DNS spoofing — does not yield a valid entitlement. expires_at bounds how long a captured genuine response stays usable after a subscription is cancelled; a consumer must reject the token past it even though the signature still verifies.

Errors

StatusCondition
401Missing or invalid token, or a token naming an account that no longer exists.

Signing keys ​

Artifacts may be signed with an Ed25519 key. The private half never leaves the client; the hub stores only the public key, and identifies it by fingerprint.

TermDefinition
public_keyBase64 (standard alphabet) of the raw 32 bytes of an Ed25519 public key. Not PEM, not SSH format.
fingerprintLowercase hex SHA-256 of those same 32 raw bytes — 64 characters. Computed by the hub, never taken from the request.

POST /api/v1/me/signing-keys ​

Register a signing key for the authenticated caller. Requires a bearer token.

Request

FieldTypeRequiredNotes
public_keystringyesBase64 of the raw 32-byte Ed25519 public key.
labelstring | nullnoA human-readable name for your own use.
json
{ "public_key": "0h1PZmn6M5G0J1p7pAzQpQ4o9x5jvB3qE5cJb2mCn0A=", "label": "laptop" }

Response — 201 Created

json
{
  "id": "e7b4d0a1-5f6c-4c2e-9a03-1d8b7c4e2f55",
  "fingerprint": "9f2c1a...64 hex characters...",
  "label": "laptop",
  "created_at": "2026-03-14T10:02:19.771004Z"
}

Errors

StatusCondition
400public_key is not valid base64, does not decode to exactly 32 bytes, or is not a valid Ed25519 point.
401Missing or invalid token.
409"a key with this fingerprint already exists". Fingerprints are unique across the hub, not just within an account.

GET /api/v1/me/signing-keys ​

List the authenticated caller's signing keys. Requires a bearer token.

Response — 200 OK — a JSON array, newest first.

json
[
  {
    "id": "e7b4d0a1-5f6c-4c2e-9a03-1d8b7c4e2f55",
    "user_id": "6f1c0e2a-4b9d-4a1f-8f0c-2f4a9d3e7b11",
    "fingerprint": "9f2c1a...",
    "label": "laptop",
    "created_at": "2026-03-14T10:02:19.771004Z",
    "revoked_at": null
  }
]

Revoked keys are not filtered out; they are returned with a non-null revoked_at. The public_key itself is deliberately omitted from this response.

Errors

StatusCondition
401Missing or invalid token.

DELETE /api/v1/me/signing-keys/:fingerprint ​

Revoke one of the authenticated caller's signing keys. Requires a bearer token.

Revocation is a soft delete: the row stays, with revoked_at set. Artifacts already signed with the key keep their signature on record, but it stops counting as valid — see signature_valid.

Response — 204 No Content.

Errors

StatusCondition
401Missing or invalid token.
404"signing key not found or already revoked" — one message covering a fingerprint that does not exist, one belonging to another account, and one already revoked. Revoking twice is therefore a 404, not a no-op success.

Artifacts ​

An artifact is a named, versioned JSON document — a policy, or a set that groups other artifacts by dependency. name@version is unique across the hub and immutable once published: there is no update endpoint, only a new version.

Visibility is one of:

ValueWho can read it
publicAnyone, including anonymous callers. The default.
privateThe owner only. Everyone else gets 404.
draftThe owner only. Same treatment as private on every read path.

POST /api/v1/artifacts ​

Publish an artifact. Requires a bearer token, and an approved account.

Request

FieldTypeRequiredNotes
namestringyes1–128 chars, must start alphanumeric, then [a-zA-Z0-9\-_.].
versionstringyes1–64 chars, must start alphanumeric, then [a-zA-Z0-9\-_.+].
contentstringyesThe artifact body, as a JSON string containing JSON — not a nested object. Must parse. Default size cap 256 KB.
descriptionstring | nullnoAt most 1000 characters.
tagsstring[]noAt most 20 tags, each at most 64 characters.
visibilitystringno"public" (default), "private", or "draft".
artifact_typestringno"artifact" (default) or "set". A set must declare at least one dependency.
dependenciesobject[]noEach { "name": …, "version": …, "required": true }. required defaults to true. Every entry must already exist.
schemaobjectnoA JSON Schema. When present, content is validated against it at push time and the schema is stored alongside the artifact.
signaturestring | nullnoBase64 Ed25519 signature. Must be sent together with key_fingerprint.
key_fingerprintstring | nullnoFingerprint of a registered, unrevoked key belonging to you.

What is signed. The message is the 64-character lowercase hex SHA-256 of the content bytes, signed as UTF-8 text — not the raw digest bytes and not the content itself.

json
{
  "name": "net-egress",
  "version": "1.2.0",
  "description": "Default egress policy for agent shells",
  "tags": ["network", "baseline"],
  "visibility": "public",
  "content": "{\"rules\":[{\"action\":\"deny\",\"dst\":\"0.0.0.0/0\"}]}"
}

Response — 201 Created

json
{
  "id": "b1c2d3e4-5f60-4718-9a2b-3c4d5e6f7081",
  "name": "net-egress",
  "version": "1.2.0",
  "description": "Default egress policy for agent shells",
  "owner": "you@example.com",
  "tags": ["network", "baseline"],
  "visibility": "public",
  "downloads": 0,
  "created_at": "2026-03-14T10:15:44.902117Z",
  "content_hash": "3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855e",
  "content_schema": null,
  "signed": false,
  "key_fingerprint": null,
  "dependencies": [],
  "artifact_type": "artifact"
}

signed is read back from what was actually persisted, so it is never true for a signature that was not verified and stored.

Errors

StatusCondition
400name or version fails its pattern or length rule.
400More than 20 tags, or a tag over 64 characters.
400description over 1000 characters.
400content over the instance's size cap (256 KB by default).
400content is not valid JSON.
400content does not conform to the supplied schema, or schema is not a valid JSON Schema.
400visibility is not one of public, private, draft.
400artifact_type is "set" with no dependencies.
400A declared dependency does not exist ("dependency 'x@1.0.0' does not exist").
400The declared dependencies would form a cycle back to this artifact.
400signature and key_fingerprint were not supplied together.
400The named key is unknown, revoked, or belongs to another account.
400The signature does not verify against the content hash.
400This instance requires signed artifacts and none was supplied.
401Missing or invalid token.
403"your account is pending admin approval".
409"artifact 'name@version' already exists".

Every one of those checks runs before anything is persisted, so a rejected push leaves nothing published — a 400 here always means the artifact does not exist.

GET /api/v1/artifacts/:name/:version ​

Fetch an artifact and its content. Token optional — required only to reach your own private or draft artifacts.

A successful fetch increments the artifact's download counter. A fetch rejected by the visibility check does not.

Response — 200 OK

json
{
  "id": "b1c2d3e4-5f60-4718-9a2b-3c4d5e6f7081",
  "name": "net-egress",
  "version": "1.2.0",
  "description": "Default egress policy for agent shells",
  "owner": "you@example.com",
  "owner_is_endorsed": false,
  "tags": ["network", "baseline"],
  "downloads": 41,
  "visibility": "public",
  "created_at": "2026-03-14T10:15:44.902117Z",
  "content": { "rules": [{ "action": "deny", "dst": "0.0.0.0/0" }] },
  "content_hash": "3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855e",
  "content_schema": null,
  "signed": true,
  "key_fingerprint": "9f2c1a...",
  "signature_valid": true,
  "artifact_type": "artifact",
  "yanked": false,
  "yanked_reason": null,
  "dependencies": [{ "name": "base-deny", "version": "1.0.0", "required": true }]
}

Note that content here is a parsed JSON value, not the string that was pushed.

signed and signature_valid are different questions. Do not treat them as one flag:

signedsignature_validMeaning
falsenullPublished without a signature.
truetrueSigned, and the signature verifies against the bytes in this response, with a key that is still valid.
truefalseClaims a signature that does not check out: it fails verification, the key was revoked, or the key can no longer be resolved.

signature_valid is recomputed on every fetch against the exact content being served — it is never a boolean cached at push time — so it describes what you actually received. A revoked key yields false, because a revoked key can no longer vouch for provenance.

Yanked artifacts are still served with yanked: true and a yanked_reason, so existing consumers keep working while being told the version is withdrawn. They are excluded from search.

Errors

StatusCondition
404No such name@version, or it is private/draft and not yours. The two are indistinguishable by design.
500The stored content no longer matches its recorded hash. The response is refused rather than serving bytes that failed an integrity check.

Search artifacts. Token optional — supplying one widens the result set to include your own private and draft artifacts.

Query parameters

NameTypeDefaultNotes
qstring—Case-insensitive substring match against name and description.
tagstring—Exact tag match. Takes precedence over q if both are given.
limitinteger20Clamped to 1–100.
offsetinteger0Negative values are treated as 0.
include_yankedbooleanfalseAccepted, but has no effect for ordinary accounts: yanked artifacts are excluded.

Results are ordered by created_at descending.

Response — 200 OK

json
{
  "results": [
    {
      "id": "b1c2d3e4-5f60-4718-9a2b-3c4d5e6f7081",
      "name": "net-egress",
      "version": "1.2.0",
      "description": "Default egress policy for agent shells",
      "owner": "you@example.com",
      "owner_is_endorsed": false,
      "tags": ["network", "baseline"],
      "downloads": 41,
      "visibility": "public",
      "created_at": "2026-03-14T10:15:44.902117Z",
      "yanked": false,
      "artifact_type": "artifact"
    }
  ],
  "total": 128
}

total is not a match count

total is the number of artifacts visible to you, not the number matching q or tag. It does not change as you vary the query, so it cannot be used to size a result set or drive pagination against a filtered search. Page by requesting until a short page comes back.

Summaries omit content, content_hash, signature fields, and dependencies — fetch the artifact itself for those.

GET /api/v1/artifacts/:name/:version/members ​

List the members of an artifact set. Unauthenticated, and no visibility check is applied.

Response — 200 OK

json
{
  "set_name": "baseline-policies",
  "set_version": "2026.03",
  "members": [
    { "name": "net-egress", "version": "1.2.0", "required": true },
    { "name": "shell-guard", "version": "0.9.1", "required": false }
  ]
}

Errors

StatusCondition
400The artifact exists but is not a set.
404No such name@version.

GET /api/v1/artifacts/:name/:version/resolve ​

Walk an artifact's dependency graph transitively. Token optional — required only for your own private or draft artifacts.

Response — 200 OK

json
{
  "root": "baseline-policies@2026.03",
  "dependencies": [
    { "name": "net-egress", "version": "1.2.0", "required": true, "depth": 1, "found": true },
    { "name": "base-deny", "version": "1.0.0", "required": true, "depth": 2, "found": true },
    { "name": "legacy-shim", "version": "0.1.0", "required": false, "depth": 2, "found": false }
  ]
}
FieldMeaning
depth1 for a direct dependency, incrementing per hop.
foundWhether that name@version currently exists on the hub. A dangling reference is reported as found: false, not as an error.

Each name@version appears at most once, at the first depth it was reached. Traversal stops at depth 50. Visibility is checked on the root artifact only; the walk itself reports names and versions, not content.

Errors

StatusCondition
404No such root name@version, or it is not yours to see.

POST /api/v1/artifacts/:artifact_id/yank ​

Withdraw a published artifact. Requires a bearer token; you must own the artifact.

Yanking does not delete anything. The artifact stays fetchable by exact name@version — flagged yanked: true — and drops out of search. This is the "stop new adopters without breaking existing ones" operation.

Request

FieldTypeRequiredNotes
reasonstring | nullnoSurfaced as yanked_reason on fetch. Send {} to omit it.

Response — 204 No Content.

Errors

StatusCondition
401Missing or invalid token.
403You do not own the artifact.
404No artifact with that id.

DELETE /api/v1/artifacts/:artifact_id/yank ​

Reverse a yank: clears yanked and yanked_reason. Requires a bearer token; you must own the artifact. Takes no body.

Response — 204 No Content. Same errors as POST.

Endorsements ​

An endorsement is one account vouching for one artifact. Each account may endorse a given artifact at most once.

LevelValue
Verified"verified" (the default)
Trusted author"trusted_author"
Official"official"

POST /api/v1/artifacts/:artifact_id/endorsements ​

Endorse an artifact. Requires a bearer token, and an approved account.

Request

FieldTypeRequiredNotes
levelstring | nullnoOne of the three values above. Defaults to "verified". Send {} for the default.

Response — 201 Created

json
{
  "id": "a2b3c4d5-6e7f-4081-9203-4a5b6c7d8e9f",
  "artifact_id": "b1c2d3e4-5f60-4718-9a2b-3c4d5e6f7081",
  "user_id": "6f1c0e2a-4b9d-4a1f-8f0c-2f4a9d3e7b11",
  "email": "you@example.com",
  "user_is_endorsed": false,
  "level": "verified",
  "created_at": "2026-03-14T11:44:02.118337Z"
}

Errors

StatusCondition
400level is not one of the three accepted values.
401Missing or invalid token.
403"your account is pending admin approval".
404No artifact with that id.
409"you have already endorsed this artifact".

Note that the existence check here does not apply artifact visibility, unlike the fetch endpoints.

GET /api/v1/artifacts/:artifact_id/endorsements ​

List an artifact's endorsements. Unauthenticated.

Response — 200 OK — a JSON array of the object shown above, newest first.

Errors

StatusCondition
404No artifact with that id.

DELETE /api/v1/artifacts/:artifact_id/endorsements/:endorsement_id ​

Withdraw your own endorsement. Requires a bearer token.

Response — 204 No Content.

Errors

StatusCondition
401Missing or invalid token — and also when the endorsement is not yours. See the note below.
404No endorsement with that id on that artifact.

Status-code exception

Deleting an endorsement that belongs to someone else answers 401, where every comparable ownership check in this API answers 403. Documented as it behaves, and may change in a future release. A client should not treat this particular 401 as a stale-session signal.

Not documented here ​

Some routes the hub serves are deliberately absent from this reference:

  • POST /api/v1/webhooks/stripe — called by Stripe, never by an API consumer. It authenticates by Stripe-Signature rather than a bearer token, and there is nothing a client can usefully do with it.
  • GET /checkout/success, GET /checkout/cancel, GET /password-reset — HTML pages the hub serves to a browser as the destination of a Stripe redirect or a reset email link. They are user-facing pages, not API endpoints.

Runtime containment for autonomous AI agents.