Proof
Proofof Holdings
How It Works
PricingDocsFAQ
Log InGet Started

Documentation

Browse all docs

Documentation

Learn how to integrate proof.holdings

Core Primitives
The mental model in one page
API Reference
Complete API documentation
SDKs
Official client libraries
Smart Reuse
Skip re-verification with existing proofs
Multi-Profile System
Multiple public profiles per account
Message Templates
Custom branding and message templates per project
Comparison
vs SMS OTP, TOTP, WebAuthn
Pricing
Plans and pricing tiers
Security
Threat model and guarantees
MCP Server
131 tools for AI agents
Integrations
n8n, Zapier, Make, and custom integrations
Resources
GitHub Docs
API Status

API Reference

Base URL: https://api.proof.holdings

Authentication: Bearer token via Authorization header

text
Authorization: Bearer pk_live_your_api_key_here

API keys are prefixed with pk_live_ for production and pk_test_ for testing.

Machine-readable spec: The full OpenAPI 3.0 specification is available at `/api/openapi.json` for SDK generation, contract testing, and AI coding assistants.


Endpoints Overview

MethodEndpointPurpose
POST/api/v1/verificationsCreate verification challenge
GET/api/v1/verificationsList verifications
GET/api/v1/verifications/:idGet verification status
POST/api/v1/verifications/:id/submitSubmit challenge code
POST/api/v1/verifications/:id/verifyTrigger DNS/HTTP check
POST/api/v1/verifications/:id/resendResend verification (email only)
POST/api/v1/verifications/:id/test-verifyAuto-verify (test mode only)
GET/api/v1/verifications/:id/eventsStream status updates (SSE)
GET/api/v1/verifications/usersList verified users (B2B)
GET/api/v1/verifications/users/:externalUserIdGet verified user details (B2B)
POST/api/v1/verifications/domainStart domain verification (B2B)
POST/api/v1/verifications/domain/:id/checkCheck domain verification (B2B)
POST/api/v1/proofs/validateValidate proof token
GET/api/v1/proofs/:id/statusCheck proof status
POST/api/v1/proofs/:id/revokeRevoke a proof
GET/api/v1/proofs/revokedGet revocation list
POST/api/v1/sessionsCreate phone verification session
GET/api/v1/sessions/:idGet session status
POST/api/v1/verification-requestsCreate multi-asset verification request
GET/api/v1/verification-requestsList verification requests
GET/api/v1/verification-requests/:idGet verification request
GET/api/v1/verification-requests/by-reference/:referenceIdGet by reference ID
DELETE/api/v1/verification-requests/:idCancel verification request
GET/api/v1/webhook-deliveriesList webhook deliveries
GET/api/v1/webhook-deliveries/:idGet webhook delivery details
GET/api/v1/webhook-deliveries/statsGet delivery statistics
POST/api/v1/webhook-deliveries/:id/retryRetry failed delivery
GET/.well-known/jwks.jsonPublic keys (RS256)
GET/healthService status

Test Mode (Sandbox)

Use test mode to build and test your integration without sending real messages, consuming quota, or incurring charges. Test mode is activated automatically when you authenticate with a test API key (pk_test_*).

How It Works

AspectProduction (pk_live_*)Test Mode (pk_test_*)
Channel deliveryReal messages sentSkipped — no real messages
Quota usageCounted toward monthly limitNot counted
BillingCharged per proofFree
WebhooksDelivered normallyDelivered with "test": true flag
Data isolationOnly sees production verificationsOnly sees test verifications
Proof tokensValid, signed JWTValid, signed JWT (identical format)

Auto-Verify Endpoint

POST /api/v1/verifications/:id/test-verify

Instantly complete a verification without user action. Only available with test API keys (pk_test_*). Returns 403 for production keys.

json
{
  "id": "507f1f77bcf86cd799439011",
  "type": "phone",
  "channel": "whatsapp",
  "status": "verified",
  "identifier": "+37069199199",
  "verified_at": "2026-02-13T12:25:00Z",
  "proof_token": "eyJhbGciOiJSUzI1NiIs...",
  "proof_expires_at": "2026-03-15T12:25:00Z",
  "test_mode": true
}

