This specification profiles [[WACZ]]. It constrains how a WACZ package is written and defines the additional members BrowserHive records inside it, so that a reader holding only the archive can tell what the capture obtained, what it did not, and under what settings it ran.

This is a working draft. It describes what BrowserHive writes today. It has no standing beyond that project and is not endorsed by the publishers of [[WACZ]].

This version adds dismissal, recording what the capture removed from the page before recording it. A package written against 1.3.0 therefore does not conform to 1.4.0. This is intended, and it is why conformsTo names a version: a reader holding an older package can still tell what it was written against. Earlier versions — 1.0.0, 1.1.0, 1.2.0 and 1.3.0 — remain published at their own URLs for that reason, but BrowserHive no longer produces them.

Up to 1.3.0 a package could be missing an element that was present when the page was served, and say nothing at all about it. A reader could not tell a consent banner that had been removed from a page that never had one. dismissal records the outcome, but an outcome alone is not checkable: an empty list of removals reads as “nothing matched” whether the capture looked for twenty selectors or for none. So the member carries the selectors that were in force as well — the same argument that makes urlPolicies mandatory on a capture that filtered nothing. With both, the removals are a subset a reader can verify against the archived HTML, and a selector that was never valid is named rather than silently counted as a miss.

Introduction

A [[WACZ]] package says what was captured. It does not say what was missed, nor under what conditions the capture ran. Those facts are knowable only at capture time, and they are lost once the capturing process exits.

That loss matters because the archive is the thing that travels. A reader opening a package three years later has no access to the logs, the API response, or the machine that produced it. If the package cannot answer “is anything missing here?”, nothing else can.

This profile therefore requires a package to carry its own account of the capture: which bodies are absent and why, which limits were in force, which requests it declined to send and which it sent but did not keep, what it removed from the page before recording it, which certificates the origins presented. None of these change what [[WACZ]] means. They record what would otherwise be unrecoverable.

Conformance

The conformance target of this specification is a package: a single file. Conformance is a property of files, not of the software that writes them. An implementation may write conforming and non-conforming packages at different times, and this specification says nothing about which.

A conforming package is a conforming [[WACZ]] package. This specification adds requirements. It removes none, and it relaxes none.

Where this specification uses the terms MUST, MUST NOT, SHOULD and MAY, they are to be interpreted as described in [[RFC2119]].

JSON member names are case-sensitive. Where a member is described as holding a timestamp, its value MUST be an [[RFC3339]] date-time in UTC.

The browserhive:capture member

The datapackage.json of a conforming package MUST contain a member named browserhive:capture whose value is a JSON object.

This member MUST NOT be nested inside config or metadata. Those names carry meaning for [[WACZ]] readers; see .

Members of browserhive:capture not defined here MAY be present. Readers MUST ignore members they do not recognise.

Conformance declaration

browserhive:capture MUST contain a member conformsTo whose value is the URL of the version of this specification the package claims to satisfy.

"browserhive:capture": {
  "conformsTo": "https://uraitakahito.github.io/browserhive-specs/wacz-profile/1.4.0/"
}

The declaration is carried here rather than in the profile member of datapackage.json, which is not available for this purpose.

A reader MUST NOT infer conformance from the mere presence of browserhive:capture. Without conformsTo, the version a package was written against is unknown.

build

browserhive:capture MUST contain a member build identifying, in machine-readable form, the software that produced the package.

MemberTypeRequiredDescription
versionstringyesRelease identifier.
revisionstringyesSource revision the build was made from.
buildTimestringyesWhen the software was built.
"build": {
  "version": "3.4.0",
  "revision": "0776cae",
  "buildTime": "2026-08-21T04:04:09.000Z"
}

The format of version is not constrained. A build made from a release carries something like "3.4.0", while a build made between releases may carry a value that includes its distance from the last one, such as "3.4.0-12-gabc1234". Both say which build produced the package. A reader should treat the value as an identifier — equal values mean the same build — rather than parse it as a version number.

[[WACZ]] already defines software. That member is prose, and no two producers write it alike, so nothing parses it. build exists so the same facts can be read by machine without guessing at a format.

browser

browserhive:capture MUST contain a member browser describing the browser that performed the capture. It MUST contain a member product whose value is a string.

