---
name: Certyo Durable Records API Integration
version: 1.0.0
description: Generate integration code for the Certyo blockchain-anchored records API
api_base: https://www.certyos.com
auth: X-API-Key header
last_updated: 2026-04-13
---

# Certyo API Integration Skill

Certyo provides tamper-proof, blockchain-anchored integrity proofs for business-critical records. Records are ingested via API, batched into Merkle trees, pinned to IPFS, and anchored on the Polygon blockchain. Any record can later be cryptographically verified against its on-chain proof.

Use this skill to generate integration code that ingests records into Certyo and verifies their integrity.

---

## Authentication

All `/api/v1/*` endpoints require an API key passed via the `X-API-Key` header.

```
X-API-Key: your-api-key-here
```

API keys are provisioned through the Certyo backoffice. Each key is tied to a client (application) within a tenant (organization). Keys support optional expiration and access levels (standard, premium, enterprise).

---

## Core Workflow

Records flow through a multi-stage pipeline:

1. **Ingest** — `POST /api/v1/records` sends a record (returns `202 Accepted`)
2. **Accumulate** — Records buffer in memory, flushed when count >= 1000 or age >= 60s
3. **Snapshot** — A `DurableRecordSnapshot` is created covering all accumulated records
4. **Anchor** — Merkle tree computed over record hashes, pinned to IPFS, anchored on Polygon
5. **Verify** — `POST /api/v1/verify/record` verifies a record against the on-chain Merkle root

Anchoring is asynchronous. After ingestion, poll the snapshot status or use the verification endpoint to check when anchoring completes.

---

## Endpoints Reference

### Ingest a Single Record

```
POST /api/v1/records
```

Ingests one record into the durable records pipeline.

**Request Body:**

```json
{
  "tenantId": "acme-corp",
  "clientId": "billing-service",
  "database": "billing",
  "collection": "invoices",
  "recordId": "INV-2026-0042",
  "recordVersion": "v1",
  "recordPayload": {
    "invoiceNumber": "INV-2026-0042",
    "amount": 1500.00,
    "currency": "USD",
    "customerId": "CUST-001"
  },
  "sourceTimestamp": "2026-04-13T10:30:00Z",
  "operationType": "upsert",
  "idempotencyKey": "billing-INV-2026-0042-v1"
}
```

| Field | Required | Description |
|-------|----------|-------------|
| `tenantId` | Yes | Organization identifier |
| `clientId` | No | Application within the tenant (used for Kafka partitioning) |
| `database` | Yes | Source database name |
| `collection` | Yes | Source collection/table name |
| `recordId` | Yes | Unique record identifier within the collection |
| `recordVersion` | No | Version tag for multi-version tracking |
| `recordPayload` | Yes | Arbitrary JSON payload to hash and preserve |
| `sourceTimestamp` | No | When the record was produced in the source system |
| `operationType` | No | Operation type: `upsert` (default), `insert`, `update`, `delete` |
| `idempotencyKey` | No | Prevents duplicate ingestion (also accepts `Idempotency-Key` header) |

**Response (202 Accepted):**

```json
{
  "recordId": "INV-2026-0042",
  "recordHash": "a1b2c3d4e5f6...sha256hex",
  "tenantId": "acme-corp",
  "acceptedAt": "2026-04-13T10:30:05Z",
  "idempotencyReplayed": false
}
```

---

### Bulk Ingest Records

```
POST /api/v1/records/bulk
```

Ingests 1 to 1000 records in a single request. Each item follows the same schema as the single-record endpoint.

**Request Body:**

```json
{
  "items": [
    {
      "tenantId": "acme-corp",
      "database": "billing",
      "collection": "invoices",
      "recordId": "INV-001",
      "recordPayload": { "amount": 100 }
    },
    {
      "tenantId": "acme-corp",
      "database": "billing",
      "collection": "invoices",
      "recordId": "INV-002",
      "recordPayload": { "amount": 200 }
    }
  ]
}
```

**Response (202 Accepted):**

```json
{
  "total": 2,
  "accepted": 2,
  "items": [
    { "recordId": "INV-001", "recordHash": "...", "tenantId": "acme-corp", "acceptedAt": "..." },
    { "recordId": "INV-002", "recordHash": "...", "tenantId": "acme-corp", "acceptedAt": "..." }
  ]
}
```

---

### Query Ingested Records

```
GET /api/v1/records?tenantId=acme-corp&database=billing&collection=invoices
```

| Parameter | Required | Description |
|-----------|----------|-------------|
| `tenantId` | Yes | Filter by tenant |
| `database` | No | Filter by source database |
| `collection` | No | Filter by source collection |
| `recordId` | No | Filter by specific record ID |
| `snapshotId` | No | Filter by snapshot ID |

---

### Get Snapshot Detail

```
GET /api/v1/snapshots/{snapshotId}
```

Returns snapshot metadata including status, batch assignment, and contained records.

**Response fields:**