Example: Full Test Flow

bash
# 1. Create verification with test key (no real message sent)
curl -X POST https://api.proof.holdings/api/v1/verifications \
  -H "Authorization: Bearer pk_test_..." \
  -H "Content-Type: application/json" \
  -d '{"type":"phone","channel":"whatsapp","identifier":"+37069199199"}'

# 2. Auto-verify (skip waiting for user action)
curl -X POST https://api.proof.holdings/api/v1/verifications/{id}/test-verify \
  -H "Authorization: Bearer pk_test_..."

# 3. Use the proof token exactly as you would in production

Webhook Behavior

Test mode webhooks are delivered normally but include "test": true in the payload:

json
{
  "event": "verification_request.completed",
  "test": true,
  "request_id": "req_abc123",
  "status": "completed",
  "proofs": [...]
}

Environment Isolation

Test and production data are completely isolated. A pk_test_ key can only see test verifications, and a pk_live_ key can only see production verifications.


Create Verification

POST /api/v1/verifications

Create a new verification challenge.

Request Body

ParameterTypeRequiredDescription
typestringYesphone, email, domain, social, wallet, telegram_bot
channelstringYesChannel for verification (see table below)
identifierstringYesThe asset to verify
client_metadataobjectNoCustom metadata for your reference

Type/Channel Compatibility

TypeValid Channels
phonewhatsapp, telegram, sms
emailemail
domaindns, http, email, auto
socialgithub, google, facebook, x, linkedin, instagram, youtube, tiktok
walletethereum, solana, bitcoin
accountcoinbase, kraken
telegram_bottelegram_bot_token

Example Request

bash
curl -X POST https://api.proof.holdings/api/v1/verifications \
  -H "Authorization: Bearer pk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "phone",
    "channel": "whatsapp",
    "identifier": "+37069199199",
    "client_metadata": {
      "user_id": "usr_123",
      "action": "login"
    }
  }'

Response

json
{
  "id": "507f1f77bcf86cd799439011",
  "type": "phone",
  "channel": "whatsapp",
  "status": "pending",
  "identifier": "+37069199199",
  "challenge": {
    "code": "X7K2M9",
    "expires_at": "2026-02-04T11:00:00Z",
    "instruction": "Send X7K2M9 via WhatsApp",
    "deep_link": "https://wa.me/37069199199?text=X7K2M9"
  },
  "created_at": "2026-02-04T10:50:00Z"
}

Get Verification

GET /api/v1/verifications/:id

Poll for verification status.

Response (Verified)

json
{
  "id": "507f1f77bcf86cd799439011",
  "type": "phone",
  "channel": "whatsapp",
  "status": "verified",
  "identifier": "+37069199199",
  "verified_at": "2026-02-04T10:51:30Z",
  "proof": {
    "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expires_at": "2026-03-06T10:51:30Z"
  }
}

Submit Challenge Code

POST /api/v1/verifications/:id/submit

Submit an OTP challenge code for email/phone verification.

Request Body

ParameterTypeRequiredDescription
challenge_codestringYesThe 6-character challenge code
source_identifierstringNoPhone/email that sent the code (for validation)

Response

Returns the updated verification object (same as Get Verification).


Resend Verification

POST /api/v1/verifications/:id/resend

Resend the verification challenge. Currently only supported for email verifications. The verification must be in pending status.

Response

json
{
  "success": true,
  "message": "Verification email sent",
  "expires_at": "2026-02-13T12:10:00Z"
}

Verification Events (SSE)

GET /api/v1/verifications/:id/events

Subscribe to real-time verification status updates via Server-Sent Events. Use this instead of polling GET /verifications/:id.

Usage

javascript
const source = new EventSource(
  'https://api.proof.holdings/api/v1/verifications/{id}/events',
  { headers: { 'Authorization': 'Bearer pk_live_...' } }
);

source.addEventListener('connected', (e) => {
  console.log('Connected:', JSON.parse(e.data));
});

source.addEventListener('status_changed', (e) => {
  const update = JSON.parse(e.data);
  if (update.status === 'verified') {
    source.close();
  }
});

Events

EventDescription
connectedInitial connection established
status_changedVerification status updated