"browser": {
  "product": "Chrome/151.0.7922.137"
}

The spelling is not defined here. The value is whatever the browser names itself as, so a package produced by a different engine carries a different shape. A reader can compare two values for equality; it cannot assume a grammar.

The value MUST NOT be a placeholder. A producer that cannot obtain it MUST NOT produce a conforming package: an archive whose recording browser is unknown is outside this profile, and omitting the member is not a way to say so.

settings

browserhive:capture MUST contain a member settings recording the settings that were in force for this capture.

Values MUST be the resolved values, not the values a request asked for. A request that says nothing still runs under some setting, and it is that setting the archive has to report.

MemberTypeRequiredDescription
signaturestringyes Either "required" or "none": whether this capture was permitted to be published unsigned.
viewportobjectyes Members width and height, both integers, in CSS pixels.
devicePixelRatiosarray of numberyes The device pixel ratios the page was loaded at, in the order it was loaded. The number of entries is the number of loads.
sessionstringyes Whether the capture ran in a browser context created for it and discarded afterwards, or in one the capturing process reuses across captures. It tells a reader whether the package could depend on state left by an earlier capture.
behaviorsarray of stringyes Behaviors that actually ran, not those that were enabled.
limitsobjectyes The body caps that were in force for this capture. Members maxResponseBytes and maxTaskBytes, both integers, in bytes.
urlPoliciesarray of objectyes What the capture did about each URL pattern that was in force, in the order the patterns were evaluated. See urlPolicies.
contentTypePoliciesarray of objectyes What the capture did about each media type that was in force. See contentTypePolicies.
acceptLanguagestringno Present only when the capture set one, since it changes what origins return.
"settings": {
  "signature": "none",
  "viewport": { "width": 1280, "height": 800 },
  "devicePixelRatios": [1],
  "session": "isolated",
  "behaviors": ["autoscroll", "autofetch"],
  "limits": {
    "maxResponseBytes": 20971520,
    "maxTaskBytes": 209715200
  },
  "urlPolicies": [
    { "pattern": "*://*.example-analytics.com/*", "action": "deny" }
  ],
  "contentTypePolicies": []
}

In this example session records that the capture ran in a context of its own, and the single entry in devicePixelRatios that it loaded the page once. The two entries under behaviors are the behaviors that ran — not the ones that were enabled. acceptLanguage is absent because this capture set none. The values themselves are the producer's to choose; this profile does not define the spellings shown here.

devicePixelRatios is a list rather than a number because a capture may load the page more than once. Recording a single value would tell a reader that one rendering is absent when it is present.

The values MUST appear in the order the page was loaded, so that a reader can say not only which renderings the archive holds but which one was drawn last — artefacts that carry the page's appearance follow the state left by the final load.

A capture that loads the page twice carries one value per load. A request that set acceptLanguage shows it here as well:

"settings": {
  "signature": "required",
  "viewport": { "width": 1280, "height": 800 },
  "devicePixelRatios": [1, 2],
  "session": "isolated",
  "behaviors": ["autoscroll"],
  "acceptLanguage": "ja,en;q=0.9",
  "limits": {
    "maxResponseBytes": 20971520,
    "maxTaskBytes": 209715200
  },
  "urlPolicies": [],
  "contentTypePolicies": [{ "prefix": "video/", "action": "no-body" }]
}

The two measure different things. maxResponseBytes applies to the body of a single response; maxTaskBytes applies to the running total of the bodies written into this package.

The difference matters to a reader. maxResponseBytes weighs one response at a time, so any number of responses under it are kept and the size of the package cannot be inferred from it. maxTaskBytes watches the accumulated total, so once the line is crossed bodies are dropped regardless of size — a small response among the dropped ones is not a contradiction.

limits MUST be present even when no body was dropped. It is the only place the archive can say which line a truncated body crossed — a record of a truncation carries the fact and the actual size, but whether that size was too much is only legible next to the line.

The truncatedUrls member of completeness does not distinguish which of the two a body crossed, even though the remedies differ (one response too large versus the whole page too heavy). A reader who needs the distinction has to look outside the package, at what the capture reported.

completeness

