Secly API.
POST a URL. Get back a signed Secly Score.
Secly is a private invite-only deterministic scan-engine-as-a-service. One HTTP call returns a frozen scan_report.v1 with screenshots, findings, and fix-prompts. These docs cover authentication, the two endpoints you actually need, signed webhooks, screenshot URLs, rate limits, and the report schema.
GETTING ACCESS
Access is invite-only. Tell us what you want to build on the Work With Us page and Steve will reply within a working day. When you're onboarded we mint you an API key of the form sk_live_… and agree a daily scan quota. Treat the key like a password — anyone with it can call the API on your account.
SDKS
The API is a small, stable HTTP surface — any HTTP client in any language will work. We also publish thin official SDKs that wrap auth, retries, and webhook-signature verification so you don't have to.
Node / TypeScript
npm install @secly/sdk
import { Secly } from "@secly/sdk";
const secly = new Secly({ apiKey: process.env.SECLY_API_KEY! });
const { scanId } = await secly.scans.create({
url: "https://yoursite.com",
webhookUrl: "https://your.app/secly",
});
const report = await secly.scans.get(scanId);
console.log(report.score.overall);
Python
pip install secly
from secly import Secly
secly = Secly(api_key=os.environ["SECLY_API_KEY"])
scan = secly.scans.create(
url="https://yoursite.com",
webhook_url="https://your.app/secly",
)
report = secly.scans.get(scan.scan_id)
print(report["score"]["overall"])
Webhook verification
Both SDKs expose a single helper that verifies the Secly-Signature header against the raw request body and the shared secret you set when you registered the webhook URL.
// Node — Express middleware
import { verifyWebhook } from "@secly/sdk";
app.post("/secly", express.raw({ type: "*/*" }), (req, res) => {
const ok = verifyWebhook({
secret: process.env.SECLY_WEBHOOK_SECRET!,
signature: req.header("Secly-Signature")!,
rawBody: req.body,
});
if (!ok) return res.status(401).end();
const report = JSON.parse(req.body.toString("utf8"));
// … handle report …
res.status(200).end();
});
SDKs are versioned independently of the API. The frozen scan_report.v1 schema means a v1 SDK will keep working across additive API updates. Sources and install pages are linked from your customer portal at onboarding.
AUTHENTICATION
Every API call needs your API key. Either header works:
Authorization: Bearer sk_live_…
# or
X-API-Key: sk_live_…
There is no Clerk session, no cookie, no OAuth dance. Keys authenticate against an internal api_client user role; they cannot be used to sign into the dashboard. If a key is leaked, revoke it from your account — revocation is immediate.
QUICKSTART
1. Submit a scan
POST /v1/scans with the URL you want audited. The response returns immediately with a scan id; the actual audit runs asynchronously.
curl https://www.secly.com/v1/scans \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yoursite.com",
"webhookUrl": "https://your.app/secly",
"externalRef": "deploy-2026-05-19-a1b2"
}'
# → 202 Accepted
{
"scanId": "scn_01HZ…",
"status": "queued",
"statusUrl": "https://www.secly.com/api/v1/scans/scn_01HZ…"
}
webhookUrl and externalRef are optional. If you supply a webhook we'll POST the report to it when the scan finishes (see Webhooks). If you don't, poll the GET endpoint below.
2. Read the report
curl https://www.secly.com/v1/scans/scn_01HZ… \
-H "Authorization: Bearer sk_live_…"
# → 200 OK (returns the full scan_report.v1 directly)
{
"schema": "scan_report.v1",
"scanId": "scn_01HZ…",
"status": "completed",
…
}
The endpoint returns the frozen scan_report.v1 document directly — no wrapper. While the scan is still running, the document comes back with status of queued or running and an empty findings/screenshots set; poll until completed or failed, or register a webhook to be pushed the final document.
WEBHOOKS
If you supply webhookUrl when creating a scan, Secly POSTs the report to it when the scan finishes. Every delivery carries a signature header so you can verify it came from us:
Secly-Signature: t=<unix>,v1=<hex>
# hex = HMAC-SHA256(secret, "<t>.<rawBody>")
Verify the HMAC, then reject anything where t is more than a few minutes old to defeat replays. Respond with any 2xx to acknowledge — anything else is treated as a failed delivery.
Retries
Initial delivery plus 3 retries (4 attempts total) on exponential backoff: 1s → 8s → 64s. We stop on the first 2xx or after the budget is exhausted. The full attempt history is stored against the scan so you can debug from the API.
SCREENSHOT URLS
Screenshots are not embedded in the report — they're referenced by short-lived signed URLs. Each URL is valid for 7 days and looks like:
GET /v1/scans/:scanId/screenshots/:key
?exp=<unix-expiry>
&sig=<base64url-hmac>
# sig = base64url(HMAC-SHA256(
# secret,
# "<scanId>|<key>|<exp>"
# ))
You don't generate these yourself — they come pre-signed inside scan_report.v1. Just fetch them. If a URL expires, re-fetch the scan to get a fresh one.
RATE LIMITS
Two limits apply per API key, both returning HTTP 429 with a hint:
- 60 requests per 60 seconds. Rolling in-memory window, per key.
- Daily scan quota. Agreed at onboarding, rolls over at 00:00 UTC. Counter lives on the key itself and is incremented atomically per successful
POST /v1/scans.
THE REPORT — scan_report.v1
The scan_report.v1 document is frozen at version 1. Additive, backwards-compatible changes (new fields, new optional metadata) may be made within v1. Any breaking change ships under a new versioned endpoint with notice.
{
"schema": "scan_report.v1",
"scanId": "scn_01HZ…",
"url": "https://yoursite.com",
"completedAt": "2026-05-19T10:31:14Z",
"score": {
"overall": 87,
"categories": {
"performance": 91,
"accessibility": 83,
"seo": 92,
"security": 78,
"best_practices": 88,
"aesthetics": 90
}
},
"findings": [\
{\
"id": "seo.canonical.missing",\
"category": "seo",\
"impact": "high",\
"title": "Missing canonical URL on /product",\
"summary": "Search engines cannot resolve the …",\
"fixPrompt": "Add a canonical link tag to …"\
}\
],
"screenshots": [\
{\
"key": "viewport-desktop",\
"url": "https://www.secly.com/v1/scans/…?exp=…&sig=…",\
"expiresAt": "2026-05-26T10:31:14Z"\
}\
]
}
ERRORS
Errors return a small JSON envelope with a stable code you can switch on:
{ "error": { "code": "rate_limited", "message": "…" } }
400 invalid_url— body missing orurlnot a valid URL401 unauthorized— missing, invalid, or revoked API key402 insufficient_credits— account is out of scan credits422 blocked_domain— target domain is on the platform blocklist429 rate_limited— per-minute window or daily scan quota hit5xx server_error— retry with backoff
AI IS OFF BY DEFAULT
The API runs the deterministic scan engine only. AI critique passes that exist on the internal dashboard engine are disabled belt-and-braces for /v1/scans calls — no LLM calls, no prompt spend, no nondeterministic findings. The Secly Score is always 100% deterministic.
QUESTIONS, BUGS, REQUESTS
Email support@secly.com or ping Steve via the Work With Us form. Every enquiry lands in his inbox and gets a reply within a working day.