Skip to content
EasyToPaste

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 secret10/min30/min60–120/min600/min
Retrieve / burn / list / revoke60/min120/min600/min1200/min
Code lookup3/min3/min3/min3/min
Abuse report5/hour5/hour5/hour5/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.

FieldTypeRequiredNotes
typestringno (default "text")"text" or "file"
modestringno (default "link")"link" or "passphrase"
envelopeVersionintegerno (default 1)must be 1
sizeBytesintegernototal ciphertext size in bytes, ≥ 0
oneTimebooleanno (default false)burn-after-read; call /burn to commit
expiresInDaysintegerno (default 1)1–31
requiresRecipientLoginbooleannogate retrieval behind a signed-in recipient (see /authorize)
mimeHintstringno≤ 128 chars
ciphertextstringexactly one of ciphertext / uploadId / chunksinline base64url ciphertext, ≤ 256 KB
uploadIdstringfrom POST /v1/secrets/uploads, for larger single-object payloads
chunksarraymulti-chunk file manifest (large files)
saltB64 / argonParamsstring / objectrequired if mode="passphrase"Argon2id parameters (memKiB ≥ 65536, iterations ≥ 3, version 19)
recipientDomainstringnorestrict retrieval to signed-in recipients on this email domain
codeModebooleannorequires 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 statuscode
400invalid_request
401unauthorized
402payment_required
403forbidden, recipient_denied
404not_found
409conflict, idempotency_conflict
410expired, revoked, already_accessed
413payload_too_large
422quota_exceeded, policy_violation
429rate_limited
500internal
501not_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.