Staxa
Staxa
  • Home
  • Dashboard
https://api.staxa.dev/api/v1
  • Overview

    • Getting Started
    • Authentication
    • Response Format
    • Error Handling
    • Rate Limits
    • Webhooks (Coming Soon)
  • API Keys

    • List API KeysGET
    • Create API KeyPOST
    • Delete API KeyDELETE
  • Tenants

    • List TenantsGET
    • Create TenantPOST
    • Get TenantGET
    • Update TenantPATCH
    • Delete TenantDELETE
  • Services

    • List ServicesGET
    • Add ServicePOST
    • Remove ServiceDELETE
    • Deploy ServicePOST
    • List Service DeploymentsGET
    • Service LogsGET
    • Deploy All ServicesPOST
    • Rollback Service DeploymentPOST
  • Deployments

    • List DeploymentsGET
    • Create DeploymentPOST
    • Get DeploymentGET
    • Rollback DeploymentPOST
  • Environment Variables

    • Get Env VarsGET
    • Bulk Set Env VarsPUT
  • Domains

    • List DomainsGET
    • Add DomainPOST
    • Verify DomainPOST
    • Delete DomainDELETE
  • Network Rules

    • List Network RulesGET
    • Create Network RulePOST
    • Get Network RuleGET
    • Delete Network RuleDELETE
  • Real-Time

    • Events (SSE)GET
    • Container LogsGET
  • Templates

    • List TemplatesGET
    • Get TemplateGET
    • List Runtime VersionsGET
  • GitHub App

    • Get GitHub InstallationGET
    • Register GitHub InstallationPOST
    • Delete GitHub InstallationDELETE
    • List GitHub ReposGET
    • List GitHub BranchesGET
    • Analyze RepositoryGET
  • Registry Credentials

    • List RegistriesGET
    • Create Registry CredentialPOST
    • Get Registry CredentialGET
    • Update Registry CredentialPATCH
    • Delete Registry CredentialDELETE
    • Verify Registry CredentialPOST
    • List Registry ReposGET
    • List Registry TagsGET
  • Dashboard

Getting Started

The Staxa API creates and manages isolated tenant environments. Everything you need to deploy and run apps on Staxa is here. Account and billing setup lives in the dashboard.

Base URL: https://api.staxa.dev/api/v1

Create a tenant:

curl -X POST https://api.staxa.dev/api/v1/tenants \
  -H "Authorization: Bearer sk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "acme-corp",
    "source_type": "github",
    "repo_url": "https://github.com/acme/webapp",
    "branch": "main"
  }'

That single call clones the repo, detects the framework, builds it, and serves the result at https://acme-corp.tenants.staxa.dev. Creation returns 202 straight away and the first deploy starts on its own; subscribe to GET /api/v1/tenants/{id}/events over SSE to watch it progress.

Authentication

The Staxa API accepts two authentication modes.

API Key (Programmatic)

For server-to-server integrations and automation. Create API keys from the dashboard at Settings → API Keys, or via POST /api/v1/api-keys.

curl https://api.staxa.dev/api/v1/tenants \
  -H "Authorization: Bearer sk_live_a1b2c3d4e5f6..."

A key is sk_live_ or sk_test_ followed by 32 hex characters. Both modes carry the same access — the mode is a label, so you can tell a production key from a development one at a glance. Only a bcrypt hash and the first 16 characters are stored, so the raw key exists nowhere but the create response.

Clerk JWT (Dashboard)

The dashboard uses this automatically. For anything you write yourself, use an API key.

curl https://api.staxa.dev/api/v1/tenants \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."

Response Format

Every response takes one of three shapes.

Single resource:

{
  "data": { ... },
  "meta": { "request_id": "..." }
}

List (paginated):

{
  "data": [ ... ],
  "pagination": { "total": 42, "limit": 20, "offset": 0 },
  "meta": { "request_id": "..." }
}

Error:

{
  "error": {
    "code": "...",
    "message": "...",
    "details": { ... }
  }
}

Paginated endpoints accept limit (1–100, default 20) and offset (default 0) as query parameters. Not every list is paginated — shorter collections such as services, domains, env vars, and network rules come back as a plain array under data, with no pagination block.

Error Handling

The API uses standard HTTP status codes:

CodeMeaning
200Success (GET, PATCH, PUT)
201Created (synchronous POST)
202Accepted (async operations: tenant creation, deploys, deletes)
204No Content (DELETE)
400Bad Request (validation)
401Unauthorized
403Forbidden
404Not Found
409Conflict (name already taken, port pool exhausted)
422Unprocessable Entity (tenant limit reached, tenant busy)
429Rate Limited
500Internal Server Error
501Not Implemented (feature not configured on this deployment)

The error.code field carries a machine-readable string to branch on: BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, UNPROCESSABLE_ENTITY, TOO_MANY_REQUESTS, INTERNAL_SERVER_ERROR, NOT_IMPLEMENTED, SERVICE_UNAVAILABLE.

Network rules and registry credentials add codes of their own: INVALID_CIDR, CIDR_TOO_WIDE, TOO_MANY_CIDRS, RULE_LIMIT_REACHED, EGRESS_MODE_REQUIRED, PORT_POOL_EXHAUSTED, REGISTRY_VERIFICATION_FAILED, REGISTRY_LIMIT_REACHED.

Rate Limits

Authenticated requests share a single limit: 100 requests per minute per provider, counted across every endpoint. The window is fixed and resets at the top of each minute.

The template catalog is public and needs no authentication, so it is limited by IP instead: 60 requests per minute.

Every response carries the rate limit headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1708790460

Going over returns 429 with code TOO_MANY_REQUESTS. If the rate limiter itself is unreachable, requests are allowed through rather than rejected.

Webhooks (Coming Soon)

Webhooks will post notifications to a URL you control when events occur in your tenant environments.

Planned event format:

{
  "event": "tenant.ready",
  "tenant_id": "ten_a1b2c3d4e5f6",
  "data": {
    "status": "ready",
    "url": "https://acme.tenants.staxa.dev"
  },
  "timestamp": "2025-02-24T14:31:00Z"
}

Planned events: tenant.ready, tenant.failed, tenant.deleted, deployment.started, deployment.succeeded, deployment.failed.

Not available yet. Get in touch if you need webhook integration before then.

API Keys

Create and manage API keys for programmatic access.

GET/api/v1/api-keys

List API Keys

List all API keys for the authenticated provider. Only the key prefix comes back, never the full key.

`prefix` is the first 16 characters of the key — enough to identify it in a list, not enough to use it. Revoked keys stay in the list with `status: "revoked"`.

GET /api/v1/api-keys

