Try it

Menu

Check a certificate

A certificate is Campaign’s signed record of one person’s verified outcome, checkable by anyone once published.

3 things you can check

The signed record

It proves
Campaign stands behind it today, under a key it publishes.
You still trust
Campaign: its servers and its key.

The fingerprint

It proves
The certificate’s exact text, to the letter.
You still trust
Whoever served you the text, until the public record matches it.

The public record

It proves
That fingerprint was added on its date, and whether it is revoked.
You still trust
Campaign’s key that allowed it, and Campaign to send a revocation.
Its Document number, no API key
GET https://campaign.udao.org/api/public/credentials/{document number}/verify
GET https://campaign.udao.org
/api/public/credentials/{document number}/verify

All 5 readsCheck it yourself

What the fingerprint covers

The fingerprint is made from the certificate’s exact text: a short document written once, when the certificate is issued, and served as it was written. That makes the text tamper-evident: change one letter and the fingerprint is another.

It covers

  • Who issued it: the organization’s name, its address on Campaign and its verified domain
  • What for: the campaign’s title, and the certificate’s title
  • When: the day of the outcome, and the day it was issued
  • How the outcome was checked, and whether it came from a trial
  • Its record number, which the public record knows it by, and a random part kept only in the text

It never covers

  • The holder’s name, which they change at any time
  • Any amount, or the reward
  • The organization’s standing with Udao
  • A revocation, which is written beside it
  • Its document number, or an id of its holder or its campaign

It is keccak-256, the recipe that makes the fingerprint, over the UTF-8 bytes of the document in RFC 8785 canonical JSON. keccak-256 is not SHA3-256, and their values differ.

The sample below is an earlier version. It covers its document number and an id Campaign gives its holder, in place of its record number and the random part.

SampleThe exact text, 589 bytes as served

{"achieved_at":"2026-09-26T05:32:11.993Z","campaign_id":"c51963e6-12cb-40d4-b09f-838a9913b642","evidence_type":"imported","id":"80757f30-0e4f-5db4-ab92-9d5245dfb9fd","issued_at":"2026-09-26T06:14:12.998Z","issuer":{"kind":"campaign","org_name":"Campaign end to end, operator mode","org_slug":"e2e-amoy","recorded_by":"Campaign"},"recipient":{"credential_id":"80757f30-0e4f-5db4-ab92-9d5245dfb9fd","identity_id":"e205c396-e3c3-48ac-975d-751db5fedfcf"},"schema":"campaign.credential","subject":"Amoy end to end 2026-09-26T05:30:20.064Z","title":"Certificate","version":2,"watermarked":false}

The public record

The public record is a register: a contract, which is a program published on a network and run by every copy of it. Its 2 registers are listed below. They run on Polygon, a public network, and until launch on Amoy, Polygon’s test network.

CredentialRegistry, the first test register

Certificates were added here from 11 September 2026 to 4 October 2026. They stay here, and a revocation of any of them is written here.

Its address
0x02Ffd60f83Fe9953ed197DFDACE9D66Fd5296442Polygon Amoy, chain id 80002 (the network’s own number)Read its source on amoy.polygonscan.com
Campaign’s issuerId
0x40c398…4c2f

CredentialRegistryV2, the register that launches

New certificates are added here, from 4 October 2026.

Its address
0x0fFd0a5681B285cd1645bF6642B498DF171fE3C7Polygon Amoy, chain id 80002 (the network’s own number)Read its source on amoy.polygonscan.com
Campaign’s issuerId
0x40c398…4c2f

Both registers’ rules

  • ERC-721, every move refusedThe standard for numbered entries, every transfer and approval refused: no entry can be sold or handed on. The first test register gives wallets no ERC-5192 signal of it; the register that launches does.
  • No burnNothing can delete an entry on either register, Campaign included.
  • No upgradeEach keeps the rules it was published with, for as long as it runs.

