openonce

API

Free, keyless, and rate limited by IP. Encrypt on your side and we will hold the bytes.

Base URL

https://api.openonce.link/api/v1
POST/secrets

Create a secret

Returns the identifier and the expiry timestamp.

Request

{
  "content": "ciphertext-or-plaintext",
  "ttl": "1d",
  "ttlSeconds": 3600,
  "views": 1,
  "password": null,
  "notifyChannel": "webhook",
  "notifyTo": "https://hooks.slack.com/services/...",
  "label": "prod db password",
  "kind": "text",
  "fileMeta": "{\"name\":\"config.env\",\"size\":2048,\"type\":\"text/plain\"}"
}

Response

{
  "id": "8f3kQ2mVxL",
  "expiresAt": "2026-08-08T12:04:11.000Z",
  "views": 1,
  "hasPassword": false,
  "notifyOnRead": true
}
GET/secrets/:id/meta

Check a secret

Tells you whether a secret is still alive and how many views remain. This never burns a view.

Response

{
  "id": "8f3kQ2mVxL",
  "alive": true,
  "viewsLeft": 1,
  "viewsTotal": 2,
  "expiresAt": "2026-08-08T12:04:11.000Z",
  "hasPassword": false,
  "kind": "text"
}
POST/secrets/:id/reveal

Reveal a secret

Consumes one view and returns the stored content. This is destructive.

Request

{
  "password": null
}

Response

{
  "content": "ciphertext-or-plaintext",
  "viewsLeft": 0,
  "kind": "file",
  "fileMeta": "{\"name\":\"config.env\",\"size\":2048,\"type\":\"text/plain\"}"
}
DELETE/secrets/:id

Destroy a secret

Removes a secret before anyone reads it.

Response

{
  "destroyed": true
}

Sending files

Set kind to "file" and pass fileMeta as a JSON string with name, size and type. The bytes go in content like any other payload — encrypt them yourself. Note that fileMeta is stored as you send it, so keep secrets out of the file name.

Burn notifications

Pass notifyChannel and notifyTo when creating a secret and we will call your endpoint the moment it is read. The payload never contains the secret itself.

POST https://your-app.com/hook
X-Openonce-Event: secret.revealed
X-Openonce-Timestamp: 1786412651
X-Openonce-Signature: sha256=9f86d081884c7d65...

{
  "event": "secret.revealed",
  "secretId": "8f3kQ2mVxL",
  "occurredAt": "2026-08-07T17:45:47.797Z",
  "viewsLeft": 0,
  "label": "prod db password"
}
Verifying the signature

Every request carries an HMAC-SHA256 signature over timestamp.rawBody. Compare it in constant time before trusting the payload.

const expected = crypto
  .createHmac('sha256', WEBHOOK_SECRET)
  .update(`${timestamp}.${rawBody}`)
  .digest('hex');

const isValid = crypto.timingSafeEqual(
  Buffer.from(signature.replace('sha256=', '')),
  Buffer.from(expected),
);

Command line

Pipe anything into a link without leaving your terminal.

npx openonce "my api key" --ttl 1h --views 2

Rate limits

Limits apply per IP address. Exceeding one returns 429 until the window resets.

Creating secrets10 per 60s
All other endpoints30 per 60s

Errors

Errors use standard status codes with a JSON body containing an error code and a human-readable message.

{
  "statusCode": 404,
  "error": "Not Found",
  "code": "SECRET_NOT_FOUND",
  "message": "This secret does not exist or has already been destroyed."
}
400CONTENT_TOO_LARGE
400INVALID_NOTIFICATION_TARGET
401PASSWORD_REQUIRED
401WRONG_PASSWORD
404SECRET_NOT_FOUND
429ThrottlerException