browserhive:capture MUST contain a member completeness, the package's own account of the bodies it does not hold.

MemberTypeRequiredDescription
bodylessUrlsarray of stringyes URLs that appeared only as 304. The body never crossed the wire, so the archive cannot hold it.
truncatedUrlsarray of stringyes URLs whose body the capture dropped on reaching a size limit.
completebooleanyes true if and only if both arrays are empty.
"completeness": {
  "bodylessUrls": [],
  "truncatedUrls": [],
  "complete": true
}

A capture that lost nothing takes this shape. complete is derived from the two arrays, so two empty arrays alongside false never occur.

A capture missing bodies for both reasons:

"completeness": {
  "bodylessUrls": [
    "https://example.com/assets/app.css"
  ],
  "truncatedUrls": [
    "https://example.com/media/hero.mp4",
    "https://example.com/media/poster.png"
  ],
  "complete": false
}

Here the stylesheet appeared only as a 304: its body never crossed the wire, so the archive cannot hold it. The video and its poster were dropped at a limit. A URL dropped twice still appears once.

Both arrays MUST be sorted, so that two captures of the same page produce comparable output.

The two are separate members because they call for different responses. A 304 is the origin's doing and cannot be recovered from here; a body dropped at a limit is this side's doing and returns if the limit is raised.

Responses that legitimately carry no body MUST NOT be listed: redirects, 204, and bodies omitted by a content-type filter the requester configured. Reporting the last of these would tell the reader only what they had already decided.

coverage

browserhive:capture MAY contain a member coverage describing how far down the page the capture reached.

MemberTypeRequiredDescription
scrollExhaustedbooleanyes true when scrolling stopped at a step limit rather than at the end of the page — that is, when material below was never requested.
scrollStepsnumberyesSteps actually taken.
scrolledPxnumberyes Approximate depth reached: steps multiplied by viewport height.
"coverage": {
  "scrollExhausted": true,
  "scrollSteps": 40,
  "scrolledPx": 32000
}

The three values are read together. scrolledPx is scrollSteps multiplied by the viewport height recorded in settings — 40 × 800 here — so a reader can check one against the other. And because scrollExhausted is true, the 32,000 pixels are where the capture stopped, not where the page ended: whatever lay below was never requested, and therefore leaves no trace anywhere else in the package.

coverage MUST be absent when no scrolling behavior reported. It MUST NOT be written with default values: scrollExhausted: false asserts that the page was covered, and a capture that never scrolled has not earned that claim.

dismissal

browserhive:capture MAY contain a member dismissal recording what the capture removed from the page before recording it, and what it looked for.

A capture that removes a consent banner changes the document it then archives. Nothing else in the package says so: the removed element leaves no trace in the WARC, in the index, or in the rendered snapshot, and a reader cannot tell it from a page that never carried one.

MemberTypeRequiredDescription
selectorsarray of objectyes The selectors that were in force, in the order they were evaluated. Members framework and selector, both strings.
heuristicobjectyes The thresholds that governed removal of elements no selector named. Members enabled (boolean), minViewportCoverageRatio (number) and minZIndex (number).
frameworkstring or nullno The framework of the first entry that matched, or "heuristic" when only the thresholds fired. null when nothing was removed.
removedSelectorsarray of stringno The selectors whose element was removed. A subset of selectors, in evaluation order.
unusableSelectorsarray of stringno Entries of selectors the reader of the page could not evaluate. These matched nothing because they were not valid selectors, which is not the same claim as matching nothing.
removedOverlaysnumberno Elements removed by the thresholds rather than by a selector.
devicePixelRationumberno Which load this observation is of; one of the entries of devicePixelRatios.
unreadabletrueno Present when removal was attempted but could not be carried out.
"dismissal": {
  "selectors": [
    { "framework": "OneTrust", "selector": "#onetrust-banner-sdk" },
    { "framework": "Cookiebot", "selector": "#CybotCookiebotDialog" }
  ],
  "heuristic": { "enabled": true, "minViewportCoverageRatio": 0.3, "minZIndex": 1000 },
  "framework": "OneTrust",
  "removedSelectors": ["#onetrust-banner-sdk"],
  "unusableSelectors": [],
  "removedOverlays": 0,
  "devicePixelRatio": 1
}

