📦 Contents

Chapter 3: Data Format II — Storage, Verification and Credit Chain of Evidence

Chapter 2 described the records a direct air capture (DAC) plant produces about itself: what the facility is, what it captured minute by minute, and how its sorbent is ageing. Those records describe gross capture. None of them, on its own, shows that a tonne of CO2 has left the atmosphere for good. That claim is made further downstream, when captured CO2 is handed to a storage operator, when an independent verifier examines the evidence, and when a registry issues a carbon removal credit that someone eventually retires against their own emissions.

This chapter covers the three records that carry that downstream story, the Storage Event, the Verification Report and the Carbon Removal Credit, all defined in PHASE-2-DATA §5–§7 of the WIA Direct Air Capture standard (WIA-ENE-050). It then looks at how they link into a chain of evidence that a third party can follow backwards from a retired credit to individual meter readings. It also covers the response and error envelopes the standard defines for its API (PHASE-2-DATA §8), and the validation and versioning rules that govern all the schemas (§9–§10).

The standard defines each record as a separate JSON Schema (draft 2020-12). What it does not yet define well are the links between records. The storage event has no field pointing to the capture data it came from; the verification report has no field listing the storage events it covered; and the credit schema has no field for the verification report that backs it, even though the issuance API request carries one. Much of this chapter closes those gaps without breaking conformance with version 1.0; fields not in the specification are marked as this book's recommendation.

All examples use the fictional facility dac-example-reykjanes-01 introduced in Chapter 2, a fictional storage site and a fictional verifier. The numbers are illustrative and chosen to be internally consistent; they are not operating data from any real plant.

3.1 The Storage Event Record

A storage event records one delivery of captured CO2 into a storage pathway: an injection batch at a geological or mineralization site, or a transfer to a utilization process. It is the first record in the chain that concerns where the CO2 went rather than how it was captured, and it is the natural anchor for a removal claim. The amount that counts towards net removal is the amount stored, not the amount captured (Chapter 1, §1.4).

3.1.1 Identity and required fields

Only four fields are required: eventId, facilityId, timestamp and amount. The event ID must match ^storage-[a-z0-9-]+$, the same lowercase-and-hyphens style as the facility ID pattern ^dac-[a-z0-9-]+$ in Chapter 2. amount is a number in tonnes of CO2 with a minimum of 0, and timestamp is an RFC 3339 date-time.

The schema does not say which moment the timestamp marks. An injection that runs for 21 hours has a start, an end and a moment when the storage operator confirmed the quantity, and those can fall on different days or even in different reporting periods. This book recommends that timestamp mean the end of injection in UTC, and that the start be recorded explicitly (see the example below). The period in which an event is counted then follows a clear rule, and a verifier can reproduce it.

The facility ID in a storage event has no pattern constraint, unlike the one in the facility record. A validator should nonetheless apply the facility pattern and, more importantly, check that the ID resolves to a known facility record. JSON Schema cannot check the second rule; §3.6 returns to this.

3.1.2 Storage site, injection data and CO2 quality

The optional blocks describe the delivery:

Two unit issues from Chapter 2 come back here. First, purity is a percentage while contaminants are in ppm, and the schema does not say whether the ppm are by volume (mole fraction) or by mass. For CO2 streams, gas specifications are normally given on a molar basis, but the record should say so, for example with a basis field in an extension. Second, the two scales must agree. A purity of 99.5 % leaves 0.5 %, or 5,000 ppm, for everything else. If the contaminant list adds up to far less than the balance, the list is incomplete, and that should be declared rather than left for a verifier to discover. The specification's own storage example states a purity of 98.5 % but lists contaminants totalling only 1,500 ppm, which accounts for a tenth of the balance.

3.1.3 Permanence and the monitoring plan

The permanence block has three fields: guaranteedYears (an integer with a minimum of 1000), monitoringPlan (a string) and insurancePolicy (a string). It is here that the schema comes closest to making a scientific claim, so it needs careful reading.

The minimum: 1000 constraint means that a storage event cannot validate with a permanence declaration shorter than 1,000 years. That fits the foundation document's classes for geological storage and mineralization, but it does not fit utilization, which is also a valid storageMethod and which PHASE-3 classifies case by case. The credit schema, by contrast, sets permanence.years to a minimum of 100. The two limits are not consistent. An implementer has two honest choices for a utilization event: omit the guaranteedYears field (it is optional) and describe the durability class in the monitoring plan, or declare a profile that excludes utilization events from the permanence block. Writing 1000 to pass validation when the pathway does not support it would put a false statement into the evidence chain.

