Skip to content

Tutorial: your first renewal

This walks the whole path once, end to end, so the pieces connect. Roughly fifteen minutes. Where the API cannot take you further, it says so rather than pretending.

You need a NextPKI tenant, and someone with the approver role - possibly you.

Console, Settings → API tokens. Create one with both certs:read and certs:renew. Copy it now; it is stored as a hash and cannot be shown again.

Terminal window
export NEXTPKI_TOKEN='npapi.…'
export NEXTPKI_BASE='https://api.nextpki.com' # api.nextpki.de also serves EU

Confirm it works and see what it carries:

Terminal window
curl -sS "$NEXTPKI_BASE/v1/auth/validate" \
-H "Authorization: Bearer $NEXTPKI_TOKEN" | jq
{
"valid": true,
"tenant_id": "11111111-1111-1111-1111-111111111111",
"name": "tutorial",
"scopes": ["certs:read", "certs:renew"]
}

If scopes lacks certs:renew, stop here and make a new token. Scopes are fixed at creation - there is no way to add one later.

Terminal window
curl -sS "$NEXTPKI_BASE/v1/certificates?limit=200" \
-H "Authorization: Bearer $NEXTPKI_TOKEN" \
| jq -r '.items[]
| select(.is_revoked == false)
| [.id, .common_name, .not_after, .issuer_dn] | @tsv' \
| sort -t$'\t' -k3 | head

Sorted by expiry, soonest first. Pick one you are willing to experiment with:

Terminal window
export CERT_ID=''

Look at it properly before renewing:

Terminal window
curl -sS "$NEXTPKI_BASE/v1/certificates/$CERT_ID" \
-H "Authorization: Bearer $NEXTPKI_TOKEN" \
| jq '{common_name, subject_dn, san_dns, not_after, trust_status, last_seen_at}'

last_seen_at is the field worth a second look. If it is weeks old, nothing is serving this certificate and renewing it is busywork - pick another. That distinction is the whole reason observations exist.

Generate these where the certificate will serve. In a tutorial that is your laptop; in production it is the web server.

Terminal window
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
-nodes -keyout tutorial.key -out tutorial.csr \
-subj "/CN=www.example.com" \
-addext "subjectAltName=DNS:www.example.com,DNS:example.com"
chmod 600 tutorial.key

Two things worth noticing. The SANs matter and the Common Name does not - clients have checked SAN for years, and a certificate with only a CN is rejected. And -nodes means the key is unencrypted, which is what a web server needs at boot; protect it with file permissions, not a passphrase you would have to type after every reboot.

Check what you built:

Terminal window
openssl req -in tutorial.csr -noout -text | grep -A2 'Subject Alternative Name'

Renewal orders go through a connector - your account at a CA. Connectors are configured in the Console under Settings → Connectors, and there is no connector API in v1, so take the ID from there:

Terminal window
export CONNECTOR_ID=''
Terminal window
curl -sS -X POST "$NEXTPKI_BASE/v1/certificates/$CERT_ID/renew" \
-H "Authorization: Bearer $NEXTPKI_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg csr "$(cat tutorial.csr)" --arg c "$CONNECTOR_ID" \
'{connector_id: $c, csr: $csr}')" | jq
{ "id": "88880000-0000-0000-0000-000000000001", "state": "pending_approval" }
Terminal window
export RENEWAL_ID='88880000-0000-0000-0000-000000000001'

Note what did not happen: no CA was contacted and no money was spent. The renewal is sitting in a queue.

Terminal window
curl -sS "$NEXTPKI_BASE/v1/requests/$RENEWAL_ID" \
-H "Authorization: Bearer $NEXTPKI_TOKEN" | jq '{state, state_reason}'

It will stay pending_approval until someone with the approver role approves it in the Console, under Renewals.

There is no API call for this and no scope that grants it. That is deliberate: it means a leaked automation token cannot spend your money. See roles and permissions.

Terminal window
while :; do
read -r state reason < <(curl -sS "$NEXTPKI_BASE/v1/requests/$RENEWAL_ID" \
-H "Authorization: Bearer $NEXTPKI_TOKEN" \
| jq -r '[.state, (.state_reason // "-")] | @tsv')
printf '%-18s %s\n' "$state" "$reason"
case "$state" in
deployed|failed|cancelled) break ;;
issued) echo "issued - see step 8"; break ;;
esac
sleep 15
done

You should see approved, csr_generated, submitted, issued. Break on all terminal states, not just the happy one, or a failed renewal loops forever.

If it fails, state_reason says why - failure modes covers the ones you will actually hit.

Terminal window
curl -sS "$NEXTPKI_BASE/v1/requests/$RENEWAL_ID" \
-H "Authorization: Bearer $NEXTPKI_TOKEN" | jq '{state, new_cert_id}'

new_cert_id identifies the certificate that now exists in your inventory. You can read its metadata:

Terminal window
NEW_ID=$(curl -sS "$NEXTPKI_BASE/v1/requests/$RENEWAL_ID" \
-H "Authorization: Bearer $NEXTPKI_TOKEN" | jq -r .new_cert_id)
curl -sS "$NEXTPKI_BASE/v1/certificates/$NEW_ID" \
-H "Authorization: Bearer $NEXTPKI_TOKEN" \
| jq '{common_name, not_before, not_after, fingerprint_sha256}'

And that is as far as v1 goes. There is no endpoint that returns the certificate bytes, so you cannot install it from here. Download it from the Console, or fetch it from your CA. Deploy a renewed certificate covers both, and the ordering that avoids an outage.

  • Scopes are fixed at creation, so get them right the first time.
  • last_seen_at decides whether a certificate is worth renewing at all.
  • The private key never leaves the host, because only the CSR is ever sent.
  • Approval is a human step by design, not an oversight.
  • Terminal states are terminal; a retry is a new renewal.
  • The loop does not close in v1 - the certificate has to come from somewhere else.