Developers
Show batch ingestion and synchronization guides and endpoints, core concepts, and the content hash calculator.
Explore the docs

Core concepts

Content hash specification

Reproduce API 4.2.0 business hashes with the shared implementation, exact JSON rules, and safe update fallback.

On this page

At a glance#

  • Hash complete, resolved business state rather than an arbitrary request or read row.
  • Inspect exact canonical JSON and verify the same code used by the API.
  • Local failure or missing context means sending the normal update, not skipping a record.
  • The creditor repair remains version 2 and requires an operator-run hash backfill.

Use the content hash calculator to paste data and inspect the exact bytes represented by its canonical JSON. Download the readable shared module, source/checksum metadata, money vectors, and public conformance vectors.

Contract and repair status

This contract targets API 4.2.0, content hash version 2. The API version, hash version, and source/build revision are separate identifiers. Confirm deployment and repair completion in your target environment; publishing these docs does not deploy the API or run a backfill.

The creditor repair includes the resolved pledge_creditor in every disbursement hash. Create omission defaults to CMK Capital; update omission preserves the existing creditor, including custom values. Explicit blank/null values retain the API's reset-to-CMK behavior. Hashing and persistence use the same normalized creditor.

Existing hashes that excluded that creditor need an operator-run backfill after deploying the corrected writer. Refresh cached sync-state hashes afterward. Monetary rounding alone needs no backfill; the creditor repair does. There is no version-3 migration in this release. Until comparability is established, send normal updates.

Exact formula

text
content_hash = lowercase_hex(SHA256(UTF8(canonical_json)))
canonical_json = JSON.stringify(sortKeys({data: canonicalPayload, table, version: 2}))

table is exactly one of clients, disbursements, operations, or payments. The compact envelope has data, table, and version keys. There is no indentation, trailing newline, salt, API key, event envelope, or idempotency key in the hash input. The output is 64 lowercase hexadecimal characters.

The shared module is generated from the API's canonicalizer and money helpers, not a second implementation. It exports canonicalJson, computeContentHash, calculateContentHash, canSkipUpdate, CONTENT_HASH_FIELDS, HASH_RULES, and CONTENT_HASH_VERSION. The low-level public functions accept only version 2 and JSON values. Historical internal v1 support is not the live public contract.

Resolve business state first

There are three different representations:

  1. Submitted request: a single {category, data} envelope, with step inside data.
  2. Prepared hash input: complete source-semantic business data after merging current state, applying defaults, and resolving relationships.
  3. Read/persisted row: may contain audit columns, database decimal strings, defaults, and relationships stored elsewhere. Do not hash an entire read response.

Partial updates preserve omitted existing values. Merging is shallow: an explicitly supplied info object replaces the old object rather than recursively merging it. Explicit null differs from omission during preparation; null properties are removed later during canonicalization. The server also validates the effective state.

EntityPreparation before hashing
ClientsResolve is_company (false when absent on create); a company has gender: company. Preserve omitted values on update.
DisbursementsResolve loan client_id or purchase links; the processor forces is_pledged: true. Always include the resolved, trimmed pledge_creditor; create/default fallback is CMK Capital, omitted updates preserve existing values. Resolve coverage, fee, gross, and current-row values.
PaymentsResolve client_id using parent/business rules and default source to client. Parent membership and other relationship validity remain server checks.
OperationsMerge current-row business fields with the partial update before hashing. Do not include generated status/history records as payload fields.

For a purchase, supply the effective purchase_clients, including [] if there are none. Each link is an object containing only a nonempty, trimmed client_id. Ingestion rejects duplicate IDs. Do not invent parent information or assume that omission of an update creditor means CMK.

Example: an update {step: "update", disbursement_id: "D-1"} cannot reveal whether the stored creditor is CMK Capital or Other Bank. Its supplied-data preview is not the full-state hash. If the stored creditor is Other Bank, retain that value in prepared state. Omission and explicitly resubmitting the unchanged creditor must give the same effective hash.

Business field projection

These are the API-owned persisted business fields. Include the business ID. For purchases, also include the separately stored purchase_clients links. Ignore legacy entity_type/entity_id and unrelated read-only columns when preparing data; the public adapter rejects unsupported root fields instead of guessing.