More generally, guaranteedYears is a declared durability class, not a measurement. What makes it credible is the monitoring plan behind it. PHASE-3 §3.1 sets out, for example, a post-injection monitoring period for geological storage. The schema stores the plan as free text. This book recommends storing a reference instead, such as a document ID with a version number or a URL with a content hash, so that the plan in force at injection time can be retrieved decades later. The same applies to insurancePolicy.

The verification sub-block inside the storage event (verifier ID, date, standard, certificate URL) records confirmation of the storage itself by the site operator's verifier. It is separate from the verification report in §3.2, which covers a whole reporting period for the DAC facility. The specification's example fills standard with ISO 27916. That standard deals with CO2 storage in connection with enhanced oil recovery, so it is not appropriate for a DAC mineralization delivery. Use the standard the storage verifier actually applied.

3.1.4 An example and its consistency checks

The following event records one mineralization injection from the fictional facility. Fields under ext are extensions recommended by this book; the rest are version 1.0 fields.

{
  "eventId": "storage-example-reykjanes-01-20260314-001",
  "facilityId": "dac-example-reykjanes-01",
  "timestamp": "2026-03-14T21:00:00Z",
  "amount": 12.6,
  "storageMethod": "mineralization",
  "storageSite": {
    "siteId": "site-example-basalt-01",
    "name": "Example Basalt Injection Site 1",
    "location": { "latitude": 63.9, "longitude": -22.4, "depth": 800 }
  },
  "injectionData": {
    "temperature": 30,
    "pressure": 25,
    "rate": 0.6,
    "duration": 21
  },
  "co2Quality": {
    "purity": 99.5,
    "contaminants": [
      { "compound": "N2",  "concentration": 3200 },
      { "compound": "O2",  "concentration": 900 },
      { "compound": "H2O", "concentration": 600 },
      { "compound": "Ar",  "concentration": 300 }
    ]
  },
  "permanence": {
    "guaranteedYears": 1000,
    "monitoringPlan": "mp-example-basalt-01 v2",
    "insurancePolicy": "POL-EXAMPLE-0001"
  },
  "verification": {
    "verifierId": "storage-verifier-example",
    "verificationDate": "2026-03-16",
    "standard": "site operator's storage monitoring procedure (example)"
  },
  "ext": {
    "injectionStart": "2026-03-14T00:00:00Z",
    "meterReadingIds": ["mtr-example-ct01-20260314"],
    "contaminantBasis": "mol/mol"
  }
}
Listing 3-1. An illustrative storage event. All values are invented.

Several consistency checks apply that JSON Schema cannot express:

  1. Rate × duration = amount. Here 0.6 t/h × 21 h = 12.6 t. Allow a declared tolerance; the authoritative amount is the integrated meter reading.
  2. Amount against the custody-transfer meter. PHASE-3 requires mass-flow metering with ±2 % uncertainty at the handover point. The event amount should agree with the referenced meter totals within that uncertainty. If there is a difference, the lower figure should be used for crediting until it is resolved.
  3. Purity against contaminants. 100 − 99.5 = 0.5 %, or 5,000 ppm, and the listed contaminants sum to 3,200 + 900 + 600 + 300 = 5,000 ppm. The list is complete.
  4. Timestamp ordering. The injection start is before timestamp, and the storage verification date is on or after it.

3.2 The Verification Report Record

The verification report is where an independent party states how many tonnes the evidence supports for a period. Its required fields are reportId (pattern ^verify-[a-z0-9-]+$), facilityId, period (an object with required startDate and endDate, both of format date) and verifiedAmount (a number ≥ 0 in tonnes).

3.2.1 What the verified amount means

The schema describes verifiedAmount as "verified tons CO2 captured". Read literally, that is gross capture, but a credit must be backed by net removal, and Chapter 1 showed how far the two can differ. A consumer cannot tell from the number alone which one it is. This book recommends that a report used for crediting always carry the net figure in verifiedAmount, after deductions for energy, embodied emissions, losses and any conservative uncertainty deduction, and that the gross figure and each deduction be recorded as separate extension fields. Chapter 6 covers the uncertainty treatment in detail.

