The attestation
The signed object every verify-family endpoint returns, field by field.
The attestation is the signed statement Attestra returns from /v1/status,
/v1/verify and /v1/verify-quorum. It asserts that at a stated time,
Attestra read a stated fact from a stated chain.
The object
{
"attestation": {
"DID": "did:eid:4tARKz6hK5GuWr9fkdzsG1Bf9X3b3agzh7RPBdjdpLWapHRh",
"CredentialID": "0xfb898d4079eed14603bbca2a245278b45af1d2acad278c6c690e067bc9eaae2c",
"CredType": "KYCVerified",
"Issuer": "0xe81fa0ecbf9c77cd095fbeb4687d78cbd98bd51092a2cc12d4276e1ade4a8e78",
"Status": "valid",
"SourceChain": "eid",
"IssuedAt": 1751360400,
"ExpiresAt": 1751360700,
"Nonce": [0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255, 0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255]
},
"signature": "0xf4793f2e134d1dcc4884967bba64ef22433cbb63006c4c1aeefc580e151f82ec3c266201648bbf018c32136f7d2c44ab4b666c54e20f22d4cc075ce66d2d2c8e00"
}Why the keys are PascalCase
This will look like a mistake the first time you see it. It is not: the Go
struct behind attestation carries no JSON tags, so encoding/json
marshals it with the field names exactly as declared in the source,
PascalCase and all. Nonce is a fixed [32]byte array, and Go renders an
array of bytes as 32 numbers, not as base64 (that encoding applies only to
byte slices) and not as hex.
The contrast is deliberate. The /v1/attestation read-back endpoint returns
a different, tagged type, so its keys are lowercase (did, credentialId,
nonce, and so on) and its nonce is a hex string, not a number array. A
client that calls both /v1/verify (or /v1/status) and /v1/attestation
needs to handle both shapes; one is not a typo of the other.
Fields
| Field | Type | What it is |
|---|---|---|
DID | string | The subject. Empty on /v1/status, which names no subject. |
CredentialID | string | The source chain's own key for the credential. On eIDChain, the claim hash. |
CredType | string | Schema or type identifier. Provenance differs by endpoint, see below. |
Issuer | string | The authoritative on-chain issuer. |
Status | string | valid or revoked. |
SourceChain | string | The chain the fact was read from. |
IssuedAt | number | Unix seconds. |
ExpiresAt | number | Unix seconds. |
Nonce | number[32] | 32 bytes, one number per byte. |
CredType is not the same fact on every endpoint
On /v1/verify, CredType is the CType hash, 0x-hex, read from chain. On
/v1/status, CredType is whatever the caller passed in ?type=, echoed
straight into the field and signed unvalidated; it is empty if the parameter
was omitted.
State the consequence plainly: on a /v1/status response, CredType is an
assertion by the caller, not a fact the oracle checked. It sits inside the
signature, which makes it look authoritative, and it is not.
The binding invariant
At least one of DID or CredentialID must identify the subject. Without
one, the signed statement is unbound: it asserts that some credential is
valid without saying which, so any holder could replay another holder's
attestation. This is why /v1/verify, which names the subject, is preferred
over /v1/status, which does not, whenever the holder matters.
Freshness
IssuedAt, ExpiresAt and Nonce are all inside the signed digest, so an
attestation cannot be re-dated and its nonce cannot be swapped after the
fact. Consumers should check ExpiresAt themselves: nothing revokes an
attestation early, and Attestra does not track which attestations have
already been consumed.
Verifying it yourself
The signature is over a keccak256 digest of these exact nine fields in this exact order, and you can check it yourself without calling Attestra. See The canonical digest for the byte layout, and Verifying an attestation for a worked example.