---
title: "Create verification"
method: POST
path: "/api/v1/verifications"
tags: ["Verifications"]
---

# Create verification

`POST /api/v1/verifications`

## Request

- Base URL: `https://api.proof.holdings`
- URL: `https://api.proof.holdings/api/v1/verifications`
- Auth: HTTP bearer

Start a new verification challenge for an asset.

The `challenge` object withholds the completion material for the pairs whose code we deliver out-of-band — `type: email`, and `type: domain` with `channel: email`. For those the code and the magic link are absent by design: the delivery to the address IS what the proof rests on, so a caller holding the code would prove nothing by submitting it back. Every other pair keeps its code, because the caller has to relay it (phone) or publish it (the TXT record or well-known file for a domain).

## Request body

- CreateVerificationRequest: Request body for creating a new verification
  - `profile_id` string: The sub-account (project) this is for; its branding and templates are used for every message. An account-wide key may name any project of its account; a project-bound key acts for its own project only (naming another answers `403 profile_mismatch`). Omitted: the key's project, else the primary project.
  - `type` 'phone' | 'email' | 'domain' | 'social' | 'wallet' | 'account' | 'telegram_bot', required: Type of asset being verified
  - `channel` 'whatsapp' | 'telegram' | 'sms' | 'email' | 'dns' | 'http' | 'auto' | 'github' | 'google' | 'facebook' | 'x' | 'linkedin' | 'instagram' | 'youtube' | 'tiktok' | 'ethereum' | 'solana' | 'bitcoin' | 'coinbase' | 'kraken' | 'telegram_bot_token', required: Channel used for verification
  - `identifier` string, required: The asset identifier to verify
  - `external_user_id` string: Your user ID for grouping verifications
  - `client_metadata` object: Custom metadata to attach to verification
  - `dns_provider` object: Required for channel=auto (domain verification)
    - `provider` string, required: DNS provider name
    - `api_token` string: API token (Cloudflare, DigitalOcean, etc.)
    - `api_key` string: API key (GoDaddy, Namecheap, etc.)
    - `api_secret` string: API secret (GoDaddy)
    - `account_id` string: Account identifier
    - `username` string: Username (Namecheap)
    - `access_key_id` string: AWS access key ID (Route53)
    - `secret_access_key` string: AWS secret access key (Route53)
    - `region` string: AWS region (Route53)
  - `email_prefix` 'admin' | 'administrator' | 'webmaster' | 'hostmaster' | 'postmaster': Email prefix for domain email verification
  - `bot_token` string: Telegram bot token (required for channel=telegram_bot_token)
  - `proof_expiry_days` integer: Per-request proof expiry override in days (1-365). Overrides account default.

## Response `201`

Verification created

- Verification: Verification object
  - `id` string, required: MongoDB ObjectId
  - `type` 'phone' | 'email' | 'domain' | 'social' | 'wallet' | 'account' | 'telegram_bot', required: Type of asset being verified
  - `channel` 'whatsapp' | 'telegram' | 'sms' | 'email' | 'dns' | 'http' | 'auto' | 'github' | 'google' | 'facebook' | 'x' | 'linkedin' | 'instagram' | 'youtube' | 'tiktok' | 'ethereum' | 'solana' | 'bitcoin' | 'coinbase' | 'kraken' | 'telegram_bot_token', required: Channel used for verification
  - `identifier` string: The identifier being verified; absent for an identifier-free phone verification, whose number is learned only when the person completes it
  - `status` 'pending' | 'verified' | 'failed' | 'expired' | 'revoked' | 'cancelled', required: Current status of the verification
  - `challenge` Challenge: Challenge details for verification
    - `code` string: Challenge code to send/enter
    - `instruction` string: Human-readable instruction for the user
    - `expires_at` string, date-time: When the challenge expires
  - `proof` Proof: Proof token after successful verification
    - `token` string, required: Signed JWT proof token (ES256; verify against the JWKS key named by the token's `kid`)
    - `expires_at` string, date-time, required: When the proof expires
    - `confidence` 'high' | 'standard' | 'degraded': Verification confidence level (present for SMS verifications)
  - `external_user_id` string
  - `attempts` integer
  - `max_attempts` integer
  - `created_at` string, date-time, required
  - `updated_at` string, date-time
  - `verified_at` string, date-time

## Other responses

- `400`: Validation error
- `401`: Unauthorized
- `402`: card_required. Only once billing is live: a free account with no card on file that has used its included proofs and the 50-proof grace this month is refused new proof requests until a card is added (add_card_url). Proofs already issued keep working. The error carries add_card_url, used, included and hard_limit.
- `403`: profile_mismatch — a project-bound API key named another project (it acts for its own project only); profile_not_owned — the named project does not belong to the account (h-subaccount-branding-per-project)
- `429`: Rate limit exceeded
- `503`: `challenge_delivery_failed` — the challenge could not be delivered to the party being verified (email service unavailable, or the send reported failure). Applies to the two mailed pairs only. NO verification is created: the row is deleted, because the create response withholds the code for these pairs and a caller holding neither the mail nor the code has no way to complete it. Nothing is billed — a proof is billed when one is issued, and an undelivered challenge never reaches that point. Retry.

---

[API](https://skmtc.dev/proof/apis/proof-holdings-api.md) · [All operations](https://skmtc.dev/proof/apis/proof-holdings-api/llms.txt) · [OpenAPI document](https://skmtc.dev/proof/apis/proof-holdings-api/revisions/b15f506de696?raw)