The uncertainty field is a number described as a percentage. It does not say at what confidence level the figure is given, or whether it is relative to the gross or the net amount. The foundation document asks for storage verification at more than 95 % confidence and a capture measurement uncertainty of ±5 %. A report should state the confidence level explicitly, for example as ext.uncertaintyConfidence: 0.95, so that the figure can be compared with those targets.

3.2.2 Period boundaries

Both dates are calendar dates with no time zone, and the schema does not say whether endDate is inclusive. Two consecutive quarterly reports can therefore overlap by a day or leave a gap, and a storage event timestamped late on the boundary date could be counted twice or not at all. This book recommends reading both dates as UTC calendar dates, with both ends inclusive, and assigning each storage event to a period by its timestamp (end of injection) under that rule. Consecutive reports should then satisfy next.startDate = previous.endDate + 1 day.

3.2.3 Verifier, standards and methodology

The verifier object records the organization, its accreditation and the lead auditor. standards is a list of strings, and methodology is free text. For greenhouse-gas claims, the usual references are ISO 14064-2 for project-level quantification and ISO 14064-3 for verification, with ISO 14065 setting requirements for verification bodies. The specification's schema description suggests ISO 27916 as an example value. As explained in §3.1.3, that standard covers storage associated with enhanced oil recovery and should not be listed as the basis of a DAC removal verification unless that is what was actually applied.

3.2.4 Findings and conclusion

Findings are a list of objects with a category, a free-text description and a severity. The conclusion is a separate field, conclusion, not status. It takes the values verified, verified-with-conditions or not-verified. The schema does not link severity to conclusion, so the rules in Table 3-1 are this book's recommendation for a validator.

Table 3-1. Finding categories and severities, with recommended effect on the conclusion
CategoryMeaningSeverity used?Recommended effect on conclusion
conformanceA requirement was checked and metNormally noNone
non-conformanceA requirement was not metminor / major / criticalMinor: at most verified-with-conditions. Major: verified-with-conditions only if the verifier quantified and deducted the effect, otherwise not-verified. Critical: not-verified
observationSomething noted that is not a breachOptionalNone
recommendationSuggested improvementOptionalNone

A report concluding verified-with-conditions should state the conditions somewhere machine-readable. The schema has no field for them, so a finding with category non-conformance and a closing date in an extension is a workable pattern. nextVerificationDue gives the date of the next verification and lets a registry flag lapsed facilities. reportUrl points to the human-readable report, and because the JSON is only a summary of that document, the URL should be paired with a content hash.

3.2.5 An example

{
  "reportId": "verify-example-reykjanes-01-2026",
  "facilityId": "dac-example-reykjanes-01",
  "period": { "startDate": "2026-01-01", "endDate": "2026-12-31" },
  "verifiedAmount": 780,
  "uncertainty": 4.0,
  "verifier": {
    "organization": "Example Verification Body Ltd.",
    "accreditation": "ISO 14065 (example accreditation reference)",
    "auditor": "restricted"
  },
  "standards": ["ISO 14064-2", "ISO 14064-3"],
  "methodology": "example-dac-methodology v1",
  "findings": [
    { "category": "non-conformance", "severity": "minor",
      "description": "Calibration certificate for flow meter CT01 expired before period end; recalibrated, no correction required." },
    { "category": "observation",
      "description": "Grid emission factor source not versioned in energy records." }
  ],
  "conclusion": "verified-with-conditions",
  "verificationDate": "2027-02-18",
  "nextVerificationDue": "2028-02-18",
  "ext": {
    "grossStored": 1000.0,
    "deductions": { "energy": 150, "embodied": 50, "losses": 10, "uncertainty": 10 },
    "uncertaintyConfidence": 0.95,
    "storageEventIds": ["storage-example-reykjanes-01-20260314-001", "..."],
    "reportSha256": "..."
  }
}
Listing 3-2. An illustrative verification report. The gross figure and deductions reuse the invented net-removal example from Chapter 1, plus an illustrative 10 t uncertainty deduction.

The arithmetic can be checked from the record: 1,000 − 150 − 50 − 10 − 10 = 780. A registry receiving this report can recompute verifiedAmount from the extension fields and reject the report if they disagree. It can also sum the amounts of the listed storage events and check that the total equals grossStored.

3.3 The Carbon Removal Credit Record