The register that launches: its rules and reads

  • It draws each entry’s picturetokenURI answers a document the register draws from the entry alone. It holds a name, a description, a picture of the certificate and its attributes. The attributes are the fingerprint, the day added, and once revoked its status and the day the revocation reached the record. No link, no name, no title.
  • A revocation is marked on the entryrevoke takes no words. The picture gains the revoked stamp with the register’s own UTC day, and the name leads with Revoked. MetadataUpdate (ERC-4906) is written once, so an app that reads the entry again shows it.
  • No entry can be sold or handed onERC-5192’s mark is set on every entry, and says nothing of revocation. reissue stays the namespace admin’s move to another address, which Campaign never makes.
  • isValid and recordStatusisValid(entry) is true while the entry is not revoked. recordStatus(record number, fingerprint) answers NotOnRecord, OnRecord, Revoked or DoesNotMatch, which wins over Revoked, with the entry, the issuer id and both dates.
  • documentURI and contractURIdocumentURI(entry) is the address of its exact text, built from a base written once for Campaign’s namespace. contractURI is the collection’s own document, fixed, with no link.
  • Dates in UTCEvery date it holds or draws is a block’s time, shown in UTC. A revocation’s day is the day it reached the register, which can fall after the organization’s decision day when the relay’s minutes cross midnight UTC.
  • Holding is not countingownerOf and balanceOf count a revoked entry too. Read isValid.

What each entry holds

FieldWhat it holds
tokenIdThe entry’s number, its tokenId, counted from 1: the key the 5 fields below are read by.
issuerIdCampaign’s namespace, the same for every certificate it issues: keccak-256 of the word campaign.
credentialIdThe record number its exact text carries: 32 random bytes. An earlier entry, like the sample (the test robot’s, added before Campaign sent additions itself), carries the document number’s 16 bytes, then 16 zero bytes.
contentHashThe fingerprint.
issuedAtWhen the entry was added, in seconds, from the block that added it. Not the day the certificate was issued.
revokedAt0 while it stands. Set once, to the time of its revocation.

On both registers every field is written once, when the entry is added; only revokedAt moves, once. On the first test register the entry also keeps the address of its exact text (tokenURI), written once. The register that launches derives that address instead (documentURI).

Both write CredentialIssued, CredentialRevoked and CredentialReissued, which the explorer decodes because the source is verified. The register that launches writes CredentialRevoked without a reason, and MetadataUpdate beside it.

How a certificate gets there

  1. Its holder presses Add to the public record on their passport, and confirms. That yes is recorded, and nothing is sent yet.
  2. Between 1 and 24 hours later, Campaign sends it to the register as a transaction, which is one signed change. The key that sends it is Campaign’s own. Its permission names the record number, the fingerprint and the certificate’s own address, and lasts 20 minutes. The holder signs nothing and pays nothing, and can stop it until anything is signed.
  3. The register checks the permission and writes the entry. Campaign reads it back from a final block, and the certificate’s page prints the day.
  • Its holder decidesNobody else, the organization included, can start or require it: Campaign sends only what its holder said yes to.
  • Never a trial’s certificateNor one whose place did not pass screening.
  • Never twiceThe register refuses a second entry for one record number.

The certificate’s own address

Each certificate is added at an address anyone can compute from its entry: nobody holds its key, and it receives nothing.

The formula
address = the last 20 bytes of keccak256(abi.encode(
  keccak256("campaign.certificate-address.v1"),
  chainId,
  register,
  recordNumber
))

A worked example

The tag
0x684961…c1f5
chainId
80002
register
0x0fFd0a5681B285cd1645bF6642B498DF171fE3C7
recordNumber
0x111111…1111
address
0x41F0987E4457D9fDDbde84f2c30D99c7fce65056

What exists in each state

CertificatePublic recordCheckable without CampaignSigned recordExact text and fingerprint
A trial’sPublic recordNeverCheckable without CampaignNothingSigned recordYes, marked a trialExact text and fingerprintYes
Not added yetPublic recordNot yetCheckable without CampaignNothingSigned recordYesExact text and fingerprintYes
AddedPublic recordYesCheckable without CampaignIts fingerprint, date and statusSigned recordYesExact text and fingerprintYes
Revoked after it was addedPublic recordYes, with the revocation once relayedCheckable without CampaignAll of itSigned recordYes; its bit on the revocation list says RevokedExact text and fingerprintYes
Revoked before an adding beganPublic recordNeverCheckable without CampaignNothingSigned recordYes; its bit on the revocation list says RevokedExact text and fingerprintYes

