VettiGuard

VettiGuard API

Browse the complete reference in the browser, copy request examples, and follow the documented validation contract without opening packaged Markdown files.

Complete browser reference

VettiGuard API

All API responses are JSON. Browser endpoints require an allowed Origin or Referer hostname. The verification endpoint is intended for server-to-server requests.

Use the application root as the base URL. Do not include /public; for example, https://vettiguard.example.com/api/v1/siteverify.

Create challenge

POST /api/v1/challenge

{
  "site_key": "vg_site_...",
  "action": "contact-form"
}

A successful response contains a challenge ID, challenge type, expiry, and the information needed to present or complete the selected challenge. The site’s workspace must be active and have remaining monthly challenge capacity. Supported challenge types are checkbox, invisible, score_based, policy, math, proof_of_work, and visual. Legacy virtual sites are treated as score_based. Every Checkbox completion is converted into a visual identify challenge after the visitor clicks the checkbox. Invisible challenges may step up according to their threshold. Score-based mode never opens a challenge; it issues a token whose score is evaluated by the customer backend. Policy mode can pass, request a visual step-up, or reject the request. A visual challenge can return an identify grid, a match board, or a drag_drop placement board; every answer is validated server-side before the normal siteverify flow.

Visual identify response

{
  "success": true,
  "challenge": {
    "challenge_id": "4d6ac1b0-2d0e-4b0b-9f50-42f36a0d3a24",
    "type": "visual",
    "visual_variant": "identify",
    "prompt": "Select all images that show animals.",
    "grid_size": "3x3",
    "items": [
      {"id": "opaque-item-a", "asset_path": "/api/v1/visual-assets/4d6ac.../opaque-item-a"},
      {"id": "opaque-item-b", "asset_path": "/api/v1/visual-assets/4d6ac.../opaque-item-b"}
    ],
    "retry_limit": 2,
    "fallback_available": true,
    "expires_in": 120
  }
}

Submit an array containing every selected item ID:

{
  "site_key": "vg_site_...",
  "challenge_id": "4d6ac1b0-2d0e-4b0b-9f50-42f36a0d3a24",
  "answer": ["opaque-item-a", "opaque-item-f"],
  "action": "contact-form",
  "telemetry": {}
}

Visual matching response

A matching challenge uses "visual_variant": "match". Submit an array of two-item arrays. Every item must be used exactly once, and both IDs in each pair must refer to the same server-side object:

{
  "site_key": "vg_site_...",
  "challenge_id": "4d6ac1b0-2d0e-4b0b-9f50-42f36a0d3a24",
  "answer": [
    ["opaque-cat-a", "opaque-cat-b"],
    ["opaque-car-a", "opaque-car-b"],
    ["opaque-tree-a", "opaque-tree-b"]
  ],
  "action": "contact-form",
  "telemetry": {}
}

Drag-and-drop response

A drag-and-drop site mode is issued as a visual challenge with "visual_variant": "drag_drop". Each item has an opaque ID and a public role of either source or target. Source items include short-lived asset URLs. Target items expose only a category label; the private source-to-category relationship is not included.

{
  "success": true,
  "challenge": {
    "challenge_id": "4d6ac1b0-2d0e-4b0b-9f50-42f36a0d3a24",
    "type": "visual",
    "visual_variant": "drag_drop",
    "prompt": "Drag each object into the correct category.",
    "grid_size": "2-objects",
    "items": [
      {"id": "source-a", "asset_path": "/api/v1/visual-assets/4d6ac.../source-a", "role": "source"},
      {"id": "source-b", "asset_path": "/api/v1/visual-assets/4d6ac.../source-b", "role": "source"},
      {"id": "target-a", "asset_path": null, "role": "target", "target_label": "Vehicles"},
      {"id": "target-b", "asset_path": null, "role": "target", "target_label": "Nature"}
    ],
    "retry_limit": 2,
    "fallback_available": true,
    "expires_in": 120
  }
}

Submit one [source_id, target_id] pair for every source and target. The order of the pairs is irrelevant, but source and target roles cannot be reversed:

{
  "site_key": "vg_site_...",
  "challenge_id": "4d6ac1b0-2d0e-4b0b-9f50-42f36a0d3a24",
  "answer": [
    ["source-a", "target-b"],
    ["source-b", "target-a"]
  ],
  "action": "contact-form",
  "telemetry": {}
}

Incorrect visual answers may return retryable and remaining_attempts. The same challenge remains active until its configured attempt limit is reached.

Accessible visual fallback

The bundled widget handles this automatically. A direct client must include the active visual challenge ID; the server consumes that challenge before issuing the configured fallback:

{
  "site_key": "vg_site_...",
  "action": "contact-form",
  "visual_fallback": true,
  "fallback_for": "4d6ac1b0-2d0e-4b0b-9f50-42f36a0d3a24"
}

Workspace quota response

