# Safeput > Documentation for Safeput's secure upload pipeline. ## Documentation - [Introduction](https://safeput.dev/docs/introduction.md): What Safeput intends to solve and how its file lifecycle works. - [Quickstart](https://safeput.dev/docs/quickstart.md): Upload, inspect, and retrieve a private file with the Safeput development API. - [Guides](https://safeput.dev/docs/guides.md): Practical Safeput integration guides, from upload creation through secure retrieval. - [Uploads](https://safeput.dev/docs/uploads.md): Single-use, size-bounded Safeput uploads and their trust boundary. - [File lifecycle](https://safeput.dev/docs/file-lifecycle.md): Conceptual Safeput upload states and their intended meaning. - [Scan results](https://safeput.dev/docs/scan-results.md): Understand the statuses and verdicts returned by the Safeput scanning pipeline. - [Retrieving files](https://safeput.dev/docs/retrieving-files.md): Retrieve policy-approved files through short-lived, controlled download URLs. - [Webhooks](https://safeput.dev/docs/webhooks.md): Receive signed, retried upload verdict events from Safeput. - [API reference](https://safeput.dev/docs/api-reference.md): Safeput development upload, verdict, and retrieval API endpoints. - [Examples](https://safeput.dev/docs/examples.md): Minimal server-side Safeput integration examples. - [Security](https://safeput.dev/docs/security.md): Safe integration boundaries for keys, untrusted files, verdicts, and downloads. - [OpenAPI](https://safeput.dev/docs/openapi.md): Machine-readable Safeput public API contract for tools, SDKs, and coding agents. - [AI & Agents](https://safeput.dev/docs/ai-agents.md): Machine-readable Safeput documentation and guidance for coding agents. ## Other - [Scan policies](https://safeput.dev/docs/scan-policies.md): Configure immutable upload size, detected-type and ZIP-member policies. ## Full Documentation --- # Introduction What Safeput intends to solve and how its file lifecycle works. Safeput is developer infrastructure intended to simplify secure file uploads. It is designed for applications that need to accept untrusted files without assembling their own quarantine, asynchronous scanning, and temporary retrieval pipeline. > Safeput is not yet a production service. The architecture and interfaces in > these docs are provisional until the first API release. ## The problem Accepting a file is only the first step. A responsible pipeline also needs to control access before scanning, process files asynchronously, distinguish clean and rejected outcomes, handle failures, and remove expired content. Safeput intends to place those concerns behind a focused API. ## Basic lifecycle ```text upload → quarantine → scan → clean/rejected → retrieve ``` An uploaded file remains untrusted while it is quarantined and scanned. A clean scan result permits temporary retrieval. A rejected result follows a separate disposition path and is not released as clean. Malware scanning reduces risk but does not guarantee that a file is harmless. Source: https://safeput.dev/docs/introduction.md --- # Quickstart Upload, inspect, and retrieve a private file with the Safeput development API. The development API creates a private upload target, processes the uploaded bytes asynchronously, and makes a policy-approved file temporarily retrievable. ## 1. Create a project key Log in to the [development dashboard](https://dev.safeput.dev/dashboard/), create a test project, and create an API key. Save the complete `sp_test_...` value when it is shown; Safeput stores only its digest and cannot reveal it again. Keep the key on your backend. ## 2. Request an upload target ```sh curl https://dev.safeput.dev/v1/uploads \ --request POST \ --header "Authorization: Bearer $SAFEPUT_API_KEY" \ --header "Content-Type: application/json" \ --header "Idempotency-Key: invoice-2026-08-15" \ --data '{"filename":"invoice.pdf","content_type":"application/pdf","size":12480}' ``` The response contains an `upload` record and an `upload_target`. The maximum declared file size is 52,428,800 bytes (50 MiB). ## 3. Upload the bytes Use the returned URL within 10 minutes and preserve every returned header: ```sh curl "$UPLOAD_URL" \ --request PUT \ --header "Authorization: $UPLOAD_AUTHORIZATION" \ --header "Content-Type: application/pdf" \ --upload-file invoice.pdf ``` Set `UPLOAD_AUTHORIZATION` to the returned `upload_target.headers.Authorization` value, not your project key. The target is single-use, including after rejection or deletion. Send a known-length file/Blob, not an unbounded chunked stream; the byte length must exactly match the declared size. A completed transfer returns `204` and does not yet mean the file is clean. The R2 bucket remains private. ## 4. Wait for inspection ```sh curl "https://dev.safeput.dev/v1/uploads/$UPLOAD_ID" \ --header "Authorization: Bearer $SAFEPUT_API_KEY" ``` Poll until `status` reaches `clean`, `rejected`, `failed`, or `expired`. Only a `clean` record with `download_available: true` can be retrieved. ## 5. Retrieve a clean file ```sh curl "https://dev.safeput.dev/v1/uploads/$UPLOAD_ID/download-url" \ --request POST \ --header "Authorization: Bearer $SAFEPUT_API_KEY" ``` Use the returned `download.url` within five minutes. Safeput streams the file with a sanitized attachment filename, its detected content type, and `no-store` headers. Read [Retrieving files](/docs/retrieving-files/) for retention and security details. Source: https://safeput.dev/docs/quickstart.md --- # Guides Practical Safeput integration guides, from upload creation through secure retrieval. Safeput keeps untrusted bytes private while they move through upload, inspection, verdict, and temporary retrieval. Start with the path that matches what you are building: - [Uploads](/docs/uploads/) explains single-use upload targets and size validation. - [File lifecycle](/docs/file-lifecycle/) defines every state and transition. - [Scan results](/docs/scan-results/) explains clean, rejected, and failed verdicts. - [Scan policies](/docs/scan-policies/) configures size, detected types, and ZIP member limits. - [Retrieving files](/docs/retrieving-files/) covers short-lived download URLs and retention. - [Webhooks](/docs/webhooks/) delivers terminal verdicts without polling. For a complete first integration, follow the [Quickstart](/docs/quickstart/) and then use the [API reference](/docs/api-reference/) or [OpenAPI specification](/docs/openapi/). Source: https://safeput.dev/docs/guides.md --- # Uploads Single-use, size-bounded Safeput uploads and their trust boundary. Safeput keeps file bytes out of your application server while placing them in private quarantine storage. ## Upload flow 1. Your trusted backend calls `POST /v1/uploads` with a project API key. 2. Safeput reserves account-wide capacity and returns a 10-minute, single-use target. 3. Your client sends a known-length file to that target with `PUT` and every returned header. 4. Your backend can read the record with `GET /v1/uploads/:id`. Use a unique `Idempotency-Key` for each logical upload attempt. Retrying the same request is safe; changing the body while reusing the key is rejected. ## Trust boundary Treat every uploaded object as untrusted. A successful `PUT` (`204`) means the Worker accepted exactly the declared bytes and durably published the private object for inspection. It does not mean the file has passed inspection. The queue consumer validates the object metadata and runs the isolated scanner. Only a later `clean` status may make a file eligible for retrieval. ## Limits - Declared size: 1 byte through 52,428,800 bytes (50 MiB) - Upload URL lifetime: 10 minutes - Started transfer deadline: 5 minutes - Storage: private R2; clients receive no R2 write credential - API key scope: one project and one `test` or `live` environment Targets use a scoped `Authorization` header; never replace it with the project key or put it in a URL/log. A file or browser Blob supplies Content-Length; omitting it returns `411`. Content-Encoding is not accepted. Short bodies, oversized bodies and size mismatches cannot publish partial objects. Once a transfer starts, its capability is consumed, even if the connection fails or the file is later rejected/deleted. Poll the existing upload after an uncertain response. Do not blindly create a duplicate: publication may be recovering. Use a new Idempotency-Key only when a new logical attempt is appropriate. Admission limits are shared across an account's projects and keys, including test keys. `429 admission_limit_exceeded` means its file, byte, outstanding or issuance budget is exhausted. These operational limits are separate from scanned-byte billing; a failed pre-scan transfer has zero billable scanned bytes. See the [API reference](/docs/api-reference/) for request and response fields. Source: https://safeput.dev/docs/uploads.md --- # File lifecycle Conceptual Safeput upload states and their intended meaning. The initial lifecycle model uses the following conceptual states. Exact names and transitions may evolve until the API is implemented. | State | Intended meaning | | --- | --- | | `waiting` | An upload target exists, but transfer is not complete. | | `uploaded` | Transfer completed and the file remains untrusted. | | `scanning` | Asynchronous malware scanning is in progress. | | `clean` | The scanner did not reject the file; temporary retrieval may become available. | | `rejected` | The scanning pipeline rejected the file. | | `failed` | The upload or processing pipeline could not complete. | | `expired` | The 72-hour clean retention window ended and retained bytes were deleted. | | `expired` | The file is no longer available for retrieval. | ## Terminal outcomes `clean`, `rejected`, `failed`, and `expired` represent different outcomes and must not be treated interchangeably. Applications should fail closed when a status is unknown or incomplete. Source: https://safeput.dev/docs/file-lifecycle.md --- # Scan policies Configure immutable upload size, detected-type and ZIP-member policies. Each project has one versioned policy. Change it in **Dashboard → Settings**. Every new upload captures a complete snapshot. Editing the project does not change an existing upload, its transfer target, or the rules used when it is scanned. An idempotent replay returns the original upload and snapshot, even if the project has since become stricter. Create a new logical upload to use the new policy. ## Size and file types `max_size_bytes` is inclusive, from 1 through 52,428,800 bytes (50 MiB). New admission above the project maximum returns `413 policy_size_limit_exceeded` without issuing a target or reserving quota. Transfer and inspection independently enforce the stored snapshot; the actual transferred size must also equal the declared size. Inspection size violations use `size_limit_exceeded`. Both the final filename extension and the detected content type must be allowed. Allowing `.zip` does not implicitly allow every type inside it. The API accepts only the supported catalog, with unique lowercase extensions and canonical MIME values. An empty allowlist denies all files in that scope. `application/octet-stream` is a neutral **declaration**, not a detected-type allowlist value. It never overrides detection or policy checks. An incompatible extension or MIME declaration produces `type_extension_mismatch`; a disallowed detected type produces `type_not_allowed`. Detection identifies supported formats; it is not full document/schema validation or a guarantee of harmless content. ## ZIP member rules `archive.allowed_extensions` and `archive.allowed_types` apply to each standalone file in a ZIP, including nested ZIPs. Detection uses extracted bytes, not just the member name. Members are read to completion, with size and CRC validation, through bounded temporary storage. Corrupt or truncated archives fail inspection. Nested ZIPs are disabled by default. When enabled, they must also be permitted by both member allowlists. Limits apply across the whole archive tree: | Setting | Platform maximum/default | | ----------------------- | -------------------------------------- | | `max_entries` | 1,000 entries, including directories | | `max_expanded_bytes` | 209,715,200 bytes (200 MiB), combined | | `max_entry_bytes` | 52,428,800 bytes (50 MiB) | | `max_depth` | 3, with the outer container at depth 1 | | `max_compression_ratio` | 100:1, per entry and combined | Projects can lower these ceilings but cannot raise them. Nested container bytes and their expanded members both count toward combined expansion. Unsafe paths, symlinks, encryption and limit violations are never treated as clean. Office OOXML files are ZIP containers, but their internal XML, relationships and VBA parts are not standalone uploads. Member allowlists do not apply to those parts; archive integrity and resource limits still do. An Office file inside a ZIP must itself pass the ZIP-member type rules, encryption and macro checks. Embedded objects under OOXML `embeddings/` parts are currently unsupported and rejected with `type_not_allowed`. These rules do not promise content-type classification of every embedded stream in legacy Office or PDF documents. ## Mandatory security checks Malware scanning and encrypted-content rejection cannot be disabled. The macro policy is `reject` by default; `allow` permits macros but never skips malware scanning. RAR, 7z, TAR and gzip are not in the configurable catalog. ## Update through the dashboard API `PUT /v1/projects/:projectId/scan-policy` requires the owner's Auth0 access token, not a project API key. Cross-account projects return `404 project_not_found`. The v2 request is a complete replacement; include every field: ```json { "schema_version": 2, "allowed_extensions": ["pdf", "png", "zip"], "macro_policy": "reject", "max_size_bytes": 10485760, "allowed_types": ["application/pdf", "image/png", "application/zip"], "archive": { "allowed_extensions": ["pdf", "png"], "allowed_types": ["application/pdf", "image/png"], "allow_nested": false, "max_entries": 100, "max_expanded_bytes": 52428800, "max_entry_bytes": 10485760, "max_depth": 3, "max_compression_ratio": 100 } } ``` The response is `{ "scan_policy": { "version": 2, ... } }`, with the current project version rather than a fixed value. Only a semantic change increments `version`; reordering an allowlist does not. `schema_version` identifies the policy format and is independent of the project's revision number. Unknown settings, duplicates and limits outside the platform ceilings return `400`. Legacy two-field updates (`allowed_extensions`, `macro_policy`) remain supported and preserve existing extended rules. Legacy upload snapshots remain unchanged; they do not acquire ZIP-member allowlists retroactively. Rules are captured when creation reads the project policy, so an edit racing with creation may result in either complete revision, never a mixture of individual fields. Source: https://safeput.dev/docs/scan-policies.md --- # Scan results Understand the statuses and verdicts returned by the Safeput scanning pipeline. Safeput processes uploads asynchronously. After a file reaches quarantine, its record moves through a small set of statuses that describe whether the file can be trusted by your application. ## Statuses | Status | Meaning | Recommended action | | --- | --- | --- | | `waiting` | The upload has been created or is awaiting processing. | Poll again later. | | `scanning` | Safeput is inspecting the quarantined object. | Keep the file unavailable. | | `clean` | The upload passed its configured policy at scan time. | Retrieve it while it is available. | | `rejected` | The file failed a security or policy check. | Show a safe, generic error. | | `failed` | The pipeline could not complete the scan. | Retry or ask the user to upload again. | | `expired` | Clean retention ended and the retained bytes were deleted. | Create a new upload if the file is needed again. | ## Example response ```json { "id": "7cc816f4-0e2e-4be0-9eaa-b9615a27ee52", "status": "clean", "filename": "quarterly-report.pdf", "content_type": "application/pdf", "size": "842193", "actual_size": "842193", "expires_at": "2026-08-22T14:32:08.000Z", "download_available": true, "created_at": "2026-08-19T14:32:08.000Z" } ``` Only treat a file as available when the API reports `clean` and `download_available` is `true`. A successful upload alone does not indicate that inspection has finished, and no scanner can guarantee harmless content. ## Polling guidance For early integrations, poll the upload endpoint with a short delay and stop when the record reaches a final status. Webhooks are planned for applications that need event-driven updates. ```ts const response = await fetch(`https://dev.safeput.dev/v1/uploads/${uploadId}`, { headers: { Authorization: `Bearer ${process.env.SAFEPUT_API_KEY}` }, }); const { upload } = await response.json(); if (upload.status === "clean" && upload.download_available) { // Request a temporary retrieval URL from your trusted backend. } ``` Source: https://safeput.dev/docs/scan-results.md --- # Webhooks Receive signed, retried upload verdict events from Safeput. Safeput can deliver a terminal upload verdict to one HTTPS endpoint per project: ```text upload.clean upload.rejected upload.failed ``` Configure the URL and subscribed events from the dashboard. The signing secret is displayed only when the endpoint is created or its secret is rotated. ## Event envelope Each request is a JSON `POST` with an immutable event ID: ```json { "id": "12c09ac1-dc57-4e4f-8b43-35f7a5fdf229", "type": "upload.clean", "api_version": "v1", "created_at": "2026-08-22T12:00:00.000Z", "data": { "upload": { "id": "7cc816f4-0e2e-4be0-9eaa-b9615a27ee52", "project_id": "47db67f6-8fbc-4c59-95eb-f5150e18d820", "status": "clean", "filename": "invoice.pdf", "verdict": { "result": "clean", "reason_code": null, "detected_type": "application/pdf" } } } } ``` Use the event `id` as an idempotency key. Safeput uses at-least-once delivery, so the same event can be received more than once. ## Verify the signature Webhook requests include: ```http Safeput-Event-Id: ... Safeput-Event-Type: upload.clean Safeput-Timestamp: 1787400000 Safeput-Signature: v1= ``` Calculate HMAC-SHA256 with the webhook signing secret over the exact string `.`. Base64url-encode the digest without padding, prepend `v1=`, and compare it in constant time. Reject stale timestamps before processing the event. ## Delivery behavior Safeput accepts any `2xx` response. Network errors, timeouts, `408`, `429`, and `5xx` responses retry with increasing delays for up to eight attempts. Redirects are not followed. Other `3xx` and `4xx` responses fail permanently and can be retried manually from the dashboard. The receiver must answer within 10 seconds. Return success only after your application has durably recorded the event. Source: https://safeput.dev/docs/webhooks.md --- # 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](/docs/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_" }, "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](/docs/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": "..." } } ``` Source: https://safeput.dev/docs/api-reference.md --- # Retrieving files Retrieve policy-approved files through short-lived, controlled download URLs. Safeput keeps every stored object private. After an upload passes its configured policy, your backend can request a five-minute download URL. Clean files remain available for 72 hours from completion of inspection. ## Before you retrieve Poll `GET /v1/uploads/:id` until `status` is `clean` and `download_available` is `true`. In Safeput, `clean` means that the file passed the configured policy at scan time; it is not a guarantee that a file is universally harmless. ```ts const response = await fetch( `https://dev.safeput.dev/v1/uploads/${uploadId}/download-url`, { method: "POST", headers: { Authorization: `Bearer ${process.env.SAFEPUT_API_KEY}`, }, }, ); const { download } = await response.json(); // Use or redirect to download.url before download.expires_at. ``` The returned URL is a bearer credential. Anyone who receives it can download the file until the link expires, so do not log, persist, or share it. ## Security checklist - Create download URLs only on a trusted server. - Authorize the current user before requesting a URL. - Keep URL lifetimes short and avoid storing signed URLs. - Treat downloaded content defensively even after it passed policy. - Handle an expired link by requesting a new one while retention remains active. ## Expected errors | HTTP status | Scenario | | --- | --- | | `404` | The upload is unknown or belongs to another project. | | `409` | The file is not in a retrievable state. | | `410` | Retention or the issued download link has expired. | | `503` | Downloads are paused or storage is temporarily unavailable. | Safeput constructs download headers from the sanitized filename and detected content type produced by inspection. The original filename and client-declared MIME type are never copied into response headers. Source: https://safeput.dev/docs/retrieving-files.md --- # Examples Minimal server-side Safeput integration examples. Use Safeput only from trusted server-side code. The browser should send the file to your backend first; your backend creates the upload record and returns only the short-lived upload target to the browser. ## Create an upload with TypeScript ```ts const apiKey = process.env.SAFEPUT_API_KEY; const response = await fetch("https://dev.safeput.dev/v1/uploads", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ filename: "invoice.pdf", content_type: "application/pdf", size: 12_480, }), }); if (!response.ok) throw new Error(`Safeput returned ${response.status}`); const { upload, upload_target: target } = await response.json(); ``` Send the exact bytes to `target.url` with `target.method` and every header from `target.headers`. Do not attach your project API key to that request. ## Wait for a terminal verdict For a first integration, poll `GET /v1/uploads/:id` with a bounded delay. In a production integration, prefer [signed webhooks](/docs/webhooks/). ```ts const terminal = new Set(["clean", "rejected", "failed", "expired"]); while (!terminal.has(upload.status)) { await new Promise((resolve) => setTimeout(resolve, 1_500)); const response = await fetch( `https://dev.safeput.dev/v1/uploads/${upload.id}`, { headers: { Authorization: `Bearer ${apiKey}` } }, ); ({ upload } = await response.json()); } ``` Request a download URL only when `upload.status === "clean"` and `upload.download_available` is true. Source: https://safeput.dev/docs/examples.md --- # Security Safe integration boundaries for keys, untrusted files, verdicts, and downloads. Safeput reduces the work required to quarantine and inspect untrusted files. It does not make arbitrary files harmless, and a clean verdict is not a substitute for application-level authorization or safe file handling. ## Keep project keys server-side An `sp_test_...` or `sp_live_...` key grants access to one project. Never embed it in browser code, mobile applications, logs, or source control. Create separate test and live projects, rotate exposed keys, and revoke unused keys. ## Treat files as untrusted until clean Do not release, parse, preview, or transform uploaded bytes while an upload is `waiting`, `uploaded`, or `scanning`. A `rejected` or `failed` upload is never available for download. A `failed` verdict means inspection did not complete; it must not be treated as clean. ## Keep retrieval private Download URLs are short-lived bearer credentials. Return them only after your application has authorized the requesting user, avoid logging them, and do not cache them. Safeput streams clean files as attachments with a sanitized filename, detected content type, `nosniff`, and `private, no-store` headers. ## Verify webhook signatures Verify the signature over the exact raw body, reject stale timestamps, process event IDs idempotently, and return success only after the event is durable. See [Webhooks](/docs/webhooks/) for the complete signing contract. Source: https://safeput.dev/docs/security.md --- # OpenAPI Machine-readable Safeput public API contract for tools, SDKs, and coding agents. The Safeput public development API is described by an OpenAPI 3.1 document: - [Download `openapi.yaml`](/openapi.yaml) - [Read the API reference](/docs/api-reference/) The specification includes only the project-key API used by customer applications. Dashboard management and internal scanner routes are intentionally excluded. Coding agents can use the specification to generate typed request code, inspect schemas, and validate an integration. Generated clients still need application- level timeouts, error handling, idempotency, and secure secret storage. Source: https://safeput.dev/docs/openapi.md --- # AI & Agents Machine-readable Safeput documentation and guidance for coding agents. Safeput documentation is available in formats that coding agents can consume without scraping the rendered interface: - [`/llms.txt`](/llms.txt) is the concise documentation map. - [`/llms-full.txt`](/llms-full.txt) contains the complete documentation set. - [`/openapi.yaml`](/openapi.yaml) is the public API contract. - Every documentation page also has a Markdown representation by adding `.md` to its route, for example [`/docs/quickstart.md`](/docs/quickstart.md). ## Recommended agent workflow 1. Read `llms.txt` to find the smallest relevant set of pages. 2. Use OpenAPI for paths, authentication, status codes, and schemas. 3. Keep the project API key in trusted backend code. 4. Use a unique idempotency key when creating an upload. 5. Treat bytes as untrusted until Safeput returns a terminal clean verdict. 6. Verify the integration with a test project before using a live project. ## MCP roadmap An MCP server is planned after the public API contract is stable. It will expose Safeput as an agent capability rather than duplicating documentation search. Candidate tools include `safeput.create_upload`, `safeput.get_upload`, `safeput.get_scan_result`, `safeput.retrieve_file`, and `safeput.get_usage`, with documentation exposed as MCP resources. Until then, OpenAPI plus Markdown documentation is the supported agent integration surface. No Safeput MCP endpoint is available today. Source: https://safeput.dev/docs/ai-agents.md