Access and authorization API reference

All Gate API endpoints are served from https://api.vernesoft.com.

This API is divided into two logical parts:

  1. Identity Management: Tenant-scoped proxy to Kratos Admin API for managing end-users.
  2. Access & Authorization: Core services for token exchange and permission checks.

Access & Authorization

Create Access Token

POST /v1/gate/tokens

Exchanges a long-lived API key for a short-lived access token that you can attach to backend or CLI requests.

The key goes in the Authorization: Bearer <key> header, as on every other call of this API, and must carry the gate scope. It is not a field of the body: a request with the key only in the body has no key at all and is answered 401 unauthorized.

Request Body

FieldTypeRequiredDescription
subjectstringYesID of the user or service account this token represents.
scopesstring[]NoOptional list of scopes to narrow the token (e.g. ['gate.tokens.read']).
ttl_secondsintegerNoLifetime in seconds, from 1 to 86400 (one day). Default 3600.

A body that is not JSON, has no subject, or has a ttl_seconds that is not a whole number from 1 to 86400 is 400 invalid_payload; the message names the allowed range. A ttl_seconds outside it is refused, not corrected: 0 or a negative number is not turned into a token that has already expired, and a value above 86400 is not cut down to a day.

Example Request

curl -X POST https://api.vernesoft.com/v1/gate/tokens \
  -H 'Authorization: Bearer vrn_9f8a7...' \
  -H 'Content-Type: application/json' \
  -d '{
    "subject": "usr_123",
    "scopes": ["gate.tokens.read"],
    "ttl_seconds": 3600
  }'

Response

{
  "access_token": "gat_live_5f0c1a9e8b7d4c3a9e2f1b0d6a8c7e45",
  "expires_at": "2026-03-17T12:00:00Z",
  "subject": "usr_123",
  "tenant_id": "ten_001"
}

Introspect Token

POST /v1/gate/tokens/introspect

Validates a Gate access token and returns its decoded attributes.

active is false when the token is unknown, expired, belongs to another tenant, or its subject is an end user you have deactivated or deleted. A deactivated user's tokens become active again when you reactivate them.

When the token is active and its subject is an end user's id, the answer also lists the user's organizations and roles in orgs — read at the moment you ask, so a role you removed a minute ago is already gone from it (a signed token made earlier would still carry it). The list is for the project the API key belongs to, with the role the user holds in each and its permissions, the first 100 in the order the user joined; "orgs_truncated": true appears, and only then, when there are more, and GET /v1/gate/identities/{id}/organizations returns them all. A user who belongs to none gets "orgs": []. A subject that is not an end user's id (a service account's name, say) has no orgs member at all. If the organizations cannot be read at that moment, the answer is 502 upstream_unavailable — never an active answer with the list silently missing.

Request Body

FieldTypeRequiredDescription
access_tokenstringYesThe token you want to introspect.

Example Request

curl -X POST https://api.vernesoft.com/v1/gate/tokens/introspect \
  -H 'Authorization: Bearer vrn_9f8a7...' \
  -H 'Content-Type: application/json' \
  -d '{
    "access_token": "gat_live_5f0c1a9e8b7d4c3a9e2f1b0d6a8c7e45"
  }'

Response

{
  "active": true,
  "subject": "usr_123",
  "tenant_id": "ten_001",
  "scopes": ["gate.tokens.read"],
  "expires_at": "2026-03-17T12:00:00Z"
}

Authorize Action

POST /v1/gate/authorize

Asks Gate whether a subject may perform an action on a resource. The answer is a decision, allowed or not, with a reason; Gate does not enforce it, your service does — it is the check you call before you act.

There are two questions, told apart by resource:

  • resource is organization:<organization id> — may this user do this in that organization? Answered from the user's role in the organization and that role's permissions, as they are right now. See Organizations and Roles.
  • Any other resource — is this subject one of your active end users? This is the only policy there is for other resources. action and resource are recorded in the decision log and not evaluated; there are no per-resource or per-action rules to configure.

The key needs the gate scope and, for organizations, the answer is about the project the key belongs to.

Request Body

FieldTypeRequiredDescription
subjectstringYesThe end user's identity id (a UUID).
actionstringYesFor an organization: the permission to check, such as invoices:write. Otherwise a label for your own records.
resourcestringYesorganization:<uuid> to check a permission in an organization; anything else for the ownership check.
contextobjectNoAccepted and ignored. Nothing is evaluated from it today.

