Skip to content

Build Your Own Implementation

This page is for developers who want to build a Universal Manifest implementation in any programming language. The specification, conformance requirements, and test fixtures are all language-neutral. You do not need TypeScript, Node.js, or any specific runtime.

For an end-to-end conformance flow (decision tree, issuer/verifier path, and report submission), use the Implementation Guide and keep the Quick Reference open while implementing. If you are upgrading existing deployments, follow the Migration Guide: v0.1 to v0.2.

A Universal Manifest implementation is a module that can:

  1. Parse a JSON-LD manifest document
  2. Validate its structure against required fields
  3. Enforce the validity window (TTL)
  4. Handle unknown fields safely (ignore, do not reject)
  5. (v0.2) Canonicalize using JCS and verify an Ed25519 signature

There are two conformance targets:

  • Consumer (Verifier) — receives manifests and uses them to drive a surface (display, app, admin panel, etc.)
  • Issuer (Signer) — produces manifests for a subject and delivers them to consumers

Most implementations start as consumers. You can add issuer capability later.


This is the minimum viable implementation for a conformant v0.1 consumer.

Accept a JSON string or object. Universal Manifests are valid JSON-LD, but you do not need a JSON-LD processor — a standard JSON parser is sufficient.

Confirm all seven required fields are present and non-empty:

FieldTypeDescription
@contextstringMust be a valid JSON-LD context URL
@typestring or arrayMust include "UniversalManifest" (check both string and array forms)
@idstring (URI)The UMID — a globally unique manifest identifier
manifestVersionstringThe spec version (e.g., "0.1.0")
subjectstring (URI)Who or what the manifest is about
issuedAtstring (ISO 8601)When the manifest was created
expiresAtstring (ISO 8601)When the manifest expires
  • Parse issuedAt and expiresAt as ISO 8601 / RFC 3339 date-time strings (with timezone).
  • MUST reject if either timestamp is not a valid ISO 8601 date-time.
  • RECOMMENDED: reject if issuedAt > expiresAt.
  • MUST reject for use if now > expiresAt. An expired manifest must not be used to grant permissions or render state.
  • You MAY retain expired manifests for debugging, but you MUST treat them as expired.
  • SHOULD sanity-check that issuedAt is not unreasonably far in the future (clock skew protection).
  • MUST ignore unknown top-level properties.
  • MUST ignore unknown shard entity shapes.
  • MUST ignore unknown items in claims, consents, devices, and pointers.

This is the forward-compatibility rule. It ensures older implementations do not break when new fields are added in future versions.

Run your implementation against the official fixture set. Your implementation MUST accept all valid fixtures and reject all invalid fixtures.

Valid fixtures (must accept):

Invalid fixtures (must reject):


v0.2 implementation checklist (consumer/verifier)

Section titled “v0.2 implementation checklist (consumer/verifier)”

v0.2 adds cryptographic integrity verification on top of v0.1. A v0.2 consumer must do everything a v0.1 consumer does, plus signature verification.

  • Confirm signature.algorithm === "Ed25519"
  • Confirm signature.canonicalization === "JCS-RFC8785"
  • MUST reject unsupported profile pairs when strict v0.2 verification is required.
  1. Remove the signature property from the manifest object.
  2. Canonicalize the remaining object using JCS (RFC 8785) — JSON Canonicalization Scheme. JCS produces a deterministic byte-level representation of any JSON value. Libraries are available for most languages (see the resources section below).
  3. The canonicalized bytes are the signing input.
  1. Obtain the public key from signature.publicKeySpkiB64 (base64-encoded SPKI format) or by resolving signature.keyRef.
  2. Decode the signature.value from base64url.
  3. Verify the Ed25519 signature over the canonical bytes using the public key.
  4. MUST reject the manifest if verification fails. You may retain it for debugging.

Valid fixtures (must accept):

Invalid fixtures (must reject):


If your system needs to produce manifests (not just consume them), implement the following:

  1. Set @id to a globally unique URI. Recommended: urn:uuid:<uuidv4>.
  2. Set subject to a stable identifier URI. Recommended: a DID, but any URI works.
  3. Set @context to the published JSON-LD context URL.
  4. Set @type to "UniversalManifest" (string) or include "UniversalManifest" in an array.
  5. Use short TTLs appropriate to your use case (hours or days, not months).
  6. Populate shards, pointers, claims, consents, and devices as needed for your domain.

