API Reference

FormShield API documentation

The FormShield API validates form submissions and scores traffic using multiple signals. Every result carries a score from 0.0 to 1.0 and a decision of allow, review, or block.

Base URL

https://api.formshield.dev

Authentication

All requests require a Bearer token in the Authorization header:

bash
curl -X POST https://api.formshield.dev/v1/check \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json"

Endpoints

POST /v1/check

Check a form submission for spam.

Request Body:

FieldTypeRequiredDescription
form_idstringYesIdentifier for your form
form_dataobjectNoUser-submitted form fields
metadataobjectNoSystem metadata (ip, email, etc.)

At least one of metadata.email, metadata.ip, or form_data must be provided.

Metadata fields:

FieldTypeDescription
emailstringUser’s email address
ipstringUser’s IP address
user_agentstringBrowser user agent
honeypot_fieldstringHidden honeypot field value
form_loaded_atnumberTimestamp when form was loaded
turnstile_tokenstringTurnstile/CAPTCHA token

Example Request:

json
{
  "form_id": "contact-form",
  "form_data": {
    "name": "John Smith",
    "message": "Hello, I'm interested in your product"
  },
  "metadata": {
    "email": "john@example.com",
    "ip": "203.0.113.42",
    "user_agent": "Mozilla/5.0..."
  }
}

Response (200): wrapped in the standard envelope. The score is a calibrated probability in [0,1] with a decision enum (not the legacy 0–10 sum + verdict/action).

json
{
  "version": "1",
  "data": {
    "score": 0.04,
    "confidence": 0.74,
    "decision": "allow",
    "categories": { "spam": 0.04, "fraud": 0.01, "cold_email": 0.02 },
    "signals": {
      "ip": { "risk": 0, "country_code": "US", "is_vpn": false, "is_datacenter": false },
      "email": { "risk": 0, "disposable": false, "domain": "example.com" },
      "content": { "risk": 0.05, "spam_probability": 0.05, "language": "en", "flags": [] }
    },
    "rule_matches": [],
    "fusion": { "method": "weighted_sum", "model_version": "v2" }
  },
  "error": null,
  "metadata": { "request_id": "req_abc123def456", "processing_time_ms": 234 }
}

Response Envelope

Every response is wrapped in a standard envelope so the boundary between the answer and request metadata is always explicit and identical across endpoints:

json
{
  "version": "1",
  "data": { },
  "error": null,
  "metadata": {
    "request_id": "req_abc123def456",
    "processing_time_ms": 12
  }
}
  • version — envelope schema version (string). Bumped only on a breaking change to the envelope itself.
  • data — the endpoint payload on success, otherwise null.
  • error{ code, message } on failure, otherwise null. Exactly one of data / error is non-null.
  • metadata — request/response info, never the answer: request_id, processing_time_ms, plus any endpoint provenance (e.g. source, built_at).

Example — GET /v1/ip/{ip} for a Tor exit node:

json
{
  "version": "1",
  "data": {
    "ip": "185.220.101.1",
    "ip_version": 4,
    "network": {
      "asn": 60729,
      "org": "TORSERVERS-NET",
      "hostname": null,
      "hostname_verified": null,
      "cloud_provider": null,
      "cloud_region": null
    },
    "location": { "country": "DE" },
    "type": { "datacenter": true, "hosting": true, "isp": false, "mobile": false, "cloud": false },
    "risk": { "proxy": true, "vpn": true, "tor": true, "residential_proxy": false, "scanner": true },
    "flags": ["datacenter", "hosting", "proxy", "vpn", "tor", "scanner"]
  },
  "error": null,
  "metadata": {
    "request_id": "req_04e3c0cc2ffd",
    "processing_time_ms": 1,
    "source": "ipatlas",
    "built_at": "2026-06-01T22:18:11+00:00",
    "enrichment": { "requested": [], "resolved": [], "credits": 1 }
  }
}

Enrichment (?include=)

GET /v1/ip/{ip} is a fast base lookup by default. Opt into extra datasets with a comma-separated ?include= (or include=all) — each adds fields and may cost an extra credit. Unknown names → 400.

datasetaddscost
regionnetwork.cloud_provider + network.cloud_region (e.g. aws / us-west-1)included
companya company block: { name, website, country, category, confidence } (by ASN)included
rdnsnetwork.hostname (reverse DNS / PTR) + network.hostname_verified (forward-confirmed)+1 credit on a cache miss

metadata.enrichment reports { requested, resolved, skipped?, cache?, credits } so the cost and coverage are explicit. Example — GET /v1/ip/3.5.1.1?include=region,company,rdns:

json
{
  "data": {
    "ip": "3.5.1.1",
    "ip_version": 4,
    "network": {
      "asn": 16509,
      "org": "AMAZON-02",
      "hostname": "s3-us-east-1.amazonaws.com",
      "hostname_verified": true,
      "cloud_provider": "aws",
      "cloud_region": "us-east-1"
    },
    "company": {
      "name": "Amazon.com",
      "website": "https://www.amazon.com",
      "country": "US",
      "category": "Hosting and Cloud Provider",
      "confidence": "high"
    },
    "location": { "country": "US" },
    "type": { "datacenter": true, "hosting": true, "isp": false, "mobile": false, "cloud": true },
    "risk": { "proxy": false, "vpn": false, "tor": false, "residential_proxy": false, "scanner": false },
    "flags": ["datacenter", "hosting", "cloud"]
  },
  "error": null,
  "metadata": {
    "source": "ipatlas",
    "enrichment": {
      "requested": ["region", "company", "rdns"],
      "resolved": ["region", "company", "rdns"],
      "cache": { "rdns": "hit" },
      "credits": 1
    }
  }
}

Error Responses

Inside the envelope, a failure populates error (and data is null):

json
{
  "version": "1",
  "data": null,
  "error": { "code": "VALIDATION_ERROR", "message": "ip is not a valid IPv4 or IPv6 address" },
  "metadata": { "request_id": "req_abc123def456", "processing_time_ms": 3 }
}

HTTP Status Codes:

StatusCodeDescription
400VALIDATION_ERRORInvalid request format
401MISSING_KEYNo Authorization header
401INVALID_KEYInvalid API key
401EXPIRED_KEYAPI key has expired
403KEY_DISABLEDAPI key is disabled
422MISSING_REQUIRED_DATANo analyzable data provided
429QUOTA_EXCEEDEDRate limit exceeded
500INTERNAL_ERRORServer error

Rate Limit Response (429): the rate-limit detail rides in metadata.

json
{
  "version": "1",
  "data": null,
  "error": { "code": "QUOTA_EXCEEDED", "message": "Monthly request quota exceeded" },
  "metadata": {
    "request_id": "req_abc123def456",
    "processing_time_ms": 1,
    "ratelimit": { "limit": 1000, "remaining": 0, "reset": 1704067200000, "reset_in_seconds": 86400 }
  }
}

Type to search…

↑↓ navigate open esc close