API overview
Everything in the UI is available via the REST API. JSON requests + responses, page-number pagination, deterministic error shapes, per-route RBAC enforcement. 190+ endpoints under /api/v1.
Base URL
The same host as the UI, under /api/v1. Example: https://cap.example.com/api/v1. Everything that follows uses relative paths rooted there.
Versioning
- Major version in the path (
/api/v1). A v2 would live at/api/v2alongside v1 during a transition window. - No breaking changes within a major. Additive changes only: new endpoints, new optional request fields, new response fields.
- When a breaking change is necessary, v2 opens for a deprecation window of ≥ 12 months, with
Deprecation+Sunsetheaders on v1 responses.
Tenancy in the URL
Every project-scoped resource is under /projects/:projectId/...:
/api/v1/projects/{projectId}/certificates
/api/v1/projects/{projectId}/acme-accounts
/api/v1/projects/{projectId}/distributions
Org-scoped resources don't carry the project prefix:
/api/v1/users
/api/v1/audit-logs
/api/v1/ca-providers
There is no cross-project read endpoint. If you need cross-project data, iterate GET /projects and fan out.
Content type
- Request:
Content-Type: application/jsonfor all mutations. The server rejects any other content type on POST/PUT/PATCH. - Response:
application/json, UTF-8. - One exception: file downloads (cert bundles, exports) return the appropriate
application/x-pem-file,application/zip, orapplication/x-pkcs12.
Pagination
All list endpoints use offset (page-number) pagination:
GET /api/v1/projects/{projectId}/certificates?page=1&page_size=20
# →
{
"certificates": [ ... ], // the key is named after the resource
"total": 137,
"page": 1,
"page_size": 20
}
page defaults to 1; page_size defaults to 20 and is clamped to a maximum of 100 (a larger value is silently reduced, not rejected). Because this is offset paging, rows inserted or deleted between requests can shift the window — a row may be seen twice or skipped. Sort explicitly and, for exports, prefer the dedicated export endpoints over walking pages.
limit: default 50, max 500.cursor: opaque. Don't parse or manipulate.- Stable sort by ID — even if records are added or deleted mid-iteration, you don't skip or double-yield.
Filtering & sorting
Per-endpoint query params. The pattern:
GET /api/v1/projects/{projectId}/certificates
?status=active,renewal_failed
&expiring_within_days=30
&issuer_type=acme
&sort=expires_at
&order=asc
Comma-separated lists for OR filters. Per-field knobs documented on each endpoint in the full reference.
Error shape
error is a flat string, not a nested object. The default shape is:
HTTP 400 Bad Request
Content-Type: application/json
{ "error": "domain not under the selected zone" }
Two endpoints add structured context alongside it. A policy violation on certificate creation:
HTTP 422 Unprocessable Entity
{
"error": "policy_violation",
"message": "key type ECDSA-P256 is not allowed by this project's policy",
"violations": [ { "rule": "allowed_key_types", ... } ]
}
And a blocked delete:
HTTP 409 Conflict
{
"error": "cannot delete: resource is referenced by other resources",
"code": "DEPENDENCY_CONFLICT",
"references": [ ... ],
"suggestion": "..."
}
There is no request_id field in error bodies, and no details array — write clients against the status code plus the error string, and treat violations / references as extras present only on those two paths.
Common status codes
| Code | Meaning in this API |
|---|---|
| 200 | OK — GET / PATCH success. |
| 201 | Created — POST that created a resource. |
| 202 | Accepted — async action queued (e.g. cert issuance). |
| 204 | No Content — DELETE success. |
| 400 | Client error — malformed JSON, bad enum. |
| 401 | No auth / expired auth. |
| 403 | Authenticated but role insufficient. |
| 404 | Resource not found (or not visible to your tenant). |
| 409 | Conflict — duplicate, state precondition failed, license limit. |
| 422 | Unprocessable — policy violation, validation failure. |
| 429 | Rate-limited (auth failures or per-tenant quotas). |
| 503 | Service temporarily unavailable (scheduler rebooting, DB primary failover). |
Idempotency
Every mutation accepts an Idempotency-Key header:
POST /api/v1/projects/{projectId}/certificates
Idempotency-Key: create-api-cert-2026-04-21
Content-Type: application/json
{ ... }
Repeating the same request within 24 h returns the original response — both on success and on error. Different bodies with the same key return 409. Skip the header if you don't need the safety; the server doesn't synthesise one.
Rate limits
- Login / refresh / OTP-verify: 10 attempts per IP per minute → 429.
- Per-tenant quotas: absent by default. Add at your reverse proxy / API gateway for multi-tenant deployments.
- CA-imposed limits: cert issuance respects the CA's weekly quotas (zone page surfaces them). Not the same as CertAutoPilot's own rate limit.
Request correlation
- Every response carries
X-Request-ID. Log it; it's the key into the audit log and the OpenTelemetry trace. - Clients can set their own request ID by sending
X-Request-ID— the server echoes it.
Client libraries
Official client: none (yet), and no OpenAPI document is served — there is no /api/v1/openapi.json. Everything is plain JSON, so any curl / ky / requests / go http.Client works; the in-app API Docs page is the authoritative endpoint reference.