When a workspace has exhausted its monthly challenge allowance, challenge issuance returns HTTP 429:

{
  "success": false,
  "error": "monthly-challenge-limit-reached",
  "message": "The workspace monthly challenge quota has been reached."
}

A workspace that becomes unavailable during issuance returns HTTP 403 with workspace-unavailable. Credentials belonging to a workspace that was already suspended or closed are rejected during site-key or secret lookup. Existing response tokens remain verifiable after a monthly quota is reached, provided the workspace and site are still active. Applications should treat these responses as service-configuration issues rather than asking the visitor to repeatedly retry.

Solve challenge

POST /api/v1/solve

{
  "site_key": "vg_site_...",
  "challenge_id": "4d6ac...",
  "answer": true,
  "action": "contact-form",
  "telemetry": {
    "elapsed_ms": 2450,
    "pointer_events": 14,
    "key_events": 0,
    "focus_events": 1,
    "touch_events": 0,
    "visibility_changes": 0,
    "webdriver": false
  }
}

A completed solve returns a short-lived, single-use response token. The browser response deliberately does not include the numeric risk score. The authoritative score is returned only by the server-to-server siteverify endpoint.

For Checkbox verification, the first valid checkbox click always returns an object-identification challenge. Invisible or policy verification can return the same step-up response according to their thresholds:

{
  "success": true,
  "challenge_required": true,
  "reason": "risk-escalation",
  "challenge": {
    "challenge_id": "4d6ac...",
    "type": "visual",
    "visual_variant": "identify",
    "prompt": "Select all images that show bicycles.",
    "items": [],
    "expires_in": 120
  }
}

The bundled widget handles this response automatically and opens the visual popup only after the user clicks the checkbox or submits an invisible/policy-protected form.

Verify response token

POST /api/v1/siteverify

This endpoint is server-to-server only. It accepts JSON and application/x-www-form-urlencoded request bodies.

{
  "secret": "vg_secret_...",
  "response": "response-token",
  "remoteip": "203.0.113.10",
  "action": "contact-form"
}

Recommended form-encoded cURL request:

curl --silent --show-error --fail-with-body \
  --request POST "https://vettiguard.example.com/api/v1/siteverify" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "secret=vg_secret_replace_me" \
  --data-urlencode "response=replace_with_widget_token" \
  --data-urlencode "remoteip=203.0.113.10" \
  --data-urlencode "action=contact-form"

Successful response:

{
  "success": true,
  "challenge_ts": "2026-07-30 21:42:18",
  "hostname": "example.com",
  "score": 0.82,
  "score_threshold": 0.5,
  "score_enforced": true,
  "challenge_type": "visual",
  "challenge_variant": "identify",
  "action": "contact-form",
  "error-codes": []
}

The backend must require success: true, an empty error-codes array, the expected action, an explicitly allowed hostname, and an acceptable score before performing the protected operation. In score-based mode, score_enforced is false; VettiGuard reports the score but leaves the allow, step-up, throttle, or reject decision to your backend.

Tokens are consumed during the first valid verification attempt, including attempts that fail configured score, action, or remote-IP policy. Do not retry the same token.

The detailed verification contract—including Linux, Windows CMD, PowerShell, JSON and form-encoded cURL commands; HTTP status handling; success and failure payloads; raw PHP cURL validation; hostname/action/score checks; proxy guidance; and the complete error-code table—is documented in:

docs/SERVER-SIDE-VERIFICATION.md

Error codes

Common browser errors include invalid-site-key, invalid-origin, workspace-unavailable, monthly-challenge-limit-reached, rate-limit-exceeded, invalid-challenge, challenge-expired, challenge-already-used, incorrect-answer, risk-rejected, visual-fallback-invalid, action-mismatch, and client-mismatch.

Verification errors include missing-input-secret, invalid-input-secret, missing-input-response, invalid-input-response, timeout-or-duplicate, action-mismatch, remoteip-mismatch, and low-score.

Complete PHP sample

A full contact-form integration with browser rendering, CSRF protection, ordinary field validation, server-to-server verification, action/hostname/score checks, readable errors, and one-time token handling is included at:

examples/php-contact-form

The optional bundled live version can be enabled at /sample/contact-form. See docs/SAMPLE-USAGE.md for configuration and deployment guidance.

Widget attribution URL

The Protected by VettiGuard attribution hyperlinks the VettiGuard name to the official public website. Set the destination on the script tag:

<script src="https://verify.example.com/assets/captcha.js"
        data-brand-url="https://www.example.com"
        defer></script>

Or override it on one widget:

<div class="vettiguard-captcha"
     data-sitekey="vg_site_replace_me"
     data-brand-url="https://www.example.com"></div>

brandUrl is also accepted by VettiGuard.render(). The widget accepts only http: and https: URLs and opens the attribution in a new tab using rel="noopener noreferrer".