View a certificate's full details
const url = 'https://example.com/api/certs/example';const options = {method: 'GET', headers: {'X-Ssoossh-Key-Secret': '<X-Ssoossh-Key-Secret>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request GET \ --url https://example.com/api/certs/example \ --header 'X-Ssoossh-Key-Secret: <X-Ssoossh-Key-Secret>'Returns a single certificate by ID if the caller is authorized: the user who approved the underlying request, or someone with auditor-level access. Returns 404 uniformly for “not found” and “not authorized” to not leak existence.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Certificate ID
Responses
Section titled “Responses”Certificate details
object
object
CriticalOptions are the options fixed into the certificate (force-command, source-address). Kept separate from Extensions because sshd treats them differently: it rejects a certificate carrying a critical option it does not understand, where an unknown extension is ignored. Populated by the detail endpoint alone, same as Extensions.
object
Options are the certificate options fixed at approval, already narrowed by server config: what a redemption actually grants, not what the client asked for.
object
The decision’s content and reasoning; see RequestDetailResponse. Populated on the detail endpoint only.
EnrollmentID is the service code this certificate was redeemed from, so the UI can link to it. Present only on a service certificate.
Extensions are the SSH certificate extensions the certificate was signed with (permit-pty, permit-agent-forwarding, …), decoded from the audit row’s JSON column rather than passed through as a string: the browser should not have to parse JSON out of JSON.
Populated by the detail endpoint alone. A list row does not display them, and carrying them on every row of a hundred-row page would be payload nobody reads.
What asked for this certificate, as the requester reported it, taken from the decision record’s snapshot rather than from the request – see model.CertificateRequestDecision. Without these the history could say who approved a certificate and what it granted but not which machine or command it was for, which is the question an incident review actually starts from.
Every one is self-reported by an unauthenticated caller and must be rendered as a claim, exactly as the approval page renders it (see RequestDetailResponse). The one field here the server established itself is DecidedSourceIP above, and that is the approver’s, not the requester’s.
ReportedUsername and ReportedHostname are the “user@host” that asked: the PAM account and machine for a pam or console certificate, the local client’s for a user one. The rest are PAM and console only, and empty on a user certificate, which has no service or terminal to report.
The pair is on a list row too, because it is what a history row leads with: the row’s subject is where the certificate was fetched from, and the approver’s identity is the same value on every row of a person’s own history. So is ReportedService, which a PAM row names (sudo, su, sshd). The remaining six stay detail-only – six more strings on every row is payload nobody reads.
RetrievedAt is when that redemption happened. Distinct from IssuedAt only by the width of the signing round trip, and carried because the retrieval log is timestamped by it — matching a certificate to a line in that log means comparing like with like.
RetrievedSourceIP is the address the service retrieve call came
from — the machine the unattended job actually ran on. Present only
on a service certificate.
It answers a different question from DecidedSourceIP below, which for a service certificate is the approver’s browser at enrollment time and is therefore identical across every certificate the code mints. This one is this certificate’s own origin.
SerialNumber is a decimal string, not a JSON number. Serials are 63
bits of randomness (internal/serial), so all but a vanishing fraction
exceed JavaScript’s Number.MAX_SAFE_INTEGER (2^53-1) and a browser
parsing one as a number silently rounds it – 3260700569889958163
reads back as 3260700569889958400. On an audit record that is a wrong
answer, and an unsearchable one: the serial an operator reads off
ssh-keygen -L never matches what is on screen.
Both tags are needed. json:",string" decides what Go writes;
tstype decides what tygo tells the browser to expect, which it
otherwise infers from the Go type and gets wrong.
Type is what was asked for. Empty when the request row behind the decision is gone: the decisions table is the permanent one by design, so the client must render a row whose type it does not know.
Example
{ "data": { "serial_number": "0", "type": "user" }}Invalid certificate ID
object
Data is always null on an error response.
Error is a human-readable message. Branch on the HTTP status rather than this string; it is for humans and logs.
ErrorCode is a machine-readable error classifier. One of the error_code constants from internal/apitypes; use this to decide whether to retry or branch on the failure mode.
Example
{ "error": "certificate request \"9f1c…\" not found", "error_code": "not_found"}No valid session
object
Data is always null on an error response.
Error is a human-readable message. Branch on the HTTP status rather than this string; it is for humans and logs.
ErrorCode is a machine-readable error classifier. One of the error_code constants from internal/apitypes; use this to decide whether to retry or branch on the failure mode.
Example
{ "error": "certificate request \"9f1c…\" not found", "error_code": "not_found"}Certificate not found or not accessible
object
Data is always null on an error response.
Error is a human-readable message. Branch on the HTTP status rather than this string; it is for humans and logs.
ErrorCode is a machine-readable error classifier. One of the error_code constants from internal/apitypes; use this to decide whether to retry or branch on the failure mode.
Example
{ "error": "certificate request \"9f1c…\" not found", "error_code": "not_found"}