Record Ingestion
Send records to Certyo via a simple POST. The request returns immediately with a 202 Accepted and the record hash. Anchoring happens asynchronously.
Request shape
{
"tenantId": "acme-corp",
"database": "production",
"collection": "orders",
"recordId": "order-12345",
"recordVersion": "1",
"operationType": "upsert",
"recordPayload": {
"orderId": "order-12345",
"customerId": "cust-789",
"amount": 299.99,
"currency": "USD"
},
"sourceTimestamp": "2026-04-12T10:00:00Z",
"idempotencyKey": "order-12345-v1-2026-04-12"
}Field reference
Required
tenantId(string) — your tenant identifierdatabase(string) — logical source database or system namecollection(string) — logical table or collection namerecordId(string) — unique identifier for this record within the database/collectionrecordPayload(object) — the record data, as arbitrary JSON
Optional
clientId(string) — sub-organization identifier; used as the Kafka partition keyrecordVersion(string) — version tag if you're tracking multiple versions of the same record. Defaults to"1".operationType(string) — what triggered the ingestion:insert,update,delete, orupsert. Defaults toupsert.sourceTimestamp(ISO 8601 datetime) — when the record was created or last modified in the source system. Defaults to ingestion time.idempotencyKey(string) — see below
Idempotency
If you send the same idempotencyKey within a 24-hour window, Certyo returns the original response instead of creating a duplicate. This is critical for retries:
{
"recordId": "order-12345",
"recordHash": "5f3c7d2e...",
"tenantId": "acme-corp",
"acceptedAt": "2026-04-12T15:30:45.123Z",
"idempotencyReplayed": true
}The only difference from the original response is idempotencyReplayed: true.
Recommended pattern: Use the tuple {recordId}-{recordVersion}-{YYYY-MM-DD} as your idempotency key. This makes retries safe while still allowing legitimate updates.
Bulk ingestion
For high-volume workloads (thousands of records at once), use the bulk endpoint:
{
"tenantId": "acme-corp",
"records": [
{ "database": "production", "collection": "orders", "recordId": "o1", "recordPayload": { ... } },
{ "database": "production", "collection": "orders", "recordId": "o2", "recordPayload": { ... } },
{ "database": "production", "collection": "orders", "recordId": "o3", "recordPayload": { ... } }
]
}The bulk endpoint accepts up to 1000 records per request. If any individual record fails validation, its error is returned in the response along with successful records.
Response
{
"recordId": "order-12345",
"recordHash": "5f3c7d2e1a9b4c8f6e2d9a1b3c5f7e2d9a1b3c5f7e2d9a1b3c5f7e2d9a1b3c",
"tenantId": "acme-corp",
"acceptedAt": "2026-04-12T15:30:45.123Z",
"idempotencyReplayed": false
}recordHash is the SHA-256 of the canonical JSON payload. You can use this later to verify without re-sending the payload.
The ingestion pipeline
Once your record is accepted, it flows through:
- Kafka — published to the
certyo.records.ingestedtopic - Accumulator — buffered in memory, partitioned by
tenantId:clientId - Snapshot — when the buffer hits 1000 records OR 60 seconds, all records are written to a single snapshot document
- Merkle tree — record hashes are organized into a Merkle tree; the root is a single 32-byte value
- IPFS pin — the full manifest (all hashes + metadata) is pinned to IPFS
- Polygon anchor — the Merkle root is written to a smart contract on Polygon mainnet in a single transaction
Total time from POST to on-chain anchor: typically 60–90 seconds.