dismissal MUST be absent when no removal was attempted. Absence therefore means the page was archived as it was served; it does not mean nothing was found.

When dismissal is present, selectors and heuristic MUST be present even when nothing was removed. They are what makes the outcome checkable: an empty removedSelectors next to twenty entries says the page carried no banner any of them names, while the same empty list next to no entries says only that nothing was looked for. This is the argument that makes urlPolicies mandatory on a capture that filtered nothing.

A package MUST NOT carry unreadable alongside framework, removedSelectors, unusableSelectors or removedOverlays. A removal that could not be carried out has observed nothing, and writing an empty result beside it would assert that the page held no banner.

Scrolling also changes what the capture sees, and what it did is already recorded — by coverage, which states how far down the page it reached. A second account of the same act here would be a copy, and one of the two would eventually be the stale one.

A capture that loads the page more than once removes elements on each load; removal is not consent, so a consent framework re-renders on the next load. removedSelectors is an observation of one document at one moment, not a union across loads, and devicePixelRatio is what ties it to the load it came from.

tls

browserhive:capture MAY contain a member tls recording what the browser saw of each HTTPS connection. If present it MUST contain hosts and chains.

tls.hosts

An object keyed by host. Each value is either an object with the members below, or null.

MemberTypeRequiredDescription
protocolstringyesNegotiated TLS version.
cipherstringyesNegotiated cipher.
subjectstringyesSubject common name, as the browser reported it.
sanarray of stringyes The host names the certificate is valid for (subjectAltName). Empty for a certificate that carries none.
issuerstringyesIssuer common name.
validFromstringyesStart of the certificate's validity period.
validTostringyesEnd of that period.
chainRefstringnoKey into tls.chains. Absent when no chain could be obtained.

A host reached over HTTPS whose connection revealed no details MUST be recorded with the value null. Omitting the key instead would read as “this host was plain HTTP”, which is a different fact.

Host name matching uses san, not subject. subject records the common name, which current readers do not match against — the value of subject MUST NOT be read as “this is the host name it was accepted for”.

A missing chainRef MUST NOT be read as “this host presented no certificate”. Chain retrieval can fail for reasons unrelated to the host.

tls.chains

An object whose keys are the values used by chainRef and whose values are arrays of base64-encoded DER certificates. Each array MUST be ordered leaf first.

Hosts sharing a chain SHOULD share one entry. Certificates are commonly reused across the hosts of one page, and storing a copy per host inflates the package for no gain.

Chains are stored rather than referenced. Certificate Transparency holds the same bytes, but evidence that depends on a third-party service remaining reachable is weaker for that dependence — and a private CA's chain, which is the case worth examining, was never logged there at all.

"tls": {
  "hosts": {
    "example.com": {
      "protocol": "TLS 1.3",
      "cipher": "AES_128_GCM",
      "subject": "example.com",
      "san": ["example.com", "www.example.com"],
      "issuer": "Example Issuing CA",
      "validFrom": "2026-06-30T20:37:58.000Z",
      "validTo": "2026-09-28T21:37:55.000Z",
      "chainRef": "9a57318f8c89976c"
    },
    "cdn.example.net": {
      "protocol": "QUIC",
      "cipher": "AES_128_GCM",
      "subject": "*.example.net",
      "san": ["*.example.net", "example.net"],
      "issuer": "Example Issuing CA",
      "validFrom": "2026-08-05T20:42:14.000Z",
      "validTo": "2026-10-28T20:42:13.000Z",
      "chainRef": "c52653c92800f6fd"
    },
    "img.example.net": {
      "protocol": "TLS 1.3",
      "cipher": "AES_128_GCM",
      "subject": "*.example.net",
      "san": ["*.example.net", "example.net"],
      "issuer": "Example Issuing CA",
      "validFrom": "2026-08-05T20:42:14.000Z",
      "validTo": "2026-10-28T20:42:13.000Z",
      "chainRef": "c52653c92800f6fd"
    },
    "api.example.org": {
      "protocol": "TLS 1.3",
      "cipher": "AES_256_GCM",
      "subject": "api.example.org",
      "san": ["api.example.org"],
      "issuer": "Example Issuing CA",
      "validFrom": "2026-05-14T09:03:11.000Z",
      "validTo": "2026-08-12T09:03:10.000Z"
    },
    "beacon.example.org": null
  },
  "chains": {
    "9a57318f8c89976c": [
      "MIIDbzCCAxSgAwIBAgIRALaMq73qMP3mE30B…",
      "MIICjDCCAhKgAwIBAgIQf5wJDMcM1Y2ZBpFB…"
    ],
    "c52653c92800f6fd": [
      "MIIIUDCCBzigAwIBAgIQHnJSlrCYIdkSw8c/…",
      "MIIFCzCCAvOgAwIBAgIQVJ5xIVMKmZ0sNHxs…"
    ]
  }
}

