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
# .env
KURIER_API_KEY=your_generated_api_key
KURIER_API_URL=https://api.kurier.xyz # or https://api-testnet.kurier.xyz
3. Basic Implementation
import axios from "axios";
import dotenv from "dotenv";
dotenv.config();
const API_URL = "https://api.kurier.xyz"; // or https://api-testnet.kurier.xyz
// Register VK (one-time)
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;
// Submit proof
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);
// Poll for finalization
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, // Base mainnet
proofOptions: { library: "snarkjs", curve: "bn128" },
proofData: { proof, publicSignals, vk: vkHash }
});
// Poll until status === "Aggregated"
// Response includes aggregationDetails with merkleProof
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/Validmay not be observed at a 5s polling cadence — the proof typically reachesIncludedInBlockwithin the first poll.IncludedInBlock= proof is in a zkVerify block.txHashandblockHashare 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(withmerkleProof,leaf,leafIndex,numberOfLeaves) is now populated. Use this for relay polling andrecordValidation.AggregationPublished= aggregation has been published to the destination chain. You don't need to wait for this —Aggregatedis 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; // 10 minutes
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") {
// Proof is on zkVerify but NOT yet aggregated — keep polling.
// txHash and blockHash are populated from IncludedInBlock onwards —
// save them now for explorer links + on-chain recordValidation inputs.
continue;
}
if (status === "Aggregated" || status === "AggregationPublished") {
// Terminal — aggregationDetails is now populated with the Merkle proof.
return { aggregationId, aggregationDetails, txHash, blockHash };
}
// Early states (Queued, Valid) — rarely observed at this polling cadence,
// just continue.
}
throw new Error("Polling timed out");
Common mistakes:
- Treating
Finalizedas terminal whenchainIdis set — aggregation hasn't happened yet - Capturing
txHashatIncludedInBlockand updating DB status to "finalized" prematurely - Lumping multiple statuses together in a single
ifblock — 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
- Mock testing: mock-testing.md
- TypeScript types: typescript-types.md
- zkVerify Explorer: zkverify-explorer.md
- API endpoints: endpoints.md
- Proof type configs: proof-types.md
- Chain IDs for aggregation: network-config.md
External Resources
- Swagger: api.kurier.xyz/docs
- Support: kurier-support@horizenlabs.io
- Discord: discord.gg/zkverify
Related Skills
hl-registry-integration— Continue the pipeline: afterAggregated, 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 workflowzkverify-contracts— Direct smart-contract verification