Reference
API reference
Safeput development upload, verdict, and retrieval API endpoints.
The development API is available at https://dev.safeput.dev/v1. It creates
private upload records, returns short-lived single-use upload targets, and exposes
controlled retrieval after inspection.
Authentication
Send a project API key as a bearer token. Use API keys only from a trusted backend; never embed them in browser or mobile application code.
Authorization: Bearer sp_test_...The key selects exactly one project and environment. A revoked, malformed, or
unknown key returns 401 invalid_api_key.
Create an upload
POST /v1/uploads
An Idempotency-Key header of at most 128 printable ASCII characters is required.
Repeating the same key and body while the upload is still waiting returns the
original upload with 200 and Idempotency-Replayed: true, without extending its
original ten-minute transfer deadline. A different body returns
409 idempotency_conflict; a non-waiting upload returns 409 upload_not_waiting;
an expired target returns 410 upload_target_expired. Use a new key for a new upload.
{ "filename": "invoice.pdf", "content_type": "application/pdf", "size": 12480}size is the expected byte count and must be between 1 and 52,428,800 bytes
(50 MiB), and cannot exceed the project’s snapshotted maximum. A stricter project
limit returns 413 policy_size_limit_exceeded. See Scan policies
for detected-type and ZIP-member rules. A successful first request returns 201:
{ "upload": { "id": "7cc816f4-0e2e-4be0-9eaa-b9615a27ee52", "status": "waiting", "filename": "invoice.pdf", "content_type": "application/pdf", "size": "12480", "actual_size": null, "created_at": "2026-08-15T18:00:00.000Z", "updated_at": "2026-08-15T18:00:00.000Z", "upload_url_expires_at": "2026-08-15T18:10:00.000Z", "expires_at": null, "download_available": false, "verdict": null }, "upload_target": { "method": "PUT", "url": "https://...", "headers": { "Content-Type": "application/pdf", "Authorization": "Bearer st_<scoped-capability>" }, "expires_at": "2026-08-15T18:10:00.000Z" }}Upload the exact file bytes to upload_target.url using its method and every
returned header, including its scoped Authorization value (not your project key).
The target expires after 10 minutes and is single-use, even after rejected bytes
are deleted. Send a known-length file/Blob with the exact declared size. Success
returns 204; a reused target returns 409, missing Content-Length 411, and
an oversized body 413. Admission or edge limits return 429. The underlying
R2 object is private; a successful transfer still requires a clean scan verdict.
Get an upload
GET /v1/uploads/:id
Returns { "upload": ... } for an upload belonging to the API key’s project.
Unknown IDs and uploads owned by other projects both return
404 upload_not_found.
The upload includes an immutable expires_at once it becomes clean and a
computed download_available flag.
Create a download URL
POST /v1/uploads/:id/download-url
Returns a bearer URL that remains valid for at most five minutes and never beyond the upload’s 72-hour clean retention deadline:
{ "download": { "method": "GET", "url": "https://dev.safeput.dev/v1/downloads/...", "expires_at": "2026-08-19T20:05:00.000Z" }}Unknown and cross-project IDs return 404; non-clean uploads return 409; an
expired upload returns 410.
Status behavior
Records move through waiting, uploaded, and scanning before reaching a
terminal verdict. Clean files remain retrievable for 72 hours. The expiry job
deletes their bytes before durably transitioning them to expired.
Dashboard management endpoints
The dashboard uses an Auth0 access token—not a project API key—to manage:
GET /v1/projects/:projectId/uploadsPUT /v1/projects/:projectId/scan-policyGET /v1/projects/:projectId/webhookPUT /v1/projects/:projectId/webhookPOST /v1/projects/:projectId/webhook/rotate-secretPOST /v1/projects/:projectId/webhook/testGET /v1/projects/:projectId/webhook-deliveriesPOST /v1/projects/:projectId/webhook-deliveries/:deliveryId/retryGET /v1/projects/:projectId/usageGET /v1/billingThe scan-policy endpoint accepts a complete v2 project policy and changes only future upload snapshots. Its schema, limits and ZIP semantics are documented in Scan policies and the OpenAPI specification.
Upload history returns up to 100 rows per page and accepts status, search
(literal filename or upload ID substring), and cursor. Delivery history returns
up to 50 rows and accepts status and cursor. Both responses include
next_cursor: pass it unchanged with the same filters for the next page; null
means there are no more results. Cursors and results are scoped to the owned
project. Changing filters starts a new traversal without a cursor.
Usage reports exact inspected bytes and billable bytes. Without from/to, these
are lifetime project totals. Date filters use an inclusive from and exclusive
to. Failed inspections remain visible for audit but have zero billable bytes;
test projects are not billed.
Account billing includes cancel_at, the metered subscription’s period_start
and period_end, safe price information, and period_usage aggregated across
owned live projects. Unknown dates and period usage are null. Usage-reporting
counts are diagnostics, not invoice totals. Scheduled cancellation does not make
an active subscription inactive before Stripe confirms cancellation.
Error shape
{ "error": { "code": "invalid_request", "message": "Files cannot exceed 50 MB.", "request_id": "..." }}