The example records five hosts. Each of the rules above shows up in it.

Values in chains are base64-encoded DER. They are cut short here for readability; a real certificate is a string of roughly 1–2 KB.

URL and media type policies

A capture may treat some requests differently from the rest: it may decline to send them, decline to record them, or record them without their bodies. browserhive:capture.settings MUST record what it did, so that a reader can tell which of those happened without access to the capture's configuration.

urlPolicies

urlPolicies is an ordered array. Each entry is an object with two members:

MemberTypeRequiredDescription
patternstringyes The pattern that was in force, spelled exactly as the capture was configured with it.
actionstringyes One of the values below.
actionRequest sentWhat the package holds
denyno A record of the request only. Nothing left the capturing machine for that URL, so no origin learned of the capture through it.
no-archiveyes A record of the request only. The origin answered; the package does not keep the answer.
no-bodyyes The request and response records, without the response body.

The array MUST be present even when it is empty, and an empty array MUST mean that no policy was in force. Without that rule a reader cannot separate a page that requested nothing unusual from a capture that declined to record what it requested, and those are different claims about the same package.

The order is the order in which the capture evaluated the patterns, and a capture MUST apply the first entry whose pattern matches. Recording the order is what lets a reader answer “which policy applied to this URL?” when more than one pattern would match it.

"urlPolicies": [
  { "pattern": "*://cdn.example.com/tracker.js", "action": "deny" },
  { "pattern": "*://cdn.example.com/*",          "action": "no-body" }
]

In this example a request for tracker.js is never sent, while everything else on the same host is fetched and recorded without its body. Reversing the two entries would change what the capture did: the broader pattern would match first, and the tracker would be fetched.

contentTypePolicies

