Build with RealHandles
Show a real verified badge beside a link on your own site. Every endpoint here is public, cross-origin, and needs no key and no account.
Is this link real?
One request, and the one most people want. Ask whether a profile URL belongs to a RealHandles identity that actually proved it.
GET https://realhandles.com/api/link-status?url=https://github.com/davidvkimball
{
"linked": true,
"username": "davidvkimball",
"displayName": "David V. Kimball",
"keyId": "dXTgJqD_AbxKL2hGD13cAuSJnj-3wUn85dCyVuc6mPk",
"method": "oauth",
"profile": "https://realhandles.com/davidvkimball"
}A URL nobody has proved answers { "linked": false } and nothing else. This is the endpoint a link-in-bio page calls before drawing a checkmark, so linked: true means the SERVER confirmed the account, not that somebody signed a file claiming it.
Link your badge back to the returned profile. A badge nobody can click through to is a badge nobody can check, and the whole argument of this product is that a reader should never have to take our word for it.
Everything about one identity
The full picture: verified accounts, self-asserted ones, the trust score, and the signed file itself so you can check our work.
GET https://realhandles.com/api/identity/davidvkimball
{
"username": "davidvkimball",
"displayName": "David V. Kimball",
"avatarUrl": "...",
"keyId": "...",
"didKey": "did:key:z6Mk...",
"trust": { "score": 100, "verifiedCount": 11, "domains": 1 },
"accounts": {
"verified": [
{ "platform": "github", "handle": "davidvkimball",
"url": "https://github.com/davidvkimball", "method": "oauth",
"verifiedAt": "2026-07-14T05:15:53.920Z",
"confirmedAt": "2026-08-19T04:00:11.402Z" }
],
"claimed": [ ... ]
},
"disavowed": [ ... ],
"jws": "eyJhbGciOiJFZERTQSIs..."
}Two dates, and they are two different facts. verifiedAt is when RealHandles verified the account. confirmedAt appears only where a platform has since been asked, by the account id recorded at verification, and still named that handle. Its absence means nothing has looked since, never that a check failed.
Both come from our own records, never from the manifest. A verifiedAt inside a signed manifest is written by the signer, so a signature over it proves who typed it and nothing about whether it is true.
The one rule
Never decide "verified" by reading method.
It is the most tempting field in the payload and it is a trap. The signer wrote that string. A signature proves who wrote a manifest, never that its contents are true, so a perfectly valid signature over { "platform": "x", "handle": "jack", "method": "tweet-proof" } is a valid signature over a lie. Anyone who can claim a free handle can publish one.
We got this wrong in four separate clients before getting it right, each time after the previous round reported itself finished, so it is worth saying plainly rather than leaving in a type definition:
- Use the accounts.verified and accounts.claimed split above. That split is our verdict, cross-checked against what we actually confirmed.
- Checking method as well is fine, as long as it can only ever SUBTRACT from what that split already granted. It is a necessary condition and never a sufficient one.
- Fail closed. If you cannot reach us, or you are unsure, show nothing or show "claimed". Never show verified.
The same applies to the raw manifest at /<handle>/realhandles.json. Verifying its signature tells you the identity published it. It does not tell you the accounts inside it are real.
Verify it yourself, without us
Do not trust this API. Every response above carries the jws, and the published verifier checks it in your own process against the key in the manifest. It would still pass if RealHandles were offline.
pnpm add @realhandles/verify
import { verifySignedManifest, computeTrustScore, jwkToDidKey } from '@realhandles/verify';
const file = await (await fetch('https://realhandles.com/davidvkimball/realhandles.json')).json();
const result = await verifySignedManifest(file);
// result.ok === true -> the manifest is signed by the key it namesZero-dependency apart from jose, and isomorphic: the same module runs in a browser, in a function, and in third-party tooling. The signed history at /<handle>/realhandles-chain.json and the OpenTimestamps proof at /<handle>/realhandles.ots are served for the same reason, and they keep being served even for a profile whose owner has hidden it. Withholding verification data on request would make us the arbiter of who can be verified, which is the role this product refuses.
The directory
Every public identity, ranked, for a listing or a search box.
GET https://realhandles.com/api/directory?limit=1
{
"count": 1,
"profiles": [
{ "username": "realhandles", "displayName": "RealHandles",
"avatarUrl": "...", "keyId": "...", "kind": "organization",
"trustScore": 34, "verifiedCount": 2,
"profile": "https://realhandles.com/realhandles",
"createdAt": "2026-08-09T20:58:07.189Z" }
]
}Matching on the device instead
For a browser extension or anything that would otherwise ask us about every page somebody visits.
GET https://realhandles.com/directory-index.json
A hashed lookup set of every publicly indexable account, small enough to hold in memory and byte-identical for every caller. Match against it locally, then fetch the one identity you matched. Our own extension is built this way on purpose: asking a server "do you know this profile?" on every page would be a browsing-history collector wearing a privacy product's clothes.
The format constants are fixed by the shipped extension. If you build against this, pin what you read and expect the set to grow rather than change shape.
For AI agents
An MCP server, so an assistant can check an identity or a link without you writing the glue.
pnpm add @realhandles/mcp
Two tools: verify a published identity, and check whether a URL belongs to one. It calls the same endpoints on this page and applies the same rule about method. Every profile also carries schema.org JSON-LD with sameAs pointing at the verified accounts only, so a crawler that never calls an API still gets the right answer.
Fair use
No key, no signup, no rate plan. These endpoints are rate limited per caller and cached at the edge, so cache what you fetch and do not poll a profile that changes a few times a year.
Reading the format rather than calling the service? That is the spec. Everything is open source at github.com/realhandles, including the verifier and the conformance vectors.