{
  "data": [
    {
      "id": "key_a1b2c3d4",
      "provider_id": "prov_a1b2c3d4",
      "name": "Production",
      "prefix": "sk_live_a1b2c3d4",
      "scopes": ["*"],
      "mode": "live",
      "last_used": "2026-01-16T08:12:00Z",
      "expires_at": null,
      "status": "active",
      "created_at": "2026-01-15T10:30:00Z"
    }
  ]
}
POST/api/v1/api-keys

Create API Key

Create a new API key. The response contains the raw key, and it is the only time you will see it.

Parameters

  • Name
    name
    Type
    string
    Description

    Required.A name for the key, for your own reference

  • Name
    scopes
    Type
    string[]
    Description

    Permission scopes Defaults to ["*"].

  • Name
    mode
    Type
    string
    Description

    Key mode: "live" or "test" Defaults to live.

  • Name
    expires_at
    Type
    string
    Description

    ISO 8601 expiration date

Error Codes

CodeCondition
400Missing required field "name"
Only `name` is required. `mode` defaults to `live`, `scopes` defaults to `["*"]`, and `expires_at` must be RFC 3339 if you send it. The `key` field holds the raw token — nothing else in the API returns it again, so copy it before you move on.

POST /api/v1/api-keys

POST
/api/v1/api-keys
curl -X POST https://api.staxa.dev/api/v1/api-keys \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"name": "Production", "mode": "live"}'
DELETE/api/v1/api-keys/{id}

Delete API Key

Revoke an API key. Takes effect immediately.

Error Codes

CodeCondition
404API key not found
The key is marked revoked rather than erased, so it keeps showing up in `GET /api-keys` with `status: "revoked"`. Deleting a key that belongs to another provider returns 404.

DELETE /api/v1/api-keys/{id}

No response body (204 No Content).

Tenants

Create, list, update, and delete tenant environments.

GET/api/v1/tenants

List Tenants

List all tenants for the authenticated provider. Paginated, with an optional status filter.

Parameters

  • Name
    status
    Type
    string
    Description

    Filter by status (e.g., ready, deploying, failed)

  • Name
    limit
    Type
    int
    Description

    Results per page Defaults to 20.

  • Name
    offset
    Type
    int
    Description

    Pagination offset Defaults to 0.

  • Name
    sort
    Type
    string
    Description

    Sort field (e.g., created_at, name)

  • Name
    search
    Type
    string
    Description

    Search by tenant name

Takes `limit` (1–100, default 20), `offset`, and `status` as query parameters. The live URL is `deployment_url`, and it stays empty until the first deploy routes successfully. Deleted tenants are excluded.

GET /api/v1/tenants

{
  "data": [
    {
      "id": "ten_a1b2c3d4",
      "provider_id": "prov_a1b2c3d4",
      "name": "acme-corp",
      "display_name": "Acme Corp",
      "status": "ready",
      "source_type": "github",
      "repo_url": "https://github.com/acme/webapp",
      "branch": "main",
      "app_port": 3000,
      "resource_size": "small",
      "egress_mode": "allow_all",
      "deployment_url": "https://acme-corp.tenants.staxa.dev",
      "created_at": "2026-01-15T10:30:00Z",
      "updated_at": "2026-01-15T10:35:00Z"
    }
  ],
  "pagination": { "total": 1, "limit": 20, "offset": 0 }
}
POST/api/v1/tenants

Create Tenant

Create a new tenant environment. Supports a single-service (legacy) or multi-service request body.

Parameters

  • Name
    name
    Type
    string
    Description

    Required.Unique tenant name (used as subdomain)

  • Name
    display_name
    Type
    string
    Description

    Display name for the tenant

  • Name
    source_type
    Type
    string
    Description

    Source type: "github" or "image" (single-service mode)

  • Name
    repo_url
    Type
    string
    Description

    Git repository URL (single-service mode)

  • Name
    branch
    Type
    string
    Description

    Git branch Defaults to main.

  • Name
    services
    Type
    object[]
    Description

    Array of service definitions (multi-service mode)

  • Name
    env
    Type
    object
    Description

    Environment variables as key-value pairs

Error Codes

CodeCondition
400Validation error (missing name, invalid source_type, duplicate service names)
409Tenant name already taken
422Tenant limit reached for this provider
Creation is asynchronous, so the call returns 202 with the tenant at `status: "pending"`. Supply either `source_type` or `services[]` — a legacy body is wrapped into one primary service for you. The first deploy is queued automatically. `auto_deploy_started` tells you whether that worked; if it did not, `auto_deploy_error` explains why and the tenant is marked failed rather than left pending. Pass `"deploy_on_create": false` to stage the configuration without deploying. Watch progress at `GET /api/v1/tenants/{id}/events`. A duplicate name returns 409, and exceeding your plan's tenant limit returns 422.

POST /api/v1/tenants

POST
/api/v1/tenants
curl -X POST https://api.staxa.dev/api/v1/tenants \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "acme-corp",
    "source_type": "github",
    "repo_url": "https://github.com/acme/webapp",
    "branch": "main"
  }'
GET/api/v1/tenants/{id}

Get Tenant

Get full details for a tenant, including its services.

Error Codes

CodeCondition
404Tenant not found
Domains are not included here — fetch them from `GET /tenants/{id}/domains`. When a deploy fails, `last_error` and `error_stage` carry the reason; `diagnostic_message` may appear mid-deploy with a non-fatal hint, such as a readiness probe that is not passing yet.

GET /api/v1/tenants/{id}

{
  "data": {
    "id": "ten_a1b2c3d4",
    "provider_id": "prov_a1b2c3d4",
    "name": "acme-corp",
    "display_name": "Acme Corp",
    "status": "ready",
    "current_stage": "ready",
    "source_type": "github",
    "repo_url": "https://github.com/acme/webapp",
    "branch": "main",
    "app_port": 3000,
    "resource_size": "small",
    "cpu_request": "25m",
    "cpu_limit": "500m",
    "memory_request": "64Mi",
    "memory_limit": "512Mi",
    "storage_limit": "5Gi",
    "current_image": "registry.staxa.dev/acme-corp:v3",
    "deployment_url": "https://acme-corp.tenants.staxa.dev",
    "egress_mode": "allow_all",
    "health_check_type": "http",
    "health_check_path": "/",
    "k8s_namespace": "ten-acme-corp",
    "services": [],
    "metadata": {},
    "created_at": "2026-01-15T10:30:00Z",
    "updated_at": "2026-01-15T10:35:00Z"
  }
}
PATCH/api/v1/tenants/{id}

Update Tenant

Partial update of a tenant. Only provided fields are changed.

Parameters

  • Name
    display_name
    Type
    string
    Description

    Display name for the tenant

  • Name
    branch
    Type
    string
    Description

    Default git branch

  • Name
    resource_size
    Type
    string
    Description

    Resource tier: small, medium, large

  • Name
    app_port
    Type
    int
    Description

    Application port

  • Name
    egress_mode
    Type
    string
    Description

    Network egress mode: open or restricted