A 30-second heartbeat keeps the connection alive through Cloudflare. Maximum concurrent SSE connections per tenant is configurable (default: 10).


Verified Users (B2B)

List Verified Users

GET /api/v1/verifications/users

List verified users grouped by external_user_id. Only includes verifications where external_user_id was set during creation.

Query parameters: page, limit (max 100)

Response

json
{
  "data": [
    {
      "external_user_id": "user_12345",
      "verification_count": 3,
      "types_verified": ["phone", "email"],
      "verifications": [
        { "id": "507f...", "type": "phone", "channel": "whatsapp", "identifier": "+3706***9199", "verified_at": "2026-02-13T12:00:00Z" }
      ],
      "first_verified_at": "2026-01-10T08:00:00Z",
      "last_verified_at": "2026-02-13T12:00:00Z"
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 42, "pages": 3 }
}

Get Verified User

GET /api/v1/verifications/users/:externalUserId

Get all verifications for a specific external user.

Response

json
{
  "external_user_id": "user_12345",
  "verification_count": 3,
  "types_verified": ["phone", "email"],
  "verifications": [
    {
      "id": "507f...",
      "type": "phone",
      "channel": "whatsapp",
      "identifier": "+3706***9199",
      "status": "verified",
      "verified_at": "2026-02-13T12:00:00Z",
      "has_proof": true,
      "proof_expires_at": "2026-03-15T12:00:00Z",
      "created_at": "2026-02-13T11:50:00Z"
    }
  ]
}

Domain Verification (B2B)

Start Domain Verification

POST /api/v1/verifications/domain

Start a domain verification flow. Returns DNS and HTTP instructions for the domain owner.

Request Body

ParameterTypeRequiredDescription
domainstringYesDomain to verify (e.g., example.com)
customer_idstringNoOptional customer identifier
verification_methodstringNomanual_dns (default) or http_file

Response

json
{
  "id": "507f1f77bcf86cd799439011",
  "domain": "example.com",
  "status": "pending",
  "verification_method": "manual_dns",
  "dns_record": {
    "type": "TXT",
    "name": "_proof.example.com",
    "value": "proof-verify=abc123"
  },
  "http_file": {
    "path": "/.well-known/proof-verify.txt",
    "content": "proof-verify=abc123"
  },
  "provider": {
    "detected": "cloudflare",
    "name": "Cloudflare"
  }
}

Check Domain Verification

POST /api/v1/verifications/domain/:id/check

Check if DNS TXT record or HTTP file has been set up correctly.

Response

json
{
  "id": "507f1f77bcf86cd799439011",
  "domain": "example.com",
  "status": "verified",
  "verified": true,
  "verified_at": "2026-02-13T12:00:00Z",
  "check_count": 1
}

Proof Status

GET /api/v1/proofs/:id/status

Quick status check for a proof without validating the full JWT. Returns whether the proof is valid, revoked, or expired.

Response

json
{
  "proof_id": "507f1f77bcf86cd799439011",
  "status": "verified",
  "is_valid": true,
  "is_revoked": false,
  "revoked_at": null,
  "revoked_reason": null,
  "expires_at": "2026-03-15T12:00:00Z",
  "is_expired": false
}

Sessions (Phone-First Flow)

Sessions provide a phone-first verification flow where the user doesn't need to provide their phone number upfront. Instead, they send a message to your bot and the phone number is captured automatically.

Create Session

POST /api/v1/sessions

ParameterTypeRequiredDescription
channelstringYestelegram or whatsapp
client_metadataobjectNoCustom metadata for your reference
bash
curl -X POST https://api.proof.holdings/api/v1/sessions \
  -H "Authorization: Bearer pk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "telegram"
  }'

Response

json
{
  "id": "sess_abc123",
  "channel": "telegram",
  "status": "pending",
  "deep_link": "https://t.me/bot?start=sess_abc123",
  "qr_code": "https://t.me/bot?start=sess_abc123",
  "expires_at": "2026-02-04T11:00:00Z",
  "instructions": "Open Telegram and tap the link to verify"
}

Get Session

GET /api/v1/sessions/:id

Poll for session status. When verified, includes the phone number and proof token.

Response (Verified)