Check it yourself: 4 levels

Each level checks what the one before it left to trust. None of them needs ownerOf, the call that names the address holding an entry: the entry is found by its record number.

SampleThe sampleEvery value below is a real certificate: the first test register’s entry 2, added by our end-to-end test robot.

Document number
80757f30-0e4f-5db4-ab92-9d5245dfb9fd
Its entry
2, on the first test register
Fingerprint
0xd3fb47…9aff
Added
26 September 2026, 06:15:02 UTC
Its page
campaign.udao.org/p/…45dfb9fdOpen
  • Its entry names campaign.invalid, the robot’s test address, for its text, for good. So the recipe reads the text on campaign.udao.org by the document number, and never needs tokenURI.
  • Its organization is the robot’s test organization, still in review, which is why its page says Udao has not reviewed it.
  • No revoked sample exists on this register yet: a revocation is read with the code and the state table above.

Level A. In a browser, about 5 minutes

It proves
That this fingerprint was added at that time, and whether it is revoked.
You still trust
The explorer (a site Campaign does not run), Campaign’s key, and Campaign to send a revocation.
  1. Open the register’s Read Contract page. Its address must match the one drawn below, character for character, beside its verified source.
  2. In tokenIdByCredentialId, enter the record number, read from the exact text (for the earlier sample, 0x, the document number’s 32 hex digits, then 32 zeros). It answers the entry’s number.
  3. In credentials, enter that number. contentHash must equal the certificate’s fingerprint, issuerId Campaign’s, and revokedAt 0.

amoy.polygonscan.comRead Contract

Contract
0x02Ffd6…6442must match, character for character

tokenIdByCredentialId

credentialId
0x80757f…0000
uint256
2the entry’s number

credentials

tokenId
2
issuerId
0x40c398…4c2fCampaign’s, always
credentialId
0x80757f…0000
contentHash
0xd3fb47…9affmust equal the certificate’s fingerprint
issuedAt
1790403302when it was added: 26 September 2026, 06:15:02 UTC
revokedAt
00: not revoked

SampleDrawn after the explorer’s Read Contract page, with the sample’s values

Open Read Contract on amoy.polygonscan.com

Level B. Recompute the fingerprint

It proves
That the text you hold is the text that was added.
You still trust
Only what level A or D trusts.
  1. Fetch the exact text by its document number, and keep the copy.
  2. Hash the bytes exactly as served, never parsed and written out again, with keccak-256.
  3. The answer must equal contentHash.

SampleFetch, keep and fingerprint the exact text

# Keep the copy: it is the certificate's exact text.
curl -s https://campaign.udao.org/api/public/credentials/80757f30-0e4f-5db4-ab92-9d5245dfb9fd/document -o exact-text.json
wc -c < exact-text.json | tr -d ' '
# keccak-256 needs OpenSSL 3.2 or later. It is not SHA3-256: never -sha3-256.
openssl dgst -keccak-256 -r exact-text.json | cut -c1-64 | sed 's/^/0x/'
Sample answer
589
0xd3fb47fcb8c1961c16cbf75976130eb37929cb15b5fcab0acfa62d6e0a2f9aff

Level C. Verify the signed record

It proves
That Campaign states this today, under its published key, and whether it is revoked.
You still trust
Campaign, which signs at each read and serves the list, with a key on its own site.
  1. Fetch the record and the key it names, and check the signature. The key’s controller must be the record’s issuer, Campaign’s own address, which lists the key.
  2. Fetch the list the record’s credentialStatus names, under the issuer’s own address. Check its signature with the same key, never a key the list names. Its issuer must be the record’s, its type BitstringStatusListCredential, its statusPurpose revocation, and now must fall between its validFrom and validUntil.
  3. Drop the leading u, decode the rest as base64url, gunzip it, and require at least 131,072 bits.
  4. Read the bit at statusListIndex, counting the left-most bit as 0. A 1 means Revoked; a 0 means not revoked.
  5. A list you cannot fetch or check means the status is unknown, never valid.
  6. For a certificate on the public record, also read revokedAt, as in level A or D. It is Revoked if either says so. On the register that launches, isValid(entry) answers it in one call.