The credit schema describes a unit that a registry has issued. Its required fields are creditId, facilityId, amount (tonnes, ≥ 0) and vintage (an integer, ≥ 2020, described as the year of removal). Optional fields cover the registry's identifiers, ownership, durability, co-benefits, price and an on-chain representation.

Table 3-2. Carbon Removal Credit fields and implementation notes
FieldType and constraintNotes
creditIdstring, no patternThe only record ID in PHASE-2-DATA without a pattern. This book recommends ^credit-[a-z0-9-]+$ for consistency, applied in a profile.
serialNumberstringThe registry's serial. Registries commonly serialise per tonne and express a block as a range; one string cannot show whether it is a single serial or a range. See §3.4.3.
amountnumber ≥ 0A fractional amount is schema-valid, but registries issue whole units. Validate as an integer at issuance.
vintageinteger ≥ 2020Year of removal. For DAC, take it from the storage event timestamps, not the capture dates. A period that crosses a year boundary produces two vintages. There is no upper bound, so reject future years.
registryenum: Puro.earth, Verra, Gold Standard, ACR, OtherValues contain a dot and a space, so they are labels rather than identifiers. With Other, record the registry name in an extension.
methodologystringName and version of the crediting methodology. It should match the verification report's methodology.
issuanceDatedateMust not be earlier than the backing report's verificationDate.
statusenum: issued, transferred, retired, cancelledLifecycle state. Transition rules are in §3.4.4.
ownerstringCurrent holder. Changes on transfer and becomes fixed at retirement. May identify an organization or an individual.
permanence.yearsinteger ≥ 100Durability claimed for the credit. Inconsistent with the storage event's minimum of 1000 (§3.1.3); the credit value should not exceed the minimum across its backing storage events.
permanence.guaranteestringFree-text description of how the durability claim is guaranteed. The schema sets no format, so treat it as a description, not as evidence.
cobenefitsarray of stringsFree text. Keep it out of any quantity calculation.
priceobject: amount, currency (^[A-Z]{3}$)The pattern accepts any three capital letters. Check against the ISO 4217 code list. Price is commercial data and belongs in the partner or private access tier (Chapter 7).
blockchainobject: network, contractAddress, tokenId, transactionHashOptional on-chain mirror. The registry record stays authoritative; Chapter 6 discusses when a chain adds value.

The most important omission is a reference to the evidence. The issuance endpoint POST /credits/issue (PHASE-4 §2) takes a verificationReportId in its request, but the credit record itself has nowhere to keep it. Once issued, a credit is linked to its facility and nothing else, so an auditor cannot tell which verification backs it. The recommendation in §3.4 is to store the report ID in the credit record.

3.4 Linking the Records into a Traceable Chain

PHASE-2-DATA lists seven data categories. Five have their own record schema (facility, operational, sorbent, storage, verification) and one more covers credits. The seventh, energy data, is carried inside the operational record's energy block and inside the facility record rather than as a separate record. Figure 3-1 shows how the records relate. Lines drawn with | are references that exist in version 1.0; lines drawn with : are references that this book recommends adding as extensions.

                    +-------------------------+
                    |  Facility               |
                    |  dac-...                |
                    +-------------------------+
                     ^        ^        ^     ^
          facilityId |        |        |     | facilityId
                     |        |        |     |
 +-----------------------+    |   +-----------------------+
 | Capture record        |    |   | Sorbent material      |
 | (operational + energy)|    |   | sorbent-...           |
 +-----------------------+    |   +-----------------------+
            ^                 | facilityId
            : meterReadingIds |
            :                 |
 +-----------------------+----+
 | Storage event         |
 | storage-...           |
 +-----------------------+
            ^
            : storageEventIds         facilityId (all records)
            :
 +-----------------------+
 | Verification report   |
 | verify-...            |
 +-----------------------+
            ^
            : verificationReportId  (present in the issuance API
            :                        request only)
 +-----------------------+
 | Carbon removal credit |
 | credit-...            |
 +-----------------------+

  |  reference defined in v1.0      :  recommended extension
Figure 3-1. Entity relationships among the DAC record types. Every record carries facilityId; the evidence links from credit back to capture are extensions.

3.4.1 Identifier patterns

Links are only as reliable as the identifiers they use. Table 3-3 collects the identifier rules. IDs should be minted once by the system that creates the record, never reused, and never derived from mutable data such as a site name.

