Issue a certificate
First-time issuance
Use an issue job when TokenTimer has no managed certificate for the identity yet and you want the result in inventory, not only on disk. TokenTimer creates the row at provisioning, links the job, and dispatches it to a capable agent.
Every other operation assumes the certificate already exists.
Request contract
- No
subjectType,subjectId, orpayload.certificateId. idempotencyKeyis required (1–128 characters: letters, digits,.,_,:,-). The same body returns the original job. A different body with the same key is HTTP 409. Concurrent retries with the same key are serialized, so both clients receive the same job.payload.certPathmust be an absolute file path. Relative paths and trailing slashes are rejected up front so they cannot burn an ACME order. Omit this field for awindows-iistarget.- No
renewalProfileon the request. A profile is derived if issuance succeeds. See Renew certificates automatically. - Remaining fields match a renew payload:
commandRef,caEndpoint,dnsZone,dnsProvider,target,sans, plus optionalacmeKind,keyRotation,reloadService,verifyHost,verifyPort, andchainPath.
By default the agent writes the full chain (leaf plus intermediates) to certPath. Set chainPath to a second absolute path to split leaf and intermediates. chainPath is not validated up front.
curl -sS -b cookies.txt -X POST \
'https://your-tokentimer.example/api/v1/workspaces/WORKSPACE_ID/certops/jobs' \
-H 'Content-Type: application/json' \
-d '{
"operation": "issue",
"idempotencyKey": "issue-www-example-com-1",
"payload": {
"target": { "type": "domain", "reference": "www.example.com" },
"sans": ["www.example.com"],
"commandRef": "certbot-csr",
"caEndpoint": "https://acme-v02.api.letsencrypt.org/directory",
"dnsZone": "example.com",
"dnsProvider": "cloudflare",
"certPath": "/etc/ssl/tokentimer/www.example.com.pem"
}
}'
Issue jobs are offered only to agents that declare evidence-claim-binding-v1. If none is registered, the job stays pending. Retry a failed issuance with a renew job against the provisioning row; that retry uses the same claim rule.
On success the certificate becomes active, fingerprint and expiry are filled from verify evidence, and a renewal profile is derived. On failure it stays provisioning. Fix the cause and renew, or retire it. A failed issuance does not raise cert_renewal_failed; watch inventory for provisioning rows. See Certificate renewal failures.
Windows IIS target
target.type: "windows-iis" enrolls into a Windows machine store and an IIS site binding. The private key stays inside Windows CNG; it is never written as a file or .pfx. Use this with a Windows agent that declares windows-cert-store-v1 and iis-binding-v1. See Windows/IIS worked example.
target.store: almost always"My"(LocalMachine\My). A different store (for exampleWebHosting) enrolls intoMyfirst (certreqcan only target that store), then mirrors into the requested store and removes theMycopy. Prefer"My"unless IIS requires another store.target.binding:{ "site", "port", "sniHost?" }. ReplacescertPath. Do not sendcertPath,chainPath, orkeyPath.- TokenTimer sets
keyMode: "os-store-managed"from the target type. - Optional
sniHostbindshostnameport=. A non-SNIipport=binding on the same port still wins for clients hitting that address (http.sysrule). The agent reports a non-fatalprecedenceWarningwhen it sees a conflict. - No
iisreset. Only the configured binding changes. The superseded certificate is kept for a grace window so rollback has a target.
{
"operation": "issue",
"idempotencyKey": "issue-iis-example-1",
"payload": {
"target": {
"type": "windows-iis",
"reference": "www.example.com",
"store": "My",
"binding": { "site": "Default Web Site", "port": 443, "sniHost": "www.example.com" }
},
"sans": ["www.example.com"],
"commandRef": "acmesh-signcsr",
"caEndpoint": "https://acme-v02.api.letsencrypt.org/directory",
"dnsZone": "example.com",
"dnsProvider": "cloudflare"
}
}
When a certificate stays at provisioning
Promotion to active needs verify-step evidence bound to the current job claim, with both a fingerprint and an expiry.
| Reason | What happened | What to do |
|---|---|---|
no_claim_bound_verify_evidence | Success reported without claim-bound verify evidence | Use an agent that declares evidence-claim-binding-v1, then renew. Check verifyHost |
verify_evidence_missing_fingerprint | Verify evidence had no SHA-256 fingerprint | renew again; inspect timeline evidence |
verify_evidence_missing_expiry | Verify evidence had no parseable end date | renew again; confirm the deployed file is a certificate |
A provisioning row is not proof that nothing landed on the host. It means TokenTimer does not yet have proof of what is there. renew against that row is the normal fix.
A certificate can also reach active and still not auto-renew, when promotion succeeds but renewal-profile derivation declines. That case sets reconciliationReason to renewal_profile_<reason> (for example renewal_profile_derivation_failed for an incomplete issue payload). CERTOPS_RENEWAL_PROFILE_DERIVATION_DECLINED records the same decline.
The same three guards apply on later renewals of an active certificate. A guard failure sets reconciliationReason and writes CERTOPS_CERTIFICATE_RENEWAL_UNRECONCILED. The renewal scheduler's idempotency key is (certificateId, notAfter), so if notAfter never advances, the next pass can replay the same "successful" renewal against the CA. The dashboard does not surface reconciliationReason as a badge. Read reconciliationReason on GET .../certops/certificates or via that audit event.