Example: a permission in an organization

curl -X POST https://api.vernesoft.com/v1/gate/authorize \
  -H 'Authorization: Bearer vrn_9f8a7...' \
  -H 'Content-Type: application/json' \
  -d '{
    "subject": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "action": "invoices:write",
    "resource": "organization:3b2f6f0e-8f0c-4a54-9f1d-0c7a5d1e2b10"
  }'

Response

{
  "allowed": true,
  "decision_id": "dec_9f8a7c1b2d3e",
  "reason": "member_with_permission",
  "role": "admin"
}

decision_id identifies the decision in Gate's logs, for support. For an organization, reason is one of a fixed set that your code can branch on:

reasonallowedMeaning
member_with_permissiontrueThe user is a member and the role they hold lists the permission.
member_without_permissionfalseThe user is a member; their role does not list the permission.
not_a_memberfalseThe user is not in the organization — never was, was removed, or is not an end user of yours.
subject_inactivefalseThe user is a member but is switched off, so authorized for nothing.
no_such_organizationfalseThe key's project has no organization with that id.

role — the name of the role the user holds there — is present for member_with_permission, member_without_permission and subject_inactive, and absent otherwise.

  • Exact match. The permission must be one the role lists, character for character. There are no wildcards: invoices:* is not a permission, invoices does not cover invoices:write, and a role holding invoices:write:all does not hold invoices:write.
  • Live. The answer is read when you ask: a permission added to a role, a user moved to another role, a membership removed — the next call has it.
  • Another project's organization looks like a missing one. An id that belongs to another project of yours, to another account, or to nobody is answered no_such_organization in every case, with nothing to tell them apart.
  • An invalid permission is a 400, not a denial. A string that no role could ever hold — upper case, an empty segment, a wildcard, the reserved nautilus: prefix, over 64 characters — is 400 invalid_payload with the reason in message, so a typo is not mistaken for "the user lacks it". So is an organization: resource that does not end in a UUID.
  • If Gate cannot reach what it needs to answer, you get a 5xx, never allowed: false.
  • Asking writes nothing, the audit log included, and the call is not billed.

The ownership check (any other resource)

curl -X POST https://api.vernesoft.com/v1/gate/authorize \
  -H 'Authorization: Bearer vrn_9f8a7...' \
  -H 'Content-Type: application/json' \
  -d '{ "subject": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "action": "read", "resource": "doc:1" }'
{
  "allowed": true,
  "decision_id": "dec_9f8a7c1b2d3e",
  "reason": "owned_by_tenant",
  "message": "subject `f47ac10b-58cc-4372-a567-0e02b2c3d479` is an identity owned by tenant `550e8400-e29b-41d4-a716-446655440000`"
}

allowed is true when subject is the id of an active end user of your account, and false for an id that is someone else's, one that does not exist, one that is not an id at all (usr_123), and a user who is switched off.

Here too reason is a code your code can branch on:

reasonallowedMeaning
owned_by_tenanttrueThe subject is an active end user of your account.
subject_inactivefalseThe subject is your end user but is switched off. The same code the organization check uses for the same fact.
not_owned_by_tenantfalseThe subject is an identity, and it is another account's.
not_a_known_identityfalseNothing has that id: it does not exist, was deleted, or is not an id at all (usr_123).

message holds the sentence a person would read, for your logs. It is a courtesy: its wording can change, and only reason is a contract. A decision about an organization has no message; its reason says it all.

Changed in October 2026. Until then reason for this check was that sentence itself (subject `…` is an identity owned by tenant `…` ). If your code matched on its text, match on reason instead: owned_by_tenant is allowed: true, and every other value is a denial.


Error Format

The Gate API uses the standard error envelope, on every route:

{
  "error": {
    "code": "invalid_payload",
    "message": "`subject` is required",
    "request_id": "req_abc123"
  }
}

code is always a string (invalid_payload, unauthorized, forbidden, not_found, conflict, …). A malformed body on POST /v1/gate/tokens, /tokens/introspect and /authorize is 400 invalid_payload, as everywhere else; it was invalid_request, without a request_id, until October 2026. See Error Format for the full list.

Include the request_id when contacting support for faster resolution.