Table 3-3. Identifier patterns across the record types
RecordID fieldPattern in v1.0Minted by
FacilityfacilityId^dac-[a-z0-9-]+$Operator, once per facility
Capture recordnone (keyed by facilityId + timestamp)—Edge collector
Sorbent materialsorbentId^sorbent-[a-z0-9-]+$Operator or supplier
Storage eventeventId^storage-[a-z0-9-]+$Operator or storage partner, as agreed
Verification reportreportId^verify-[a-z0-9-]+$Verifier
Carbon removal creditcreditIdnone (book recommends ^credit-[a-z0-9-]+$)Issuing platform; the registry separately mints serialNumber

Two points follow from the table. First, capture records have no ID of their own. A storage event can therefore refer to them only by a time range, or, as recommended here, by the IDs of custody-transfer meter readings, which are the quantity that actually crossed the plant boundary. Second, the patterns allow only lowercase letters, digits and hyphens, so external identifiers such as registry serial numbers, which often contain capitals, cannot be used directly as WIA IDs and must be stored in their own fields.

3.4.2 A walk through the chain

With the recommended references in place, the evidence can be followed backwards from a credit:

{
  "creditId": "credit-example-reykjanes-01-2026-a",
  "serialNumber": "EXREG-2026-000001..EXREG-2026-000780",
  "facilityId": "dac-example-reykjanes-01",
  "amount": 780,
  "vintage": 2026,
  "registry": "Other",
  "methodology": "example-dac-methodology v1",
  "issuanceDate": "2027-03-10",
  "status": "issued",
  "owner": "org-example-operator",
  "permanence": { "years": 1000 },
  "ext": {
    "registryName": "Example Registry",
    "verificationReportId": "verify-example-reykjanes-01-2026",
    "serialRange": { "prefix": "EXREG-2026-", "first": 1, "last": 780 }
  }
}
Listing 3-3. An illustrative credit issued against the report in Listing 3-2. The registry and serial format are invented.
  1. The credit names its verification report in ext.verificationReportId, and its amount, 780, equals the report's verifiedAmount.
  2. The report lists the storage events it covered in ext.storageEventIds. Their amounts sum to grossStored (1,000 t in the example), and every event timestamp falls within the report period under the inclusive-date rule in §3.2.2.
  3. Each storage event names the custody-transfer meter readings in ext.meterReadingIds. Their totals agree with the event amount within the ±2 % meter uncertainty.
  4. The meter readings and the capture records for the same interval both belong to dac-example-reykjanes-01. The capture records carry the energy data from which the energy deduction was calculated.

Each step is mechanical: resolve the ID, compare quantities, compare times. That is what "verifiable" means in practice.

3.4.3 One tonne, one credit

Double counting happens when the same removal backs more than one claim. In data terms it is a referential-integrity violation, and it can be prevented by a small set of invariants that the issuing platform checks before every issuance:

// Illustrative pre-issuance check (JavaScript). Returns a list of violations.
function checkIssuance(request, db) {
  const errors = [];
  const report = db.reports.get(request.verificationReportId);
  if (!report) return ["unknown verification report"];
  if (report.facilityId !== request.facilityId) errors.push("facility mismatch");
  if (!["verified", "verified-with-conditions"].includes(report.conclusion))
    errors.push("report not verified");

  const alreadyIssued = db.credits
    .filter(c => c.ext?.verificationReportId === report.reportId
              && c.status !== "cancelled")
    .reduce((sum, c) => sum + c.amount, 0);
  if (alreadyIssued + request.amount > report.verifiedAmount)
    errors.push("issuance would exceed verifiedAmount");

  for (const id of report.ext?.storageEventIds ?? []) {
    const other = db.reports.findByStorageEvent(id)
      .filter(r => r.reportId !== report.reportId && !r.withdrawn);
    if (other.length > 0) errors.push(`storage event ${id} claimed twice`);
  }
  if (!Number.isInteger(request.amount)) errors.push("amount must be whole tonnes");
  return errors;
}
Listing 3-4. A pre-issuance invariant check. Function and field names are illustrative; ext fields are this book's recommendation.

Cancelled credits are excluded from the running total, so a cancelled tranche can be reissued. This is correct for administrative cancellations, such as a credit issued with the wrong owner. It is not correct for a cancellation caused by a reversal, where the stored CO2 is later found to have leaked. In that case the report's effective verifiedAmount must also be reduced. The standard has no field for this. Chapter 6 discusses how registries handle reversals.

