How to read an RDAP JSON response
An RDAP JSON response reads in four groups: identity, events, entities, and status, plus a few blocks you can skip. Walked through with one real response for example.com, field by field, in the order you actually need them.An RDAP response reads in four groups, in this order: object identity (what record this is), events (when things happened to it), entities (who's attached to it), and status (what's restricted right now). A handful of supporting blocks, nameservers, DNSSEC data, notices, links, only matter if you're answering a more specific question than "what is this domain's current state."
The response we're reading
This is a real response, fetched live on 2026-08-11 with curl https://rdap.verisign.com/com/v1/domain/example.com. example.com is IANA's own reserved documentation domain, not a real registrant's, which is exactly why it's a safe one to walk through field by field: nothing here is private, and the registrar's abuse contact is already blank in the live data, not redacted by me.
One caveat before the walkthrough: RDAP responses aren't static documents, they're live query results. The expiration date in this exact response will be wrong by the time you read this, because that's the whole point of the events block below. Treat every value here as "what this endpoint returned on 2026-08-11," not as a permanent fact about the domain.
Object identity: what record this even is
{
"objectClassName": "domain",
"handle": "2336799_DOMAIN_COM-VRSN",
"ldhName": "EXAMPLE.COM"
}objectClassName tells you what kind of RDAP object you're looking at, this response is a domain object, but a nameserver or entity lookup returns the same envelope shape with a different class name and different fields inside it. handle is the registry's internal identifier, useful for support tickets, not for anything you'd compute against. ldhName is the domain in LDH form, letters, digits, hyphens, plain ASCII, the same form DNS itself uses. An internationalized domain would show its punycode form here (xn--...) with a separate unicodeName field carrying the human-readable version.
Events: four timestamps that answer different questions
"events": [
{ "eventAction": "registration", "eventDate": "1995-08-14T04:00:00Z" },
{ "eventAction": "expiration", "eventDate": "2026-08-13T04:00:00Z" },
{ "eventAction": "last changed", "eventDate": "2026-01-16T18:26:50Z" },
{ "eventAction": "last update of RDAP database", "eventDate": "2026-08-11T09:09:48Z" }
]Three of these are facts about the domain. registration and expiration are self-explanatory, and last changed is whatever the registry last modified about the record, which could be a renewal, a nameserver update, or a registrant change; the response doesn't say which. The fourth one, last update of RDAP database, isn't a domain event at all. It's a timestamp about the RDAP server's own data, when it last refreshed the copy it's serving you. Confusing that one with a real registry event is an easy mistake, since it sits in the same array with the same shape as the other three.
This is also the block that turns a one-time read into a monitoring problem: reading domain expiry monitoring means checking this same expiration field on a schedule, not once.
Entities and vcardArray: the structure that trips people up
Contact data doesn't sit in flat fields. It's nested inside entities, and each entity's actual contact details are inside a vcardArray, a jCard structure, which is a JSON encoding of the vCard format wrapped in its own array-of-arrays shape:
"entities": [
{
"objectClassName": "entity",
"handle": "376",
"roles": ["registrar"],
"vcardArray": [
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", "RESERVED-Internet Assigned Numbers Authority"]
]
],
"entities": [
{
"objectClassName": "entity",
"roles": ["abuse"],
"vcardArray": [
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", ""],
["tel", {"type": "voice"}, "uri", ""],
["email", {}, "text", ""]
]
]
}
]
}
]Two things to notice. First, vcardArray's second element is itself an array of 4-item arrays: property name, parameters, value type, then the value. ["fn", {}, "text", "..."] reads as "formatted name, no parameters, a text value, and here it is." Second, entities nest inside entities: the registrar entity here contains its own abuse-role entity, rather than listing abuse contact as a flat field on the parent. A domain lookup can carry a registrant entity, a technical contact, an administrative contact, and a registrar, each with its own nested vcardArray, not a fixed set of top-level fields.
The blank fn, tel, and email values on the abuse contact aren't a parsing failure. That's the registry choosing to return empty values for that field, the same differentiated-access-control behavior covered in WHOIS vs RDAP: a public RDAP query doesn't always get the same fields a logged-in registrar view would.
Status: EPP codes, not something RDAP invented
"status": [
"client delete prohibited",
"client transfer prohibited",
"client update prohibited"
]These three strings are EPP status codes, a fixed vocabulary that predates RDAP and also shows up in WHOIS output, RDAP returns them as a clean array instead of a line of prose. client transfer prohibited specifically is the lock that domain hijacking detection watches for: it being present is normal and protective, it disappearing without you requesting it is the signal worth alerting on.
What to skip unless you need it
- nameservers: the domain's current delegation.
example.com's happen to be Cloudflare's (ELLIOTT.NS.CLOUDFLARE.COM,HERA.NS.CLOUDFLARE.COM) even though the domain itself is IANA-reserved, a small reminder that delegation and ownership are separate facts the response keeps separate too. - secureDNS: present only if DNSSEC is signed. When it is,
dsDatacarries the DS record fields (keyTag,algorithm,digestType,digest), useful for verifying a delegation chain, irrelevant if you're not touching DNSSEC. - notices, links, and rdapConformance: boilerplate: terms of service, a self-referencing link back to the query, and which RDAP extensions the server supports. Worth knowing they exist so an unfamiliar block doesn't stall a parser, not worth reading on a normal lookup.
Reading one response by hand is a five-minute exercise once you know the four groups. Watching that same structure on a schedule is the actual operational problem: the events block for expiration, the status array for an unexpected lock removal, and the entities block for a registrant or nameserver change are the three places a real incident shows up first, long before an uptime check would ever notice something wrong.