TableBusiness fields
clientsclient_id, name, email, phone, address, country, is_company, registration_number, gender, contact_person, info
disbursementsdisbursement_id, kind, client_id, disbursement_amount, gross_value, upfront_fee, disbursement_date, maturity_date, installment_count, operation_date, payment_date, currency, total_expected_margin, estimated_delivery_date, goods_description, goods_status, decision_date, decision, credit_score, reason, approved_amount, is_pledged, pledge_creditor, bank_ref, insurer_coverage, insurer_name, info
operationsoperation_id, disbursement_id, operation_name, operation_description, operation_start_date, operation_end_date, operation_revenue, operation_cost, currency, location, asset_id, asset_type, asset_value, asset_description, asset_purchase_date, asset_condition, depreciation_amount, depreciation_period, depreciation_method, info
paymentspayment_id, disbursement_id, client_id, source, payment_date, due_date, payment_total, payment_principal, payment_fee, installment_number, currency, status, bank_ref, info

Canonicalization rules

Apply these rules to prepared data, not as a replacement for preparation or ingestion validation.

Exclusions and objects

  • Exclude top-level step, content_hash, event_id, original_event_id, created_at, updated_at, change_date, and change_reason.
  • Omit null object properties recursively. Omit empty top-level objects after normalization, so info: {} equals omission. Nested empty objects remain.
  • Preserve array order and null array entries. The exception is purchase_clients, ordered by the string value of client_id using the current runtime's localeCompare.
  • Object keys use JavaScript's default string sort (UTF-16 code-unit ordering), followed by JavaScript property enumeration during JSON.stringify. Integer-index-like keys are emitted numerically before other keys even after string sorting. Use the exact reference output, not a generic sorted-JSON library.
  • Use native JavaScript JSON string escaping. Do not normalize Unicode, add spaces, or append a newline before hashing. Nested field names matching excluded top-level names are not excluded.

Version-2 purchase ordering is locale-dependent. The conservative public adapter marks multi-client purchase hashes preview-only rather than promising a server match. You can still inspect canonical JSON and verify implementation output, but send normal updates unless ordering and complete state have been independently established.

Strings and dates

These root fields use String(value).trim(): client_id, name, email, phone, address, country, registration_number, contact_person, gender, disbursement_id, payment_id, operation_id, currency, kind, status, and source.

Other strings retain whitespace during canonicalization, including nested metadata and operation_name. Creditor and purchase-link trimming happens during preparation, not through the generic string-field rule.

Date fields are disbursement_date, maturity_date, operation_date, payment_date, estimated_delivery_date, decision_date, operation_start_date, operation_end_date, asset_purchase_date, and due_date.

Preserve a date-only YYYY-MM-DD. Parse accepted timestamps and emit UTC with Z, removing a zero millisecond component and trailing fractional zeros. For example, 2026-01-01T01:00:00+01:00 becomes 2026-01-01T00:00:00Z. The low-level helper preserves unparseable strings; that is not evidence that ingestion accepts them. Validate requests separately and use documented ISO date forms.

Money and other numbers

Money fields are disbursement_amount, gross_value, upfront_fee, total_expected_margin, approved_amount, operation_revenue, operation_cost, asset_value, depreciation_amount, payment_total, payment_principal, and payment_fee.

Normalize finite parsed JavaScript numbers to cents, rounding halfway cases away from zero, then render a JSON string with two decimals. Examples: 10.255 becomes "10.26", -10.255 becomes "-10.26", and -0.004 becomes "0.00". Decimal arithmetic starts from the shortest decimal representation of the parsed number, including exponent notation; do not substitute binary multiplication plus Math.round.

The rounded result must fit NUMERIC(15,2): -9999999999999.99 through 9999999999999.99. Derived gross sums the individually rounded amount and fee in integer cents: 10.005 + 10.005 becomes "20.02".

The low-level module also accepts persisted money strings with an optional sign, digits, and up to two fractional digits. Those strings are for prepared/persisted values; ingestion requests still require JSON numbers. More-than-cent persisted strings and out-of-range values fail.

insurer_coverage is a ratio, not money: expand decimal notation and remove insignificant trailing zeros (0.9000 becomes "0.9"). Other finite integer numbers remain JSON numbers. Non-integer numbers, including inside info, become expanded decimal strings without trailing zeros. Do not round metadata, counts, or ratios as money. Integer number serialization still follows native JSON.stringify.