3.4.4 How status propagates

The credit status enum implies a lifecycle, but the specification does not state the allowed transitions. The following rules are this book's recommendation and match common registry practice:

Table 3-4. Recommended credit status transitions
FromToTriggerWhat else changes
(none)issuedRegistry issuanceserialNumber, issuanceDate and owner set
issued / transferredtransferredChange of holderowner updated; the transfer is kept in an append-only history
issued / transferredretiredHolder uses the credit against a claimTerminal. Retirement beneficiary and purpose recorded; the on-chain mirror, if any, must also be marked unusable
issued / transferredcancelledAdministrative error or reversalTerminal. Reason recorded; for a reversal the backing report's effective amount is reduced
retired / cancelledany—Not allowed

The registry is the source of truth for status. Every other copy, whether an operator's dashboard, a corporate carbon-accounting system or a token on a chain, is a mirror and must follow the registry. Retirement should propagate as an event rather than wait for the next scheduled synchronisation, because the risk is a mirror that still shows a retired credit as available. A buyer could purchase it a second time from a party relying on the stale copy. Chapter 6 shows that the specification's sample smart contract sets a retired flag but does not burn or lock the token, which is exactly this kind of divergence.

3.5 The API Envelope

PHASE-2-DATA §8 defines how the API wraps records. Chapter 4 covers the endpoints themselves; this section covers the envelope, because it is part of the data format.

3.5.1 Success and error envelopes in the specification

A successful response has four members: success (true), data (the record or list), metadata (timestamp, version, requestId) and, for lists, pagination (page, limit, total). An error response sets success to false and replaces data with an error object that has a code, a message and free-form details. The specification's single example code is INVALID_FACILITY_ID, with details naming the field and the rejected value.

The envelope has three weaknesses: the HTTP status code is absent from the body, the error codes are not catalogued anywhere in the standard, and a response can report only one failure at a time.

3.5.2 RFC 9457 Problem Details

The README and the reference-standards lists in PHASE-1 and PHASE-2 cite IETF RFC 9457, Problem Details for HTTP APIs, which defines a media type application/problem+json with standard members type (a URI identifying the problem type), title, status, detail and instance, and allows extension members. The standard therefore cites one error format and specifies another.

Table 3-5. Mapping the WIA error object to RFC 9457
WIA §8.2 memberRFC 9457 equivalentComment
success: falseHTTP status 4xx/5xx and media typeRedundant under RFC 9457; clients branch on the status code
error.codetype (URI) plus an extension member codeThe URI can resolve to documentation of the code
error.messagetitle (fixed per type) and detail (this occurrence)RFC 9457 separates the generic from the specific
error.detailsExtension members, for example errorsAn array allows several validation failures at once
metadata.requestIdinstance or an extension memberKeep the request ID for support and audit logs
metadata.versionExtension memberUseful during a deprecation window (§3.6)
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://example.com/wia/direct-air-capture/problems/validation",
  "title": "Record failed validation",
  "status": 422,
  "detail": "2 fields in the storage event are invalid.",
  "instance": "urn:uuid:5b1c2f6e-0d8a-4c61-9f1e-2a7d3c9b8e10",
  "code": "VALIDATION_FAILED",
  "version": "1.0.0",
  "errors": [
    { "pointer": "/facilityId", "code": "INVALID_FACILITY_ID",
      "detail": "must match ^dac-[a-z0-9-]+$" },
    { "pointer": "/co2Quality/purity", "code": "OUT_OF_RANGE",
      "detail": "must be between 0 and 100" }
  ]
}
Listing 3-5. A validation error expressed as RFC 9457 Problem Details. The type URI uses a placeholder domain.

This book recommends RFC 9457 for errors, for four reasons. The standard already cites it, so adopting it resolves an inconsistency instead of adding a dependency. It is a registered IETF format that API gateways, HTTP client libraries and monitoring tools already recognise. Its extension mechanism keeps everything the WIA object carries, including the INVALID_FACILITY_ID code, so no information is lost. And a type URI per problem gives a natural place to publish the error-code catalogue that the specification lacks. The success envelope can stay as specified. Mixing the two causes no conflict, because clients choose the parser by status code and media type. What should be avoided is supporting both error formats on the same endpoint, since clients then need two error paths. If a deployment must keep the §8.2 format for existing clients, it should declare this in its profile manifest and offer RFC 9457 through content negotiation.

