Every Davi card publishes a public, unauthenticated JSON document describing who holds it, who issued it, and where to reach them. Reading it needs no API key, account or SDK.
| Property | Value |
|---|---|
| Format version | 1.0 |
| Media type | application/vnd.davi.card+json |
| Compatibility | Fields may be added in minor versions. Fields are never renamed or removed |
| Live | The document, its discovery, and resolving documents from other hosts |
| Not built | Reserved fields, and a trust model that would give a document from another host authority |
URLs
Each card has exactly one document URL, under /c/:
https://davi.social/c/{uid}/json ← the document
https://davi.social/c/{uid} ← the card page, which advertises it
https://davi.social/{username} ← the profile page, which advertises the primary card's document
curl -H "Accept: application/vnd.davi.card+json" \
"https://davi.social/c/CARD_UID/json"
The profile page advertises the document and does not serve it, so each document has one canonical URL. Reaching a document from a username takes one extra hop.
Discovery
A card page or profile page points at its document with an alternate link:
<link rel="alternate"
type="application/vnd.davi.card+json"
href="https://davi.social/c/{uid}/json">
Fetch the page, find the link, fetch the document:
const ACCEPT = "text/html, application/vnd.davi.card+json, application/json";
const page = await fetch(url, { headers: { Accept: ACCEPT } });
const html = await page.text();
// rel="alternate" carrying the card media type; resolve it against the page URL,
// since the href may be relative.
const match = html.match(
/<link[^>]+type=["']application\/vnd\.davi\.card\+json["'][^>]*>/i,
);
const href = match?.[0].match(/href=["']([^"']+)["']/i)?.[1];
const documentUrl = href ? new URL(href, page.url).toString() : null;
A profile page carries the link only when the owner has a primary card. Without one, there is no card document for that profile.
Transport
| Behaviour | Detail |
|---|---|
| Content type | application/vnd.davi.card+json |
Accept | text/html, application/vnd.davi.card+json, application/json negotiates HTML or the document; application/vnd.davi.card+json, application/json asks only for the document |
| Caching | Cache-Control: public, max-age=60, stale-while-revalidate=600 |
| Validators | ETag on every response; send If-None-Match to get a 304 |
Vary | Accept, on the document endpoint /c/{uid}/json |
| CORS | Access-Control-Allow-Origin: *, GET/OPTIONS |
| Errors | RFC 9457 application/problem+json |
A request for a known document URL with the document-only Accept value takes
one hop and does not depend on intermediaries keying their caches on Accept.
Negotiating against a page takes two.
A missing card returns 404:
{
"type": "https://davi.social/problems/card-not-found",
"title": "Card not found",
"status": 404
}
The same body is returned for a card that doesn't exist and for blank unclaimed inventory.
The document
Only version is required. The smallest valid document:
{ "version": "1.0" }
A populated document (the schema's own example):
{
"version": "1.0",
"uid": "ABC123",
"url": "https://davi.social/c/ABC123",
"status": "claimed",
"wallet_address": "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
"issuer": null,
"issuer_url": null,
"issuer_logo_url": null,
"owner_type": "person",
"owner_username": "alice",
"owner_name": "Alice Doe",
"owner_given_name": "Alice",
"owner_family_name": "Doe",
"owner_avatar_url": "https://cdn.davi.social/avatars/alice.png",
"owner_avatar_alt": "Alice Doe",
"owner_bio": "Full-stack developer",
"owner_job_title": "Software Engineer",
"owner_company": "Acme Corp",
"owner_profile_url": "https://davi.social/alice",
"owner_links": [
{
"label": "LinkedIn",
"url": "https://linkedin.com/in/alice",
"rel": "social",
"verified": false
}
],
"owner_vcard_url": "https://davi.social/alice/vcf",
"updated_at": "2026-07-26T10:00:00Z",
"proof": null
}
The card
| Field | Meaning |
|---|---|
version | Required. MAJOR.MINOR, matching ^1\.[0-9]+$. |
uid | The card's public identifier. |
url | The card's page. |
status | claimed, unclaimed, or frozen. |
wallet_address | The bound ledger account, if any. |
updated_at | Advisory content timestamp. |
The issuer
| Field | Meaning |
|---|---|
issuer | Who issued the card. |
issuer_url | The issuer's URL. |
issuer_logo_url | The issuer's logo. |
The holder
| Field | Meaning |
|---|---|
owner_type | person or organization. |
owner_username | Resolves to their profile. |
owner_name | The display name. The only name field that applies to every holder type. |
owner_given_name, owner_family_name | Refine it for people; absent for organizations. |
owner_avatar_url, owner_avatar_alt | Avatar and its alt text. |
owner_bio, owner_job_title, owner_company | Profile details. |
owner_profile_url | Their profile page. |
owner_vcard_url | A text/vcard representation of the same identity. |
owner_links | Public links. See owner_links. |
Name fields are named for role, not position, matching OIDC claims, vCard N
components and JSContact name component kinds. The given/family split is stored
data, not derived from owner_name.
owner_links
An array of objects:
{ "label": "Site", "url": "https://ada.example", "rel": "custom", "verified": false }
| Field | Meaning |
|---|---|
url | Required. |
label | Optional. |
rel | Optional. social or custom. |
verified | Reserved. |
owner_links never contains an email address or a phone number.
What Davi emits today
The field list above belongs to the format. In the documents Davi serves, several fields are constant. Other hosts may send other values.
| Field | In Davi's documents |
|---|---|
owner_type | Always "person". Davi doesn't issue org-held cards. |
owner_links[].rel | Always null. Link provenance isn't recorded. |
updated_at | Always null. No content timestamp is tracked. |
issuer_url | An identifier of the form https://davi.social/o/{slug}. It does not resolve: there is no public route at /o/. Don't render it as a link. |
proof, owner_links[].verified | Reserved. |
The document publishes only what a holder has chosen to make public. Owner-facing state, such as a holder's private label for a card or which card they consider primary, is not included. Internal primary keys are not published; every identifier in the document is resolvable.
Reserved fields
Present in the schema, not usable in 1.0:
| Field | Status |
|---|---|
proof | Reserved for a Davi-issued attestation binding wallet_address to the host serving the document. Must be null or absent in 1.0. |
owner_links[].verified | Must be false in 1.0. There is no verification mechanism; do not render a verified badge on its basis. |
owner_type: "organization" | Reserved. Davi doesn't issue org-held cards; a federated host may describe one. |
Rules for consumers
Normative:
- Ignore unknown fields. New fields are added in minor versions.
- Never fail on an unrecognized minor version. Reject only an unrecognized major.
- Absent and
nullmean different things.nullis "known to be empty"; absent is "this host doesn't implement it." - The only field you may require is
version. Everything else can be missing. - A document with no wallet is valid.
wallet_addressis absent for unclaimed inventory, or from a host that doesn't model wallets. - Don't depend on
updated_atfor correctness. It is advisory; caching is governed byETagandCache-Control. - Schema validation does not check string syntax. URL and timestamp fields
carry JSON Schema
formatannotations, but neither reference implementation enables a format checker, so"owner_avatar_url": "not a url"validates. Parse a URL before fetching it and a timestamp before comparing it. - Host-specific fields are namespaced under a reversed domain
(
"social.davi.foo","com.example.bar"), including Davi's own. The flat namespace is the interoperable one. owner_*andissuer_*are reserved group prefixes. A new unprefixed field describes the card itself, not its holder or issuer.- Check
statusfor liveness, not theowner_*fields. A frozen card still carries its holder's details.
const isLive = card.status === "claimed"; // not: card.owner_name != null
Validation
A document is checked in two stages.
Protocol. The payload must be an object with a version whose major
component is supported. Any minor is accepted. A document with an unrecognized
major is unsupported, not malformed, and is not validated against the 1.x
field rules.
const parts = /^(\d+)\.(\d+)$/.exec(card?.version ?? "");
if (!parts) throw new Error("missing or malformed version");
if (Number(parts[1]) !== 1) throw new Error(`unsupported major ${parts[1]}`);
Field shapes. Documents from a host you don't run are untrusted input and
are validated against the schema in The document. Davi skips
this stage for documents it built from its own typed model. format is not
enforced (see Rules for consumers).
Content types. The vendor type, application/json, and any +json
structured suffix may carry a document, since a federated host may serve it
without the vendor type. This also matches application/ld+json, so the body
is parsed before the header is trusted. A text/html response is a page, and
discovery applies.
The JSON shape, the media type and the rules on this page are the whole contract. Davi's TypeScript and Python implementations agree on these behaviours and differ in function shape (one returns a result object, the other raises).
Documents from other hosts
A card document may be served by any host, including a person's own domain.
Every field except version may be absent because a host publishes what it has.
Davi resolves a document from an arbitrary URL in this order:
- If the URL has no path (
https://ada.example), tryhttps://ada.example/.well-known/davi-card. An absent or unusable well-known document falls through to step 2. With a path present this step is skipped, because a path names a particular subject. - Fetch the URL negotiating on
Accept. If a card document comes back, stop. - If HTML comes back, find the
rel="alternate"link and fetch that.
Davi does not serve /.well-known/davi-card. Its documents live under
/c/{uid}/json and are found by discovery.
wallet_address is a claim, not a credential
Nothing binds a wallet address to the host serving the document. Any site
can publish any address. A wallet_address from a third-party document may be
looked up (Davi resolves it against its own wallet records), but must never on
its own grant a balance, a reward, a membership, or any other entitlement.
proof, the attestation that would bind a wallet to the
host that published it, is unbuilt. Every field in a document from an untrusted
host is self-asserted.
Server-side fetching
A caller-supplied URL fetched from your infrastructure is an SSRF surface. Davi's resolver applies these limits, and a server-side fetcher needs all of them:
| Limit | Value |
|---|---|
| Scheme | https only |
| Host | Resolved first; rejected unless every address is public (this covers cloud metadata endpoints such as 169.254.169.254) |
| Discovered links | The same host check is applied to the href discovery returns |
| Body | 512 KiB |
| Timeout | 5 s |
| Redirects | 3 |
Next
- Core Concepts: how cards, wallets and profiles relate.
- Card Template Format: the artwork format.