Safeput
Type to search documentation.

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.

http
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.

json
{
"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:

json
{
"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:

json
{
"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:

text
GET /v1/projects/:projectId/uploads
PUT /v1/projects/:projectId/scan-policy
GET /v1/projects/:projectId/webhook
PUT /v1/projects/:projectId/webhook
POST /v1/projects/:projectId/webhook/rotate-secret
POST /v1/projects/:projectId/webhook/test
GET /v1/projects/:projectId/webhook-deliveries
POST /v1/projects/:projectId/webhook-deliveries/:deliveryId/retry
GET /v1/projects/:projectId/usage
GET /v1/billing

The 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

json
{
"error": {
"code": "invalid_request",
"message": "Files cannot exceed 50 MB.",
"request_id": "..."
}
}

Last updated Sep 4, 2026