Documentation

Card JSON Format

The public card identity document: URLs, discovery, transport, fields, consumer rules, and resolution from other hosts.

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.

PropertyValue
Format version1.0
Media typeapplication/vnd.davi.card+json
CompatibilityFields may be added in minor versions. Fields are never renamed or removed
LiveThe document, its discovery, and resolving documents from other hosts
Not builtReserved 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

BehaviourDetail
Content typeapplication/vnd.davi.card+json
Accepttext/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
CachingCache-Control: public, max-age=60, stale-while-revalidate=600
ValidatorsETag on every response; send If-None-Match to get a 304
VaryAccept, on the document endpoint /c/{uid}/json
CORSAccess-Control-Allow-Origin: *, GET/OPTIONS
ErrorsRFC 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

FieldMeaning
versionRequired. MAJOR.MINOR, matching ^1\.[0-9]+$.
uidThe card's public identifier.
urlThe card's page.
statusclaimed, unclaimed, or frozen.
wallet_addressThe bound ledger account, if any.
updated_atAdvisory content timestamp.

The issuer

FieldMeaning
issuerWho issued the card.
issuer_urlThe issuer's URL.
issuer_logo_urlThe issuer's logo.

The holder

FieldMeaning
owner_typeperson or organization.
owner_usernameResolves to their profile.
owner_nameThe display name. The only name field that applies to every holder type.
owner_given_name, owner_family_nameRefine it for people; absent for organizations.
owner_avatar_url, owner_avatar_altAvatar and its alt text.
owner_bio, owner_job_title, owner_companyProfile details.
owner_profile_urlTheir profile page.
owner_vcard_urlA text/vcard representation of the same identity.
owner_linksPublic 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.

An array of objects:

{ "label": "Site", "url": "https://ada.example", "rel": "custom", "verified": false }
FieldMeaning
urlRequired.
labelOptional.
relOptional. social or custom.
verifiedReserved.

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.

FieldIn Davi's documents
owner_typeAlways "person". Davi doesn't issue org-held cards.
owner_links[].relAlways null. Link provenance isn't recorded.
updated_atAlways null. No content timestamp is tracked.
issuer_urlAn 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[].verifiedReserved.

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:

FieldStatus
proofReserved for a Davi-issued attestation binding wallet_address to the host serving the document. Must be null or absent in 1.0.
owner_links[].verifiedMust 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 null mean different things. null is "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_address is absent for unclaimed inventory, or from a host that doesn't model wallets.
  • Don't depend on updated_at for correctness. It is advisory; caching is governed by ETag and Cache-Control.
  • Schema validation does not check string syntax. URL and timestamp fields carry JSON Schema format annotations, 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_* and issuer_* are reserved group prefixes. A new unprefixed field describes the card itself, not its holder or issuer.
  • Check status for liveness, not the owner_* 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:

  1. If the URL has no path (https://ada.example), try https://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.
  2. Fetch the URL negotiating on Accept. If a card document comes back, stop.
  3. 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:

LimitValue
Schemehttps only
HostResolved first; rejected unless every address is public (this covers cloud metadata endpoints such as 169.254.169.254)
Discovered linksThe same host check is applied to the href discovery returns
Body512 KiB
Timeout5 s
Redirects3

Next