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.
Generate from the spec
Section titled “Generate from the spec”The OpenAPI document is 3.1 and is what the service is built against. Point a generator at it:
curl -sSO https://docs.nextpki.com/openapi/nextpki-v1.yaml
# TypeScript types only - no runtime dependencynpx openapi-typescript nextpki-v1.yaml -o nextpki.d.ts
# Full client, pick your languagenpx @openapitools/openapi-generator-cli generate \ -i nextpki-v1.yaml -g python -o ./nextpki-pythonopenapi-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.
Or write it yourself
Section titled “Or write it yourself”Python
Section titled “Python”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.
TypeScript
Section titled “TypeScript”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; } }}Whatever you write, get these right
Section titled “Whatever you write, get these right”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.