Docs / Surface / api

API Reference

All endpoints require a Bearer token: Authorization: Bearer <api-key>.

You can provide your own correlation ID by sending the X-Request-ID header, and Surface echoes it in the response and webhook payload. If omitted, one is generated automatically.

---

POST /api/scan

Scan a file upload.

Request: multipart/form-data

Field Required Description
file Yes File to scan
profile_id No Scan profile ID to use (defaults to the profile linked to the API key)

Query params

Param Default Description
defer false Set to true to return immediately without waiting for results (async mode)

Response: see Understanding Results below.

---

POST /api/scan/payload

Scan a raw text or binary payload for threats.

Request

json

{
  "payload": "string",
  "label": "optional filename hint for format detection",
  "encoding": "raw"
}
Field Required Description
payload Yes File content as raw text (default) or base64-encoded string (max decoded size 10 MB)
encoding No "raw" (default) or "base64" for binary payloads
label No Filename hint for format detection (e.g. "test.php")

Response: same shape as file scan. See Understanding Results.

---

Async / Deferred Scanning

For large files or high-throughput workflows, add ?defer=true to the scan request. Files larger than 25 MB are deferred automatically even without the flag.

bash

curl -X POST "https://app.tendrl.com/surface/api/scan?defer=true" \
  -H "Authorization: Bearer YOUR_KEY" \
  -F "file=@large_archive.zip"

Returns immediately with HTTP 202 Accepted:

json

{
  "scanId": "uuid",
  "requestId": "uuid",
  "status": "pending",
  "message": "Scan queued. Poll GET /api/scan/{scanId} for results."
}

If you've configured a webhook, results are also POSTed to your endpoint when the scan finishes.

---

Understanding Results

Every scan returns a JSON response with a safety score and threat details:

json

{
  "requestId": "550e8400-e29b-41d4-a716-446655440000",
  "name": "report.pdf",
  "hash": "e3b0c44298fc1c149afbf4c8...",
  "size": 245760,
  "contentType": "application/pdf",
  "safetyScore": {
    "score": 95,
    "threatLevel": "Clean",
    "confidence": "High",
    "confidenceScore": 0.85,
    "confidenceReason": "3 engines analyzed; signature check clear; no rule matches",
    "primaryThreat": "No threats detected",
    "threatSummary": "No threats detected",
    "enginesUsed": ["Malware Signatures", "YARA", "Static Analysis"],
    "recommendedAction": "Allow"
  },
  "scanTimeMs": 1250,
  "scanType": "file",
  "timestamp": 1741500000
}

Key fields

Field Description
requestId Correlation ID for this scan. Send X-Request-ID to provide your own
safetyScore.score 0-100 safety score. Higher is safer
safetyScore.threatLevel Clean (86-100), Informational (71-85), Suspicious (31-70), or Malicious (0-30)
safetyScore.confidence High, Medium, or Low: how certain the verdict is
safetyScore.confidenceScore Numeric confidence (0-1) for programmatic use
safetyScore.confidenceReason Human-readable explanation of the confidence level
safetyScore.primaryThreat Name of the detected threat, or "No threats detected"
safetyScore.threatSummary Brief description of what was found
safetyScore.enginesUsed Which scan engines analyzed this file
safetyScore.recommendedAction Allow, Review, or Block
safetyScore.cveFindings CVE records (id, description, CVSS, references) when a scan matches known vulnerabilities
hash SHA-256 hash of the scanned content
scanTimeMs Scan duration in milliseconds
scanType "file" or "payload"

Safety score ranges

Score Range Threat Level Recommended Action
86-100 Clean Allow, safe to use
71-85 Informational Allow, minor findings worth noting
31-70 Suspicious Review before using
0-30 Malicious Block or quarantine

Scores combine signals from all engines. A file flagged by multiple independent engines scores lower than one flagged by a single check.

Tip

Don't want to think about numbers? Use recommendedAction; it is always one of three values: Allow, Review, or Block. No threshold logic required on your end.

Payload scan additional fields

Payload scans include additional engine results for agentic/content security:

Field Description
codeExtraction Embedded code blocks found in text payloads. Returns blocksFound, indicators[], and dangerBlocks[]
promptInjection Prompt injection detection. Returns detected, risk level, and findings[]
sensitiveData Exposed credentials, API keys, or PII. Returns detected and findings[]
toolCallAnalysis Suspicious tool or function call patterns. Returns detected, toolCalls count, and findings[]

---

GET /api/scan/{scanId}

Retrieve a deferred scan result by scan ID.

While the scan is in progress, returns a pending envelope:

json

{
  "scanId": "uuid",
  "requestId": "uuid",
  "status": "pending",
  "filename": "large_archive.zip",
  "createdAt": "2025-03-09T12:00:00Z"
}

When complete, the response is a top-level envelope with summary fields, and the full synchronous-style scan result nested under result:

json

{
  "scanId": "uuid",
  "requestId": "uuid",
  "status": "complete",
  "filename": "large_archive.zip",
  "fileHash": "e3b0c44298fc1c149afbf4c8...",
  "fileSize": 245760,
  "contentType": "application/zip",
  "safetyScore": 95,
  "threatLevel": "Clean",
  "primaryThreat": "No threats detected",
  "scanTimeMs": 1250,
  "creditsUsed": 1,
  "createdAt": "2025-03-09T12:00:00Z",
  "result": {
    "...": "full scan result, same shape as a synchronous scan (see Understanding Results)"
  }
}
Note

The polled completed response is not the same flat shape as a synchronous scan. The summary fields (safetyScore, threatLevel, …) sit at the top level, and the complete sync-style result (including the nested safetyScore object and engine details) is under the result key.

---

GET /api/account/history/{scan_id}

Retrieve a previous scan result by ID.

---

GET /api/account/history

Paginated scan history.

Query params

Param Default Description
limit 25 Results per page (max 100)
page 1 Page number

---

GET /api/account/usage

Current month usage for the authenticated account.

json

{
  "scans_used": 43,
  "max_scans": 100,
  "scans_remaining": 57,
  "max_file_size_mb": 10,
  "plan_tier": "free",
  "reset_at": "2025-04-01T00:00:00Z",
  "daily_volume": { "dates": ["2025-03-08", "2025-03-09"], "counts": [12, 31] }
}

max_scans and max_file_size_mb come from your plan tier (Free starts at 100 scans/month and 10 MB; higher tiers raise both).

---

Rate Limits

Resource Default
Scan writes per minute 30
Read endpoints per minute 60
Per-account plan rate limit 10 (Free) / 60 (Starter) / 200 (Standard) / 500 (Pro) requests per minute, applied in addition to the defaults above
Max file size Plan-tier dependent (10 MB on Free, up to 500 MB on Pro)

When a per-minute limit is reached the API returns 429 Too Many Requests. Monthly scan quotas depend on your plan; when exhausted, scan requests return 402. See tendrl.com/pricing for details.

---

Error codes

Status Code Meaning
401 unauthorized Missing or invalid API key
402 quota_exceeded Monthly scan limit reached
413 file_too_large File exceeds size limit
415 unsupported_type File type not allowed by scan profile
429 rate_limited Too many requests