The record follows the Open Badges 3.0 data model. It carries 2 proofs made with one key, in the W3C eddsa-rdfc-2022 and eddsa-jcs-2022 suites: check either one. Its status is a W3C Bitstring Status List entry. In Campaign’s own test, the Digital Credentials Consortium’s verifier-core 1.0.0-beta.11 checks the record’s signature and reads its bit on the list.

SampleCheck the signature against the published key

// check-signature.mjs, Node 18 or later: nothing to install.
import { createHash, createPublicKey, verify } from "node:crypto";

const record = await (await fetch("https://campaign.udao.org/api/public/credentials/80757f30-0e4f-5db4-ab92-9d5245dfb9fd/verify")).json();
const key = await (await fetch("https://campaign.udao.org/api/public/credentials/signing-key")).json();

// RFC 8785: keys sorted, no spaces.
const jcs = (v) => v === null || typeof v !== "object" ? JSON.stringify(v)
  : Array.isArray(v) ? "[" + v.map(jcs).join(",") + "]"
  : "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + jcs(v[k])).join(",") + "}";
const sha256 = (v) => createHash("sha256").update(jcs(v)).digest();
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const base58 = (text) => {
  let n = 0n;
  for (const c of text) n = n * 58n + BigInt(ALPHABET.indexOf(c));
  const hex = n.toString(16);
  const zeros = text.length - text.replace(/^1+/, "").length;
  return Buffer.concat([Buffer.alloc(zeros), Buffer.from(hex.length % 2 ? "0" + hex : hex, "hex")]);
};

// eddsa-jcs-2022: Ed25519 over the 2 digests, the proof's options first.
const { proof: proofs, ...document } = record;
const { proofValue, ...options } = proofs.find((one) => one.cryptosuite === "eddsa-jcs-2022");
const raw = base58(key.publicKeyMultibase.slice(1)).subarray(2); // after 0xed01
const spki = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), raw]);
const publicKey = createPublicKey({ key: spki, format: "der", type: "spki" });
// The key is the issuer's: its controller is the record's issuer.
const valid = options.verificationMethod === key.id && key.controller === record.issuer.id
  && verify(null, Buffer.concat([sha256(options), sha256(document)]), publicKey, base58(proofValue.slice(1)));
console.log(valid ? "valid" : "INVALID");
// Its status is an entry on the revocation list: the next check reads it.
console.log(record.credentialStatus.type);

A shell has no Ed25519, so this level is in Node and Python.

Sample answer
valid
BitstringStatusListEntry

SampleRead the certificate’s bit on the revocation list

// check-status.mjs, Node 18 or later: npm install jsonld
import { createHash, createPublicKey, verify } from "node:crypto";
import { gunzipSync } from "node:zlib";
import jsonld from "jsonld";

const record = await (await fetch("https://campaign.udao.org/api/public/credentials/80757f30-0e4f-5db4-ab92-9d5245dfb9fd/verify")).json();
const entry = record.credentialStatus;
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const base58 = (text) => {
  let n = 0n;
  for (const c of text) n = n * 58n + BigInt(ALPHABET.indexOf(c));
  const hex = n.toString(16);
  const zeros = text.length - text.replace(/^1+/, "").length;
  return Buffer.concat([Buffer.alloc(zeros), Buffer.from(hex.length % 2 ? "0" + hex : hex, "hex")]);
};
// RDF Dataset Canonicalization, the eddsa-rdfc-2022 suite's.
const nquads = (doc) => jsonld.canonize(doc, { algorithm: "RDFC-1.0", format: "application/n-quads", safe: true });
const sha256 = (text) => createHash("sha256").update(text).digest();

