Verify
Verify a W3C Verifiable Presentation end to end and receive an attestation that names its subject.
POST /v1/verify
This is the headline endpoint of the API. The body is a W3C Verifiable Presentation. The oracle checks it end to end and, if it holds up, signs and returns an attestation naming the credential and the holder who presented it.
How this differs from Status
Status takes a bare credential hash: the
caller supplies only credentialId, and the resulting attestation has an
empty DID, because there is no subject in the request for the oracle to
name.
Verify is different because the presentation itself carries everything
needed: the subject DID (as the id of the credentialSubject) and the
credential hash (inside credentialStatus.id). Nothing about the subject is
supplied out of band, so the resulting attestation both names the subject and
is bound to it. It cannot be replayed by a different holder claiming the same
credential, because it states who presented it, not merely that the
credential exists and is valid.
How dispatch works
The oracle reads credentialStatus.type out of the presented credential and
routes the presentation to whichever verifier is registered for that type.
The type is a property the credential itself declares, not something
Attestra's code assumes about the chain it came from: the oracle is told how
to check a given presentation, it does not guess.
Which types are registered today
Exactly one verifier is registered in this deployment: EidChainRevocation2024,
for eIDChain credentials. A presentation whose credentialStatus.type
is anything else has no verifier to dispatch to and the request fails with
422 and the error no verifier registered for credentialStatus.type.
The dispatch mechanism itself is chain-agnostic by design: adding a chain
means writing one adapter that registers under the type it understands,
nothing about /v1/verify itself changes. What is chain-specific is which
adapters this deployment has actually registered, and today that is one.
A presentation built from an Ethereum or Hyperledger Fabric
credential does not verify against this endpoint; there is no registered
verifier for it.
What is actually checked
For a presentation the oracle can dispatch, the SDK performs the full W3C
check before anything is signed: it confirms the holder's binding to the
presentation, recomputes the claim hash and compares it against what the
credential declares, checks the credential has not expired, and reads the
attestation's status from the source chain. An attestation is only built
after all four checks pass; any failure returns 422 rather than a signed
result.
Where each attestation field comes from
The field mapping is easy to get wrong by assuming it mirrors the credential's own claims. It does not, for two fields in particular:
| Attestation field | Where it comes from |
|---|---|
DID | the id of the first credentialSubject in the presentation |
CredentialID | the claim hash, parsed out of credentialStatus.id (urn:eidchain:attestation:0x<claimHash>) |
CredType | the CType hash, 0x-hex, read from chain |
Issuer | the attester, 0x-hex, read from chain |
SourceChain | the method the adapter is registered under, from config |
Status | derived from the on-chain revocation flag |
Issuer and Status are deliberately not taken from the credential's own
issuer field or its self-declared status: both are read from the chain, so
a credential cannot assert its own issuer or its own validity into a
signature that is supposed to be independent of it.
CredType is also worth a second look: on this endpoint it is the CType hash
as 0x-hex, not a human-readable name like KYCVerified. (That string
appears elsewhere in this documentation as a synthetic test value; it is not
what /v1/verify produces.)
The optional freshness fields
nonce, issuedAt and expiresAt can ride alongside the presentation in
the same JSON body, as sibling top-level keys next to the VP fields. They
must arrive all three together or not at all: a partial set is refused
rather than half-filled from the node's own clock.
When supplied, each is bounded:
nonceis exactly 32 bytes, hex-encoded, and must not be all zero.issuedAtmust sit within 300 seconds of the node's own clock.expiresAtmust be strictly greater thanissuedAt.- The window between them,
expiresAt - issuedAt, is at most 900 seconds.
Omit all three and the node generates them itself: issuedAt is now,
expiresAt is five minutes later, and nonce is freshly generated.
Why you would send them
Two reasons. First, if you intend to call
Verify quorum instead of a single
node, every peer needs to sign over the same digest for the signatures to be
countable together; supplying nonce, issuedAt and expiresAt yourself is
what makes that possible. Second, the nonce is inside the signed digest, so
if you generated it as a challenge, you can confirm the attestation you get
back was minted against that exact challenge rather than a different one
swapped in along the way.
That second point has a real limit, worth stating precisely rather than implying more than is true. The nonce you supply is stamped into the attestation and covered by the signature, which is enough to prove the attestation was minted for your request and cannot be re-dated or replayed under a different challenge. It does not yet prove the holder answered a fresh challenge from you: the presentation's own challenge is not currently checked against the nonce you supplied. Treat the nonce as binding the attestation to your request, not as evidence of a completed challenge-response with the holder.
Request
Request bodies are capped at 1 MiB.
curl -X POST https://<host>/v1/verify \
-H "Authorization: Bearer idc_live_..." \
-H "Content-Type: application/json" \
-d @presentation.jsonResponse
{
"attestation": {
"DID": "did:eid:4tARKz6hK5GuWr9fkdzsG1Bf9X3b3agzh7RPBdjdpLWapHRh",
"CredentialID": "0xfb898d4079eed14603bbca2a245278b45af1d2acad278c6c690e067bc9eaae2c",
"CredType": "0x...",
"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"
}The attestation object has no JSON tags on the source struct, so its keys
are the exact Go field names in PascalCase, and Nonce, a fixed 32-byte
array, is rendered as 32 numbers, not base64 and not hex. See The
attestation for the shape in full.
Status codes
| Code | Meaning |
|---|---|
| 200 | The presentation verified; the body is a signed attestation naming the subject. |
| 400 | Malformed request body, or the supplied freshness values were invalid. |
| 422 | Verification failed: bad holder signature, tampered claim, revoked credential, or an unrecognised credentialStatus.type. |
| 500 | Signing the attestation failed. |