Skip to content

The renewal state machine

A renewal is a long-running process, not a request/response call. It has eight states, and the transitions between them are enforced by a database trigger - an invalid jump is rejected outright rather than silently applied. That means the state you read is a state the system actually reached.

pending_approval ──► approved ──► csr_generated ──► submitted ──► issued ──► deployed
│ │ │ │ │
└──► cancelled ├──► failed ├──► failed ├──► failed ├──► failed
└──► cancelled └──► cancelled └──► cancelled└──► cancelled

deployed, failed and cancelled are terminal. Nothing leaves them - not even a retry. A retry is a new renewal row, which means your integration should never expect an ID to come back to life.

Two consequences worth designing around:

  • pending_approval can only become approved or cancelled. It cannot fail, because nothing has been attempted yet.
  • Every non-terminal state after approval can fail or be cancelled, because from approved onwards real work with a real CA is happening.
State What has happened What moves it on
pending_approval The request exists. Nothing has been sent anywhere A human with the approver role approves it
approved Spending is authorised The worker picks it up and prepares the CSR
csr_generated A CSR is ready - either the one you supplied or an ephemeral one from the scheduler The worker submits the order to the CA
submitted The order is with the CA. For ACME this is where the challenge happens The CA issues
issued The certificate exists and is stored in your inventory Deployment confirms
deployed The certificate is in service - terminal
failed Something went wrong; state_reason says what - terminal, open a new renewal
cancelled A person stopped it - terminal

Ordering a certificate from a commercial CA costs money. NextPKI therefore never lets an API token, an automation or an agent cause issuance on its own: the certs:renew scope can create a renewal, and only a person with the approver role can approve one.

This is a deliberate split. The worst a leaked automation token can do is fill your approval queue with noise.

When state is failed, state_reason carries the cause. These are the ones you will actually see:

state_reason Meaning What to do
worker_timeout The renewal sat in a non-terminal state for over an hour and was swept up Usually a stuck CA order. Open a new renewal
Circuit breaker The connector saw 3 failures in 10 minutes and paused for 30 Wait, or check whether the CA is down
Spending cap The connector’s monthly cap would be exceeded Raise the cap in the Console, or use a different connector
Issuer allowlist The issued certificate’s issuer DN was not on the connector’s allowlist Either the CA changed its issuing CA, or something is wrong. Investigate before widening the list
Anomaly detected Renewal rate exceeded five times the connector baseline Deliberate bulk operation? Approve again. Otherwise investigate
CA-specific errors Validation failure, rate limit, bad CSR Read the message; most are actionable

The circuit breaker, spending caps and anomaly detection are protections against a runaway loop spending your money. If you are doing a legitimate bulk migration, raise the caps first rather than fighting the breaker.

A certificate flagged for auto-renewal does not need you to create anything. A scheduler opens a renewal itself once the certificate enters a 30-day window before expiry, using an ephemeral server-side CSR.

Approval is still required. Auto-renewal removes the “remember to do it” step, not the authorisation step.

If you want to supply your own CSR - because the private key must be generated on the serving host - do not rely on the scheduler. Create the renewal yourself through the API with your CSR.

Terminal window
while :; do
state=$(curl -sS "https://api.nextpki.com/v1/requests/$ID" \
-H "Authorization: Bearer $NEXTPKI_TOKEN" | jq -r .state)
echo "$state"
case "$state" in
deployed|failed|cancelled) break ;;
esac
sleep 15
done

Break on the terminal states, not on the one you hope for - otherwise a failed renewal loops forever. Fifteen seconds is reasonable while you are actively waiting; do not poll every renewal in your estate on that interval, and see rate limits.