How reinsurers and partner platforms verify claim digests anchored on-chain after Gooval approval.
Call the public on-chain view via JSON-RPC eth_call. No Gooval account or private key required. Defaults to Base Sepolia testnet; switch to Custom for mainnet Proxy + RPC from your integration pack.
This guide is for reinsurers and partner insurance platforms. It explains how to verify claim digests that Gooval writes to the blockchain after platform approval.
After approval, Gooval anchors a claim digest to the on-chain contract. The read-only method getClaim is a public view: any node can read it via JSON-RPC eth_call — no Gooval account and no write permission required.
Verification: hash the claim snapshot from the API with the shared algorithm, then compare it to on-chain dataHash. All partners use the same algorithm.
This is not asymmetric signature verification and there is no separate signing private key. Tamper-evidence comes from Keccak-256 over the canonicalized UTF-8 byte string, with the result stored on-chain.
| Item | Testnet (current) |
|---|---|
| Network | Base Sepolia |
| Chain ID | 84532 |
| Contract (Proxy) | 0x9CeB85F44653974501cB694124b2825fbe194B1b |
| Public RPC example | https://sepolia.base.org |
Always call getClaim on the Proxy address above. Mainnet addresses are provided in formal integration materials.
function getClaim(string calldata claimNo) external view returns (
bytes32 dataHash,
string claimNo,
string currency,
uint256 amount
);amount: minor units of the original currency (fiat cents). Example: "12.34" USD → 1234claimNo is empty and dataHash is bytes32(0) — do not treat as anchored.claimNo and the on-chain snapshot via the reinsurer query API (see “On-chain snapshot fields”).getClaim(claimNo) and compare: dataHash == H; claimNo / currency match the snapshot; amount equals the snapshot amount converted to integer cents (half-up).Identifiers in the API response:
| Field | Value | Notes |
|---|---|---|
hashAlgorithm | keccak256 | Ethereum Keccak-256 (not NIST SHA3-256) |
canonicalization | sorted_json_strip_url_query | See rules below |
H = keccak256(UTF8(C))
Rules to build the canonical string C:
documentUrl / document_url and the value is a string: strip URL query and fragment; keep only scheme://host/path.downloadUrl / download_url (presigned URLs change and must not participate in the hash)., and : (no spaces); non-ASCII kept as UTF-8 (equivalent to Python json.dumps(..., sort_keys=True, separators=(",", ":"), ensure_ascii=False)).Output format: 0x + 64 lowercase hex characters (32 bytes).
Input to on-chain dataHash is the following 8 fields (digest at platform approval). You must use this snapshot to verify the on-chain hash — do not substitute other payload shapes.
| Field | Type | Description |
|---|---|---|
claimNo | string | Platform claim number, e.g. C2026082410000001 |
claimId | string | Internal platform claim ID |
amount | string | Decimal amount string, e.g. "12.34" (not cents) |
currency | string | Currency uppercase, e.g. USD |
payoutTarget | string | Payout target, e.g. merchant / customer |
status | string | Status at approval time |
storeNumber | string | Store number |
spNumber | string | Shipping-protection number; empty string "" if none |
The same on-chain record also stores:
| On-chain field | Meaning |
|---|---|
currency | Same as snapshot |
amount | Integer cents: round half-up of amount × 100. Example: "12.34" → 1234; "10.005" → 1001 |
Pass the plaintext claimNo to getClaim.
claimDataThe reinsurer query API may return claimData as:
| Shape | API dataHash | Equals on-chain dataHash? |
|---|---|---|
| Platform 8-field snapshot | On-chain hash | Yes — hash claimData with the algorithm above |
| Full insurer filing (attachments, etc.) | Hash of that payload alone | Usually no — only proves that API JSON was not rewritten |
For on-chain matching, always:
getClaim(claimNo).dataHash directlyIf claimData includes attachments: use downloadUrl to download; when hashing, remove downloadUrl and strip query from documentUrl.
On-chain snapshot:
{
"claimNo": "C2026082410000001",
"claimId": "c1",
"amount": "12.34",
"currency": "USD",
"payoutTarget": "merchant",
"status": "platform_merchant_approved",
"storeNumber": "100001",
"spNumber": ""
}Unique canonical C (sorted keys, no spaces):
{"amount":"12.34","claimId":"c1","claimNo":"C2026082410000001","currency":"USD","payoutTarget":"merchant","spNumber":"","status":"platform_merchant_approved","storeNumber":"100001"}Keccak-256 of that UTF-8 byte string:
H = 0x1b36d3febe4f24c59d5fbe3205b01a1b3223ba647ad9d302e449ee5b7ec69840Matching on-chain fields:
| On-chain field | Meaning |
|---|---|
claimNo | C2026082410000001 |
currency | USD |
amount | 1234 |
dataHash | Same H as above |
documentUrl normalization example (illustrates URL rules — not the on-chain object):
Input fragment:
{
"documentList": [
{
"documentUrl": "https://bucket.example.com/evidence/x.jpg?X-Amz-Signature=abc",
"downloadUrl": "https://cdn.example.com/x.jpg?token=zz"
}
]
}Form that participates in the hash:
{"documentList":[{"documentUrl":"https://bucket.example.com/evidence/x.jpg"}]}{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_call",
"params": [
{
"to": "0x9CeB85F44653974501cB694124b2825fbe194B1b",
"data": "<getClaim calldata>"
},
"latest"
]
}data: function selector = first 4 bytes of keccak256("getClaim(string)"), then ABI-encoded string claimNo. Decode the return as (bytes32, string, string, uint256).
from eth_abi import encode
from eth_utils import keccak
selector = keccak(text="getClaim(string)")[:4]
calldata = "0x" + (selector + encode(["string"], ["C2026082410000001"])).hex()import { Contract, JsonRpcProvider } from "ethers";
const abi = [
"function getClaim(string claimNo) view returns (tuple(bytes32 dataHash, string claimNo, string currency, uint256 amount))",
];
const provider = new JsonRpcProvider("https://sepolia.base.org");
const contract = new Contract(
"0x9CeB85F44653974501cB694124b2825fbe194B1b",
abi,
provider
);
const row = await contract.getClaim("C2026082410000001");
console.log({
dataHash: row.dataHash,
claimNo: row.claimNo,
currency: row.currency,
amount: row.amount.toString(),
});Dependency: pycryptodome (or an equivalent Keccak-256). Do not use hashlib.sha3_256.
import json
from urllib.parse import urlsplit, urlunsplit
from Crypto.Hash import keccak
def strip_url_query(url: str) -> str:
raw = (url or "").strip()
if not raw:
return ""
parts = urlsplit(raw)
return urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))
def strip_document_urls(value):
if isinstance(value, dict):
out = {}
for key, item in value.items():
if key in {"downloadUrl", "download_url"}:
continue
if key in {"documentUrl", "document_url"} and isinstance(item, str):
out[key] = strip_url_query(item)
else:
out[key] = strip_document_urls(item)
return out
if isinstance(value, list):
return [strip_document_urls(item) for item in value]
return value
def claim_data_hash(payload: dict) -> str:
cleaned = strip_document_urls(payload)
canonical = json.dumps(
cleaned, sort_keys=True, separators=(",", ":"), ensure_ascii=False
)
digest = keccak.new(digest_bits=256)
digest.update(canonical.encode("utf-8"))
return "0x" + digest.hexdigest()
snapshot = {
"claimNo": "C2026082410000001",
"claimId": "c1",
"amount": "12.34",
"currency": "USD",
"payoutTarget": "merchant",
"status": "platform_merchant_approved",
"storeNumber": "100001",
"spNumber": "",
}
assert claim_data_hash(snapshot) == (
"0x1b36d3febe4f24c59d5fbe3205b01a1b3223ba647ad9d302e449ee5b7ec69840"
)import { keccak256, stringToHex } from "viem";
function stripUrlQuery(url: string): string {
try {
const u = new URL(url);
return `${u.protocol}//${u.host}${u.pathname}`;
} catch {
return url.split("?")[0].split("#")[0];
}
}
function stripDocumentUrls(value: unknown): unknown {
if (Array.isArray(value)) return value.map(stripDocumentUrls);
if (value && typeof value === "object") {
const out: Record<string, unknown> = {};
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
if (key === "downloadUrl" || key === "download_url") continue;
if ((key === "documentUrl" || key === "document_url") && typeof item === "string") {
out[key] = stripUrlQuery(item);
} else {
out[key] = stripDocumentUrls(item);
}
}
return out;
}
return value;
}
function sortKeys(value: unknown): unknown {
if (Array.isArray(value)) return value.map(sortKeys);
if (value && typeof value === "object") {
const out: Record<string, unknown> = {};
for (const key of Object.keys(value as object).sort()) {
out[key] = sortKeys((value as Record<string, unknown>)[key]);
}
return out;
}
return value;
}
function claimDataHash(payload: object): `0x${string}` {
const canonical = JSON.stringify(sortKeys(stripDocumentUrls(payload)));
return keccak256(stringToHex(canonical));
}When converting amount to cents, use half-up decimal rounding consistent with “round to cents” — avoid floating-point error.
On-chain getClaim is publicly readable; business fields and attachment downloads require the reinsurer query API (partner key):
GET /reinsurer/v1/claims/{partnerClaimNo}
x-chain-query-key: <query key issued by Gooval>
x-chain-timestamp: <optional Unix seconds; must be within ±300s of server time>{partnerClaimNo}: platform claimNo (starts with C…) or internal claim ID403Success response includes: claimNo, dataHash, hashAlgorithm, canonicalization, claimData, and chain.contractAddress / chain.txHash / chain.chainId.
Recommended flow:
claimNo, the on-chain snapshot (or reconstructable 8 fields), and chain.contractAddresseth_call getClaim(claimNo) against the Proxy; compare dataHash and cent amountclaimData is a full insurer filing: use the same algorithm to verify “API dataHash ↔ payload after stripping downloadUrl”; do not assume it equals on-chain dataHashField-level API details follow the integration docs.
downloadUrl; strip query / fragment from documentUrlamount is in centsgetClaim on the Proxy address (testnet above)getClaim returns empty claimNo or zero hash, treat as not yet anchored