Docs

Veil Guard is a single HTTP API. Every request has a source type, every response has a risk score, list of matched attack types, redacted text, and a suggested action.

Authentication

Bearer token in the Authorization header. Keys are issued through the self-serve email verification flow at /console and look like vgk_….

Authorization: Bearer vgk_2xlWiRzk-3GbOEwvHsQenAScnpYduF_D

Self-serve signup

Create a free key with a one-time email verification flow. The hosted UI for this lives at /console.

POST /v1/keys/create
{
  "email": "you@company.com"
}

POST /v1/keys/verify
{
  "token": "one-time verification token"
}

GET /v1/account

{
  "name": "you",
  "email": "you@company.com",
  "tier": "free",
  "requests_total": 42,
  "limit": 1000,
  "reset_at": 1776397761.4,
  "stripe_customer_linked": false,
  "has_active_subscription": false
}

POST /v1/scan

Request

{
  "text": "string, max 200,000 chars",
  "source": "user|rag|tool_output|web|system",
  "context": "optional — surrounding text for split-attack detection",
  "llm_judge": false   // Enterprise tier only; default off
}

Response

{
  "risk_score": 0.98,
  "attack_types": ["direct_injection", "system_prompt_leak"],
  "confidence": 0.88,
  "redacted_text": "<<GUARD_REDACTED>>",
  "reasoning": "regex:direct_injection/… + regex:system_prompt_leak/…",
  "action": "block",   // allow | flag | block
  "latency_ms": 3,
  "signals": {
    "regex_findings": 2,
    "context_supplied": true,
    "context_findings": 1,
    "context_used": true,
    "unicode_triggered": false,
    "context_unicode_triggered": false,
    "classifier_available": true,
    "classifier_prob": 0.92,
    "semantic_available": true,
    "semantic_similarity": 0.88,
    "semantic_match": "direct_injection",
    "llm_judge_used": false
  }
}

context is not separately returned or redacted. It is used as a short boundary window so Guard can catch instructions that are split across surrounding text and the primary payload.

Per-source thresholds

SourceBlock aboveWhy
user0.80Naturally noisy
rag0.55Documents shouldn't speak in imperatives
tool_output0.50Tools shouldn't prefix with "You are…"
web0.50Untrusted by default
system0.30System is controlled — any injection is high signal

POST /v1/scan/stream

Server-Sent Events for long documents. Splits on paragraph boundaries (fallback to 2KB chunks), emits start, one chunk per piece, then a final done with the aggregate.

event: start
data: {"chunks": 3}

event: chunk
data: {"index": 0, "risk_score": 0.0, "attack_types": [], "action": "allow", "snippet": "…"}

event: chunk
data: {"index": 1, "risk_score": 0.98, "attack_types": ["direct_injection"], "action": "block", "snippet": "Ignore previous…"}

event: done
data: {"risk_score": 0.98, "attack_types": [...], "action": "block", "chunks_scanned": 3, "latency_ms": 140}

GET /v1/usage

{
  "tier": "starter",
  "requests_total": 842,
  "limit": 10000,
  "reset_at": 1776397761.4
}

POST /v1/billing/portal

Creates a Stripe customer-portal session so a paying user can update payment method, change plan, or cancel without contacting support.

{
  "return_url": "https://guard.veil-api.com/console"
}

Integration: OpenAI SDK

import httpx
from openai import OpenAI

client = OpenAI()
GUARD = "https://guard.veil-api.com"
GUARD_KEY = "vgk_…"

def guard_scan(text, source="user"):
    r = httpx.post(
        f"{GUARD}/v1/scan",
        headers={"Authorization": f"Bearer {GUARD_KEY}"},
        json={"text": text, "source": source},
        timeout=5,
    )
    return r.json()

user_msg = request_body["message"]
verdict = guard_scan(user_msg, "user")
if verdict["action"] == "block":
    return {"error": "input blocked", "reasoning": verdict["reasoning"]}

resp = client.chat.completions.create(
    model="gpt-5",
    messages=[{"role": "user", "content": user_msg}],
)

Integration: Anthropic SDK

import httpx
from anthropic import Anthropic

client = Anthropic()

def guarded_message(user_text, retrieved_docs):
    v1 = httpx.post(GUARD + "/v1/scan",
                    headers={"Authorization": f"Bearer {GUARD_KEY}"},
                    json={"text": user_text, "source": "user"}).json()
    if v1["action"] == "block":
        raise ValueError("user input blocked: " + v1["reasoning"])

    # Indirect injection is the bigger risk — scan the docs too.
    for doc in retrieved_docs:
        v2 = httpx.post(GUARD + "/v1/scan",
                        headers={"Authorization": f"Bearer {GUARD_KEY}"},
                        json={"text": doc, "source": "rag"}).json()
        if v2["action"] == "block":
            retrieved_docs.remove(doc)

    return client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[{"role": "user", "content": user_text + "\n\n" + "\n\n".join(retrieved_docs)}],
    )

Integration: LangChain

from langchain_core.runnables import RunnableLambda
import httpx

def guard_gate(source="user"):
    def _gate(text):
        r = httpx.post(GUARD + "/v1/scan",
                       headers={"Authorization": f"Bearer {GUARD_KEY}"},
                       json={"text": text, "source": source}).json()
        if r["action"] == "block":
            raise ValueError(f"Guard blocked: {r['reasoning']}")
        return text
    return RunnableLambda(_gate)

chain = guard_gate("user") | prompt | llm

Integration: LlamaIndex

from llama_index.core.node_parser import NodeParser
import httpx

def filter_injected_nodes(nodes):
    clean = []
    for n in nodes:
        r = httpx.post(GUARD + "/v1/scan",
                       headers={"Authorization": f"Bearer {GUARD_KEY}"},
                       json={"text": n.text, "source": "rag"}).json()
        if r["action"] != "block":
            clean.append(n)
    return clean

retrieved = retriever.retrieve(query)
safe_nodes = filter_injected_nodes(retrieved)

Integration: LlamaStack

# Use Guard as a shield that runs before every generation step.
from llama_stack_client import LlamaStackClient
import httpx

def guard_shield(message, source="user"):
    r = httpx.post(GUARD + "/v1/scan",
                   headers={"Authorization": f"Bearer {GUARD_KEY}"},
                   json={"text": message, "source": source}).json()
    return r["action"] != "block", r

client = LlamaStackClient(base_url=...)
ok, verdict = guard_shield(user_input, "user")
if not ok:
    raise RuntimeError(verdict["reasoning"])

Error codes

StatusMeaning
400Invalid source or malformed body
401Missing or invalid API key
403Key not enrolled in Veil Guard
429Rate limit or monthly quota exceeded
502Upstream Stripe or model error

Limits

  • 200,000 chars per scan (all tiers).
  • 60 scans/minute per API key. 120/min per IP for the public demo.
  • Monthly quotas: Free 1k, Starter 10k, Growth 100k, Enterprise 1M.
  • LLM-as-judge is Enterprise-only and must be opted in per request with "llm_judge": true.