let status = "status unknown"; // until every check below passes
try {
  const list = await (await fetch(entry.statusListCredential)).json();
  // The published key, as in step 1: never the one the list names.
  const key = await (await fetch("https://campaign.udao.org/api/public/credentials/signing-key")).json();
  const now = new Date();
  const { proof, ...unsecured } = list;
  const { proofValue, ...options } = proof;
  const raw = base58(key.publicKeyMultibase.slice(1)).subarray(2); // after 0xed01
  const spki = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), raw]);
  const publicKey = createPublicKey({ key: spki, format: "der", type: "spki" });
  const data = Buffer.concat([
    sha256(await nquads({ "@context": unsecured["@context"], ...options })),
    sha256(await nquads(unsecured)),
  ]);
  // Signed with the record's key, by its issuer, at its issuer's address;
  // the list's type, purpose and dates; its proof.
  const valid = key.id === proof.verificationMethod && key.controller === record.issuer.id
    && list.issuer.id === record.issuer.id && entry.statusListCredential.startsWith(record.issuer.id)
    && proof.cryptosuite === "eddsa-rdfc-2022"
    && list.type.includes("BitstringStatusListCredential")
    && list.credentialSubject.statusPurpose === "revocation"
    && new Date(list.validFrom) <= now && now <= new Date(list.validUntil)
    && verify(null, data, publicKey, base58(proofValue.slice(1)));
  console.log(valid ? "valid" : "INVALID");
  // Drop the u, decode base64url, gunzip: at least 131,072 bits.
  const bits = gunzipSync(Buffer.from(list.credentialSubject.encodedList.slice(1), "base64url"));
  console.log(bits.length * 8);
  if (valid && bits.length * 8 >= 131072) {
    // The left-most bit is 0. A 1 is revoked.
    const index = Number(entry.statusListIndex);
    const bit = (bits[Math.floor(index / 8)] >> (7 - (index % 8))) & 1;
    status = bit === 1 ? "revoked" : "not revoked";
  }
} catch {}
console.log(status);
Sample answer
valid
131072
not revoked

The list is one file for every certificate, so fetching it names none. It carries bits and Campaign’s name: no document number, date or reason. Campaign keeps no record of who fetches it; the host it runs on keeps its own request logs for a while.

Level D. Any node, without the explorer

It proves
What level A proves, with nobody between you and the network.
You still trust
The node you ask (run your own), Campaign’s key, and Campaign to send a revocation.
  1. Ask any Amoy node for its chain id. It must be 80002.
  2. Call tokenIdByCredentialId, then credentials, at the register’s address, and match them as in level A.

SampleRead the entry from an Amoy node

NODE=https://polygon-amoy-bor-rpc.publicnode.com
REGISTER=0x02Ffd60f83Fe9953ed197DFDACE9D66Fd5296442
rpc() {
  curl -s "$NODE" -H "Content-Type: application/json" \
    -d '{"jsonrpc":"2.0","id":1,"method":"'"$1"'","params":'"$2"'}' \
    | sed -E 's/.*"result":"0x([0-9a-f]*)".*/\1/'
}
call() { rpc eth_call '[{"to":"'"$REGISTER"'","data":"'"$1"'"},"latest"]'; }

printf '%d\n' "0x$(rpc eth_chainId '[]')"
# tokenIdByCredentialId: the record number, from the exact text (this earlier sample's: its document number's 32 hex digits, then 32 zeros).
ENTRY=$(call 0xa0daaf6c80757f300e4f5db4ab929d5245dfb9fd00000000000000000000000000000000)
echo "$ENTRY"
# credentials(entry): issuerId, credentialId, contentHash, issuedAt, revokedAt.
call "0xe0574e3f$ENTRY" | fold -w 64
Sample answer
80002
0000000000000000000000000000000000000000000000000000000000000002
40c398d6e1fb1e64fa89a0f2f232df777e9e84f3ef558102d564475b8a404c2f
80757f300e4f5db4ab929d5245dfb9fd00000000000000000000000000000000
d3fb47fcb8c1961c16cbf75976130eb37929cb15b5fcab0acfa62d6e0a2f9aff
000000000000000000000000000000000000000000000000000000006ab762e6
0000000000000000000000000000000000000000000000000000000000000000