contentTypePolicies is an array whose entries carry a prefix (matched against the response's media type) and an action. no-body is the only action this version defines: a media type is known only once the response has arrived, so deny cannot apply.

The array MUST be present even when it is empty, for the same reason as urlPolicies.

"contentTypePolicies": [
  { "prefix": "video/", "action": "no-body" }
]

Records of withheld requests

For each request to which a deny or no-archive policy applied, the package MUST contain a WARC metadata record, hereafter a record of a withheld request. Its WARC-Target-URI MUST be the request URL, and its application/warc-fields body MUST contain:

FieldRequiredDescription
actionyes The action that applied: deny or no-archive. This is the field that says whether the request was sent.
patternyes The pattern that matched, spelled exactly as it appears in urlPolicies, so that a reader can find it there.
methodyes The request method.
resourceTypeno What the page was fetching, when the capture knows it.
WARC/1.1
WARC-Type: metadata
WARC-Target-URI: https://www.example-analytics.com/collect?v=2
WARC-Date: 2026-09-06T02:14:31.882Z
Content-Type: application/warc-fields
Content-Length: 112

action: deny
pattern: *://*.example-analytics.com/*
method: POST
resourceType: XHR

A record of a withheld request carries no response and no body. It asserts that the page asked for the URL and that this profile's producer decided what to do about it — not that the request failed, and not that the origin returned nothing. A capture that could not reach an origin MUST NOT report it this way.

A no-body policy produces no record of this kind. The request and response records are written as usual, and the absent body is reported the way any other absent body is.

Records of withheld requests are not required to appear in the package's CDXJ index. A reader looking for them reads the WARC.

The storage member

browserhive:capture MUST contain a member storage recording what web storage the captured origins held. The member records the presence and shape of that storage; it MUST NOT contain stored values.

Web storage is recorded because it cannot be reconstructed later. It never crosses the network, so no WARC record holds it: a page that renders from localStorage renders from data the archive does not contain. Without this member a reader cannot tell such a page from one that genuinely had nothing to show, and completeness does not help — it reports bodies the package does not hold, and storage never became a body.

{
  "browserhive:capture": {
    "storage": {
      "profile": "browserhive:storage/1",
      "stage": "after-behaviors",
      "valuesRecorded": false,
      "origins": [
        {
          "origin": "https://www.example.co.jp",
          "local": { "keys": 2, "bytes": 32, "digest": "sha256:b58f31fdc511fb94c356ae7f6d64ac71e5bf056584653c3f3ec61a1f08bfaa0f" },
          "session": { "keys": 1, "bytes": 13, "digest": "sha256:8ce272dea9fc6d7902b23f10411da49a7fb2e3c954ba9f1580cc087292eaafc2" }
        },
        { "origin": "https://ads.example.net", "unreadable": true }
      ]
    }
  }
}
  

Members

storage MUST contain profile, stage, valuesRecorded and origins.

Origin entries

Each entry MUST contain origin, the origin in the serialised form of [[WACZ]]'s enclosing document — scheme, host and, when not the default, port.

Web storage is partitioned by origin, and a page routinely embeds documents from origins other than its own — an advertisement, an analytics frame, an embedded player. Each of those has its own storage, which the page's own origin cannot see. So this is not a record of “the page's storage”: it is one entry per origin the capture was able to observe, and an entry for a third party is the expected case rather than an anomaly.

An origin that cannot hold storage at all — an opaque origin, such as a document at about:blank or a data: URL — has nothing to record and does not appear.

An entry MUST contain either both local and session, or the single member unreadable with the value true. The second form says the capture could not read that origin's storage. It is distinct from an origin whose storage was empty, which is written as areas with "keys": 0. Recording them the same way would put back the ambiguity this member exists to remove.

An entry here and a line in storage/origins.jsonl are two halves of one record. The entry says what an origin's storage looked like — how many keys, how many bytes, which digest — and is always present. The line says what it contained, and is present only when the values were asked for; valuesRecorded states which of the two a package holds. An origin recorded as unreadable has no line, because there is nothing to put in one.

local and session each MUST contain:

A digest MUST NOT be treated as a commitment to the value. The values are frequently low entropy — a flag, a theme name, a timestamp — and such a digest is trivially reversed by guessing.

The storage directory

[[WACZ]] permits additional files and directories, provided they stay out of archive, indexes and pages, and are listed in the resources section of datapackage.json. A conforming package MAY use that allowance for a second entry.

A conforming package MAY contain an entry at storage/origins.jsonl. When present, it MUST be declared in resources like any other file, and it MUST be JSON Lines: one JSON object per line, each object recording the web storage one origin held.

When the entry is present, valuesRecorded MUST be true. When it is absent, valuesRecorded MUST be false. A reader can therefore tell whether a package carries stored values without opening the archive body.

{
  "profile": "browserhive:storage/1",
  "origin": "https://www.example.co.jp",
  "takenAt": "2026-09-06T01:22:14.881Z",
  "stage": "after-behaviors",
  "areas": {
    "local": { "feed.cache": "[{\"id\":1}]", "ui.theme": "dark" },
    "session": { "nav.scroll": "820" }
  }
}
  

Each line MUST contain profile, origin, takenAt as an [[RFC3339]] timestamp, stage and areas. areas MUST contain local and session, each an object mapping key to value exactly as the area held them. An origin recorded as unreadable in storage MUST NOT have a line here.

The accessibility directory

[[WACZ]] permits additional files and directories, provided they stay out of archive, indexes and pages, and are listed in the resources section of datapackage.json. A conforming package uses that allowance for one entry.

A conforming package MAY contain an entry at accessibility/axtree.jsonl. When present, it MUST be declared in resources like any other file, and it MUST be JSON Lines: one JSON object per line, each object recording one snapshot of the accessibility tree the browser computed for the captured page.

The tree is recorded because it cannot be reconstructed later. It is derived from the DOM and the layout tree, it contains nodes with no corresponding DOM node (CSS generated content, list markers), and aria-owns reparents nodes so its shape differs from the document tree. Replaying the archive against a future browser produces a different tree. This is the same reason tls is recorded rather than referenced.

Snapshot members

Each line MUST contain the members profile, url, takenAt, stage and tree.

A line SHOULD also carry devicePixelRatio, viewport and nodes — respectively the pass the snapshot came from, the viewport in effect, and the number of nodes before pruning. The last of these lets a reader see how much was discarded.

{
  "profile": "browserhive:axtree/1",
  "url": "https://www.iana.org/",
  "takenAt": "2026-08-22T02:55:56.759Z",
  "stage": "after-behaviors",
  "devicePixelRatio": 1,
  "viewport": { "width": 1280, "height": 800 },
  "nodes": 255,
  "tree": [
    {
      "role": "RootWebArea",
      "name": "Internet Assigned Numbers Authority",
      "children": [
        { "role": "heading", "name": "Domain Names", "level": 2 },
        { "role": "image", "name": "logo",
          "url": "https://www.iana.org/_img/iana-logo-header.svg" }
      ]
    }
  ]
}
    

Shown across several lines for readability. In the file each snapshot occupies exactly one line.

Node shape

Every node MUST carry role. A node MUST NOT carry any member other than role, name, level, url, checked, expanded, disabled, required, value and children.

Two of those carry more weight than the rest:

A member whose value is absent, empty or false MUST NOT be written. Writing defaults on every node costs more than the content it surrounds.

Pruning

Nodes the browser marks as ignored MUST NOT appear in tree. Their children MUST be kept and attached to the nearest retained ancestor.

This is what keeps hidden text out of the record: subtrees removed by aria-hidden or display: none are ignored nodes, so they never reach the file. A reader can therefore treat the tree as a description of what was visible, which is a stronger claim than anything the WARC payload alone supports.

Nodes whose role is generic, none or InlineTextBox MUST NOT appear in tree either, and their children MUST be kept in the same way. These roles carry no meaning of their own; keeping them doubles the depth of the tree without adding anything a reader can use.

A producer MUST NOT drop nodes for being outside the viewport. Doing so is reasonable for an agent, which can only act on what it can see, but an archive that discards the part of the page nobody scrolled to is not a record of that page.

Constraints inherited from replay

The requirements in this section do not come from [[WACZ]]. They come from what existing readers of WACZ packages do, and a package that ignores them is unreadable in practice even though it satisfies the base specification.

These are recorded here so a package author can satisfy them. Why each one exists is a matter of implementation history and is documented outside this specification.

Security and privacy considerations

tls is not proof of provenance. Certificates are public: anyone can obtain one and present a copy. Holding these fields says nothing about who answered a request. Two narrower things are worth the storage — issuer changes when a connection is intercepted, and the validity period can be checked against the time the capture claims — and neither survives the capture unless it is written down.

completeness asserts absence, not correctness. It reports bodies this package empirically does not hold. It does not assert that the bodies it does hold are what the origin served.

A package records URLs in completeness and host names in tls. Where the captured page carried personal data in URLs, those URLs travel with the archive.

The accessibility tree carries the names the browser computed for elements, which are drawn from the visible text of the page. That text is already present in the WARC payload, so recording the tree exposes nothing the package did not already hold. The pruning rules make its exposure narrower than the payload's: ignored nodes are excluded, so text hidden by aria-hidden or display: none does not appear in the tree even though it appears in the response body.

storage exposes nothing the package did not already hold. It records key counts, byte counts and digests. It does not record values, and a digest is not a value — though, as noted above, a digest over low-entropy content is not a hiding place either.

A package that also contains storage/origins.jsonl does expose more. Web storage routinely holds bearer tokens, session identifiers and unsent drafts, none of which appear in any WARC payload; this is the one place in this profile where a conforming package can carry material the capture never received over the network. Producers SHOULD leave the directory out by default. A reader can tell the two cases apart without opening the archive body, because valuesRecorded says which one this is.

browser names the build that did the recording, and it is required. A producer that presents a different identity to the origin — a substituted User-Agent, for example — does not thereby change this member: what is sent to the server and what the package reports are separate. Operators who treat their build version as sensitive should know that a conforming package discloses it.