v0.2 issuer requirements (adding signatures)

Section titled “v0.2 issuer requirements (adding signatures)”
  1. Produce a manifest with manifestVersion: "0.2" and all required fields.
  2. Remove the signature property from the object (if present from a previous version).
  3. Canonicalize using JCS (RFC 8785).
  4. Sign with Ed25519.
  5. Embed the signature object with: algorithm, canonicalization, keyRef or publicKeySpkiB64, created, and value.

These are the language-neutral artifacts you need for any implementation:

All fixtures are available as standalone JSON-LD files that you can download and use directly in your test suite:

  • Valid v0.1 fixtures: examples/v0.1/ and examples/v0.1/stubs/
  • Invalid v0.1 fixtures: examples/v0.1/invalid/
  • Valid v0.2 fixtures: examples/v0.2/
  • Invalid v0.2 fixtures: examples/v0.2/invalid/

These are also served on the site under /harness/fixtures/ — see the fixture links in the checklists above.


These are the external capabilities your implementation needs, by version:

  • JSON parser — built into every modern language
  • ISO 8601 date-time parser — most standard libraries include this
  • UUID generator (issuer only) — for generating @id values
  • JCS (RFC 8785) canonicalization — deterministic JSON serialization. Available as libraries in many languages:
    • Python: json-canonicalization
    • Go: github.com/nicholasgrigoriadis/jcs or github.com/nicholasgrigoriadis/universal-manifest
    • Rust: json-canonicalization
    • Java: org.erdtman:jcs
    • JavaScript: canonicalize (npm)
    • C#: various implementations available
  • Ed25519 signature library — widely available:
    • Python: cryptography or pynacl
    • Go: crypto/ed25519 (stdlib)
    • Rust: ed25519-dalek
    • Java: java.security (JDK 15+) or Bouncy Castle
    • C#: NSec or libsodium-net

The TypeScript helper (universal-manifest npm package) is a working reference you can study, but it is not required. It demonstrates one way to implement the validation logic. The full source is a single file that you can read to understand the approach.

When building your own implementation, use the TypeScript helper as a reference for behavior, not as a dependency. Your implementation should pass the same conformance fixtures independently.


Here is a language-neutral pseudocode sketch for a minimal v0.1 consumer:

function validateManifestV01(json):
// Step 1: Parse
manifest = parseJson(json)
if manifest is not an object:
reject("not a JSON object")
// Step 2: Required fields
for field in ["@context", "@type", "@id", "manifestVersion", "subject", "issuedAt", "expiresAt"]:
if field not in manifest or manifest[field] is empty:
reject("missing required field: " + field)
// Step 3: Type check
type = manifest["@type"]
if type is a string:
if type != "UniversalManifest":
reject("@type must include UniversalManifest")
else if type is an array:
if "UniversalManifest" not in type:
reject("@type must include UniversalManifest")
else:
reject("@type must be string or array")
// Step 4: Timestamp validation
issuedAt = parseIso8601(manifest["issuedAt"])
expiresAt = parseIso8601(manifest["expiresAt"])
if issuedAt or expiresAt failed to parse:
reject("invalid timestamp format")
if issuedAt > expiresAt:
reject("issuedAt must not be after expiresAt")
// Step 5: TTL enforcement
if now() > expiresAt:
reject("manifest has expired")
// Step 6: Unknown fields -- do nothing, just ignore them
return manifest // valid

  1. Start with v0.1. Get the basic parse-validate-TTL flow working first. Add v0.2 signature verification later.
  2. Use the fixtures as your test suite. The conformance fixtures are the ground truth. If your implementation accepts all valid fixtures and rejects all invalid ones, it is conformant.
  3. Do not hardcode field lists. The unknown-field tolerance rule means your parser must not fail on unexpected properties. Use allowlists for the fields you read, and ignore everything else.
  4. Test the @type array form. Some manifests use "@type": "UniversalManifest" (string) and others use "@type": ["UniversalManifest"] (array). Your implementation must handle both.
  5. Keep TTL enforcement strict. Expired manifests must be rejected for use. This is a MUST requirement, not a recommendation.