| Field | Description |
|-------|-------------|
| `snapshotId` | Unique snapshot identifier |
| `status` | `PendingAnchor`, `Batched`, `Anchored`, or `Failed` |
| `recordIds` | Array of record IDs in this snapshot |
| `recordCount` | Number of records in the snapshot |
| `snapshotHash` | SHA-256 hash of the snapshot |
| `batchId` | Anchor batch assignment (present after batching) |
| `merkleRoot` | Merkle root hash (present after anchoring) |
| `anchorTimestamp` | When the batch was anchored on-chain |

---

### Get Snapshot Lifecycle

```
GET /api/v1/snapshots/{snapshotId}/lifecycle
```

Returns the state transition timeline (Ingested -> PendingAnchor -> Batched -> Anchored).

---

### Retry Failed Snapshot

```
POST /api/v1/snapshots/{snapshotId}/retry
```

Re-queues a failed snapshot for anchoring.

---

### Verify Record Integrity

```
POST /api/v1/verify/record
```

Cryptographically verifies a record against the on-chain Merkle root.

**Request Body:**

```json
{
  "collection": "invoices",
  "recordId": "INV-2026-0042",
  "tenantId": "acme-corp",
  "database": "billing",
  "recordVersion": "v1",
  "payload": {
    "invoiceNumber": "INV-2026-0042",
    "amount": 1500.00,
    "currency": "USD",
    "customerId": "CUST-001"
  }
}
```

| Field | Required | Description |
|-------|----------|-------------|
| `collection` | Yes | Source collection |
| `recordId` | Yes | Record ID to verify |
| `tenantId` | No | Tenant (optional if implied by API key) |
| `database` | No | Source database |
| `recordVersion` | No | Specific version to verify |
| `payload` | No | JSON payload for tamper detection (recomputes hash and compares) |
| `batchId` | No | Batch ID for zero-database verification (IPFS + Polygon only) |

**Response:**

```json
{
  "verified": true,
  "verificationStatus": "pass",
  "reasonCategory": "full_match",
  "verifiedAt": "2026-04-13T12:00:00Z",
  "verificationId": "ver_abc123",
  "snapshotId": "snap_xyz789",
  "snapshotHash": "a1b2c3...",
  "batchId": "batch_20260413...",
  "merkleRoot": "d4e5f6...",
  "merkleProof": ["hash1", "hash2", "hash3"],
  "anchoredOnChain": true,
  "chainId": 137,
  "contractAddress": "0xeeCD2FAD7841E113BCeEB39c704c78B91E35D6f2",
  "onChainMerkleRoot": "d4e5f6...",
  "onChainAnchoredAt": "2026-04-13T11:55:00Z",
  "ipfsEvidence": {
    "ipfsCid": "bafkrei...",
    "ipfsGatewayUrl": "https://gateway.pinata.cloud/ipfs/bafkrei...",
    "hashMatchesIpfs": true,
    "merkleRootMatchesChain": true
  }
}
```

---

### Verification History

```
GET /api/v1/verify/history?tenantId=acme-corp&recordId=INV-2026-0042
```

Returns an audit trail of all verification attempts for the given tenant/record.

---

### Health Check

```
GET /api/Admin/health
```

No authentication required. Returns system health status.

---

## Rate Limiting

Default: **1000 requests per hour** per API key.

**Response headers on every request:**

| Header | Description |
|--------|-------------|
| `X-RateLimit-Limit` | Total requests allowed per hour |
| `X-RateLimit-Remaining` | Remaining requests in current window |
| `X-RateLimit-Reset` | Unix timestamp when the limit resets |

**When rate limited (429 Too Many Requests):**

```json
{
  "status": 429,
  "error": "rate_limit_exceeded",
  "message": "Rate limit exceeded. Try again later.",
  "retryAfter": 120,
  "resetTime": "2026-04-13T11:00:00Z"
}
```

Use the `Retry-After` header value to implement exponential backoff.

---

## Idempotency

To prevent duplicate record ingestion, include an idempotency key:

- **Header:** `Idempotency-Key: your-unique-key`
- **Body field:** `"idempotencyKey": "your-unique-key"`

If a record with the same idempotency key was already ingested, the API returns the original response with `idempotencyReplayed: true` instead of creating a duplicate.

**Best practice:** Use a deterministic key like `{database}-{collection}-{recordId}-{version}`.

---

## Error Handling

| Status | Meaning | Action |
|--------|---------|--------|
| `202` | Accepted for processing | Record is in the pipeline |
| `200` | Success | Request completed |
| `400` | Validation error | Check the `message` and `details` fields |
| `401` | Missing or invalid API key | Verify your `X-API-Key` header |
| `404` | Resource not found | Check the ID in your request |
| `429` | Rate limit exceeded | Wait `Retry-After` seconds, then retry |
| `500` | Server error | Retry with exponential backoff |

**Error response shape:**

```json
{
  "error": "error_code",
  "message": "Human-readable description",
  "details": ["field-level errors if applicable"]
}
```

---

## Code Examples

### curl — Ingest a record