Line by line

  1. 80002the chain id: Amoy
  2. 000000…0002the entry’s number
  3. 40c398…4c2fissuerId: Campaign’s
  4. 80757f…0000credentialId: the record number
  5. d3fb47…9affcontentHash: the fingerprint
  6. 000000…62e6issuedAt: 26 September 2026, 06:15:02 UTC
  7. 000000…0000revokedAt: 0, not revoked

For an employer’s system

5 public reads, with no API key and no account, which a browser on any site can read. The first 3 are limited to 120 a minute for one caller. The key and the revocation list are not, and a shared cache holds the key for 5 minutes and the list for 1.

  • GET/api/public/credentials/{document number}/verifyThe signed record, as application/vc+ld+json. The certificate’s own address answers the same document to a request whose Accept names JSON.
  • GET/api/public/credentials/{document number}/documentThe exact text, as written when the certificate was issued.
  • GET/api/public/credentials/{document number}/chainIts public record, read when you ask and kept a minute, unsigned: status is matches, revoked, does_not_match, revocation_pending or unreadable, with checked_at. Only once the entry answers the certificate’s record number and Campaign’s issuer does it say more than unreadable. An entry at the certificate’s own address also carries chain_id, registry, record_number, token_id and tx_hash.
  • GET/api/public/credentials/signing-keyThe public key every proof names, as a Multikey. Its controller, Campaign’s own address, answers a request whose Accept names JSON with its controller document, which lists the key.
  • GET/api/public/credentials/status/1The revocation list, signed by Campaign: one bit for each certificate, 1 once it is revoked. A copy of a record you were given still finds its bit after its holder hides the certificate.

The first 3 answer 404 credential_not_found alike for:

  • an unpublished passport
  • a certificate its holder hid
  • an organization Udao has not approved, other than its own test organization
  • a number that names nothing

The second also answers it for a certificate issued before exact texts were kept. The third also answers it for one that is not on the public record.

The list answers 404 status_list_not_found to any number but 1.

Revocations and reissues

The organization that issued a certificate decides to revoke it:

  • Its verifier, during the campaign’s review window
  • Its owner, after it, even once settled
  • Its system, through the API

A reward already reserved stays with the person. The register’s revoke and the outcome.revoked your webhook receives are the same act, said by 2 systems.

  • At onceCampaign’s pages say Revoked, with the date. The certificate’s bit on the revocation list turns on for good: anyone checking it sees it within a minute.
  • Within minutesCampaign’s relay sends revoke to the register with Campaign’s operator key each minute. It counts it done once a final block shows revokedAt, its date. A send not seen in a block is sent again after 15 minutes.
  • Its reasonNone: the first test register’s reason is left empty, the register that launches takes none, and the record behind it stays with Campaign.

Reading a revocation on the first test register

  • revokedAtIn credentials(entry): 0 while it stands, the time it was revoked after.
  • The CredentialRevoked logIts topic is below. The explorer’s Read Contract answers revokedAt, and its event log decodes the register’s events, because the source is verified.
  • Holding is not countingownerOf and balanceOf still count a revoked certificate. Read revokedAt.
  • Not in wallet appsWallet and marketplace apps, and the explorer’s page for the single item, show a revoked certificate unchanged.
CredentialRevoked topic
0x434cf4…a4ed

Reading a revocation on the register that launches

  • isValidfalse once the entry is revoked; revokedAt in credentials(entry), as on the first register.
  • MetadataUpdateWritten once, with the revocation, so an app that reads the entry again shows its stamp. A copy an app keeps can stay unchanged until it reads the entry again.
CredentialRevoked topic on the register that launches
0xce470a…fbbf

An entry moves to another address only by reissue, which the namespace’s admin alone can call and which writes CredentialReissued. Nothing in the product calls it today.

Who holds which role

On the test network one key holds both admin roles. On Polygon itself a multisig holds them from launch, when every role is accounted for.

CredentialRegistry, the first test register