Error Codes

CodeCondition
404Tenant not found
`resource_size` must be `small`, `medium`, or `large`; `egress_mode` must be `allow_all` or `restricted`; `app_port` must be between 1 and 65535. Changes to build or runtime settings take effect on the next deploy — trigger one with `POST /tenants/{id}/deployments`.

PATCH /api/v1/tenants/{id}

{
  "display_name": "Acme Corp Updated",
  "repo_url": "https://github.com/acme/webapp",
  "branch": "release",
  "dockerfile_path": "docker/Dockerfile",
  "build_context": ".",
  "resource_size": "medium",
  "app_port": 3000,
  "egress_mode": "restricted",
  "health_check_type": "http",
  "health_check_path": "/healthz",
  "health_check_port": 3000
}
DELETE/api/v1/tenants/{id}

Delete Tenant

Delete a tenant and every resource it owns.

Error Codes

CodeCondition
404Tenant not found
Returns 202. The tenant is soft-deleted right away — the name is freed and it drops out of `GET /tenants` — while containers, volumes, and routes are torn down in the background.

DELETE /api/v1/tenants/{id}

{
  "data": {
    "status": "accepted",
    "message": "tenant deletion queued"
  }
}

Services

Manage individual services inside a multi-service tenant.

GET/api/v1/tenants/{id}/services

List Services

List all services for a tenant.

A plain array, not paginated. Services deploy in `deploy_priority` order, lowest first.

GET /api/v1/tenants/{id}/services

{
  "data": [
    {
      "id": "svc_9f8e7d6c",
      "tenant_id": "ten_a1b2c3d4",
      "name": "web",
      "display_name": "Web",
      "source_type": "github",
      "repo_url": "https://github.com/acme/webapp",
      "branch": "main",
      "dockerfile_path": "Dockerfile",
      "build_context": ".",
      "framework": "nextjs",
      "port": 3000,
      "is_routable": true,
      "is_primary": true,
      "status": "running",
      "image_tag": "v3",
      "deploy_priority": 0,
      "resource_size": "small",
      "health_check_type": "http",
      "health_check_path": "/",
      "created_at": "2026-01-15T10:30:00Z",
      "updated_at": "2026-01-15T10:35:00Z"
    }
  ]
}
POST/api/v1/tenants/{id}/services

Add Service

Add a new service to an existing tenant.

Parameters

  • Name
    name
    Type
    string
    Description

    Required.Unique service name within the tenant

  • Name
    source_type
    Type
    string
    Description

    Required.Source type: "github" or "image"

  • Name
    repo_url
    Type
    string
    Description

    Git repository URL

  • Name
    branch
    Type
    string
    Description

    Git branch Defaults to main.

  • Name
    image_url
    Type
    string
    Description

    Container image URL (for source_type "image")

  • Name
    port
    Type
    int
    Description

    Service port

  • Name
    is_routable
    Type
    boolean
    Description

    Whether the service receives HTTP traffic Defaults to false.

  • Name
    resource_size
    Type
    string
    Description

    Resource tier: small, medium, large Defaults to small.

Error Codes

CodeCondition
400Missing required field (name or source_type)
409Service name already exists in this tenant
422Tenant is not in a deployable state
Returns 202 — creating the service also queues a deploy of it. `name` and `source_type` are required; `port` defaults to 8080 and `is_routable` to true. A service added this way is never primary. A duplicate name returns 409, and a tenant that is mid-deploy returns 422.

POST /api/v1/tenants/{id}/services

{
  "name": "api",
  "display_name": "API",
  "source_type": "github",
  "repo_url": "https://github.com/acme/backend",
  "branch": "main",
  "dockerfile_path": "Dockerfile",
  "build_context": ".",
  "port": 8080,
  "is_routable": true,
  "deploy_priority": 1,
  "resource_size": "small",
  "health_check_type": "http",
  "health_check_path": "/healthz"
}
DELETE/api/v1/tenants/{id}/services/{serviceName}

Remove Service

Remove a service from a tenant. The primary service cannot be removed.

Error Codes

CodeCondition
400Cannot remove the primary service
404Service not found
Addressed by service name, not ID. Removing the primary service returns 400. The tenant's aggregate status is recalculated from whatever services remain.

DELETE /api/v1/tenants/{id}/services/{serviceName}

No response body (204 No Content).
POST/api/v1/tenants/{id}/services/{serviceName}/deploy

Deploy Service

Trigger a deployment for one service.

Returns 202 with the tracking deployment record. `version` counts up per service. Subscribe to the tenant events stream to follow progress; a tenant that is already deploying returns 422.

POST /api/v1/tenants/{id}/services/{serviceName}/deploy

{
  "data": {
    "id": "dep_x1y2z3",
    "tenant_id": "ten_a1b2c3d4",
    "service_id": "svc_9f8e7d6c",
    "provider_id": "prov_a1b2c3d4",
    "image_tag": "pending",
    "source_type": "github",
    "deploy_type": "full",
    "trigger": "api",
    "status": "pending",
    "version": 4,
    "is_current": false,
    "metadata": {},
    "created_at": "2026-01-15T10:30:00Z"
  }
}
GET/api/v1/tenants/{id}/services/{serviceName}/deployments

List Service Deployments

List deployment history for one service.

Paginated with `limit` and `offset`. `is_current` is scoped to this service: it marks the deployment currently serving traffic for THIS service, and every service in a tenant has its own current deployment. `has_config_snapshot` reports whether the deployment's configuration was captured, and `has_retained_secrets` whether its secret values are still held. A deployment that captured no secrets reports `has_retained_secrets: true`, because a rollback restores it in full. Together they tell you what a rollback to that deployment would restore before you request it. Roll one of these back with `POST /api/v1/tenants/{id}/services/{serviceName}/deployments/{depId}/rollback`.

GET /api/v1/tenants/{id}/services/{serviceName}/deployments

{
  "data": [
    {
      "id": "dep_x1y2z3",
      "tenant_id": "ten_a1b2c3d4",
      "service_id": "svc_9f8e7d6c",
      "image_tag": "registry.staxa.dev/acme-corp-web:v3",
      "source_ref": "abc123def456",
      "source_type": "github",
      "deploy_type": "full",
      "trigger": "api",
      "status": "succeeded",
      "started_at": "2026-01-15T10:30:05Z",
      "completed_at": "2026-01-15T10:32:00Z",
      "duration_ms": 115000,
      "version": 3,
      "is_current": true,
      "has_config_snapshot": true,
      "has_retained_secrets": true,
      "created_at": "2026-01-15T10:30:00Z"
    }
  ],
  "pagination": { "total": 3, "limit": 20, "offset": 0 }
}
GET/api/v1/tenants/{id}/services/{serviceName}/logs

Service Logs

Stream logs for one service.

