Skip to main contentCertyo Developer Portal

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

POST /api/v1/recordsjson
{
  "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 identifier
  • database (string) — logical source database or system name
  • collection (string) — logical table or collection name
  • recordId (string) — unique identifier for this record within the database/collection
  • recordPayload (object) — the record data, as arbitrary JSON

Optional

  • clientId (string) — sub-organization identifier; used as the Kafka partition key
  • recordVersion (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, or upsert. Defaults to upsert.
  • 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:

Duplicate request responsejson
{
  "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:

POST /api/v1/records/bulkjson
{
  "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

202 Acceptedjson
{
  "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.

Canonical JSON
Before hashing, Certyo normalizes the payload: sorts object keys alphabetically, uses RFC 8259 escaping, and strips whitespace. This ensures the same logical payload always hashes to the same value.

The ingestion pipeline

Once your record is accepted, it flows through:

  1. Kafka — published to the certyo.records.ingested topic
  2. Accumulator — buffered in memory, partitioned by tenantId:clientId
  3. Snapshot — when the buffer hits 1000 records OR 60 seconds, all records are written to a single snapshot document
  4. Merkle tree — record hashes are organized into a Merkle tree; the root is a single 32-byte value
  5. IPFS pin — the full manifest (all hashes + metadata) is pinned to IPFS
  6. 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.