JSON numeric parsing can already have lost precision, including for unsafe integers. A different-language implementation must match parsed-number semantics; a decimal library alone cannot recover the original value.

Canonical defaults versus prepared defaults

The canonicalizer sets company gender to company when is_company is true and defaults missing payment source to client. Disbursement canonical defaults are is_pledged: true, insurer_coverage: "0.9", upfront_fee: "0.00", and missing gross_value from amount plus fee.

It does not fetch an existing creditor, insert the client processor's is_company: false, or resolve parent relationships. Supply those in prepared state. In particular, low-level hashing of a disbursement that omits pledge_creditor is only a supplied-data preview, not the repaired API's complete state.

Worked example and test vectors

This prepared loan example demonstrates canonicalization, not all fields required by ingestion:

json
{"disbursement_id":"D-1","kind":"loan","client_id":"C-1","disbursement_amount":10.255,"is_pledged":true,"pledge_creditor":"CMK Capital"}

Exact canonical JSON:

json
{"data":{"client_id":"C-1","disbursement_amount":"10.26","disbursement_id":"D-1","gross_value":"10.26","insurer_coverage":"0.9","is_pledged":true,"kind":"loan","pledge_creditor":"CMK Capital","upfront_fee":"0.00"},"table":"disbursements","version":2}

Expected digest:

text
879dd0b9c9e50c01af7eee66e8e5b226eb7afe3bf16ae6185ad50b48aa9b50fb

The public fixtures contain prepared input, exact JSON, and expected digests for clients, operations, payments, and loan/purchase disbursements. Load each case directly in the calculator. The separate money fixtures cover positive/negative halfway rounding, negative zero, equivalent persisted values, and derived gross. Match both JSON and digest, not just one example hash.

Shared module and safe fallback

In a browser, import /sdk/content-hash-v2.mjs; in Node, download it and use a local relative import. No API credentials or network access are needed to calculate a hash.

js
import { canonicalJson, computeContentHash } from './content-hash-v2.mjs';

const prepared = { client_id: 'C-1', name: 'Alice', is_company: false };
const exactJson = canonicalJson('clients', prepared);
const digest = await computeContentHash('clients', prepared);

The calculator's wrapper accepts a business object plus a table or infers the table from a single {category, data} envelope. Envelopes are always preview-only. prepared: true is your assertion of complete resolved state, not an automatic validation result; known missing context can still make can_compare false.

For an existing server ID, integration pseudocode is:

js
import { calculateContentHash, canSkipUpdate } from './content-hash-v2.mjs';

let local = null;
try {
  local = await calculateContentHash(preparedState, { table, prepared: true });
} catch {
  local = null;
}

const remote = {
  content_hash_version: syncState.content_hash_version,
  content_hash: existingRecord.content_hash,
};
if (!canSkipUpdate(local, remote)) {
  await sendNormalUpdate(originalPayload);
}

Only complete, comparable matching version-2 hashes skip a write. A mismatch, failed calculation, missing context, or unsupported version sends the normal update. Never compare two empty/error hashes as equal. A missing server ID takes the create path. Do not infer deletion authorization from a missing source record.

Fallback does not bypass server validation, parent checks, request idempotency, or server-side hash errors. The API can still reject invalid requests or return 200 unchanged. A fresh update uses a fresh event key; an exact retry preserves its original key and body. Never send the calculated content_hash inside ingestion data.

Tool boundaries and reproducibility

The public tool accepts finite JSON values up to 1 MiB and 32 nested levels. It rejects unsupported root fields, delete/batch inputs, special prototype-related property names, and excessive persisted decimal text. These conservative local limits do not redefine API acceptance; use normal updates where local calculation is unavailable.

Inputs are never uploaded, placed in share links, or automatically stored. A checksum comparison is against the digest you paste, not a live server query. Editing input invalidates results and copy actions. Exact canonical JSON downloads contain no added newline; the formatted data preview is not what gets hashed.

The checked-in module is rebuilt from reviewed API source. Metadata records the base Git revision and LF-normalized source checksums, including changes not yet committed at that revision. Documentation builds verify the module, metadata, fixture digests, and field coverage without needing the private API checkout. Publication does not confirm operator backfill completion.

Validate locally. Verify in sandbox.
The JSON validator checks structure and rounding. Credentials, existing records, and server-side state still require your test environment.

Search documentation

Start typing to find your next step.

    navigate Enter openLocal, private search