Publications API
Automate PDF publications, replacements and reading analytics.
Authentication and limits
Use workspace keys with publications:read and/or publications:write on a Publications entitlement that includes API access. QR-only keys do not receive publication access automatically. All APIs share the existing 120 requests/minute/workspace limit. Send an Idempotency-Key for each mutation; reuse it only with the same request.
Upload PDFs privately in three steps: reserve, upload directly to storage, finalize. Maximum 20 MB and 300 pages. Finalization rejects malformed, encrypted, interactive-form, script-bearing and embedded-file PDFs. Existing PDFs remain live until replacement validation succeeds.
Endpoints
| Method | Path | Action |
|---|---|---|
| GET, POST | /publications | List / create draft |
| GET, PATCH, DELETE | /publications/{id} | Read / update / delete |
| POST | /publications/{id}/uploads | Reserve PDF upload |
| POST | /publications/{id}/finalize | Validate and commit |
| POST | /publications/{id}/duplicate | Duplicate as draft |
| GET | /publications/{id}/analytics | Reading analytics |
Create, upload, publish
// Backend only. Keep the workspace key secret.
const base = process.env.LINKWAVE_URL + '/api/v1/publications';
async function call(path, method, body, key) {
const response = await fetch(base + path, {
method,
headers: {
Authorization: 'Bearer ' + process.env.LINKWAVE_API_KEY,
'Content-Type': 'application/json', 'Idempotency-Key': key
}, body: JSON.stringify(body)
});
const result = await response.json();
if (!response.ok) throw new Error(result.error.message);
return result.data;
}
const pub = await call('', 'POST', { title: 'Catalogue' }, 'catalogue:create:v1');
const upload = await call('/' + pub.id + '/uploads', 'POST',
{ bytes: pdfBytes.length }, 'catalogue:upload:v1');
const stored = await fetch(upload.signed_url, {
method: 'PUT', headers: { 'Content-Type': 'application/pdf', 'x-upsert': 'false' },
body: pdfBytes
});
if (!stored.ok) throw new Error('PDF upload failed');
await call('/' + pub.id + '/finalize', 'POST',
{ upload_id: upload.upload_id }, 'catalogue:finalize:v1');
const published = await call('/' + pub.id, 'PATCH',
{ status: 'published' }, 'catalogue:publish:v1');
console.log(published.viewer_url, published.embed_url);Replacement and access
Repeat uploads and finalize against the same ID with new keys to replace a PDF. The viewer URL and QR codes stay unchanged. PATCH status to archived to stop public access. Visibility workspace requires active membership. Download and print settings control viewer actions, not DRM.
Analytics include deduplicated reading sessions, pages visited, active-tab heartbeats, and supported PDF link clicks. They are approximate client-reported metrics, not verified human readership. OCR is not included.
OpenAPI specification