Parameters

  • Name
    lines
    Type
    int
    Description

    Number of log lines to return Defaults to 100.

  • Name
    follow
    Type
    boolean
    Description

    Stream logs in real-time (SSE) Defaults to false.

`lines` defaults to 100 and caps at 5000. `follow=true` holds the connection open and streams new output, up to ten minutes. With no running pods you get 404; on a deployment without log streaming configured, 501.

GET /api/v1/tenants/{id}/services/{serviceName}/logs

GET
/api/v1/tenants/{id}/services/{serviceName}/logs
curl "https://api.staxa.dev/api/v1/tenants/ten_a1b2c3d4/services/api/logs?lines=200&follow=true" \
  -H "Authorization: Bearer sk_live_..."
POST/api/v1/tenants/{id}/deploy

Deploy All Services

Deploy every service in a tenant at once.

Returns 202 with one tenant-level deployment record covering the whole run. Services build and deploy in `deploy_priority` order, and one failing service does not stop the others — the tenant's final status is derived from all of them together.

POST /api/v1/tenants/{id}/deploy

{
  "data": {
    "id": "dep_x1y2z3",
    "tenant_id": "ten_a1b2c3d4",
    "provider_id": "prov_a1b2c3d4",
    "image_tag": "pending",
    "source_type": "github",
    "deploy_type": "full",
    "trigger": "api",
    "status": "pending",
    "version": 5,
    "is_current": false,
    "metadata": {},
    "created_at": "2026-01-15T10:30:00Z"
  }
}
POST/api/v1/tenants/{id}/services/{serviceName}/deployments/{depId}/rollback

Rollback Service Deployment

Roll one service back to an earlier deployment of that same service.

Returns 202. Restores one service to an earlier deployment of that same service, leaving every other service alone. Use this rather than the tenant-level rollback for anything in a multi-service tenant. The tenant-level endpoint redeploys the primary workload, so restoring a service's image through it would deploy that image tenant-wide, and it rejects service-scoped targets for that reason. No rebuild happens: the target's image already exists in the registry and is redeployed as-is. Configuration comes from the target's snapshot, with database credentials and inter-service discovery variables re-derived live on top, because those describe current topology. `config_source` reports which configuration was applied — `snapshot`, `partial` or `current` — with the same meaning as the tenant-level rollback, and `{"refresh_config": true}` opts into current configuration with the old image. Returns 404 when the deployment does not exist, belongs to another tenant, or belongs to a DIFFERENT service of this tenant. A sibling service's deployment is not visible on this route, so it reads as not found rather than as a validation error. Returns 422 when the target did not complete successfully, when it has no image to restore, or when the tenant is not in a deployable state (`pending`, `ready` or `failed`). The new deployment is versioned in the service's own sequence, not the tenant's, so its `version` continues that service's history. Configuration retention: a deployment's settings and non-secret variables are kept for the life of the deployment row, so `has_config_snapshot` stays true. Only secret VALUES are bounded, to the most recent 3 deployments per service by default — operators can change this with the `config_snapshots.secret_retention` platform config key, which is read with a 30 second cache and needs no restart.

POST /api/v1/tenants/{id}/services/{serviceName}/deployments/{depId}/rollback

POST
/api/v1/tenants/{id}/services/{serviceName}/deployments/{depId}/rollback
curl -X POST \
  https://api.staxa.dev/api/v1/tenants/ten_a1b2c3d4/services/web/deployments/dep_x1y2z3/rollback \
  -H "Authorization: Bearer $STAXA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"refresh_config": false}'

Deployments

Trigger, inspect, and roll back deployments.

GET/api/v1/tenants/{id}/deployments

List Deployments

List all deployments for a tenant, newest first.

Paginated with `limit` and `offset`. Status moves through `pending` → `building` → `deploying` → `succeeded` or `failed`. Deployments scoped to a single service also carry a `service_id`. `has_config_snapshot` reports whether the deployment's configuration was captured, and `has_retained_secrets` whether its secret values are still held. A deployment that captured no secrets reports `has_retained_secrets: true`, because a rollback restores it in full. Together they tell you what a rollback to that deployment would restore before you request it.

GET /api/v1/tenants/{id}/deployments

{
  "data": [
    {
      "id": "dep_x1y2z3",
      "tenant_id": "ten_a1b2c3d4",
      "provider_id": "prov_a1b2c3d4",
      "image_tag": "registry.staxa.dev/acme-corp:v3",
      "source_ref": "abc123def456",
      "source_type": "github",
      "deploy_type": "full",
      "trigger": "api",
      "status": "succeeded",
      "started_at": "2026-01-15T10:30:05Z",
      "completed_at": "2026-01-15T10:32:00Z",
      "duration_ms": 115000,
      "version": 3,
      "is_current": true,
      "has_config_snapshot": true,
      "has_retained_secrets": true,
      "metadata": {},
      "created_at": "2026-01-15T10:30:00Z"
    }
  ],
  "pagination": { "total": 5, "limit": 20, "offset": 0 }
}
POST/api/v1/tenants/{id}/deployments

Create Deployment

Trigger a new deployment for a tenant.

Parameters

  • Name
    branch
    Type
    string
    Description

    Git branch to deploy

  • Name
    commit_sha
    Type
    string
    Description

    Specific commit to deploy

Returns 202. Every field is optional — an empty body builds the configured branch at its latest commit, with `source_type` falling back to `github`, `deploy_type` to `full`, `trigger` to `api`, and `image_tag` to `pending` until the build assigns a real tag. Use `source_ref` to pin a commit SHA or tag. Only tenants in `pending`, `ready`, or `failed` accept new deployments; anything else returns 422.

POST /api/v1/tenants/{id}/deployments

{
  "image_tag": "registry.staxa.dev/acme-corp:v4",
  "source_ref": "abc123def456",
  "source_type": "github",
  "deploy_type": "full",
  "trigger": "api"
}
GET/api/v1/tenants/{id}/deployments/{depId}

Get Deployment

Get details for one deployment.

On a failure, `error_stage` names the pipeline stage that broke and `build_logs` holds the captured build output. Both are omitted on a clean deploy. `has_config_snapshot` reports whether the deployment's configuration was captured, and `has_retained_secrets` whether its secret values are still held. A deployment that captured no secrets reports `has_retained_secrets: true`, because a rollback restores it in full. Together they tell you what a rollback to that deployment would restore before you request it.

GET /api/v1/tenants/{id}/deployments/{depId}

