Skip to content

Client libraries

There is no official NextPKI SDK. For an API with six endpoints that is a feature, not an omission: a generated client brings a dependency, a build step and a generator’s opinions, in exchange for saving you a few lines of HTTP.

Two approaches, and the honest recommendation is the second.

The OpenAPI document is 3.1 and is what the service is built against. Point a generator at it:

Terminal window
curl -sSO https://docs.nextpki.com/openapi/nextpki-v1.yaml
# TypeScript types only - no runtime dependency
npx openapi-typescript nextpki-v1.yaml -o nextpki.d.ts
# Full client, pick your language
npx @openapitools/openapi-generator-cli generate \
-i nextpki-v1.yaml -g python -o ./nextpki-python

openapi-typescript is the one worth using unreservedly: it emits types and no runtime code, so you get autocompletion and compile-time checking with nothing to maintain.

Full generators produce a lot of code for six endpoints, and some emit clients that reject unknown response fields - which will break the first time a field is added. See versioning.

import os, json, urllib.request, urllib.error
class NextPKI:
def __init__(self, token=None, base="https://api.nextpki.com"):
self.base = base.rstrip("/")
self.token = token or os.environ["NEXTPKI_TOKEN"]
def _call(self, method, path, body=None):
req = urllib.request.Request(
self.base + path, method=method,
data=json.dumps(body).encode() if body else None,
headers={
"Authorization": f"Bearer {self.token}",
**({"Content-Type": "application/json"} if body else {}),
},
)
try:
with urllib.request.urlopen(req) as r:
return json.load(r)
except urllib.error.HTTPError as e:
detail = e.read().decode(errors="replace")
raise RuntimeError(f"{method} {path}{e.code}: {detail}") from e
def validate(self):
return self._call("GET", "/v1/auth/validate")
def certificates(self, **params):
"""Yields every certificate, paging transparently."""
limit, offset = params.pop("limit", 200), 0
extra = "".join(f"&{k}={v}" for k, v in params.items() if v is not None)
while True:
page = self._call("GET", f"/v1/certificates?limit={limit}&offset={offset}{extra}")
items = page["items"]
yield from items
if len(items) < limit:
return
offset += limit
def certificate(self, cert_id):
return self._call("GET", f"/v1/certificates/{cert_id}")
def renew(self, cert_id, connector_id, csr_pem):
return self._call("POST", f"/v1/certificates/{cert_id}/renew",
{"connector_id": connector_id, "csr": csr_pem})
def request(self, request_id):
return self._call("GET", f"/v1/requests/{request_id}")
api = NextPKI()
print(api.validate()["scopes"])
for c in api.certificates(cert_type="tls_server"):
print(c["common_name"], c["not_after"])
package nextpki
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type Client struct {
Base string
Token string
HTTP *http.Client
}
func New(token string) *Client {
return &Client{
Base: "https://api.nextpki.com",
Token: token,
HTTP: &http.Client{Timeout: 30 * time.Second},
}
}
func (c *Client) call(method, path string, body, out any) error {
var rdr io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return err
}
rdr = bytes.NewReader(b)
}
req, err := http.NewRequest(method, c.Base+path, rdr)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.Token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.HTTP.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
detail, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<12))
return fmt.Errorf("%s %s: %s: %s", method, path, resp.Status, detail)
}
if out == nil {
return nil
}
return json.NewDecoder(resp.Body).Decode(out)
}
type Certificate struct {
ID string `json:"id"`
CommonName *string `json:"common_name"`
IssuerDN string `json:"issuer_dn"`
NotAfter time.Time `json:"not_after"`
IsRevoked bool `json:"is_revoked"`
TrustStatus string `json:"trust_status"`
LastSeenAt time.Time `json:"last_seen_at"`
}
func (c *Client) Certificates(limit, offset int) ([]Certificate, error) {
var page struct{ Items []Certificate `json:"items"` }
err := c.call("GET",
fmt.Sprintf("/v1/certificates?limit=%d&offset=%d", limit, offset), nil, &page)
return page.Items, err
}

Note CommonName *string - the field is nullable, and a plain string silently turns null into "". Same for the nullable IDs on renewal requests.

export class NextPKI {
constructor(
private token: string,
private base = "https://api.nextpki.com",
) {}
private async call<T>(method: string, path: string, body?: unknown): Promise<T> {
const res = await fetch(this.base + path, {
method,
headers: {
Authorization: `Bearer ${this.token}`,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) throw new Error(`${method} ${path}${res.status}: ${await res.text()}`);
return res.json() as Promise<T>;
}
async *certificates(limit = 200): AsyncGenerator<Certificate> {
for (let offset = 0; ; offset += limit) {
const page = await this.call<{ items: Certificate[] }>(
"GET", `/v1/certificates?limit=${limit}&offset=${offset}`);
yield* page.items;
if (page.items.length < limit) return;
}
}
}

Do not reject unknown fields. New fields arrive without a version bump. A strict decoder is the most common way a client breaks.

Treat enums as open sets. Map unknown values to a sensible default rather than throwing. See enumerations.

Retry only 429 and 5xx, with jitter. A 403 will still be a 403. See rate limits.

Respect nullability. common_name, org_id, previous_cert_id, new_cert_id and state_reason are all nullable.

Never log the token. Redact the Authorization header if you log requests.

Page with limit=200. One request beats four.