3.5.3 Status that the records do not carry

One more mismatch belongs here. POST /storage/events returns "status": "pending-verification", but the storage event schema has no status field. The state a client has just been told about therefore cannot be read back from the record. This book recommends adding a status extension to the storage event with values such as pending-verification, verified and rejected, and setting it when the event is covered by a report with a positive conclusion.

3.6 Validation Rules and Schema Versioning

3.6.1 Six rule types, and what they miss

PHASE-2-DATA §9 lists six validation rules. Each maps to JSON Schema keywords, as Table 3-6 shows with examples from this chapter's records.

Table 3-6. The six validation rules applied to storage, verification and credit records
RuleJSON Schema keywordsExample in this chapter
Required fieldsrequiredA credit without vintage is rejected
Type checkingtypevintage must be an integer; "2026" as a string fails
Formatformatperiod.startDate must be a full date; reportUrl a URI
Rangeminimum, maximumpurity 0–100; guaranteedYears ≥ 1000
Enumerationenumconclusion, status, registry, storageMethod
PatternpatterneventId, reportId, price.currency

Three practical notes apply when these rules are put into a validator. First, in JSON Schema 2020-12 the format keyword is an annotation by default. A validator checks it only when format assertion is enabled; in Ajv, for example, that means adding the ajv-formats plugin. Without it, a malformed date passes silently. Second, none of the schemas sets additionalProperties: false. That is what makes the extensions in this chapter possible, but it also means a misspelt field such as verfiedAmount is accepted and the real field is simply missing. A deployment profile should either close the schemas and allow extensions only under a named member (this chapter uses ext), or run a separate check that reports unknown top-level fields. Third, everything in §3.1.4 and §3.4.3 lies outside JSON Schema: rate × duration, purity against contaminants, issued totals against verified amounts, exclusive storage events. A conformant validator runs schema validation first and then a second, cross-record pass. Only the second pass protects against double counting.

3.6.2 Versioning schemas

Schemas follow Semantic Versioning (PHASE-2-DATA §10.1): MAJOR for breaking structural changes, MINOR for backward-compatible additions, PATCH for fixes and clarifications. Deprecated fields are marked "deprecated": true. That keyword belongs to the JSON Schema meta-data vocabulary, so standard tooling can surface it. Because it is an annotation, a validator does not reject a deprecated field; it only reports it. Every record should state which schema version it was written against, either through the envelope's metadata.version or through a version in its $schema or $id URI, because a record's meaning depends on the version.

3.6.3 Conflicting deprecation windows

The specification gives several different time periods for the same process, and an implementer should know this before promising anything to users:

Table 3-7. Deprecation and overlap periods stated in the specification
SourceStatement
PHASE-1 and PHASE-2 generic documents, §6Major versions overlap for at least 90 days; deprecated versions then enter a 12-month sunset
Annex F (in each PHASE document)Breaking changes require at least 90 days of overlap
Annex H (in each PHASE document)The prior major version is maintained in parallel for at least 180 days, then a 12-month sunset
PHASE-2-DATA §10.2A 6-month deprecation period before a deprecated field is removed
Annex E (PHASE-2-DATA)Renaming or removing fields must trigger a minor version bump, while §10.1 and Annex H call breaking changes major

The 90-day and 180-day figures conflict directly. The 6-month field rule and the version-overlap rules could be read as governing different things (field removal versus parallel maintenance of whole versions), and the 12-month sunset follows the overlap in both readings. The Annex E statement about minor bumps contradicts the definition of MAJOR. Until the working group publishes an erratum, this book recommends a conservative reading. Treat any rename or removal as a major change; keep the prior major version for at least 180 days, which satisfies both overlap figures; give deprecated fields at least six months' notice; and state the windows actually applied in the implementation's profile manifest (Chapter 8). A deployment that commits to the longest period in each case complies with every version of the rule.

For the records in this chapter, versioning has an additional dimension. A verification report or credit remains valid evidence for decades, long after the schema it was written against has been retired, and verification records are to be kept permanently (FOUNDATION §5.1.3). Old records must never be rewritten when the schema changes. They are read with the schema version they declare, and any migration produces a new derived record that points back to the original.

Chapter Summary

Chapter 4 turns to the interfaces that carry these records: discovery, authentication, and the REST, GraphQL and WebSocket endpoints.