Quick start
Send some text, get two links back: one for the recipient and one for yourself. The base URL is onetime.fortion.cloud and every endpoint lives under /api/v1.
curl -s https://onetime.fortion.cloud/api/v1/secret \ -H 'content-type: application/json' \ -d '{"secret":"hunter2","ttl_days":7}'
Creating a link
POST /api/v1/secret
The body is JSON. Only secret is required; ttl_days is 1 to 30 (default 14) and passphrase is an optional extra password.
{
"secret": "hunter2",
"ttl_days": 14,
"passphrase": ""
}
// 201 Created
{
"secret_url": "https://onetime.fortion.cloud/s/7Kq2…#Xf9…",
"receipt_url": "https://onetime.fortion.cloud/m/3Bd1…#Qa2…",
"kind": "text",
"size": 7,
"has_passphrase": false,
"expires_at": "2026-09-09T12:00:00Z",
"receipt_expires_at": "2026-09-16T12:00:00Z",
"ttl_days": 14
}
File upload
POST /api/v1/secret/file
Files go up as multipart/form-data. The file field must come last — the server reads the metadata before it starts streaming the body to disk, so that it never has to hold the whole file in memory.
curl -s https://onetime.fortion.cloud/api/v1/secret/file \ -F 'ttl_days=7' \ -F 'passphrase=' \ -F 'file=@report.pdf' # must be the last field
Password generation
POST /api/v1/generate
The server makes the password itself and turns it straight into a link. alphabet is letters, alphanumeric or symbols, length is 8 to 128. With return_value: false the password never appears in the response at all.
{
"length": 24,
"alphabet": "symbols",
"ttl_days": 14,
"return_value": false
}
// 201 Created — "value" stays null unless return_value is true
{ "secret_url": "…", "receipt_url": "…", "value": null }
Reading
POST /api/v1/peek
POST /api/v1/reveal
GET /api/v1/download
Start with peek: it reports the kind, the size and whether a password is needed, without burning anything. Only reveal with confirm: true hands out the content and deletes it. For files it returns a ticket good for five minutes against /api/v1/download.
// the key is the part of the link after the # POST /api/v1/peek { "key": "Xf9…" } // 200 { "kind": "text", "state": "new", "has_passphrase": false, "size": 7 } POST /api/v1/reveal { "key": "Xf9…", "confirm": true } // 200 { "kind": "text", "value": "hunter2", "revealed_at": "…" } // files answer with a ticket instead of a body // 200 { "download_url": "/api/v1/download", "download_ticket": "…", "ticket_expires_in": 300 } GET /api/v1/download X-Onetime-Ticket: "…"
Status
POST /api/v1/receipt
The receipt tells you what state the link is in and when things happened to it. It returns nothing about the recipient beyond timestamps.
{ "key": "Qa2…" }
// 200 — state is new | consumed | burned | destroyed | expired
{
"state": "new",
"kind": "text",
"size": 7,
"has_passphrase": false,
"created_at": "2026-08-26T12:00:00Z",
"secret_expires_at": "2026-09-09T12:00:00Z",
"peeked_at": null,
"consumed_at": null,
"passphrase_failures": 0,
"receipt_expires_at": "2026-09-16T12:00:00Z"
}
Deleting
POST /api/v1/receipt/burn
As long as nobody has read the link, you can destroy the content. There is no undo.
{ "key": "Qa2…", "confirm": true }
Errors
Errors are application/problem+json per RFC 9457. Branch on the code field, not on the text — the text changes and is translated.
// 410 Gone — content-type: application/problem+json { "code": "already_revealed", "title": "Already revealed", "detail": "This link has already been used." }
| code | HTTP | When it happens |
|---|---|---|
| not_found | 404 | We don't know this link. |
| already_revealed | 409, 410 | The content has already been read and is deleted. |
| burned | 410 | The sender cancelled this link. |
| destroyed | 410 | The content was deleted after repeated wrong passwords. |
| passphrase_required | 401 | This link is password protected. |
| bad_passphrase | 403 | That password is wrong. |
| too_many_attempts | 429 | You have used up your attempts and the content is deleted. |
| confirmation_required | 400 | Confirm that you want to see the content. |
| payload_too_large | 413 | The content is too large. |
| empty | 400 | Add the content you want to send. |
| storage_full | 507 | We have run out of space. Try again shortly. |
| files_disabled | 403 | File sending is switched off right now. |
| read_only | 503 | Maintenance is in progress. New links cannot be created right now. |
| quota_exceeded | 429 | Too many links from your network. Try again shortly. |
| ticket_expired | 410 | The download link expired. Reload the page. |
| invalid_ttl | 400 | Validity has to be between 1 and 30 days. |
| rate_limited | 429 | One moment — a lot of requests are coming from your network. Try again in a minute. |
| internal | 500 | Something broke on our side. Please try again in a moment. |
Limits
By default text up to 1 MB, files up to 50 MB and validity between 1 and 30 days; the form shows the exact figures. Five wrong password attempts destroy the content. Rate limiting is per IP and answers 429 with a Retry-After header.
For AI agents
When an agent has to hand a password to a human, use POST /api/v1/generate with return_value: false. The server makes the password and the agent only ever sees a link — so the password never enters the agent's context, its logs or the conversation history.
curl -s https://onetime.fortion.cloud/api/v1/generate \ -H 'content-type: application/json' \ -d '{"length":24,"alphabet":"symbols","return_value":false}' \ | jq -r '.secret_url' # the generated password never passes through the agent
A machine-readable description of the service lives at /llms.txt.