{
  "data": {
    "id": "dep_x1y2z3",
    "tenant_id": "ten_a1b2c3d4",
    "provider_id": "prov_a1b2c3d4",
    "image_tag": "registry.staxa.dev/acme-corp:v3",
    "source_ref": "abc123def456",
    "source_type": "github",
    "deploy_type": "full",
    "trigger": "api",
    "status": "failed",
    "started_at": "2026-01-15T10:30:05Z",
    "completed_at": "2026-01-15T10:31:10Z",
    "duration_ms": 65000,
    "error_stage": "build",
    "error_message": "build failed: exit status 1",
    "build_logs": "...",
    "version": 3,
    "is_current": false,
    "has_config_snapshot": true,
    "has_retained_secrets": true,
    "metadata": {},
    "created_at": "2026-01-15T10:30:00Z"
  }
}
POST/api/v1/tenants/{id}/deployments/{depId}/rollback

Rollback Deployment

Roll back to a specific earlier deployment, restoring its image and the configuration it ran with.

Returns 202. `{depId}` is the deployment to RESTORE: the rollback runs as a new deployment that redeploys that deployment's image. By default it also restores the configuration that deployment ran with, rather than applying today's configuration to an old image. The response reports which was applied in `config_source`: - `snapshot` — image and configuration both restored, secrets included. - `partial` — settings and non-secret variables restored; the target's secret values are past the retention window, so current values are used for those. This is the usual result for an older deployment, and is normally what you want: a rotated credential should be current. - `current` — the deployment predates configuration snapshots, so current configuration is used. Send `{"refresh_config": true}` to keep the old image but deliberately use current configuration. A bare POST with no body keeps the default. Fails with 422 when the target did not complete successfully, when it has no image to restore, or when it is service-scoped — a service's deployment cannot be restored here, because this endpoint redeploys the tenant's primary workload and would push that image tenant-wide. Use `POST /api/v1/tenants/{id}/services/{serviceName}/deployments/{depId}/rollback` instead. Returns 404 when the deployment does not exist or belongs to another tenant. A rollback fails rather than deploying if the target named secret variables that no longer exist and whose values are past the retention window; the error names the keys. Recreate them, or retry with `refresh_config`. Configuration retention: a deployment's settings and non-secret variables are kept for the life of the deployment row, so `has_config_snapshot` stays true. Only secret VALUES are bounded, to the most recent 3 deployments per service by default — operators can change this with the `config_snapshots.secret_retention` platform config key, which is read with a 30 second cache and needs no restart.

POST /api/v1/tenants/{id}/deployments/{depId}/rollback

{
  "refresh_config": false
}

Environment Variables

Manage environment variables for tenant workloads.

GET/api/v1/tenants/{id}/env

Get Env Vars

List environment variables for a tenant.

Variables flagged `is_secret` come back with their value replaced by `[redacted]` — nothing in the API reveals them again. `source` is `user` for anything you set and `system` for values Staxa injects, such as database credentials.

GET /api/v1/tenants/{id}/env

{
  "data": [
    {
      "id": "6f1c9a2e-8b34-4d51-9f77-2a0c5e8b1d43",
      "key": "NODE_ENV",
      "value": "production",
      "is_secret": false,
      "source": "user"
    },
    {
      "id": "b28d7f04-15ce-4a9b-8e60-7c3f9a1d6e52",
      "key": "DATABASE_URL",
      "value": "[redacted]",
      "is_secret": true,
      "source": "system"
    }
  ]
}
PUT/api/v1/tenants/{id}/env

Bulk Set Env Vars

Create or update environment variables in bulk.

The request body is a flat `Record<string, string>`, not a nested object. Keys are merged, not replaced: anything you send is created or overwritten, and anything you leave out is kept as it was. `updated` counts the keys in your request. Variables set this way are always non-secret and owned by you (`source: "user"`). Values are encrypted at rest and reach your containers on the next deploy. There is no endpoint for deleting a single variable — set it to an empty string instead.

PUT /api/v1/tenants/{id}/env

{
  "NODE_ENV": "production",
  "DATABASE_URL": "postgres://...",
  "API_KEY": "secret123"
}

Domains

Custom domain management with DNS verification.

GET/api/v1/tenants/{id}/domains

List Domains

List custom domains for a tenant.

Point your A record at the `server_ip` field, which is only present when the platform has one configured. Domain `status` runs `pending` → `verified` → `active`; `ssl_status` tracks certificate issuance separately.

GET /api/v1/tenants/{id}/domains

{
  "data": {
    "domains": [
      {
        "id": "dom_a1b2c3",
        "tenant_id": "ten_a1b2c3d4",
        "domain": "app.acme.com",
        "type": "custom",
        "status": "verified",
        "verification_record": "staxa-verify=ver_a1b2c3d4",
        "verified_at": "2026-01-15T11:00:00Z",
        "ssl_status": "active",
        "is_primary": true,
        "service_id": "svc_9f8e7d6c",
        "created_at": "2026-01-15T10:30:00Z",
        "updated_at": "2026-01-15T11:00:00Z"
      }
    ],
    "server_ip": "65.108.x.x"
  }
}
POST/api/v1/tenants/{id}/domains

Add Domain

Add a custom domain to a tenant.

Parameters

  • Name
    domain
    Type
    string
    Description

    Required.Custom domain name

  • Name
    is_primary
    Type
    boolean
    Description

    Set as primary domain Defaults to false.

  • Name
    service_id
    Type
    string
    Description

    Route to a specific service (multi-service tenants)

Returns 201. `type` defaults to `custom`, which is what generates the `verification_record` you need for the TXT check. `service_id` is optional and must be a service ID (`svc_...`) belonging to this tenant — leave it out to route the domain at the primary service. Domains must be valid hostnames of 253 characters or fewer.

POST /api/v1/tenants/{id}/domains

{
  "domain": "app.acme.com",
  "type": "custom",
  "is_primary": true,
  "service_id": "svc_9f8e7d6c"
}
POST/api/v1/tenants/{id}/domains/{domId}/verify

Verify Domain

Run the DNS check for a custom domain.

Always returns 200 — the shape tells you the outcome. The check looks for a TXT record at `_staxa-verify.<your-domain>` matching `verification_record`; until that resolves you get the pending shape back, and you can call this as often as you need. Once it passes, the domain object is returned and the SSL certificate is issued automatically. A domain that is already verified or active is returned unchanged.

POST /api/v1/tenants/{id}/domains/{domId}/verify

**Still pending:**
```json
{
  "data": {
    "status": "pending",
    "verification_record": "staxa-verify=ver_a1b2c3d4",
    "instructions": "Add a TXT record for _staxa-verify.app.acme.com with value: staxa-verify=ver_a1b2c3d4",
    "server_ip": "65.108.x.x"
  }
}
```

**Verified:**
```json
{
  "data": {
    "id": "dom_a1b2c3",
    "tenant_id": "ten_a1b2c3d4",
    "domain": "app.acme.com",
    "type": "custom",
    "status": "verified",
    "verification_record": "staxa-verify=ver_a1b2c3d4",
    "verified_at": "2026-01-15T11:00:00Z",
    "ssl_status": "pending",
    "is_primary": true,
    "created_at": "2026-01-15T10:30:00Z",
    "updated_at": "2026-01-15T11:00:00Z"
  }
}
```
DELETE/api/v1/tenants/{id}/domains/{domId}

