| name | zkverify-kurier |
| description | Kurier REST API for simplified proof verification. Use when you want the simplest integration path, don't need real-time events, or are building serverless/REST-based architectures. Covers API key setup, proof submission, status polling, and aggregation. |
Kurier REST API Integration
Kurier provides HTTP-based proof verification - the simplest integration path for zkVerify.
When to Use Kurier
- Serverless/REST architectures - No WebSocket required
- Simple integrations - HTTP POST/GET instead of blockchain SDKs
- Polling-based workflows - Check status periodically
- Quick prototyping - No blockchain library dependencies
For real-time events or direct chain access, see zkverify-sdk or zkverify-rpc.
Quick Start
1. Prerequisites
- Sign up at kurier.xyz (mainnet) or testnet.kurier.xyz
- Generate an API key from the dashboard
- Install dependencies:
npm i axios dotenv
2. Environment Setup
KURIER_API_KEY=your_generated_api_key
KURIER_API_URL=https://api.kurier.xyz
3. Basic Implementation
import axios from "axios";
import dotenv from "dotenv";
dotenv.config();
const API_URL = "https://api.kurier.xyz";
const vkResponse = await axios.post(`${API_URL}/api/v1/register-vk/${process.env.API_KEY}`, {
proofType: "groth16",
proofOptions: { library: "snarkjs", curve: "bn128" },
vk: vkey
});
const vkHash = vkResponse.data.vkHash;
const submitResponse = await axios.post(`${API_URL}/api/v1/submit-proof/${process.env.API_KEY}`, {
proofType: "groth16",
vkRegistered: true,
proofOptions: { library: "snarkjs", curve: "bn128" },
proofData: { proof, publicSignals, vk: vkHash }
});
console.log("Job ID:", submitResponse.data.jobId);
while (true) {
const status = await axios.get(
`${API_URL}/api/v1/job-status/${process.env.API_KEY}/${submitResponse.data.jobId}`
);
if (status.data.status === "Finalized") break;
await new Promise(r => setTimeout(r, 5000));
}
4. With Aggregation (Cross-Chain)
Add chainId to enable aggregation for L1 verification:
const response = await axios.post(`${API_URL}/api/v1/submit-proof/${API_KEY}`, {
proofType: "groth16",
vkRegistered: true,
chainId: 8453,
proofOptions: { library: "snarkjs", curve: "bn128" },
proofData: { proof, publicSignals, vk: vkHash }
});
Job Status Lifecycle
Without Aggregation (no chainId)
Queued → Valid → Submitted → IncludedInBlock → Finalized
Finalized is terminal. The proof is on zkVerify and has a stable txHash.
With Aggregation (chainId set — this is what you want for on-chain use)
[Queued → Valid] → IncludedInBlock → AggregationPending → Aggregated → (AggregationPublished)
Queued / Valid may not be observed at a 5s polling cadence — the proof typically reaches IncludedInBlock within the first poll.
IncludedInBlock = proof is in a zkVerify block. txHash and blockHash are populated from this point forward.
AggregationPending = waiting to be included in an aggregation batch. This is the longest-lived state — typically 1–3 minutes.
Aggregated = terminal for downstream use. aggregationDetails (with merkleProof, leaf, leafIndex, numberOfLeaves) is now populated. Use this for relay polling and recordValidation.
AggregationPublished = aggregation has been published to the destination chain. You don't need to wait for this — Aggregated is sufficient.
Observed timing (Base Sepolia, April 2026): ~5s to IncludedInBlock, ~25s to AggregationPending, ~165s to Aggregated. Total: ~3 minutes end-to-end (submit → Aggregated).
Important: With chainId set you typically do NOT see Finalized — the proof goes straight from AggregationPending to Aggregated. Do not wait for Finalized in the aggregation path. Stop polling as soon as you see Aggregated (or AggregationPublished).
Polling Best Practices
When polling with aggregation enabled (chainId set), handle each status distinctly:
const POLL_INTERVAL_MS = 5000;
const MAX_POLL_ATTEMPTS = 120;
for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
const res = await fetch(`${API_URL}/api/v1/job-status/${API_KEY}/${jobId}`);
const { status, txHash, aggregationId, aggregationDetails } = await res.json();
if (status === "Failed") {
throw new Error("Proof verification failed");
}
if (status === "IncludedInBlock" || status === "AggregationPending") {
continue;
}
if (status === "Aggregated" || status === "AggregationPublished") {
return { aggregationId, aggregationDetails, txHash, blockHash };
}
}
throw new Error("Polling timed out");
Common mistakes:
- Treating
Finalized as terminal when chainId is set — aggregation hasn't happened yet
- Capturing
txHash at IncludedInBlock and updating DB status to "finalized" prematurely
- Lumping multiple statuses together in a single
if block — handle each distinctly
API Endpoints
| Endpoint | Method | Description |
|---|
/api/v1/register-vk/{API_KEY} | POST | Register verification key |
/api/v1/submit-proof/{API_KEY} | POST | Submit proof for verification |
/api/v1/job-status/{API_KEY}/{jobId} | GET | Check job status |
See endpoints.md for complete API reference with request/response schemas.
Job Status States
| Status | StatusId | Description |
|---|
Queued | 0 | Waiting for processing |
Valid | 1 | Passed optimistic verification |
Submitted | 2 | Submitted to blockchain mempool |
IncludedInBlock | 3 | Transaction in block |
Finalized | 4 | Transaction finalized |
Failed | 5 | Processing failed |
Aggregated | 6 | Proof aggregated (requires chainId) |
AggregationPublished | 7 | Aggregation published to destination chain |
Response Fields by Status
The job-status response includes additional fields as the proof progresses:
| Field | Available from | Description |
|---|
jobId | Always | Job identifier |
status | Always | Current status string |
statusId | Always | Numeric status code |
proofType | Always | Proof type (e.g., "groth16") |
chainId | Always | Destination chain (null if no aggregation) |
txHash | IncludedInBlock onwards | zkVerify extrinsic hash — use for explorer links: https://zkverify-testnet.subscan.io/extrinsic/{txHash} |
blockHash | IncludedInBlock onwards | zkVerify block hash |
aggregationId | Aggregated onwards | Sequential integer — use as attestationId in HL Marketplace contracts |
aggregationDetails | Aggregated onwards | Object with leaf, leafIndex, numberOfLeaves, merkleProof, receipt, root |
Important: txHash is the zkVerify blockchain extrinsic hash, not a Base/Ethereum transaction hash. Use it to build explorer links for proof transparency in your UI.
zkVerify Explorer
View finalized proofs on the zkVerify blockchain explorer:
Format: https://zkverify.subscan.io/extrinsic/{txHash}
After a proof reaches "Finalized" status, use the txHash from the job status response to view details on the zkVerify blockchain explorer. The explorer shows proof verification details, block information, transaction hashes, and gas fees.
See zkverify-explorer.md for complete details.
Additional Resources
Documentation
External Resources
Related Skills
hl-registry-integration — Continue the pipeline: after Aggregated, wait for attestation relay on Base Sepolia and record the validation on the HL Marketplace ValidationGateway.
zkverify-sdk — TypeScript SDK with real-time events (alternative to polling)
zkverify-attestation — Cross-chain attestation workflow
zkverify-contracts — Direct smart-contract verification