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:
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:
| Field | Type | Required | Description |
|---|---|---|---|
form_id | string | Yes | Identifier for your form |
form_data | object | No | User-submitted form fields |
metadata | object | No | System metadata (ip, email, etc.) |
At least one of metadata.email, metadata.ip, or form_data must be provided.
Metadata fields:
| Field | Type | Description |
|---|---|---|
email | string | User’s email address |
ip | string | User’s IP address |
user_agent | string | Browser user agent |
honeypot_field | string | Hidden honeypot field value |
form_loaded_at | number | Timestamp when form was loaded |
turnstile_token | string | Turnstile/CAPTCHA token |
Example Request:
{
"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).
{
"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:
{
"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, otherwisenull.error—{ code, message }on failure, otherwisenull. Exactly one ofdata/erroris 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:
{
"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.
| dataset | adds | cost |
|---|---|---|
region | network.cloud_provider + network.cloud_region (e.g. aws / us-west-1) | included |
company | a company block: { name, website, country, category, confidence } (by ASN) | included |
rdns | network.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:
{
"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):
{
"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:
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request format |
| 401 | MISSING_KEY | No Authorization header |
| 401 | INVALID_KEY | Invalid API key |
| 401 | EXPIRED_KEY | API key has expired |
| 403 | KEY_DISABLED | API key is disabled |
| 422 | MISSING_REQUIRED_DATA | No analyzable data provided |
| 429 | QUOTA_EXCEEDED | Rate limit exceeded |
| 500 | INTERNAL_ERROR | Server error |
Rate Limit Response (429): the rate-limit detail rides in metadata.
{
"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 }
}
}