Delete Domain

Remove a custom domain from a tenant.

For a domain that reached `verified` or `active`, the route, certificate, and any managed CNAME record are cleaned up first.

DELETE /api/v1/tenants/{id}/domains/{domId}

No response body (204 No Content).

Network Rules

Inbound and outbound network access control.

GET/api/v1/tenants/{id}/network-rules

List Network Rules

List network rules for a tenant, with optional filtering.

Parameters

  • Name
    direction
    Type
    string
    Description

    Filter: "inbound" or "outbound"

  • Name
    status
    Type
    string
    Description

    Filter by status Defaults to active.

A plain array, not paginated. Filter with `direction` (`inbound` or `outbound`) and `status`, which defaults to `active`. `connection_string` is computed on create and read, not stored, so it is absent from this list.

GET /api/v1/tenants/{id}/network-rules

{
  "data": [
    {
      "id": "nr_a1b2c3",
      "tenant_id": "ten_a1b2c3d4",
      "provider_id": "prov_a1b2c3d4",
      "direction": "inbound",
      "protocol": "tcp",
      "description": "Database access from office",
      "target_service": "database",
      "target_port": 5432,
      "source_cidrs": ["203.0.113.0/24"],
      "allocated_port": 30100,
      "expires_at": "2026-01-16T10:30:00Z",
      "status": "active",
      "created_at": "2026-01-15T10:30:00Z",
      "updated_at": "2026-01-15T10:30:00Z"
    }
  ]
}
POST/api/v1/tenants/{id}/network-rules

Create Network Rule

Create a network rule. Inbound TCP rules allocate a port; outbound rules require restricted egress mode.

Parameters

  • Name
    direction
    Type
    string
    Description

    Required."inbound" or "outbound"

  • Name
    protocol
    Type
    string
    Description

    Required.Protocol: "tcp" or "udp"

  • Name
    target_service
    Type
    string
    Description

    Target service name (inbound)

  • Name
    target_port
    Type
    int
    Description

    Target port (inbound)

  • Name
    source_cidrs
    Type
    string[]
    Description

    Allowed source IPs (inbound)

  • Name
    destination_cidrs
    Type
    string[]
    Description

    Allowed destination IPs (outbound)

  • Name
    destination_port
    Type
    int
    Description

    Destination port (outbound)

  • Name
    ttl_hours
    Type
    int
    Description

    Auto-expire after N hours

  • Name
    description
    Type
    string
    Description

    Free-text description of the rule

Error Codes

CodeCondition
400Invalid CIDR format, CIDR too wide, or too many CIDRs
409NodePort pool exhausted (inbound)
422Rule limit reached, or egress_mode must be "restricted" for outbound rules
Returns 201. `direction` and `protocol` are required — `inbound` or `outbound`, `tcp` or `udp`. Inbound TCP rules get a port from the platform pool in `allocated_port`; when the pool is empty you get 409 `PORT_POOL_EXHAUSTED`. Outbound rules only work on a tenant with `egress_mode: "restricted"`, otherwise 422 `EGRESS_MODE_REQUIRED`. Each one is folded into a single network policy covering every active outbound rule. `ttl_hours` sets an expiry, after which the rule is cleaned up automatically; leave it out to use the platform default. CIDRs are validated on the way in — 400 with `INVALID_CIDR`, `CIDR_TOO_WIDE` (`/0` and `/1` are blocked), or `TOO_MANY_CIDRS`. Past your rule limit you get 422 `RULE_LIMIT_REACHED`.

POST /api/v1/tenants/{id}/network-rules

**Inbound rule:**
```json
{
  "direction": "inbound",
  "protocol": "tcp",
  "target_service": "database",
  "target_port": 5432,
  "source_cidrs": ["203.0.113.0/24"],
  "description": "Database access from office",
  "ttl_hours": 24
}
```

**Outbound rule:**
```json
{
  "direction": "outbound",
  "protocol": "tcp",
  "destination_cidrs": ["10.0.0.0/8"],
  "destination_port": 443,
  "description": "Allow HTTPS to internal network"
}
```
GET/api/v1/tenants/{id}/network-rules/{ruleId}

Get Network Rule

Get details for one network rule.

A rule belonging to another tenant returns 404 rather than 403.

GET /api/v1/tenants/{id}/network-rules/{ruleId}

{
  "data": {
    "id": "nr_a1b2c3",
    "tenant_id": "ten_a1b2c3d4",
    "provider_id": "prov_a1b2c3d4",
    "direction": "inbound",
    "protocol": "tcp",
    "description": "Database access from office",
    "target_service": "database",
    "target_port": 5432,
    "source_cidrs": ["203.0.113.0/24"],
    "allocated_port": 30100,
    "expires_at": "2026-01-16T10:30:00Z",
    "status": "active",
    "created_at": "2026-01-15T10:30:00Z",
    "updated_at": "2026-01-15T10:30:00Z"
  }
}
DELETE/api/v1/tenants/{id}/network-rules/{ruleId}

Delete Network Rule

Delete a network rule. Inbound rules release the allocated port.

Deleting an outbound rule rebuilds the egress policy from the rules that remain; removing the last one drops the policy entirely and the tenant falls back to whatever its `egress_mode` allows.

DELETE /api/v1/tenants/{id}/network-rules/{ruleId}

No response body (204 No Content).

Real-Time

SSE event streams and live container logs.

GET/api/v1/tenants/{id}/events

Events (SSE)

Subscribe to deployment events over Server-Sent Events (SSE).

Three event names come down the wire: `connected` once on open, `deployment` for every pipeline update, and `ping` every 15 seconds to hold the connection alive. On connect, the events already recorded for the tenant's latest deployment are replayed first, so a client that joins late still sees the full picture. Deployment stages run `validate` → `provision` → `database` → `build` → `arch_check` → `deploy` → `route` → `health_check` → `ready`, with `progress` climbing from 0 to 100. Deletions emit `deleting` and `deleted`. A failure ends the stream on the stage that broke — read `error_stage` from the deployment for the detail.

GET /api/v1/tenants/{id}/events

GET
/api/v1/tenants/{id}/events
curl -N https://api.staxa.dev/api/v1/tenants/ten_a1b2c3d4/events \
  -H "Authorization: Bearer sk_live_..." \
  -H "Accept: text/event-stream"
GET/api/v1/tenants/{id}/logs

Container Logs

Stream container logs for a tenant.

Parameters

  • Name
    lines
    Type
    int
    Description

    Number of log lines to return Defaults to 200.

  • Name
    follow
    Type
    boolean
    Description

    Stream logs as they arrive Defaults to false.

