sk_live_...
Issued to Pro/Business tenants. Every response comes back environment: production, licensedForProduction: true.
API reference
Classifies a URL or IP as malicious, suspicious, or not_malicious, and recommends an action, allow, warn, or block, in under 500 ms. Checking an IP also returns recently associated hosts. Every tenant can keep its own private block list; an allow list is on Pro and Business. This page documents the HTTP contract, source of truth is the OpenAPI document generated straight from the API's own schemas, browsable in full at docs.skanqrcode.com.
# The only API host, no separate staging server
https://api.skanqrcode.com
Authentication
Every operation requires a bearer key except GET /health, GET /openapi.json, and POST /v1/billing/webhook (Stripe-signed, not key-authenticated). Never pass the key as a query parameter.
Authorization: Bearer sk_live_<id>_<secret>
Issued to Pro/Business tenants. Every response comes back environment: production, licensedForProduction: true.
Issued to free/sandbox tenants, same host as above, no separate server. Every response comes back environment: sandbox, licensedForProduction: false.
The prefix is determined by plan, not chosen by the caller: the key you are issued already tells the API which one you have.
Quickstart
var req = URLRequest(url: URL(string: "https://api.skanqrcode.com/v1/check")!)
req.httpMethod = "POST"
req.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
req.httpBody = try JSONEncoder().encode(["target": url])
let (data, _) = try await URLSession.shared.data(for: req)
let result = try JSONDecoder().decode(CheckResponse.self, from: data)
if result.action == "block" { return } // never UIApplication.shared.open(url)Full SDKs and runnable examples: github.com/Skanqrcode/sdk-examples
Endpoints
Classify a URL or IP address
Classifies one URL or IP as malicious, suspicious, or not_malicious, and recommends an action, allow, warn, or block, via a fixed mapping from verdict, identical on every plan. Always returns a verdict. Counts one unit of quota per successful call, cached results included; unparseable input and blocked requests are not billed.
Request
{
"target": "https://example.com/login"
}Response
{
"verdict": "malicious",
"action": "block",
"mode": "url",
"reasons": ["ACTIVE_THREAT_FEED_MATCH", "KNOWN_MALWARE_MATCH"],
"finalUrl": null,
"cached": false,
"executionTimeMs": 38,
"environment": "production",
"licensedForProduction": true,
"requestId": "req_01j9z8qaenp0s2c4d6f8g0h1jk"
}Also accepts an IP address as target (e.g. "203.0.113.42"), with an optional userId field for per-user result caching. An IP check adds "mode": "ip" and a related array: up to 20 hosts recently associated with that IP, each with its own verdict.
Read this month's usage and remaining quota
Returns a monthly usage summary for the calling tenant only. Defaults to the current calendar month (UTC) when the month query param is omitted. Reads from the billing aggregates, so it can lag real-time usage by up to about an hour, meant for reporting, not a hot-path quota decision.
Request
GET /v1/usage?month=2026-09
Response
{
"tenantId": "ten_01j9z2k3f5g6h7a1b2c3d4e5f6",
"month": "2026-09",
"monthlyQuota": 50000,
"totalRequests": 42817,
"availableRequests": 7183
}Read the hourly usage breakdown and rate-limit utilization
An hour-by-hour usage breakdown for one calendar month (UTC), for understanding when and how close to the per-minute rate limit usage happened. hourlyCapacity is a reading aid, not a separate quota, enforcement always happens per minute.
Request
GET /v1/usage/hourly?month=2026-09
Response
{
"tenantId": "ten_01j9z2k3f5g6h7a1b2c3d4e5f6",
"month": "2026-09",
"rpmLimit": 30,
"hourlyCapacity": 1800,
"hours": [
{
"hour": "2026-09-01T09:00:00Z",
"mode": "url",
"total": 1766,
"capacityUsedPercent": 98.1,
"blockedRpm": 34,
"blockedQuota": 0,
"malicious": 5,
"suspicious": 40,
"notMalicious": 1721,
"cached": 1200
}
]
}List allow list entries
Lists this tenant's allow list entries, newest first. Pro and Business plans only. Entries are private to the tenant.
Request
GET /v1/allow-list?limit=100
Response
{
"entries": [
{
"id": "le_5b1e2d3c4f5a6b7c8d9e0f1a2b3c4d5e",
"matchType": "domain",
"value": "malicious-example.com",
"createdAt": "2026-09-01T09:30:00Z"
}
],
"nextCursor": null
}Add an allow list entry
Adds a URL, host, domain, or IP to the allow list. An allowed target is returned as not_malicious, unless confirmed malicious, in which case it comes back suspicious (ALLOW_LIST_CONFLICT). Adding an entry that already exists returns the existing one. Requires an admin-scoped key. Pro and Business plans only. Takes effect within about a minute.
Request
{
"matchType": "domain",
"value": "malicious-example.com"
}Response
{
"id": "le_5b1e2d3c4f5a6b7c8d9e0f1a2b3c4d5e",
"matchType": "domain",
"value": "malicious-example.com",
"createdAt": "2026-09-01T09:30:00Z"
}Remove an allow list entry
Removes one allow list entry by id. Requires an admin-scoped key. Pro and Business plans only.
Request
DELETE /v1/allow-list/le_5b1e2d3c4f5a6b7c8d9e0f1a2b3c4d5e
Response
204 No Content
List block list entries
Lists this tenant's block list entries, newest first. Available on every plan. Entries are private to the tenant.
Request
GET /v1/block-list?limit=100
Response
{
"entries": [
{
"id": "le_5b1e2d3c4f5a6b7c8d9e0f1a2b3c4d5e",
"matchType": "domain",
"value": "malicious-example.com",
"createdAt": "2026-09-01T09:30:00Z"
}
],
"nextCursor": null
}Add a block list entry
Adds a URL, host, domain, or IP to the block list. A blocked target always comes back malicious (BLOCK_LIST_MATCH). Adding an entry that already exists returns the existing one. Requires an admin-scoped key. Available on every plan. Takes effect within about a minute.
Request
{
"matchType": "domain",
"value": "malicious-example.com"
}Response
{
"id": "le_5b1e2d3c4f5a6b7c8d9e0f1a2b3c4d5e",
"matchType": "domain",
"value": "malicious-example.com",
"createdAt": "2026-09-01T09:30:00Z"
}Remove a block list entry
Removes one block list entry by id. Requires an admin-scoped key. Available on every plan.
Request
DELETE /v1/block-list/le_5b1e2d3c4f5a6b7c8d9e0f1a2b3c4d5e
Response
204 No Content
Also present: GET /health (liveness, no key) and GET /openapi.json (this contract, as JSON, no key). POST /v1/billing/webhook exists for the billing provider only, not for API clients.
Errors
Every error response is { "error": { "code", "message", "requestId" } }. message is for a human; branch on code.
Rate limits & headers
Requests per minute are enforced per plan; the monthly quota is separate and reported by GET /v1/usage. Both show up in these response headers, no separate call needed to check them.
The plan's requests-per-minute limit, or on a 429, whichever limit (per-minute or monthly) was exceeded.
Requests left in the current window. 0 when limited.
Seconds until the limit resets.
On a 429 only: seconds to wait before retrying.
Per-stage timings on /v1/check: auth, rate limit, quota, pipeline.
Identifier to quote when contacting support.
No API key yet?
Free is 1,000 API calls a month, sandboxed. No card.