```bash
curl -X POST https://www.certyos.com/api/v1/records \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "tenantId": "acme-corp",
    "database": "billing",
    "collection": "invoices",
    "recordId": "INV-2026-0042",
    "recordPayload": {
      "invoiceNumber": "INV-2026-0042",
      "amount": 1500.00,
      "currency": "USD"
    },
    "idempotencyKey": "billing-INV-2026-0042-v1"
  }'
```

### curl — Verify a record

```bash
curl -X POST https://www.certyos.com/api/v1/verify/record \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "collection": "invoices",
    "recordId": "INV-2026-0042",
    "tenantId": "acme-corp",
    "database": "billing"
  }'
```

### JavaScript (fetch)

```javascript
const API_BASE = "https://www.certyos.com";
const API_KEY = "YOUR_API_KEY";

// Ingest a record
async function ingestRecord(record) {
  const response = await fetch(`${API_BASE}/api/v1/records`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": API_KEY,
    },
    body: JSON.stringify({
      tenantId: "acme-corp",
      database: record.database,
      collection: record.collection,
      recordId: record.id,
      recordPayload: record.payload,
      idempotencyKey: `${record.collection}-${record.id}`,
    }),
  });

  if (response.status === 429) {
    const retryAfter = response.headers.get("Retry-After") || 60;
    throw new Error(`Rate limited. Retry after ${retryAfter}s`);
  }

  if (!response.ok) {
    const err = await response.json();
    throw new Error(`Ingestion failed: ${err.message}`);
  }

  return response.json(); // { recordId, recordHash, acceptedAt, ... }
}

// Verify a record
async function verifyRecord(collection, recordId) {
  const response = await fetch(`${API_BASE}/api/v1/verify/record`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": API_KEY,
    },
    body: JSON.stringify({ collection, recordId, tenantId: "acme-corp" }),
  });

  const result = await response.json();
  return result; // { verified, verificationStatus, merkleProof, ... }
}
```

### Python (requests)

```python
import requests

API_BASE = "https://www.certyos.com"
API_KEY = "YOUR_API_KEY"
HEADERS = {"Content-Type": "application/json", "X-API-Key": API_KEY}

def ingest_record(tenant_id, database, collection, record_id, payload):
    """Ingest a single record into the Certyo pipeline."""
    response = requests.post(
        f"{API_BASE}/api/v1/records",
        headers=HEADERS,
        json={
            "tenantId": tenant_id,
            "database": database,
            "collection": collection,
            "recordId": record_id,
            "recordPayload": payload,
            "idempotencyKey": f"{collection}-{record_id}",
        },
    )

    if response.status_code == 429:
        retry_after = int(response.headers.get("Retry-After", 60))
        raise Exception(f"Rate limited. Retry after {retry_after}s")

    response.raise_for_status()
    return response.json()

def verify_record(tenant_id, collection, record_id):
    """Verify a record's integrity against the on-chain Merkle root."""
    response = requests.post(
        f"{API_BASE}/api/v1/verify/record",
        headers=HEADERS,
        json={
            "tenantId": tenant_id,
            "collection": collection,
            "recordId": record_id,
        },
    )
    response.raise_for_status()
    return response.json()

# Example usage
result = ingest_record("acme-corp", "billing", "invoices", "INV-001", {"amount": 100})
print(f"Record {result['recordId']} accepted, hash: {result['recordHash']}")

# Wait for anchoring, then verify
proof = verify_record("acme-corp", "invoices", "INV-001")
print(f"Verified: {proof['verified']}, Status: {proof['verificationStatus']}")
```

---

## Integration Patterns

### Pattern 1: Fire-and-forget ingestion
Send records and trust the pipeline. Check verification later on demand.

```
POST /api/v1/records  -->  202 Accepted  -->  (done)
```

### Pattern 2: Ingest and poll for anchoring
After ingestion, poll the snapshot status until `Anchored`.

```
POST /api/v1/records  -->  202  -->  GET /api/v1/snapshots/{id}  -->  status: "Anchored"
```

### Pattern 3: Batch ingestion
Send records in bulk (up to 1000 per call) for high-throughput scenarios.

```
POST /api/v1/records/bulk  -->  202  -->  { total: 1000, accepted: 1000 }
```

### Pattern 4: End-to-end verification
Ingest, wait, verify, and export the proof.

```
POST /api/v1/records       -->  202 (recordHash returned)
GET  /api/v1/snapshots     -->  status: "Anchored"
POST /api/v1/verify/record -->  { verified: true, merkleProof: [...], ipfsEvidence: {...} }
```

---

## Blockchain Details

- **Chain:** Polygon mainnet (ChainId: 137)
- **Contract:** `0xeeCD2FAD7841E113BCeEB39c704c78B91E35D6f2`
- **Proof storage:** IPFS (Pinata) + Polygon smart contract
- **Merkle tree:** SHA-256 binary tree over record hashes within each batch
- **Verification:** On-chain Merkle root comparison + IPFS manifest cross-check