`lines` defaults to 100 and caps at 5000. `follow=true` holds the connection open and streams new output, up to ten minutes. This reads the tenant's primary workload — for a specific service in a multi-service tenant, use `GET /tenants/{id}/services/{serviceName}/logs`. With no running pods you get 404.

GET /api/v1/tenants/{id}/logs

GET
/api/v1/tenants/{id}/logs
curl "https://api.staxa.dev/api/v1/tenants/ten_a1b2c3d4/logs?lines=50&follow=true" \
  -H "Authorization: Bearer sk_live_..."

Templates

Pre-built application templates and the runtime versions you can build against.

GET/api/v1/templates

List Templates

List available application templates for quick-start tenant creation.

Public — no authentication needed, limited to 60 requests per minute per IP. Narrow the list with a `category` query parameter.

GET /api/v1/templates

{
  "data": [
    {
      "id": "tmpl_nextjs_postgres",
      "slug": "nextjs-postgres",
      "name": "Next.js + PostgreSQL",
      "description": "Full-stack Next.js app with PostgreSQL database",
      "framework": "nextjs",
      "runtime": "node",
      "runtime_version": "20",
      "db_engine": "postgres",
      "db_version": "16",
      "default_env": {},
      "default_size": "small",
      "default_port": 3000,
      "category": "fullstack",
      "sort_order": 1,
      "is_featured": true,
      "is_active": true,
      "created_at": "2026-01-15T10:30:00Z",
      "updated_at": "2026-01-15T10:30:00Z"
    }
  ]
}
GET/api/v1/templates/{id}

Get Template

Get details for one template, by ID or slug.

Public, same as the list endpoint. The path segment is matched against the template ID first and then its slug, so `/templates/nextjs-postgres` works as well as `/templates/tmpl_nextjs_postgres`. Image-based templates carry an `image` instead of a `starter_repo`.

GET /api/v1/templates/{id}

{
  "data": {
    "id": "tmpl_nextjs_postgres",
    "slug": "nextjs-postgres",
    "name": "Next.js + PostgreSQL",
    "description": "Full-stack Next.js app with PostgreSQL database",
    "framework": "nextjs",
    "runtime": "node",
    "runtime_version": "20",
    "db_engine": "postgres",
    "db_version": "16",
    "default_env": {},
    "default_size": "small",
    "default_port": 3000,
    "category": "fullstack",
    "sort_order": 1,
    "is_featured": true,
    "is_active": true,
    "created_at": "2026-01-15T10:30:00Z",
    "updated_at": "2026-01-15T10:30:00Z"
  }
}
GET/api/v1/config/runtimes

List Runtime Versions

List the runtime versions the platform can build against.

The set is platform configuration, so it changes as runtimes are added or retired — read it rather than hardcoding a list. Use these values for `runtime_version` when creating a tenant or a service.

GET /api/v1/config/runtimes

{
  "data": {
    "node": ["22", "20", "18"],
    "python": ["3.13", "3.12", "3.11", "3.10"],
    "go": ["1.24", "1.23", "1.22"],
    "ruby": ["3.4", "3.3", "3.2"],
    "java": ["23", "21", "17", "11"],
    "php": ["8.4", "8.3", "8.2"],
    "rust": ["1.83", "1.80", "1.77"],
    "dotnet": ["9", "8"],
    "elixir": ["1.17", "1.16", "1.15"],
    "deno": ["2.1", "1.46"],
    "bun": ["1.1", "1.0"]
  }
}

GitHub App

GitHub App installation and repository access.

GET/api/v1/github/installation

Get GitHub Installation

Get the GitHub App installation status for the authenticated provider.

Always 200 — check `connected` rather than the status code. When nothing is installed, `install_url` is where you send the user to install the app. `repository_selection` is `all` or `selected`, and a suspended installation carries `suspended_at`.

GET /api/v1/github/installation

**Not connected:**
```json
{
  "data": {
    "connected": false,
    "install_url": "https://github.com/apps/staxa/installations/new"
  }
}
```

**Connected:**
```json
{
  "data": {
    "connected": true,
    "installation": {
      "id": "ghi_a1b2c3d4",
      "provider_id": "prov_a1b2c3d4",
      "installation_id": 12345678,
      "account_type": "Organization",
      "account_login": "acme-org",
      "account_id": 87654321,
      "app_slug": "staxa",
      "repository_selection": "selected",
      "status": "active",
      "created_at": "2026-01-15T10:30:00Z",
      "updated_at": "2026-01-15T10:30:00Z"
    }
  }
}
```
POST/api/v1/github/installation

Register GitHub Installation

Register a GitHub App installation for the provider. Called after GitHub redirects back from the install flow.

Parameters

  • Name
    installation_id
    Type
    int
    Description

    Required.GitHub App installation ID from the OAuth callback

Returns 200. The installation is verified against GitHub before anything is written, and the account metadata in the response comes from GitHub rather than the request — an ID GitHub does not recognise returns 400. Calling it again for the same provider updates the existing record instead of creating a second one.

POST /api/v1/github/installation

{
  "installation_id": 12345678
}
DELETE/api/v1/github/installation

Delete GitHub Installation

Remove the GitHub App installation link from the provider account.

This only unlinks the installation on the Staxa side. The app stays installed on GitHub until it is removed there too, so a later `POST /github/installation` can re-link it.

DELETE /api/v1/github/installation

No response body (204 No Content).
GET/api/v1/github/repos

List GitHub Repos

List repositories accessible through the GitHub App installation.

Which repositories appear depends on what the installation was granted — everything, or just the ones the user picked. With no installation registered you get 400.

GET /api/v1/github/repos

{
  "data": [
    {
      "id": 123456789,
      "full_name": "acme-org/webapp",
      "private": true,
      "default_branch": "main",
      "html_url": "https://github.com/acme-org/webapp"
    }
  ]
}
GET/api/v1/github/repos/{owner}/{repo}/branches

List GitHub Branches

List branches for one repository.

The repository has to be one the installation can reach, otherwise the call fails. With no installation registered you get 400.

GET /api/v1/github/repos/{owner}/{repo}/branches

{
  "data": [
    { "name": "main" },
    { "name": "develop" },
    { "name": "feature/auth" }
  ]
}
GET/api/v1/github/repos/{owner}/{repo}/analyze

Analyze Repository

Inspect a repository and detect its runtime, framework, and how to run it.

Takes an optional `branch`, defaulting to `main`. The repository tree and its dependency manifests are read through the GitHub App installation; nothing is cloned and nothing is written. When a Dockerfile is present, its base image decides `detected_runtime` and `detected_version`; otherwise both are inferred from the framework. `has_dockerfile: false` means Staxa will generate one for you at build time. Use the result to prefill a create-tenant call — `framework`, `runtime_version`, and `detected_port` map onto `framework`, `runtime_version`, and `app_port`. With no installation registered you get 400.