json
{
  "id": "sess_abc123",
  "channel": "telegram",
  "status": "verified",
  "phone_number": "+37069199199",
  "verified_at": "2026-02-04T10:51:30Z",
  "verification_id": "507f1f77bcf86cd799439011",
  "proof": {
    "token": "eyJhbGciOiJSUzI1NiIs...",
    "expires_at": "2026-03-06T10:51:30Z"
  }
}

Verification Requests (Multi-Asset)

Verification requests let you bundle multiple assets into a single verification flow. Useful for P2P trust, vendor onboarding, and KYB workflows.

Create Verification Request

POST /api/v1/verification-requests

ParameterTypeRequiredDescription
assetsarrayYesAssets to verify (see below)
reference_idstringNoYour external tracking ID
action_typestringNoverification, 2fa, login, custom
action_contextobjectNo{ title, description } for display
redirect_urlstringNoWhere to redirect after completion
callback_urlstringNoWebhook URL for completion notification
expires_innumberNoSeconds until expiry (default 24h, max 7d)
partial_okbooleanNoAccept partial completion

Each asset in the assets array:

ParameterTypeRequiredDescription
typestringYesphone, email, domain, social, wallet, telegram_bot
identifierstringNoAsset to verify (null = user provides)
channelstringNoPreferred channel
allowed_channelsarrayNoRestrict to specific channels
requiredbooleanNoWhether this asset is required (default true)
bash
curl -X POST https://api.proof.holdings/api/v1/verification-requests \
  -H "Authorization: Bearer pk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "reference_id": "deal_12345",
    "assets": [
      { "type": "domain", "identifier": "example.com" },
      { "type": "email", "identifier": "[email protected]" }
    ],
    "action_type": "verification",
    "redirect_url": "https://your-app.com/verified"
  }'

Response

json
{
  "id": "507f1f77bcf86cd799439011",
  "status": "pending",
  "reference_id": "deal_12345",
  "assets": [
    { "type": "domain", "identifier": "example.com", "status": "pending" },
    { "type": "email", "identifier": "[email protected]", "status": "pending" }
  ],
  "verification_url": "https://proof.holdings/verify/507f1f77bcf86cd799439011",
  "created_at": "2026-02-04T10:50:00Z",
  "expires_at": "2026-02-05T10:50:00Z"
}

Get Verification Request

GET /api/v1/verification-requests/:id

List Verification Requests

GET /api/v1/verification-requests

Query parameters: page, limit (max 100), status, reference_id

Get by Reference ID

GET /api/v1/verification-requests/by-reference/:referenceId

Cancel Verification Request

DELETE /api/v1/verification-requests/:id

Verification Request Statuses

StatusDescription
pendingWaiting for verifications to complete
partialSome assets verified, others pending
completedAll required assets verified
expiredRequest expired before completion
cancelledRequest was cancelled

Validate Proof

POST /api/v1/proofs/validate

Validate a proof token via API.

Request Body

ParameterTypeRequiredDescription
proof_tokenstringYesThe JWT proof token
identifierstringNoOptional identifier to verify against

Response (Valid)

json
{
  "valid": true,
  "verification": {
    "id": "507f1f77bcf86cd799439011",
    "type": "phone",
    "channel": "whatsapp",
    "verified_at": "2026-02-04T10:51:30Z",
    "expires_at": "2026-03-06T10:51:30Z"
  }
}

Response (Invalid)

json
{
  "valid": false,
  "reason": "expired",
  "message": "jwt expired"
}

Offline Verification

Proofs are signed with RS256 and can be verified without calling our API.

1. Fetch Public Key (cache 24h)

bash
curl https://api.proof.holdings/.well-known/jwks.json
json
{
  "keys": [{
    "kty": "RSA",
    "n": "0vx7agoebGcQ...",
    "e": "AQAB",
    "alg": "RS256",
    "use": "sig",
    "kid": "proof-holdings-2026-01"
  }]
}

2. Fetch Revocation List (cache 5min)

bash
curl https://api.proof.holdings/api/v1/proofs/revoked

3. Verify Locally

javascript
import jwt from 'jsonwebtoken';

// Verify signature
const payload = jwt.verify(proofToken, publicKey, { algorithms: ['RS256'] });

