Credentials
A verifiable credential is a signed statement, such as “this person holds this diploma”, made by an issuer that is identified by a DID. POST /v1/credentials/verify checks that the credential is well formed, that the issuer’s key signed it, and that it is within its dates and not revoked.
DID.is acts as a verification service. You receive the credential yourself and send it to DID.is to check. DID.is does not decide whether the issuer is an authority for what the credential says. For SD-JWT VC credentials from EU Digital Identity Wallets, see EU wallet credentials.
- Structure and data model
- Issuer DID resolves
- Signature from an issuer key
- Validity dates
- Revocation status
What is checked
Section titled “What is checked”- Structure. The data model (W3C VC 2.0 or 1.1), the required members, and that a proof is present.
- Issuer. The issuer is a DID and it resolves.
- Signature. The proof verifies with a key from the issuer DID’s
assertionMethod. - Dates.
validFrom/validUntil, ornbf/expfor JWTs, include the current time. - Revocation. Status list entries: the list credential is fetched and verified (same issuer, matching purpose, unexpired), then the bit is read.
- Audience. For a JWT,
audmust matchexpectedAudiencewhen you supply one.
What is not checked
Section titled “What is not checked”- Issuer authority. A valid proof shows the issuer’s key signed the credential. It does not show the issuer is entitled to make these claims.
- Truth of the claims. DID.is checks signatures, not the facts in
credentialSubject. - RDF-canonicalized and selective-disclosure Data Integrity suites. These are reported as
UNSUPPORTED, never as valid.
Every response repeats these limits in its limitations array.
Request
Section titled “Request”{ "credential": { "…": "JSON-LD credential object, or a compact JWT string" }, "expectedAudience": "optional, checked against a JWT aud"}The endpoint uses the intensive budget (10 requests per minute). See Errors and rate limits.
curl -X POST https://did.is/api/v1/credentials/verify \ -H 'content-type: application/json' \ -d '{"credential":{"@context":["https://www.w3.org/ns/credentials/v2"],"type":["VerifiableCredential"],"issuer":"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK","validFrom":"2026-01-01T00:00:00Z","credentialSubject":{"id":"did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLdmQR63zLFKfvUwi3"}}}'import { DidisClient } from "@didis/client";
const didis = new DidisClient();const v = await didis.verifyCredential(credential);console.log(v.status, v.headline);for (const c of v.checks) console.log(c.id, c.status, c.detail);from didis import DidisClient
with DidisClient() as didis: v = didis.verify_credential(credential) print(v["status"], v["headline"]) for c in v["checks"]: print(c["id"], c["status"], c["detail"])#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let credential = serde_json::json!({ "@context": ["https://www.w3.org/ns/credentials/v2"], "type": ["VerifiableCredential"], "issuer": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", "validFrom": "2026-01-01T00:00:00Z", "credentialSubject": { "id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLdmQR63zLFKfvUwi3" } }); let r: serde_json::Value = reqwest::Client::new() .post("https://did.is/api/v1/credentials/verify") .json(&serde_json::json!({ "credential": credential })) .send() .await? .error_for_status()? .json() .await?; println!("{} {}", r["status"], r["headline"]); for c in r["checks"].as_array().into_iter().flatten() { println!("{} {} {}", c["id"], c["status"], c["detail"]); } Ok(())}To check the audience of a JWT credential, pass { expectedAudience } in TypeScript or expected_audience= in Python.
The credential in the curl example has no proof, so the live response is:
{ "status": "MALFORMED", "headline": "Not a well-formed verifiable credential: the credential carries no proof.", "format": "UNKNOWN", "dataModel": "2.0", "issuer": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", "subject": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLdmQR63zLFKfvUwi3", "types": ["VerifiableCredential"], "checks": [ { "id": "structure", "label": "Credential structure", "status": "FAIL", "detail": "the credential carries no proof" } ], "statusList": [], "claims": { "id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLdmQR63zLFKfvUwi3" }, "limitations": [ "A valid proof shows the credential was signed by a key the issuer DID authorises; it does not show the issuer is authoritative for these claims.", "Linked Data proofs requiring RDF canonicalization are reported as UNSUPPORTED." ], "valid": false, "errors": ["the credential carries no proof"]}(Abridged: the response also echoes the credential.)
Reading the result
Section titled “Reading the result”Overall status
Section titled “Overall status”The overall status is the most severe outcome, in this order:
MALFORMED › INVALID › UNSUPPORTED › REVOKED › SUSPENDED › EXPIRED › NOT_YET_VALID › INDETERMINATE › VALID
status |
What it means | What to do |
|---|---|---|
VALID |
Structure, issuer, signature, dates and status all passed. | Apply your own policy on the issuer. |
MALFORMED |
Not a well-formed credential, for example no proof. | Reject it. |
INVALID |
The signature or another check failed. | Reject it. The failing check gives the reason. |
UNSUPPORTED |
A proof suite or format DID.is does not verify. | DID.is draws no conclusion. |
REVOKED, SUSPENDED |
The issuer’s status list marks it. | Reject it. |
EXPIRED, NOT_YET_VALID |
Outside its validity dates. | Reject, or ask the holder for a current credential. |
INDETERMINATE |
A check could not be completed, for example the status list was unreachable. | Do not read it as valid. Retry later. |
Treat only VALID as accepted.
Credential checks
Section titled “Credential checks”id |
Checks |
|---|---|
structure |
Data model, required members, a proof is present. |
issuer |
The issuer is a DID and resolves. |
proof |
The signature verifies with a key from the issuer DID’s assertionMethod. |
validity |
The validity window (validFrom/validUntil, or nbf/exp) includes now. |
status |
Status list entries: the list credential is fetched, verified (same issuer, matching purpose, unexpired) and the bit is read. |
jwt.aud |
A JWT audience matches expectedAudience. |
Checks after a fatal failure are not run, so the list can be shorter than this table.
Supported formats
Section titled “Supported formats”- W3C VC Data Model 2.0 and 1.1.
- Data Integrity:
eddsa-jcs-2022,ecdsa-jcs-2019(P-256). - VC-JOSE (
vc+jwt) and VC-JWT 1.1 with EdDSA, ES256 or ES256K. - Bitstring Status List v1.0 and StatusList2021 entries.
- SD-JWT VC (
dc+sd-jwt): see EU wallet credentials.
RDF-canonicalized suites and selective-disclosure suites are reported as UNSUPPORTED. See Status lists for the status rules.
Next steps
Section titled “Next steps”- Paste a credential into did.is/verify to see the same checks in the browser.
- To accept a credential only if its issuer also meets rules (method, key type, domain binding), pass it to
/v1/policies/evaluate.