GET /api/v1/github/repos/{owner}/{repo}/analyze

GET
/api/v1/github/repos/{owner}/{repo}/analyze
curl "https://api.staxa.dev/api/v1/github/repos/acme-org/webapp/analyze?branch=main" \
  -H "Authorization: Bearer sk_live_..."

Registry Credentials

Private container registry authentication.

GET/api/v1/registries

List Registries

List saved container registry credentials.

Credentials themselves are never returned by any endpoint — they are encrypted at rest and only decrypted when Staxa talks to the registry. `status` is `active` or `error`; when it is `error`, `last_error` carries the reason from the last attempt.

GET /api/v1/registries

{
  "data": [
    {
      "id": "rcr_a1b2c3d4",
      "provider_id": "prov_a1b2c3d4",
      "name": "Docker Hub",
      "registry_type": "dockerhub",
      "registry_url": "https://index.docker.io/v1/",
      "namespace": "acme",
      "status": "active",
      "last_verified": "2026-01-15T10:30:00Z",
      "created_at": "2026-01-15T10:30:00Z",
      "updated_at": "2026-01-15T10:30:00Z"
    }
  ]
}
POST/api/v1/registries

Create Registry Credential

Save credentials for a private container registry.

Returns 201. `registry_type` must be `dockerhub`, `ghcr`, `ecr`, or `gcr`, and the shape of `credentials` follows from it. `registry_url` defaults per type — `https://index.docker.io/v1/`, `https://ghcr.io`, `https://gcr.io` — but is required for ECR, since the URL carries your account and region. Credentials are checked against the registry before they are stored, so a bad secret returns 422 `REGISTRY_VERIFICATION_FAILED` and nothing is saved. That is also why the credential comes back already `active`. Past your credential limit you get 422 `REGISTRY_LIMIT_REACHED`. `namespace` scopes repository listings to one organisation or project.

POST /api/v1/registries

**Docker Hub or GHCR:**
```json
{
  "name": "Docker Hub",
  "registry_type": "dockerhub",
  "registry_url": "https://index.docker.io/v1/",
  "namespace": "acme",
  "credentials": {
    "username": "acme",
    "password": "dckr_pat_..."
  }
}
```

**ECR:**
```json
{
  "name": "Production ECR",
  "registry_type": "ecr",
  "registry_url": "https://123456789012.dkr.ecr.us-east-1.amazonaws.com",
  "credentials": {
    "aws_access_key_id": "AKIA...",
    "aws_secret_access_key": "...",
    "aws_region": "us-east-1"
  }
}
```

**GCR or Artifact Registry:**
```json
{
  "name": "GCR",
  "registry_type": "gcr",
  "registry_url": "https://gcr.io",
  "credentials": {
    "service_account_json": "{\"type\":\"service_account\",...}"
  }
}
```
GET/api/v1/registries/{credId}

Get Registry Credential

Get details for a saved registry credential. The credentials themselves are never returned.

A credential belonging to another provider returns 404 rather than 403.

GET /api/v1/registries/{credId}

{
  "data": {
    "id": "rcr_a1b2c3d4",
    "provider_id": "prov_a1b2c3d4",
    "name": "Docker Hub",
    "registry_type": "dockerhub",
    "registry_url": "https://index.docker.io/v1/",
    "namespace": "acme",
    "status": "active",
    "last_verified": "2026-01-15T10:30:00Z",
    "created_at": "2026-01-15T10:30:00Z",
    "updated_at": "2026-01-15T10:30:00Z"
  }
}
PATCH/api/v1/registries/{credId}

Update Registry Credential

Rename a registry credential or rotate its secret.

Both fields are optional. New credentials are verified before they replace the old ones — if verification fails you get 422 and the stored credential is left alone, so a rotation can never lock you out by half-applying. `registry_type` and `registry_url` are fixed at creation; to change either, create a new credential.

PATCH /api/v1/registries/{credId}

{
  "name": "Docker Hub (rotated)",
  "credentials": {
    "username": "acme",
    "password": "dckr_pat_new..."
  }
}
DELETE/api/v1/registries/{credId}

Delete Registry Credential

Delete a saved registry credential.

Deployments that pull images with this credential will start failing on their next build, so repoint them first.

DELETE /api/v1/registries/{credId}

No response body (204 No Content).
POST/api/v1/registries/{credId}/verify

Verify Registry Credential

Re-test that the saved credentials can still authenticate with the registry.

On success the credential comes back with `status: "active"` and a fresh `last_verified`. On failure you get 422 `REGISTRY_VERIFICATION_FAILED`, and the credential is marked `error` with the reason stored in `last_error` — worth calling after a token rotation on the registry side.

POST /api/v1/registries/{credId}/verify

{
  "data": {
    "id": "rcr_a1b2c3d4",
    "provider_id": "prov_a1b2c3d4",
    "name": "Docker Hub",
    "registry_type": "dockerhub",
    "registry_url": "https://index.docker.io/v1/",
    "namespace": "acme",
    "status": "active",
    "last_verified": "2026-01-16T09:00:00Z",
    "created_at": "2026-01-15T10:30:00Z",
    "updated_at": "2026-01-16T09:00:00Z"
  }
}
GET/api/v1/registries/{credId}/repos

List Registry Repos

List repositories available in the registry.

Parameters

  • Name
    q
    Type
    string
    Description

    Search query to filter repos

  • Name
    limit
    Type
    int
    Description

    Maximum results to return Defaults to 25.

Takes `q` to filter by name and `limit` (1–100, default 25). Results are scoped to the credential's `namespace` where the registry supports it.

GET /api/v1/registries/{credId}/repos

{
  "data": [
    {
      "name": "acme/webapp",
      "full_url": "index.docker.io/acme/webapp",
      "description": "Frontend application",
      "is_private": true,
      "updated_at": "2026-01-14T18:02:00Z"
    }
  ]
}
GET/api/v1/registries/{credId}/repos/{repo}/tags

List Registry Tags

List tags for one repository in the registry.

Repository paths may contain slashes, as in `library/nginx` — the whole path goes between `/repos/` and `/tags`, and wildcard matching handles it. Takes `limit` (1–100, default 25). Which fields are populated depends on the registry; some return a digest and size, others only the tag name.

GET /api/v1/registries/{credId}/repos/{repo}/tags

{
  "data": [
    {
      "name": "latest",
      "digest": "sha256:abc123...",
      "size_bytes": 52428800,
      "pushed_at": "2026-01-14T18:02:00Z"
    },
    {
      "name": "v1.2.3",
      "digest": "sha256:def456...",
      "size_bytes": 52428800,
      "pushed_at": "2026-01-10T11:40:00Z"
    }
  ]
}

Was this page helpful?

© 2026 Staxa. All rights reserved.

Follow us on GitHub