DEFAULT_ADMIN_ROLEGrants and removes every role, its own too.
Udao’s deployer key0x670c4d2564D1A365BFAB47Df09575CCc4E56f4cD
ISSUER_ADMINOpens an issuer namespace, and can hand one to a new admin.
Udao’s deployer key0x670c4d2564D1A365BFAB47Df09575CCc4E56f4cD
PLATFORM_SIGNERSigns the permission an adding needs.
Nobody: this register adds nothing more.
Admin of the campaign namespaceRevokes and reissues its entries, and can hand the namespace on.
Campaign’s operator0x3639ddBB7Dab03b38Af34BDe80E10CC7E42D55De

It is retired: its admin roles stay with the deployer key, and no multisig takes them.

CredentialRegistryV2, the register that launches

DEFAULT_ADMIN_ROLEGrants and removes every role, its own too.
Udao’s deployer key0x670c4d2564D1A365BFAB47Df09575CCc4E56f4cD
ISSUER_ADMINOpens an issuer namespace, and can hand one to a new admin.
Udao’s deployer key0x670c4d2564D1A365BFAB47Df09575CCc4E56f4cD
PLATFORM_SIGNERSigns the permission an adding needs.
Campaign’s permission signer0xcc75638b0B025ffb76b93783aD63D8657cDC74CE
Admin of the campaign namespaceRevokes and reissues its entries, and can hand the namespace on.
Campaign’s operator0x3639ddBB7Dab03b38Af34BDe80E10CC7E42D55De

Read off both registers on 4 October 2026. No role can change an entry’s facts.

What adding makes public

It shows

  • The certificate’s own address, computed from its record number: nobody holds its key, it receives nothing, and it holds this certificate alone.
  • Its record number, its fingerprint, the time it was added, and Campaign’s issuer id.
  • The address of its exact text, which answers only while its holder’s passport is published and the certificate shown.
  • For a certificate also published from a small listed campaign: its campaign’s title, which can narrow it to that campaign’s few rewarded people.
  • For a revocation: its time, with no reason; on the register that launches, the picture’s revoked stamp with that day.
  • On the register that launches, a picture and words any app reading the entry can show, drawn from these facts alone: it adds no fact.

It never shows

  • A name
  • An email address
  • The passport’s address
  • Evidence or notes
  • An amount

A certificate nobody added leaves nothing on the public record at all.

What changes at launch

  • CredentialRegistryV2, the register that launches, runs here first. The same verified source goes to Polygon itself, chain id 137, at a new address, and its entries count from 1 again there. Its explorer is polygonscan.com.
  • Entries on the test network are not carried over. Both of its registers stay described here, with their dates.
  • Every addition Campaign sends is at its own computed address, as on the test network.
  • A multisig takes the admin roles from the single deployer key.

The limits

  • It proves the statement, not the outcomeThe record proves the organization’s statement was added and has not changed since. Not that the outcome happened, and not that the person showing it is its holder: the name is on no record.
  • Before it is added, every check rests on usThe signed record and the exact text come from Campaign’s servers. Until the public record matches the fingerprint, they check Campaign against itself.
  • Holders: keep a copy of the exact textIts address answers only while your passport is published and the certificate shown. Your passport keeps a copy for you to take, and a copy you kept still proves itself against the public record.
  • This is a test networkAmoy is Polygon’s test network, and its entries are not carried to launch.

Glossary

On our pagesTechnical term
The public recordThe register: CredentialRegistry, the first test register, then CredentialRegistryV2, the register that launches
Its entryOne token of the register, under ERC-721
The entry’s numberIts tokenId
The fingerprintcontentHash: keccak-256 of the exact text
The exact textThe canonical JSON document, RFC 8785
A certificateA credential, as the reads’ paths and codes name it
The document numberThe certificate’s id, printed on it as Document number and ending its page’s address
The record numberThe credential id on the register: 32 random bytes, in the exact text
Added onThe UTC day of the block that added it
RevokedrevokedAt, set once; outcome.revoked for a partner
Record referenceThe hash of the transaction that added it
Its published rulesThe contract’s verified source
TestnetAmoy, Polygon’s test network
Nobody can change it, not even usEvery field written once, with no setter and no upgrade
Signed by Campaign2 DataIntegrityProofs with one key, eddsa-rdfc-2022 and eddsa-jcs-2022
The revocation listA W3C Bitstring Status List: one bit for each certificate