NSFW Image Scan API
Scan image content asynchronously. A public client can purchase one scan entitlement with BCH, while a permanent privileged token issued by a super administrator can submit scans without creating a payment order.
Quick start
Permanent privileged token
One privileged token works with both the NSFW and ClamAV APIs. It has no expiry, charge, usage quota, or API rate limit, although a super administrator can revoke it. File validation, the 100 MiB maximum, Idempotency-Key, scanner readiness, and other safety checks still apply.
CashLink nodes may use a privileged token with GET https://test-linking.cc/api/v2/security-scans/provider to discover local capacity. Delegated submissions carry X-CashLink-Scan-Delegation-From; a provider rejects its own instance, a second delegation, or a non-local effective backend, limiting delegation to one hop. The provider returns probability, frame, and model data; the caller keeps its own NSFW threshold.
The plaintext token is shown only once when an administrator creates it. Store it like a password and never put it in a URL, client-side source, or logs.
curl --request POST 'https://test-linking.cc/api/v2/nsfw-scans' \
--header 'Authorization: Bearer YOUR_PRIVILEGED_TOKEN' \
--header 'Idempotency-Key: nsfw-2026-0001' \
--form 'file=@./photo.jpg'
A new scan returns 202 Accepted. Poll the returned scan ID with the same token:
curl --header 'Authorization: Bearer YOUR_PRIVILEGED_TOKEN' \
'https://test-linking.cc/api/v2/nsfw-scans/019c0000-0000-7000-8000-000000000001'
One-time BCH payment
- Read the current quote.
- Create an anonymous payment order and immediately store the one-time
order_token. - After saving the token, activate the order and pay the exact BCH amount from its
bip21value or QR image. - Poll the order until
entitlement_statusisavailable. - Submit one image with the same
order_token; the entitlement atomically becomesconsumed.
curl 'https://test-linking.cc/api/v2/nsfw-scans/quote'
curl --request POST 'https://test-linking.cc/api/v2/nsfw-scans/payment-orders' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: nsfw-order-2026-0001' \
--data '{"refund_address":"bitcoincash:YOUR_MAINNET_CASHADDR"}'
A new order returns 201 Created and the one-time order_token. Replaying the same request returns 200 OK without revealing the token again. Activate the order within 15 minutes. Its price is fixed when the order is created, so later administrator price changes do not alter it.
curl --request POST 'https://test-linking.cc/api/v2/nsfw-scans/payment-orders/ORDER_UUID/activate' \
--header 'Authorization: Bearer YOUR_ORDER_TOKEN'
curl --header 'Authorization: Bearer YOUR_ORDER_TOKEN' \
'https://test-linking.cc/api/v2/nsfw-scans/payment-orders/ORDER_UUID'
An entitlement becomes available when a sufficient zero-confirmation payment is detected. A later chain reversal revokes an unused entitlement; if a scan was already submitted, the system records a financial exception for administrator review.
Endpoints
| Method | Path | Authorization | Purpose |
|---|---|---|---|
GET |
/quote |
None | Current per-scan price and limit |
POST |
/payment-orders |
None | Create an anonymous BCH order |
POST |
/payment-orders/{order}/activate |
Order token | Activate an order after saving its token |
GET |
/payment-orders/{order} |
Order token | Read payment and entitlement state |
GET |
/payment-orders/{order}/events?after=0 |
Order token | Read payment events after a revision |
GET |
/payment-orders/{order}/qr |
Order token | Fetch the BCH payment QR as SVG |
GET |
/payment-orders/{order}/refunds |
Order token | Read refund state |
POST |
/ |
Order or privileged token | Submit one image |
GET |
/{scan} |
Token used for submission | Poll scan state and result |
GET |
/api/v2/security-scans/provider |
Privileged token | Discover non-secret local scanner capacity for one-hop delegation |
Successful JSON responses use a data envelope. Payment-order creation also has a top-level order_token. JSON and QR responses are private and must not be cached.
Upload rules
- Send
multipart/form-datawith one field namedfile. - Remote
urlinput is prohibited; the service scans only uploaded bytes. - The file must be non-empty and no larger than 100 MiB.
- The server-detected content must be JPEG, PNG, WebP, or GIF. A filename extension does not replace content validation.
Idempotency-Keyis required and may contain at most 255 characters.
Scan states and result
The normal states are queued and processing. A transient dependency failure may produce deferred; terminal states are completed and failed. After 24 hours the full result becomes expired, and reads return 410 Gone.
{
"data": {
"id": "019c0000-0000-7000-8000-000000000001",
"scan_type": "nsfw",
"status": "completed",
"attempt_count": 1,
"result": {
"verdict": "safe",
"probability": 0.031,
"threshold": 0.85,
"sampled_frames": 1,
"highest_risk_frame": 0,
"model_version": "model-version",
"duration_ms": 84
},
"error": null,
"created_at": "2026-09-11T12:00:00+08:00",
"completed_at": "2026-09-11T12:00:01+08:00",
"expires_at": "2026-09-12T12:00:01+08:00"
}
}
verdict is safe or nsfw. Use the returned threshold when interpreting the probability. Model version, sampled-frame count, and the highest-risk frame are returned for auditability.
Idempotency and retries
Use a stable Idempotency-Key for both order creation and scan submission. Repeating the same file with the same credential, scan type, and key returns the original scan without consuming another entitlement. Reusing the key for different content returns 409 Conflict.
A 503 response means no scan was created and no paid entitlement was consumed. Retry later with the same key. A final technical scan failure deletes the source and restores the same paid entitlement to available.
Errors and security
| Status | Meaning |
|---|---|
401 |
Bearer token is missing, invalid, or revoked |
404 |
Resource is absent, the token does not own it, or the service is disabled |
409 |
Idempotency conflict, invalid order state, or unavailable entitlement |
410 |
The 24-hour result retention period has elapsed |
422 |
Invalid field, CashAddr, file size, or detected image format |
503 |
Scanner unavailable; no scan or entitlement consumption occurred |
Source bytes exist only in private quarantine. After the result checkpoint, the source is deleted before a terminal state is exposed. The API never returns the filename, filesystem path, raw scanner output, or infrastructure details.
Download the complete machine-readable OpenAPI 3.1 contract.