Developer API
Build on the same zero-knowledge secrets API that powers EasyToPaste. Content is encrypted in the caller's environment before it reaches us; the server stores only ciphertext and a minimal metadata envelope, and never sees plaintext or decryption keys.
Overview
The EasyToPaste API lets you create, retrieve, and manage secrets — encrypted text or file payloads addressed by an opaque locator. All endpoints are versioned under /v1 and speak JSON.
Zero-knowledge model: your application (or your users' browsers) must encrypt content before calling POST /v1/secrets — for example with AES-256-GCM. The API accepts and returns only ciphertext, an initialization vector/GCM tag, and non-sensitive envelope metadata (locator, size, type, expiry, one-time flag, view status). It cannot decrypt, inspect, or recover the content of any secret. See the How It Works and Security pages for the full architecture.
Base URL
https://api.easytopaste.com Every request and response uses Content-Type: application/json. Responses carry an X-Request-Id header — include it when contacting support about a specific call.
Authentication
Most secrets endpoints (create, retrieve, burn, code lookup) work anonymously, at a lower daily quota. Signing in raises your quota and unlocks account-scoped endpoints (listing your secrets, revoking before expiry, data export). Authenticate by sending a bearer JSON Web Token:
Authorization: Bearer <accessToken> Obtaining a token
Create an account with POST /v1/identity/signup, verify the email address, then exchange credentials for tokens with:
POST /v1/identity/login
Content-Type: application/json
{
"email": "you@example.com",
"password": "your-password"
}
-> 200 OK
{
"userId": "...",
"accessToken": "<RS256 JWT>",
"accessTokenExpiresAt": "2026-07-08T12:15:00+00:00",
"refreshToken": "<opaque token>",
"refreshTokenExpiresAt": "2026-08-07T12:00:00+00:00",
"mfaRequired": false,
"user": { "id": "...", "email": "you@example.com", "locale": null }
} Access tokens are RS256-signed, short-lived (15 minutes), issued for the easytopaste.com issuer and easytopaste-api audience. Exchange the refresh token for a new pair with POST /v1/identity/refresh before it expires (refresh tokens last 30 days and rotate on every use).
Verifying tokens (JWKS)
If you need to verify an EasyToPaste access token independently (for example in a backend integration), fetch the public signing keys from the JWKS endpoint — no authentication required, cacheable for one hour:
GET /v1/identity/.well-known/jwks.json Rate limits
All endpoints are rate-limited per caller (per-IP when anonymous, per-account when signed in). Every response carries the current window state:
X-RateLimit-Limit-Minute: 30
X-RateLimit-Limit-Hour: -1
X-RateLimit-Limit-Day: 60
X-RateLimit-Remaining-Minute: 29
X-RateLimit-Remaining-Hour: -1
X-RateLimit-Remaining-Day: -1
X-RateLimit-Reset: 1751980860 A value of -1 means that window is not separately enforced. When you exceed a limit, the API returns 429 with a Retry-After header (seconds).
| Action | Anonymous | Signed in (free) | Plus / Team | Enterprise |
|---|---|---|---|---|
| Create secret | 10/min | 30/min | 60–120/min | 600/min |
| Retrieve / burn / list / revoke | 60/min | 120/min | 600/min | 1200/min |
| Code lookup | 3/min | 3/min | 3/min | 3/min |
| Abuse report | 5/hour | 5/hour | 5/hour | 5/hour |
Separately, daily secret-creation quotas apply: 10/day for anonymous callers and 50/day for signed-in free-tier accounts (higher on paid plans). Account signup is capped at 3 per IP per hour, and retrieval-code lookups additionally lock a code out for 15 minutes after 5 failed attempts within 5 minutes, to deter brute-forcing.
Core endpoints
The full endpoint list (including presigned multi-chunk uploads, recipient authorization, code lookup, and account/identity management) is in the Swagger UI. The four endpoints below cover the core secret lifecycle.
Create a secret
POST /v1/secrets — anonymous or signed in. Supports an Idempotency-Key header so retried requests are safe to replay.
| Field | Type | Required | Notes |
|---|---|---|---|
| type | string | no (default "text") | "text" or "file" |
| mode | string | no (default "link") | "link" or "passphrase" |
| envelopeVersion | integer | no (default 1) | must be 1 |
| sizeBytes | integer | no | total ciphertext size in bytes, ≥ 0 |
| oneTime | boolean | no (default false) | burn-after-read; call /burn to commit |
| expiresInDays | integer | no (default 1) | 1–31 |
| requiresRecipientLogin | boolean | no | gate retrieval behind a signed-in recipient (see /authorize) |
| mimeHint | string | no | ≤ 128 chars |
| ciphertext | string | exactly one of ciphertext / uploadId / chunks | inline base64url ciphertext, ≤ 256 KB |
| uploadId | string | — | from POST /v1/secrets/uploads, for larger single-object payloads |
| chunks | array | — | multi-chunk file manifest (large files) |
| saltB64 / argonParams | string / object | required if mode="passphrase" | Argon2id parameters (memKiB ≥ 65536, iterations ≥ 3, version 19) |
| recipientDomain | string | no | restrict retrieval to signed-in recipients on this email domain |
| codeMode | boolean | no | requires mode="passphrase"; returns a short human-readable retrieval code |
Response — 201 Created:
{
"locator": "AbCd12Ef",
"expiresAt": "2026-07-09T12:00:00+00:00",
"createdAt": "2026-07-08T12:00:00+00:00",
"retrieveUrl": "https://easytopaste.com/s/AbCd12Ef"
} Retrieve a secret
GET /v1/secrets/{locator} — returns envelope metadata plus the ciphertext (inline, a presigned download URL, or a chunk manifest). Does not commit a burn; for one-time secrets, call POST /v1/secrets/{locator}/burn separately once the ciphertext has been fetched and decrypted.
-> 200 OK
{
"locator": "AbCd12Ef",
"type": "text",
"mode": "link",
"oneTime": false,
"requiresRecipientLogin": false,
"envelopeVersion": 1,
"expiresAt": "2026-07-09T12:00:00+00:00",
"status": "active",
"ciphertext": "BASE64URL_ENCRYPTED_PAYLOAD"
} Possible status / error outcomes: 404 not_found, 410 expired, 410 revoked, 410 already_accessed (one-time secret already burned), or 403 recipient_denied if the secret requires a signed-in recipient.
Burn a secret (one-time)
POST /v1/secrets/{locator}/burn — commits the burn for a one-time secret. Only valid once: exactly one concurrent caller wins; every other caller (including retries) receives 410 already_accessed.
-> 200 OK
{ "burned": true } Revoke a secret
POST /v1/secrets/{locator}/revoke — requires authentication; only the owner (or an org admin for org-scoped secrets) may revoke a secret before its natural expiry.
Authorization: Bearer <accessToken>
-> 200 OK
{ "revoked": true } Code examples: create + retrieve
Each example creates a secret from a pre-encrypted ciphertext payload, then retrieves it back. Replace BASE64URL_ENCRYPTED_PAYLOAD with content you encrypted client-side — the API itself performs no encryption or decryption.
curl
# 1. Create a secret (anonymous — no Authorization header required)
curl -X POST https://api.easytopaste.com/v1/secrets \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"type": "text",
"mode": "link",
"envelopeVersion": 1,
"sizeBytes": 42,
"oneTime": false,
"expiresInDays": 1,
"ciphertext": "BASE64URL_ENCRYPTED_PAYLOAD"
}'
# -> 201 Created
# { "locator": "AbCd12Ef", "expiresAt": "...", "createdAt": "...",
# "retrieveUrl": "https://easytopaste.com/s/AbCd12Ef" }
# 2. Retrieve it
curl https://api.easytopaste.com/v1/secrets/AbCd12Ef
# -> 200 OK
# { "locator": "AbCd12Ef", "type": "text", "mode": "link", "status": "active",
# "ciphertext": "BASE64URL_ENCRYPTED_PAYLOAD", ... }
# Signed-in calls (higher daily quota) add:
# -H "Authorization: Bearer $ACCESS_TOKEN" Python
import uuid
import requests
BASE_URL = "https://api.easytopaste.com"
# Optional: an access token from POST /v1/identity/login raises your daily
# quota from the anonymous cap to the signed-in free-tier cap. Omit it and
# the request is still accepted anonymously.
headers = {
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
# "Authorization": "Bearer " + access_token,
}
# 1. Create a secret. Encrypt your content client-side (e.g. AES-256-GCM)
# before calling this endpoint -- the API only ever stores ciphertext.
create_resp = requests.post(
f"{BASE_URL}/v1/secrets",
headers=headers,
json={
"type": "text",
"mode": "link",
"envelopeVersion": 1,
"sizeBytes": 42,
"oneTime": False,
"expiresInDays": 1,
"ciphertext": "BASE64URL_ENCRYPTED_PAYLOAD",
},
)
create_resp.raise_for_status()
secret = create_resp.json()
print("Share URL:", secret["retrieveUrl"])
# 2. Retrieve it
get_resp = requests.get(f"{BASE_URL}/v1/secrets/{secret['locator']}")
get_resp.raise_for_status()
print(get_resp.json()) JavaScript (fetch)
const BASE_URL = 'https://api.easytopaste.com';
// 1. Create a secret. Encrypt client-side (e.g. AES-256-GCM via the Web
// Crypto SubtleCrypto API) before calling this endpoint -- the API only
// ever stores ciphertext.
const createResp = await fetch(BASE_URL + '/v1/secrets', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
// Authorization: 'Bearer ' + accessToken, // optional, higher quota tier
},
body: JSON.stringify({
type: 'text',
mode: 'link',
envelopeVersion: 1,
sizeBytes: 42,
oneTime: false,
expiresInDays: 1,
ciphertext: 'BASE64URL_ENCRYPTED_PAYLOAD',
}),
});
const secret = await createResp.json();
console.log('Share URL:', secret.retrieveUrl);
// 2. Retrieve it
const getResp = await fetch(BASE_URL + '/v1/secrets/' + secret.locator);
const data = await getResp.json();
console.log(data); Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const baseURL = "https://api.easytopaste.com"
func main() {
// 1. Create a secret. Encrypt client-side (e.g. AES-256-GCM) before
// calling this endpoint -- the API only ever stores ciphertext.
body, _ := json.Marshal(map[string]interface{}{
"type": "text",
"mode": "link",
"envelopeVersion": 1,
"sizeBytes": 42,
"oneTime": false,
"expiresInDays": 1,
"ciphertext": "BASE64URL_ENCRYPTED_PAYLOAD",
})
req, _ := http.NewRequest("POST", baseURL+"/v1/secrets", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "generate-a-uuid-per-request")
// req.Header.Set("Authorization", "Bearer "+accessToken) // optional
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var created struct {
Locator string `json:"locator"`
RetrieveURL string `json:"retrieveUrl"`
}
data, _ := io.ReadAll(resp.Body)
json.Unmarshal(data, &created)
fmt.Println("Share URL:", created.RetrieveURL)
// 2. Retrieve it
getResp, err := http.Get(baseURL + "/v1/secrets/" + created.Locator)
if err != nil {
panic(err)
}
defer getResp.Body.Close()
getData, _ := io.ReadAll(getResp.Body)
fmt.Println(string(getData))
} PHP
<?php
$baseUrl = 'https://api.easytopaste.com';
// 1. Create a secret. Encrypt client-side (e.g. AES-256-GCM via
// openssl_encrypt) before calling this endpoint -- the API only ever
// stores ciphertext.
$payload = json_encode([
'type' => 'text',
'mode' => 'link',
'envelopeVersion' => 1,
'sizeBytes' => 42,
'oneTime' => false,
'expiresInDays' => 1,
'ciphertext' => 'BASE64URL_ENCRYPTED_PAYLOAD',
]);
$ch = curl_init($baseUrl . '/v1/secrets');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
// 'Authorization: Bearer ' . $accessToken, // optional
],
]);
$secret = json_decode(curl_exec($ch), true);
curl_close($ch);
echo "Share URL: " . $secret['retrieveUrl'] . "\n";
// 2. Retrieve it
$ch2 = curl_init($baseUrl . '/v1/secrets/' . $secret['locator']);
curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
$retrieved = json_decode(curl_exec($ch2), true);
curl_close($ch2);
print_r($retrieved); Error format
Every non-2xx response uses the same envelope:
{
"error": {
"code": "quota_exceeded",
"message": "DAILY_CREATE_CAP_HIT",
"request_id": "b3c1...",
"quota": {
"kind": "daily_create",
"limit": 10,
"used": 10,
"scope": "guest",
"upgradePath": "plus",
"resetsAt": "2026-07-09T00:00:00+00:00"
}
}
} | HTTP status | code |
|---|---|
| 400 | invalid_request |
| 401 | unauthorized |
| 402 | payment_required |
| 403 | forbidden, recipient_denied |
| 404 | not_found |
| 409 | conflict, idempotency_conflict |
| 410 | expired, revoked, already_accessed |
| 413 | payload_too_large |
| 422 | quota_exceeded, policy_violation |
| 429 | rate_limited |
| 500 | internal |
| 501 | not_implemented |
Resources
- Interactive Swagger UI — try every endpoint, including identity, MFA, and presigned multi-chunk uploads.
- Raw OpenAPI 3.1 spec — generate a client SDK in your language of choice.
- Postman collection — pre-built requests for the secrets, identity, and abuse-report endpoints.
- How It Works — the zero-knowledge architecture in depth.
- Security — encryption details and vulnerability disclosure.
Questions or found an issue with these docs? Email support@easytopaste.com.