Foundation PoH API Developer Guide
The Foundation Proof of Humanity (PoH) API provides human verification as a service. It allows developers to verify real humans using ePassport ZK proofs, bind Semaphore identity commitments, and cast anonymous on-chain votes -- all through a simple REST API.
Base URL:
https://evoting-api-213114263206.us-east1.run.app
All endpoints are prefixed with /api/v1/poh/.
Quick Start
1. Get an API key. Contact the Foundation team to provision an API key for your project. You will receive a key in the format poh_live_.... Store it securely -- it is shown only once.
2. Make your first call. Check the status of a nullifier to confirm your key works:
curl -X GET \
https://evoting-api-213114263206.us-east1.run.app/api/v1/poh/status/0x1a2b3c4d \
-H "X-PoH-API-Key: poh_live_your_key_here"
3. Integrate the verification flow. The typical integration follows these steps:
- Verify passport -- Submit a ZK proof from an NFC-scanned ePassport
- Attach commitment -- Bind a Semaphore identity commitment to the verified identity
- Cast votes -- Use Semaphore ZK proofs for anonymous on-chain voting
Authentication
All endpoints require an API key passed in the X-PoH-API-Key HTTP header. The key is SHA-256 hashed on the server side and looked up in Firestore.
X-PoH-API-Key: poh_live_your_key_here
The POST /attach-commitment endpoint additionally requires a Firebase JWT in the Authorization header, issued after ePassport verification:
Authorization: Bearer <firebase-jwt>
The admin POST /api-keys endpoint uses a separate X-API-Key header for admin authentication.
Rate Limits & Quotas
| Plan | Requests / min | Monthly Quota |
|---|---|---|
| free | 10 | 1,000 |
| pro | 100 | 100,000 |
| enterprise | 1,000 | unlimited |
When you exceed your rate limit or monthly quota, the API returns 429 Too Many Requests. Upgrade your plan or wait for the limit to reset.
Endpoints Reference
POST /verify-passport
Verify an ePassport ZK proof. Accepts a proof generated from an NFC-scanned ePassport, verifies it, and returns a nullifier with an assigned trust tier.
Request:
curl -X POST \
https://evoting-api-213114263206.us-east1.run.app/api/v1/poh/verify-passport \
-H "Content-Type: application/json" \
-H "X-PoH-API-Key: poh_live_your_key_here" \
-d '{
"attestation_id": "att_9f3a2b1c",
"proof": { "pi_a": [...], "pi_b": [...], "pi_c": [...] },
"pub_signals": { "signal1": "value1" },
"userContextData": { "device": "iPhone 15", "locale": "en-US" }
}'
Response (200):
{
"nullifier": "0x1a2b3c4d...",
"trustTier": "passport_zk",
"verifiedAt": "2026-04-14T12:00:00Z"
}
| Field | Type | Description |
|---|---|---|
| nullifier | string | Unique nullifier derived from the proof |
| trustTier | string | Trust tier based on verification strength |
| verifiedAt | string | ISO-8601 timestamp of verification |
Errors: 400 invalid proof, 401 missing/invalid API key, 429 rate limited.
POST /attach-commitment
Bind a Semaphore identity commitment to a verified identity. After passport verification, the client generates a Semaphore identity commitment and attaches it to their nullifier.
Requires both an API key and a Bearer JWT.
Request:
curl -X POST \
https://evoting-api-213114263206.us-east1.run.app/api/v1/poh/attach-commitment \
-H "Content-Type: application/json" \
-H "X-PoH-API-Key: poh_live_your_key_here" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-d '{
"commitment": "12345678901234567890"
}'
Response (200):
{
"success": true,
"commitment": "12345678901234567890"
}
| Field | Type | Description |
|---|---|---|
| success | boolean | Whether the commitment was attached |
| commitment | string | The commitment that was attached |
Errors: 400 invalid request, 401 missing/invalid API key or JWT.
GET /group
Retrieve the Semaphore group. Returns all commitments in the group along with the current member count and Merkle root.
Request:
curl -X GET \
https://evoting-api-213114263206.us-east1.run.app/api/v1/poh/group \
-H "X-PoH-API-Key: poh_live_your_key_here"
Response (200):
{
"commitments": [
"12345678901234567890",
"98765432109876543210"
],
"memberCount": 42,
"merkleRoot": "0xabcdef..."
}
| Field | Type | Description |
|---|---|---|
| commitments | string[] | List of Semaphore identity commitments |
| memberCount | integer | Total members in the group |
| merkleRoot | string | Current Merkle root of the Semaphore group |
Errors: 401 missing/invalid API key.
POST /anonymous-vote
Cast an anonymous on-chain vote. Submits a Semaphore ZK proof to cast an anonymous vote on a Solana proposal. The nullifier prevents double-voting.
Request:
curl -X POST \
https://evoting-api-213114263206.us-east1.run.app/api/v1/poh/anonymous-vote \
-H "Content-Type: application/json" \
-H "X-PoH-API-Key: poh_live_your_key_here" \
-d '{
"proposalId": "prop_abc123",
"optionId": "opt_yes",
"proof": { "pi_a": [...], "pi_b": [...], "pi_c": [...] },
"nullifier": "0x1a2b3c4d...",
"signal": "0xsignal...",
"externalNullifier": "0xexternal..."
}'
Response (200):
{
"success": true,
"transactionSignature": "5K8z...txSig"
}
| Field | Type | Description |
|---|---|---|
| success | boolean | Whether the vote was recorded |
| transactionSignature | string | Solana transaction signature |
Errors: 400 invalid proof, 401 missing/invalid API key, 409 duplicate vote (nullifier already used for this proposal).
GET /status/{nullifier}
Look up verification status by nullifier. Returns whether a nullifier exists, its proof type, trust tier, verification timestamp, and whether a Semaphore commitment is bound.
Request:
curl -X GET \
https://evoting-api-213114263206.us-east1.run.app/api/v1/poh/status/0x1a2b3c4d \
-H "X-PoH-API-Key: poh_live_your_key_here"
Response (200):
{
"exists": true,
"nullifier": "0x1a2b3c4d...",
"proofType": "passport_zk",
"trustTier": "passport_zk",
"verifiedAt": "2026-04-14T12:00:00Z",
"hasCommitment": true
}
| Field | Type | Description |
|---|---|---|
| exists | boolean | Whether a record exists for this nullifier |
| nullifier | string | The queried nullifier |
| proofType | string | Type of proof used (only when exists is true) |
| trustTier | string | Trust tier (only when exists is true) |
| verifiedAt | string | Verification timestamp (only when exists is true) |
| hasCommitment | boolean | Whether a Semaphore commitment is bound |
Errors: 401 missing/invalid API key.
POST /api-keys (Admin)
Provision a new PoH API key. Admin-only endpoint. The plain-text key is returned only once.
Request:
curl -X POST \
https://evoting-api-213114263206.us-east1.run.app/api/v1/poh/api-keys \
-H "Content-Type: application/json" \
-H "X-API-Key: your_admin_key_here" \
-d '{
"owner": "acme-corp",
"plan": "pro"
}'
Response (200):
{
"key": "poh_live_abc123...",
"key_id": "key_7f8e9d0c",
"plan": "pro",
"rate_limit": 100,
"monthly_quota": 100000,
"message": "Store this key securely. It will not be shown again."
}
| Field | Type | Description |
|---|---|---|
| key | string | Plain-text API key (shown only once) |
| key_id | string | Key identifier for management operations |
| plan | string | Pricing plan (free, pro, enterprise) |
| rate_limit | integer | Requests per minute |
| monthly_quota | integer | Monthly request quota (-1 for unlimited) |
| message | string | Reminder to store the key |
Errors: 401 missing/invalid admin API key.
Error Handling
All errors return a JSON object with an error field:
{
"error": "Human-readable error message."
}
Common Error Codes
| HTTP Status | Meaning | Example |
|---|---|---|
| 400 | Bad Request | Invalid proof, missing required fields |
| 401 | Unauthorized | Missing or invalid X-PoH-API-Key header |
| 409 | Conflict | Duplicate vote -- nullifier already used for this proposal |
| 429 | Too Many Requests | Rate limit or monthly quota exceeded |
| 500 | Internal Server Error | Unexpected server-side failure |
TypeScript / Fetch Examples
Verify a passport
const BASE_URL = "https://evoting-api-213114263206.us-east1.run.app";
const API_KEY = "poh_live_your_key_here";
async function verifyPassport(attestationId: string, proof: object, pubSignals: object) {
const res = await fetch(`${BASE_URL}/api/v1/poh/verify-passport`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-PoH-API-Key": API_KEY,
},
body: JSON.stringify({
attestation_id: attestationId,
proof,
pub_signals: pubSignals,
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(`Verification failed (${res.status}): ${err.error}`);
}
return res.json(); // { nullifier, trustTier, verifiedAt }
}
Check verification status
async function getStatus(nullifier: string) {
const res = await fetch(`${BASE_URL}/api/v1/poh/status/${nullifier}`, {
headers: { "X-PoH-API-Key": API_KEY },
});
if (!res.ok) {
const err = await res.json();
throw new Error(`Status check failed (${res.status}): ${err.error}`);
}
return res.json(); // { exists, nullifier, proofType, trustTier, verifiedAt, hasCommitment }
}
Cast an anonymous vote
async function castVote(
proposalId: string,
optionId: string,
proof: object,
nullifier: string,
signal: string,
externalNullifier: string
) {
const res = await fetch(`${BASE_URL}/api/v1/poh/anonymous-vote`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-PoH-API-Key": API_KEY,
},
body: JSON.stringify({
proposalId,
optionId,
proof,
nullifier,
signal,
externalNullifier,
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(`Vote failed (${res.status}): ${err.error}`);
}
return res.json(); // { success, transactionSignature }
}
Full integration example
// 1. Verify the user's passport
const verification = await verifyPassport("att_9f3a2b1c", zkProof, pubSignals);
console.log("Verified:", verification.nullifier);
// 2. Attach a Semaphore commitment (requires JWT from step 1)
const commitRes = await fetch(`${BASE_URL}/api/v1/poh/attach-commitment`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-PoH-API-Key": API_KEY,
"Authorization": `Bearer ${firebaseJwt}`,
},
body: JSON.stringify({ commitment: semaphoreIdentity.commitment.toString() }),
});
// 3. Fetch the Semaphore group to generate a proof
const group = await fetch(`${BASE_URL}/api/v1/poh/group`, {
headers: { "X-PoH-API-Key": API_KEY },
}).then(r => r.json());
// 4. Generate a Semaphore proof and cast a vote
const vote = await castVote("prop_abc123", "opt_yes", semaphoreProof, nullifier, signal, extNullifier);
console.log("Vote tx:", vote.transactionSignature);