Attestra
Endpoints

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 fieldWhere it comes from
DIDthe id of the first credentialSubject in the presentation
CredentialIDthe claim hash, parsed out of credentialStatus.id (urn:eidchain:attestation:0x<claimHash>)
CredTypethe CType hash, 0x-hex, read from chain
Issuerthe attester, 0x-hex, read from chain
SourceChainthe method the adapter is registered under, from config
Statusderived 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:

  • nonce is exactly 32 bytes, hex-encoded, and must not be all zero.
  • issuedAt must sit within 300 seconds of the node's own clock.
  • expiresAt must be strictly greater than issuedAt.
  • 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.json

Response

{
  "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

CodeMeaning
200The presentation verified; the body is a signed attestation naming the subject.
400Malformed request body, or the supplied freshness values were invalid.
422Verification failed: bad holder signature, tampered claim, revoked credential, or an unrecognised credentialStatus.type.
500Signing the attestation failed.

On this page