// Check not revoked
const isRevoked = revokedList.some(r => r.proof_id === payload.sub);

// Check not expired
const isValid = !isRevoked && payload.exp > Date.now() / 1000;

Revoke Proof

POST /api/v1/proofs/:id/revoke

Revoke a proof (GDPR deletion, fraud, etc).

Request Body

ParameterTypeRequiredDescription
reasonstringYesuser_request, fraud, key_compromise, asset_transferred, other

Response

json
{
  "success": true,
  "verification_id": "507f1f77bcf86cd799439011",
  "status": "revoked",
  "revoked_at": "2026-02-04T12:00:00Z"
}

Webhook Deliveries

Monitor and manage webhook deliveries for your account. Webhooks are sent when verification requests complete, partially complete, or expire.

List Webhook Deliveries

GET /api/v1/webhook-deliveries

Query parameters: page, limit (max 100), status (pending, delivered, failed), event_type, verification_request_id

Response

json
{
  "data": [
    {
      "id": "507f1f77bcf86cd799439011",
      "verification_request_id": "507f1f77bcf86cd799439012",
      "event_type": "verification_request.completed",
      "url": "https://yourapp.com/webhook",
      "status": "delivered",
      "attempts": 1,
      "max_attempts": 3,
      "response_status": 200,
      "created_at": "2026-02-13T12:00:00Z",
      "updated_at": "2026-02-13T12:00:01Z"
    }
  ],
  "pagination": { "hasMore": false }
}

Get Webhook Delivery

GET /api/v1/webhook-deliveries/:id

Returns full delivery details including request payload and response body.

Get Delivery Statistics

GET /api/v1/webhook-deliveries/stats

json
{
  "total": 150,
  "delivered": 142,
  "failed": 5,
  "pending": 3,
  "success_rate": 94.67
}

Retry Failed Delivery

POST /api/v1/webhook-deliveries/:id/retry

Retry a failed webhook delivery. Only available for deliveries with status failed.

Webhook Event Types

EventDescription
verification_request.completedAll required assets verified
verification_request.partialSome assets verified (when partial_ok is true)
verification_request.expiredRequest expired before completion
verification.completedIndividual verification completed

Webhook Signature

Webhooks include an HMAC-SHA256 signature in the X-Proof-Signature header. Verify this against your webhook secret to ensure authenticity.

javascript
const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Verification Statuses

StatusDescription
pendingChallenge created, waiting for user
verifiedSuccessfully verified, proof available
failedMax attempts exceeded
expiredChallenge expired
revokedProof was revoked

Error Responses

All errors return:

json
{
  "error": {
    "code": "error_code",
    "message": "Human-readable message"
  }
}
CodeHTTPDescription
validation_error400Invalid request parameters
invalid_identifier400Identifier format is invalid
invalid_channel_for_type400Channel doesn't match type
unauthorized401Invalid or missing API key
not_found404Resource not found
challenge_expired400Verification challenge expired
rate_limited429Too many requests

Rate Limits

  • 100 requests/minute per API key
  • Webhook deliveries: automatic retry with exponential backoff

See Also

FeatureEndpointsDocumentation
Smart ReuseGET /api/v1/verify/request/:id/proofs, POST .../auto-verifySmart Reuse
Multi-ProfileGET/POST /api/v1/me/profiles, PATCH/DELETE .../profiles/:idMulti-Profile System
Message TemplatesGET/POST /api/v1/me/projects, PUT .../templates/:channel/:typeMessage Templates

Related

SDKsMCP ServerPricing
Last updated February 18, 2026
Proof
Proofof Holdings

Trust infrastructure for humans and AI agents. Verify control, delegate authority, get human approval — with cryptographic proof.

XGitHubLinkedIn
© 2026 Proof of Holdings

A service of LT Telecom (Uždaroji akcinė bendrovė "LT telekomunikacijos")

Product

  • How It Works
  • Verification Types
  • Human Approvals
  • FAQ

Developers

  • Docs
  • MCP Server
  • Integrations
  • OpenAPI Spec
  • GitHub Docs
  • API Status

Company

  • Brand Kit
  • About
  • Privacy
  • Terms