Public API Documentation
Authentication, endpoints, rate limits, and integration examples.
PressDown Public API
This API is designed for end users who create API keys in Settings -> API Keys.
Authentication
Use your API key as a Bearer token:
Authorization: Bearer pd_live_xxxxxxxxxxxxxxxxxxxxx
- Base URL (local):
http://localhost:3000/api/public/v1 - Base URL (recommended production via nginx):
https://api.pressdown.app - Content type:
application/json - API keys can be revoked at any time from the settings panel.
- API has dynamic per-token rate limits configured in DB:
requests_per_minute(default:60)stamps_per_minute(default:6)
You can update limits at runtime (no rebuild) by editing:
update public.api_rate_limits set value_int = 120 where key = 'requests_per_minute';
update public.api_rate_limits set value_int = 12 where key = 'stamps_per_minute';
Endpoints
1) Get credits balance
GET /credits (nginx public host)
or GET /api/public/v1/credits (direct app route)
Returns remaining and total credits for the authenticated user.
cURL
curl -X GET "https://api.pressdown.app/credits" \
-H "Authorization: Bearer YOUR_API_KEY"
Response
{
"credits": {
"remaining": 842,
"total": 1200
}
}
2) List books with published editions
GET /books (nginx public host)
or GET /api/public/v1/books (direct app route)
Returns all user books with nested published_editions.
cURL
curl -X GET "https://api.pressdown.app/books" \
-H "Authorization: Bearer YOUR_API_KEY"
Response
{
"books": [
{
"id": "book-uuid",
"title": "My Book",
"subtitle": "Optional subtitle",
"author": "Jane Doe",
"status": "Published",
"created_at": "2026-03-15T10:00:00.000Z",
"updated_at": "2026-03-15T12:00:00.000Z",
"published_editions": [
{
"id": "edition-uuid",
"version_number": 2,
"created_at": "2026-03-15T12:00:00.000Z",
"metadata": {}
}
]
}
]
}
3) Create stamping job
POST /stamps (nginx public host)
or POST /api/public/v1/stamps (direct app route)
Queues a stamping job from a published edition.
Request body
{
"edition_id": "edition-uuid",
"stamp_text": "CONFIDENTIAL",
"format": "pdf",
"webhook_url": "https://your-app.com/webhooks/pressdown"
}
webhook_urlis optional but recommended.- When provided, PressDown sends a
POSTwebhook after stamping completes.
cURL
curl -X POST "https://api.pressdown.app/stamps" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"edition_id": "edition-uuid",
"stamp_text": "CONFIDENTIAL",
"format": "pdf",
"webhook_url": "https://your-app.com/webhooks/pressdown"
}'
Response
{
"stamp_id": "stamp-uuid",
"job_id": "job-uuid",
"status": "pending",
"message": "Stamping job queued successfully."
}
Webhook payload (success)
{
"event": "stamp.completed",
"stamp_id": "stamp-uuid",
"job_id": "job-uuid",
"book_id": "book-uuid",
"edition_id": "edition-uuid",
"download_url": "https://...signed-url...",
"expires_at": "2026-03-16T12:00:00.000Z",
"generated_at": "2026-03-15T12:00:00.000Z"
}
download_url is a time-limited signed URL (24h) and does not require user session cookies.
Webhook payload (failure)
{
"event": "stamp.failed",
"stamp_id": "stamp-uuid",
"job_id": "job-uuid",
"book_id": "book-uuid",
"edition_id": "edition-uuid",
"error": "Error details"
}
JavaScript Example (Node.js)
const API_KEY = process.env.PRESSDOWN_API_KEY;
const BASE = process.env.PRESSDOWN_API_BASE_URL || "https://api.pressdown.app";
async function api(path, options = {}) {
const res = await fetch(`${BASE}${path}`, {
...options,
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
...(options.headers || {})
}
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || `HTTP ${res.status}`);
}
return res.json();
}
async function run() {
const credits = await api("/credits");
console.log("Credits:", credits);
const books = await api("/books");
console.log("Books:", books.books.length);
const edition = books.books[0]?.published_editions?.[0];
if (!edition) return;
const stamp = await api("/stamps", {
method: "POST",
body: JSON.stringify({
edition_id: edition.id,
stamp_text: "CONFIDENTIAL",
webhook_url: "https://your-app.com/webhooks/pressdown"
})
});
console.log("Stamp job:", stamp);
}
run().catch(console.error);
Python Example
import os
import requests
BASE = os.environ.get("PRESSDOWN_API_BASE_URL", "https://api.pressdown.app")
API_KEY = os.environ["PRESSDOWN_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
credits = requests.get(f"{BASE}/credits", headers=HEADERS).json()
print("Credits:", credits)
books = requests.get(f"{BASE}/books", headers=HEADERS).json()
print("Books:", len(books.get("books", [])))
books_list = books.get("books", [])
if books_list and books_list[0].get("published_editions"):
edition_id = books_list[0]["published_editions"][0]["id"]
stamp = requests.post(
f"{BASE}/stamps",
headers={**HEADERS, "Content-Type": "application/json"},
json={
"edition_id": edition_id,
"stamp_text": "CONFIDENTIAL",
"webhook_url": "https://your-app.com/webhooks/pressdown",
},
).json()
print("Stamp job:", stamp)
Error format
All errors return:
{
"error": "Error message"
}
Common status codes:
400invalid payload401missing/invalid API key402insufficient credits404resource not found429rate limit exceeded500server error